|
10 | 10 | //
|
11 | 11 | //===----------------------------------------------------------------------===//
|
12 | 12 |
|
| 13 | +import ArgumentParser |
13 | 14 | import Foundation
|
14 | 15 | import SwiftFormat
|
15 | 16 | import TSCBasic
|
16 | 17 | import TSCUtility
|
17 | 18 |
|
18 | 19 | /// 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 | + |
21 | 27 | /// The path to the JSON configuration file that should be loaded.
|
22 | 28 | ///
|
23 | 29 | /// 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? |
25 | 34 |
|
26 | 35 | /// The filename for the source code when reading from standard input, to include in diagnostic
|
27 | 36 | /// messages.
|
28 | 37 | ///
|
29 | 38 | /// If not specified and standard input is used, a dummy filename is used for diagnostic messages
|
30 | 39 | /// 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? |
32 | 42 |
|
| 43 | + enum ToolMode: String, CaseIterable, ExpressibleByArgument { |
| 44 | + case format |
| 45 | + case lint |
| 46 | + case dumpConfiguration = "dump-configuration" |
| 47 | + } |
| 48 | + |
33 | 49 | /// The mode in which to run the tool.
|
34 | 50 | ///
|
35 | 51 | /// 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 |
37 | 56 |
|
38 | 57 | /// Whether or not to format the Swift file in-place
|
39 | 58 | ///
|
40 | 59 | /// 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 |
42 | 64 |
|
43 | 65 | /// Whether or not to run the formatter/linter recursively.
|
44 | 66 | ///
|
45 | 67 | /// 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 |
47 | 72 |
|
| 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 | + |
48 | 83 | /// Advanced options that are useful for developing/debugging but otherwise not meant for general
|
49 | 84 | /// 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 | + ] |
144 | 90 | }
|
145 | 91 |
|
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") |
172 | 99 | }
|
173 | 100 |
|
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") |
177 | 104 | }
|
178 | 105 |
|
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") |
181 | 108 | }
|
182 | 109 |
|
183 |
| - if !opts.paths.isEmpty && !opts.recursive { |
184 |
| - for path in opts.paths { |
| 110 | + if !paths.isEmpty && !recursive { |
| 111 | + for path in paths { |
185 | 112 | var isDir: ObjCBool = false
|
186 | 113 | 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 | + """ |
190 | 119 | )
|
191 | 120 | }
|
192 | 121 | }
|
193 | 122 | }
|
194 |
| - } catch { |
195 |
| - stderrStream.write("error: \(error)\n\n") |
196 |
| - parser.printUsage(on: stderrStream) |
197 |
| - exit(1) |
198 | 123 | }
|
199 |
| - return opts |
200 | 124 | }
|
0 commit comments