From fe7d0beffa68e9f02d54e1da5bff5c3faf6b16a0 Mon Sep 17 00:00:00 2001 From: Juri Pakaste Date: Sun, 12 May 2024 17:06:24 +0300 Subject: [PATCH 1/6] Add CLI tool dotenvy-tool and subcommand check check checks the syntax of the input file or stdin. --- Package.resolved | 15 ++++++++++ Package.swift | 16 ++++++++++ Sources/CLI/CLI.swift | 69 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 Package.resolved create mode 100644 Sources/CLI/CLI.swift diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..0f971f3 --- /dev/null +++ b/Package.resolved @@ -0,0 +1,15 @@ +{ + "originHash" : "682277e7540b497651926f1feab560576cebfe00619f90d1a718c9ab02e85378", + "pins" : [ + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "46989693916f56d1186bd59ac15124caef896560", + "version" : "1.3.1" + } + } + ], + "version" : 3 +} diff --git a/Package.swift b/Package.swift index 28d1e3e..e73cbc6 100644 --- a/Package.swift +++ b/Package.swift @@ -5,15 +5,31 @@ import PackageDescription let package = Package( name: "DotEnvy", products: [ + .executable( + name: "dotenv-tool", + targets: [ + "CLI", + ] + ), .library( name: "DotEnvy", targets: ["DotEnvy"] ), ], + dependencies: [ + .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.1"), + ], targets: [ .target( name: "DotEnvy" ), + .executableTarget( + name: "CLI", + dependencies: [ + "DotEnvy", + .product(name: "ArgumentParser", package: "swift-argument-parser"), + ] + ), .testTarget( name: "DotEnvyTests", dependencies: ["DotEnvy"] diff --git a/Sources/CLI/CLI.swift b/Sources/CLI/CLI.swift new file mode 100644 index 0000000..5ac189b --- /dev/null +++ b/Sources/CLI/CLI.swift @@ -0,0 +1,69 @@ +import ArgumentParser +import DotEnvy +import Foundation + +@main +struct Tool: ParsableCommand { + static var configuration = CommandConfiguration( + commandName: "dotenvy-tool", + abstract: "Tool for working with dotenv files", + subcommands: [Check.self] + ) +} + +struct Check: ParsableCommand { + static var configuration + = CommandConfiguration( + abstract: "Check syntax of input.", + discussion: """ + In case of a syntax error, the error is printed out and the command exits + with failure code \(ExitCode.failure.rawValue). + + If there are no problems reading the input, nothing is printed and the command exits + with \(ExitCode.success.rawValue). + """ + ) + + @Argument(help: "Input file. Standard input is used if omitted") + var file: FileURL? + + func validate() throws { + _ = try self.file?.url.checkResourceIsReachable() + } + + func run() throws { + let string = try readInput(self.file) + do { + _ = try DotEnvironment.parse(string: string) + } catch let error as ParseErrorWithLocation { + print(error.formatError(source: string)) + throw ExitCode.failure + } + } +} + +struct FileURL: ExpressibleByArgument { + var url: URL + + init?(argument: String) { + // the new URL(filePath:directoryHint:) is not available on Linux + let url = URL(fileURLWithPath: argument, isDirectory: false) + guard url.isFileURL else { + return nil + } + self.url = url + } +} + +private func readInput(_ file: FileURL?) throws -> String { + let data: Data + if let file { + data = try Data(contentsOf: file.url) + } else { + data = FileHandle.standardInput.readDataToEndOfFile() + } + guard let string = String(data: data, encoding: .utf8) else { + throw ValidationError("Input could not be decoded as UTF-8") + } + return string +} From d2b2fd5340f58c4ffecc9bd5557fccb15b1f7b96 Mon Sep 17 00:00:00 2001 From: Juri Pakaste Date: Sun, 12 May 2024 17:11:28 +0300 Subject: [PATCH 2/6] Use stderr for error output --- Sources/CLI/CLI.swift | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Sources/CLI/CLI.swift b/Sources/CLI/CLI.swift index 5ac189b..e8ee57e 100644 --- a/Sources/CLI/CLI.swift +++ b/Sources/CLI/CLI.swift @@ -16,7 +16,7 @@ struct Check: ParsableCommand { = CommandConfiguration( abstract: "Check syntax of input.", discussion: """ - In case of a syntax error, the error is printed out and the command exits + In case of a syntax error, the error is printed to standard error and the command exits with failure code \(ExitCode.failure.rawValue). If there are no problems reading the input, nothing is printed and the command exits @@ -36,7 +36,8 @@ struct Check: ParsableCommand { do { _ = try DotEnvironment.parse(string: string) } catch let error as ParseErrorWithLocation { - print(error.formatError(source: string)) + FileHandle.standardError.write(Data(error.formatError(source: string).utf8)) + FileHandle.standardError.write(Data("\n".utf8)) throw ExitCode.failure } } From a3911906d92a2c2a9f25055d3713275ea16a625e Mon Sep 17 00:00:00 2001 From: Juri Pakaste Date: Sun, 12 May 2024 17:25:52 +0300 Subject: [PATCH 3/6] Shorter help lines --- Sources/CLI/CLI.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/CLI/CLI.swift b/Sources/CLI/CLI.swift index e8ee57e..d65b231 100644 --- a/Sources/CLI/CLI.swift +++ b/Sources/CLI/CLI.swift @@ -16,11 +16,11 @@ struct Check: ParsableCommand { = CommandConfiguration( abstract: "Check syntax of input.", discussion: """ - In case of a syntax error, the error is printed to standard error and the command exits - with failure code \(ExitCode.failure.rawValue). + In case of a syntax error, the error is printed to standard error + and the command exits with failure code \(ExitCode.failure.rawValue). - If there are no problems reading the input, nothing is printed and the command exits - with \(ExitCode.success.rawValue). + If there are no problems reading the input, nothing is printed + and the command exits with \(ExitCode.success.rawValue). """ ) From 931d153dea929d62ffbb82fb80e2f4af4a228131 Mon Sep 17 00:00:00 2001 From: Juri Pakaste Date: Sun, 12 May 2024 17:26:03 +0300 Subject: [PATCH 4/6] Add another command for printing out JSON --- Sources/CLI/CLI.swift | 45 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/Sources/CLI/CLI.swift b/Sources/CLI/CLI.swift index d65b231..668048f 100644 --- a/Sources/CLI/CLI.swift +++ b/Sources/CLI/CLI.swift @@ -7,7 +7,7 @@ struct Tool: ParsableCommand { static var configuration = CommandConfiguration( commandName: "dotenvy-tool", abstract: "Tool for working with dotenv files", - subcommands: [Check.self] + subcommands: [Check.self, JSON.self] ) } @@ -43,6 +43,49 @@ struct Check: ParsableCommand { } } +struct JSON: ParsableCommand { + static var configuration + = CommandConfiguration( + abstract: "Convert input to JSON.", + discussion: """ + The input is converted to a JSON object. + + In case of a syntax error, the error is printed to standard error and the + command exits with failure code \(ExitCode.failure.rawValue). + + If there are no problems reading the input, the JSON value is printed to + standard output and the command exits with \(ExitCode.success.rawValue). + """ + ) + + @Argument(help: "Input file. Standard input is used if omitted") + var file: FileURL? + + @Flag(help: "Pretty print JSON") + var pretty: Bool = false + + func validate() throws { + _ = try self.file?.url.checkResourceIsReachable() + } + + func run() throws { + let string = try readInput(self.file) + do { + let values = try DotEnvironment.parse(string: string) + let json = try JSONSerialization.data( + withJSONObject: values, + options: self.pretty ? [.prettyPrinted, .sortedKeys] : [] + ) + FileHandle.standardOutput.write(json) + FileHandle.standardOutput.write(Data("\n".utf8)) + } catch let error as ParseErrorWithLocation { + FileHandle.standardError.write(Data(error.formatError(source: string).utf8)) + FileHandle.standardError.write(Data("\n".utf8)) + throw ExitCode.failure + } + } +} + struct FileURL: ExpressibleByArgument { var url: URL From ef4d83f440c03dbacd1fe4a607c4dbe5cab73024 Mon Sep 17 00:00:00 2001 From: Juri Pakaste Date: Sat, 18 May 2024 14:44:10 +0300 Subject: [PATCH 5/6] Better input handling in cli --- Sources/CLI/CLI.swift | 93 ++++++++++++++++++++++++++----------------- 1 file changed, 57 insertions(+), 36 deletions(-) diff --git a/Sources/CLI/CLI.swift b/Sources/CLI/CLI.swift index 668048f..1cbdf44 100644 --- a/Sources/CLI/CLI.swift +++ b/Sources/CLI/CLI.swift @@ -24,22 +24,14 @@ struct Check: ParsableCommand { """ ) - @Argument(help: "Input file. Standard input is used if omitted") - var file: FileURL? - - func validate() throws { - _ = try self.file?.url.checkResourceIsReachable() - } + @Option( + name: [.customShort("i"), .long], + help: "Input. Standard input is used with -. If omitted, try to use .env in cwd" + ) + var input: Input? func run() throws { - let string = try readInput(self.file) - do { - _ = try DotEnvironment.parse(string: string) - } catch let error as ParseErrorWithLocation { - FileHandle.standardError.write(Data(error.formatError(source: string).utf8)) - FileHandle.standardError.write(Data("\n".utf8)) - throw ExitCode.failure - } + _ = try loadInput(self.input) } } @@ -58,30 +50,37 @@ struct JSON: ParsableCommand { """ ) - @Argument(help: "Input file. Standard input is used if omitted") - var file: FileURL? + @Option( + name: [.customShort("i"), .long], + help: "Input. Standard input is used with -. If omitted, try to use .env in cwd" + ) + var input: Input? @Flag(help: "Pretty print JSON") var pretty: Bool = false - func validate() throws { - _ = try self.file?.url.checkResourceIsReachable() + func run() throws { + let values = try loadInput(self.input) + let json = try JSONSerialization.data( + withJSONObject: values, + options: self.pretty ? [.prettyPrinted, .sortedKeys] : [] + ) + FileHandle.standardOutput.write(json) + FileHandle.standardOutput.write(Data("\n".utf8)) } +} - func run() throws { - let string = try readInput(self.file) - do { - let values = try DotEnvironment.parse(string: string) - let json = try JSONSerialization.data( - withJSONObject: values, - options: self.pretty ? [.prettyPrinted, .sortedKeys] : [] - ) - FileHandle.standardOutput.write(json) - FileHandle.standardOutput.write(Data("\n".utf8)) - } catch let error as ParseErrorWithLocation { - FileHandle.standardError.write(Data(error.formatError(source: string).utf8)) - FileHandle.standardError.write(Data("\n".utf8)) - throw ExitCode.failure +enum Input: ExpressibleByArgument { + case stdin + case fileURL(FileURL) + + init?(argument: String) { + if argument == "-" { + self = .stdin + } else if let fileURL = FileURL(argument: argument) { + self = .fileURL(fileURL) + } else { + return nil } } } @@ -99,12 +98,34 @@ struct FileURL: ExpressibleByArgument { } } -private func readInput(_ file: FileURL?) throws -> String { - let data: Data - if let file { - data = try Data(contentsOf: file.url) +private func loadInput(_ input: Input?) throws -> [String: String] { + if let input = input { + let string = try readInput(input) + do { + return try DotEnvironment.parse(string: string) + } catch let error as ParseErrorWithLocation { + FileHandle.standardError.write(Data(error.formatError(source: string).utf8)) + FileHandle.standardError.write(Data("\n".utf8)) + throw ExitCode.failure + } } else { + do { + return try DotEnvironment.loadValues() + } catch let error as LoadError { + FileHandle.standardError.write(Data(error.description.utf8)) + FileHandle.standardError.write(Data("\n".utf8)) + throw ExitCode.failure + } + } +} + +private func readInput(_ input: Input) throws -> String { + let data: Data + switch input { + case .stdin: data = FileHandle.standardInput.readDataToEndOfFile() + case let .fileURL(fileURL): + data = try Data(contentsOf: fileURL.url) } guard let string = String(data: data, encoding: .utf8) else { throw ValidationError("Input could not be decoded as UTF-8") From f7f6e786f156acd6c274f46da483670c7f279ead Mon Sep 17 00:00:00 2001 From: Juri Pakaste Date: Sat, 18 May 2024 15:30:26 +0300 Subject: [PATCH 6/6] Add dotenv-tool to README --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 507302c..64b6dcd 100644 --- a/README.md +++ b/README.md @@ -63,3 +63,13 @@ outputs Error on line 1: Unterminated quote ``` + +## Command Line + +There's also a command line tool, `dotenv-tool`. It supports checking dotenv files for syntax errors and converting +them to JSON. To install, run: + +```sh +swift build -c release +cp .build/release/dotenv-tool /usr/local/bin +```