Skip to content

Commit 4fdfd5f

Browse files
authored
Merge pull request #1 from juri/cli
Add command line tool
2 parents 56b0823 + f7f6e78 commit 4fdfd5f

File tree

4 files changed

+175
-0
lines changed

4 files changed

+175
-0
lines changed

Package.resolved

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"originHash" : "682277e7540b497651926f1feab560576cebfe00619f90d1a718c9ab02e85378",
3+
"pins" : [
4+
{
5+
"identity" : "swift-argument-parser",
6+
"kind" : "remoteSourceControl",
7+
"location" : "https://github.com/apple/swift-argument-parser.git",
8+
"state" : {
9+
"revision" : "46989693916f56d1186bd59ac15124caef896560",
10+
"version" : "1.3.1"
11+
}
12+
}
13+
],
14+
"version" : 3
15+
}

Package.swift

+16
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,31 @@ import PackageDescription
55
let package = Package(
66
name: "DotEnvy",
77
products: [
8+
.executable(
9+
name: "dotenv-tool",
10+
targets: [
11+
"CLI",
12+
]
13+
),
814
.library(
915
name: "DotEnvy",
1016
targets: ["DotEnvy"]
1117
),
1218
],
19+
dependencies: [
20+
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.3.1"),
21+
],
1322
targets: [
1423
.target(
1524
name: "DotEnvy"
1625
),
26+
.executableTarget(
27+
name: "CLI",
28+
dependencies: [
29+
"DotEnvy",
30+
.product(name: "ArgumentParser", package: "swift-argument-parser"),
31+
]
32+
),
1733
.testTarget(
1834
name: "DotEnvyTests",
1935
dependencies: ["DotEnvy"]

README.md

+10
Original file line numberDiff line numberDiff line change
@@ -63,3 +63,13 @@ outputs
6363
6464
Error on line 1: Unterminated quote
6565
```
66+
67+
## Command Line
68+
69+
There's also a command line tool, `dotenv-tool`. It supports checking dotenv files for syntax errors and converting
70+
them to JSON. To install, run:
71+
72+
```sh
73+
swift build -c release
74+
cp .build/release/dotenv-tool /usr/local/bin
75+
```

Sources/CLI/CLI.swift

+134
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import ArgumentParser
2+
import DotEnvy
3+
import Foundation
4+
5+
@main
6+
struct Tool: ParsableCommand {
7+
static var configuration = CommandConfiguration(
8+
commandName: "dotenvy-tool",
9+
abstract: "Tool for working with dotenv files",
10+
subcommands: [Check.self, JSON.self]
11+
)
12+
}
13+
14+
struct Check: ParsableCommand {
15+
static var configuration
16+
= CommandConfiguration(
17+
abstract: "Check syntax of input.",
18+
discussion: """
19+
In case of a syntax error, the error is printed to standard error
20+
and the command exits with failure code \(ExitCode.failure.rawValue).
21+
22+
If there are no problems reading the input, nothing is printed
23+
and the command exits with \(ExitCode.success.rawValue).
24+
"""
25+
)
26+
27+
@Option(
28+
name: [.customShort("i"), .long],
29+
help: "Input. Standard input is used with -. If omitted, try to use .env in cwd"
30+
)
31+
var input: Input?
32+
33+
func run() throws {
34+
_ = try loadInput(self.input)
35+
}
36+
}
37+
38+
struct JSON: ParsableCommand {
39+
static var configuration
40+
= CommandConfiguration(
41+
abstract: "Convert input to JSON.",
42+
discussion: """
43+
The input is converted to a JSON object.
44+
45+
In case of a syntax error, the error is printed to standard error and the
46+
command exits with failure code \(ExitCode.failure.rawValue).
47+
48+
If there are no problems reading the input, the JSON value is printed to
49+
standard output and the command exits with \(ExitCode.success.rawValue).
50+
"""
51+
)
52+
53+
@Option(
54+
name: [.customShort("i"), .long],
55+
help: "Input. Standard input is used with -. If omitted, try to use .env in cwd"
56+
)
57+
var input: Input?
58+
59+
@Flag(help: "Pretty print JSON")
60+
var pretty: Bool = false
61+
62+
func run() throws {
63+
let values = try loadInput(self.input)
64+
let json = try JSONSerialization.data(
65+
withJSONObject: values,
66+
options: self.pretty ? [.prettyPrinted, .sortedKeys] : []
67+
)
68+
FileHandle.standardOutput.write(json)
69+
FileHandle.standardOutput.write(Data("\n".utf8))
70+
}
71+
}
72+
73+
enum Input: ExpressibleByArgument {
74+
case stdin
75+
case fileURL(FileURL)
76+
77+
init?(argument: String) {
78+
if argument == "-" {
79+
self = .stdin
80+
} else if let fileURL = FileURL(argument: argument) {
81+
self = .fileURL(fileURL)
82+
} else {
83+
return nil
84+
}
85+
}
86+
}
87+
88+
struct FileURL: ExpressibleByArgument {
89+
var url: URL
90+
91+
init?(argument: String) {
92+
// the new URL(filePath:directoryHint:) is not available on Linux
93+
let url = URL(fileURLWithPath: argument, isDirectory: false)
94+
guard url.isFileURL else {
95+
return nil
96+
}
97+
self.url = url
98+
}
99+
}
100+
101+
private func loadInput(_ input: Input?) throws -> [String: String] {
102+
if let input = input {
103+
let string = try readInput(input)
104+
do {
105+
return try DotEnvironment.parse(string: string)
106+
} catch let error as ParseErrorWithLocation {
107+
FileHandle.standardError.write(Data(error.formatError(source: string).utf8))
108+
FileHandle.standardError.write(Data("\n".utf8))
109+
throw ExitCode.failure
110+
}
111+
} else {
112+
do {
113+
return try DotEnvironment.loadValues()
114+
} catch let error as LoadError {
115+
FileHandle.standardError.write(Data(error.description.utf8))
116+
FileHandle.standardError.write(Data("\n".utf8))
117+
throw ExitCode.failure
118+
}
119+
}
120+
}
121+
122+
private func readInput(_ input: Input) throws -> String {
123+
let data: Data
124+
switch input {
125+
case .stdin:
126+
data = FileHandle.standardInput.readDataToEndOfFile()
127+
case let .fileURL(fileURL):
128+
data = try Data(contentsOf: fileURL.url)
129+
}
130+
guard let string = String(data: data, encoding: .utf8) else {
131+
throw ValidationError("Input could not be decoded as UTF-8")
132+
}
133+
return string
134+
}

0 commit comments

Comments
 (0)