Skip to content

Commit 0067ab6

Browse files
committed
Adopt ArgumentParser for command-line tool
1 parent 70a543a commit 0067ab6

File tree

7 files changed

+197
-295
lines changed

7 files changed

+197
-295
lines changed

Package.resolved

Lines changed: 11 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Package.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ let package = Package(
2323
dependencies: [
2424
.package(
2525
url: "https://github.com/apple/swift-syntax",
26-
.revision("swift-DEVELOPMENT-SNAPSHOT-2020-01-29-a")
26+
.branch("master")
2727
),
2828
.package(url: "https://github.com/apple/swift-tools-support-core.git", from: "0.0.1"),
29+
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "0.0.1"),
2930
],
3031
targets: [
3132
.target(
@@ -69,6 +70,7 @@ let package = Package(
6970
"SwiftFormatCore",
7071
"SwiftSyntax",
7172
"SwiftToolsSupport-auto",
73+
"ArgumentParser",
7274
]
7375
),
7476
.testTarget(

Sources/swift-format/CommandLineOptions.swift

Lines changed: 66 additions & 142 deletions
Original file line numberDiff line numberDiff line change
@@ -10,191 +10,115 @@
1010
//
1111
//===----------------------------------------------------------------------===//
1212

13+
import ArgumentParser
1314
import Foundation
1415
import SwiftFormat
1516
import TSCBasic
1617
import TSCUtility
1718

1819
/// Collects the command line options that were passed to `swift-format`.
19-
struct CommandLineOptions {
20-
20+
struct SwiftFormatCommand: ParsableCommand {
21+
static var configuration = CommandConfiguration(
22+
commandName: "swift-format",
23+
abstract: "Format or lint Swift source code.",
24+
discussion: "When no files are specified, it expects the source from standard input."
25+
)
26+
2127
/// The path to the JSON configuration file that should be loaded.
2228
///
2329
/// If not specified, the default configuration will be used.
24-
var configurationPath: String? = nil
30+
@Option(
31+
name: .customLong("configuration"),
32+
help: "The path to a JSON file containing the configuration of the linter/formatter.")
33+
var configurationPath: String?
2534

2635
/// The filename for the source code when reading from standard input, to include in diagnostic
2736
/// messages.
2837
///
2938
/// If not specified and standard input is used, a dummy filename is used for diagnostic messages
3039
/// about the source from standard input.
31-
var assumeFilename: String? = nil
40+
@Option(help: "When using standard input, the filename of the source to include in diagnostics.")
41+
var assumeFilename: String?
3242

43+
enum ToolMode: String, CaseIterable, ExpressibleByArgument {
44+
case format
45+
case lint
46+
case dumpConfiguration = "dump-configuration"
47+
}
48+
3349
/// The mode in which to run the tool.
3450
///
3551
/// If not specified, the tool will be run in format mode.
36-
var mode: ToolMode = .format
52+
@Option(
53+
default: .format,
54+
help: "The mode to run swift-format in. Either 'format', 'lint', or 'dump-configuration'.")
55+
var mode: ToolMode
3756

3857
/// Whether or not to format the Swift file in-place
3958
///
4059
/// If specified, the current file is overwritten when formatting
41-
var inPlace: Bool = false
60+
@Flag(
61+
name: .shortAndLong,
62+
help: "Overwrite the current file when formatting ('format' mode only).")
63+
var inPlace: Bool
4264

4365
/// Whether or not to run the formatter/linter recursively.
4466
///
4567
/// If set, we recursively run on all ".swift" files in any provided directories.
46-
var recursive: Bool = false
68+
@Flag(
69+
name: .shortAndLong,
70+
help: "Recursively run on '.swift' files in any provided directories.")
71+
var recursive: Bool
4772

73+
/// The list of paths to Swift source files that should be formatted or linted.
74+
@Argument(help: "One or more input filenames")
75+
var paths: [String]
76+
77+
@Flag(help: "Print the version and exit")
78+
var version: Bool
79+
80+
@Flag(help: .hidden) var debugDisablePrettyPrint: Bool
81+
@Flag(help: .hidden) var debugDumpTokenStream: Bool
82+
4883
/// Advanced options that are useful for developing/debugging but otherwise not meant for general
4984
/// use.
50-
var debugOptions: DebugOptions = []
51-
52-
/// The list of paths to Swift source files that should be formatted or linted.
53-
var paths: [String] = []
54-
}
55-
56-
/// Process the command line argument strings and returns an object containing their values.
57-
///
58-
/// - Parameters:
59-
/// - commandName: The name of the command that this tool was invoked as.
60-
/// - arguments: The remaining command line arguments after the command name.
61-
/// - Returns: A `CommandLineOptions` value that contains the parsed options.
62-
func processArguments(commandName: String, _ arguments: [String]) -> CommandLineOptions {
63-
let parser = ArgumentParser(
64-
commandName: commandName,
65-
usage: "[options] [filename or path ...]",
66-
overview:
67-
"""
68-
Format or lint Swift source code.
69-
70-
When no files are specified, it expects the source from standard input.
71-
"""
72-
)
73-
74-
let binder = ArgumentBinder<CommandLineOptions>()
75-
binder.bind(
76-
option: parser.add(
77-
option: "--mode",
78-
shortName: "-m",
79-
kind: ToolMode.self,
80-
usage: "The mode to run swift-format in. Either 'format', 'lint', or 'dump-configuration'."
81-
)
82-
) {
83-
$0.mode = $1
84-
}
85-
binder.bind(
86-
option: parser.add(
87-
option: "--version",
88-
shortName: "-v",
89-
kind: Bool.self,
90-
usage: "Prints the version and exists"
91-
)
92-
) { opts, _ in
93-
opts.mode = .version
94-
}
95-
binder.bindArray(
96-
positional: parser.add(
97-
positional: "filenames or paths",
98-
kind: [String].self,
99-
optional: true,
100-
strategy: .upToNextOption,
101-
usage: "One or more input filenames",
102-
completion: .filename
103-
)
104-
) {
105-
$0.paths = $1
106-
}
107-
binder.bind(
108-
option: parser.add(
109-
option: "--configuration",
110-
kind: String.self,
111-
usage: "The path to a JSON file containing the configuration of the linter/formatter."
112-
)
113-
) {
114-
$0.configurationPath = $1
115-
}
116-
binder.bind(
117-
option: parser.add(
118-
option: "--assume-filename",
119-
kind: String.self,
120-
usage: "When using standard input, the filename of the source to include in diagnostics."
121-
)
122-
) {
123-
$0.assumeFilename = $1
124-
}
125-
binder.bind(
126-
option: parser.add(
127-
option: "--in-place",
128-
shortName: "-i",
129-
kind: Bool.self,
130-
usage: "Overwrite the current file when formatting ('format' mode only)."
131-
)
132-
) {
133-
$0.inPlace = $1
134-
}
135-
binder.bind(
136-
option: parser.add(
137-
option: "--recursive",
138-
shortName: "-r",
139-
kind: Bool.self,
140-
usage: "Recursively run on '.swift' files in any provided directories."
141-
)
142-
) {
143-
$0.recursive = $1
85+
var debugOptions: DebugOptions {
86+
[
87+
debugDisablePrettyPrint ? .disablePrettyPrint : [],
88+
debugDumpTokenStream ? .dumpTokenStream : [],
89+
]
14490
}
14591

146-
// Add advanced debug/developer options. These intentionally have no usage strings, which omits
147-
// them from the `--help` screen to avoid noise for the general user.
148-
binder.bind(
149-
option: parser.add(
150-
option: "--debug-disable-pretty-print",
151-
kind: Bool.self
152-
)
153-
) {
154-
$0.debugOptions.set(.disablePrettyPrint, enabled: $1)
155-
}
156-
binder.bind(
157-
option: parser.add(
158-
option: "--debug-dump-token-stream",
159-
kind: Bool.self
160-
)
161-
) {
162-
$0.debugOptions.set(.dumpTokenStream, enabled: $1)
163-
}
164-
165-
var opts = CommandLineOptions()
166-
do {
167-
let args = try parser.parse(arguments)
168-
try binder.fill(parseResult: args, into: &opts)
169-
170-
if opts.inPlace && (ToolMode.format != opts.mode || opts.paths.isEmpty) {
171-
throw ArgumentParserError.unexpectedArgument("--in-place, -i")
92+
mutating func validate() throws {
93+
if version {
94+
throw CleanExit.message("0.0.1")
95+
}
96+
97+
if inPlace && (mode == .format || paths.isEmpty) {
98+
throw ValidationError("'--in-place' is only valid when formatting files")
17299
}
173100

174-
let modeSupportsRecursive = ToolMode.format == opts.mode || ToolMode.lint == opts.mode
175-
if opts.recursive && (!modeSupportsRecursive || opts.paths.isEmpty) {
176-
throw ArgumentParserError.unexpectedArgument("--recursive, -r")
101+
let modeSupportsRecursive = mode == .format || mode == .lint
102+
if recursive && (!modeSupportsRecursive || paths.isEmpty) {
103+
throw ValidationError("'--recursive' is only valid when formatting or linting files")
177104
}
178105

179-
if opts.assumeFilename != nil && !opts.paths.isEmpty {
180-
throw ArgumentParserError.unexpectedArgument("--assume-filename")
106+
if assumeFilename != nil && !paths.isEmpty {
107+
throw ValidationError("'--assume-filename' is only valid when reading from stdin")
181108
}
182109

183-
if !opts.paths.isEmpty && !opts.recursive {
184-
for path in opts.paths {
110+
if !paths.isEmpty && !recursive {
111+
for path in paths {
185112
var isDir: ObjCBool = false
186113
if FileManager.default.fileExists(atPath: path, isDirectory: &isDir), isDir.boolValue {
187-
throw ArgumentParserError.invalidValue(
188-
argument: "'\(path)'",
189-
error: ArgumentConversionError.custom("for directories, use --recursive option")
114+
throw ValidationError(
115+
"""
116+
'\(path)' is a path to a directory, not a Swift source file.
117+
Use the '--recursive' option to handle directories.
118+
"""
190119
)
191120
}
192121
}
193122
}
194-
} catch {
195-
stderrStream.write("error: \(error)\n\n")
196-
parser.printUsage(on: stderrStream)
197-
exit(1)
198123
}
199-
return opts
200124
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import Foundation
2+
import SwiftSyntax
3+
4+
struct FormatError: LocalizedError {
5+
var message: String
6+
7+
var errorDescription: String? { message }
8+
9+
static func readSource(path: String) -> FormatError {
10+
FormatError(message: "Unable to read source for linting from \(path).")
11+
}
12+
13+
static func unableToLint(path: String, message: String) -> FormatError {
14+
FormatError(message: "Unable to lint \(path): \(message).")
15+
}
16+
17+
static func unableToFormat(path: String, message: String) -> FormatError {
18+
FormatError(message: "Unable to format \(path): \(message).")
19+
}
20+
21+
static func invalidSyntax(location: SourceLocation, message: String) -> FormatError {
22+
FormatError(message: "Unable to format at \(location): \(message).")
23+
}
24+
25+
static var exitWithDiagnosticErrors: FormatError {
26+
// The diagnostics engine has already printed errors to stderr.
27+
FormatError(message: "")
28+
}
29+
}
30+

0 commit comments

Comments
 (0)