Skip to content

Commit 44a0230

Browse files
authored
Fix some papercuts with the command plugin (#78)
Motivation: The command plugin has a few rough edges which make it harder to use than it should. Modifications: - Check if help was requested before doing any other arg parsing. - Print help if no arguments were passed at all. - Check all input files end in '.proto', if they don't it's likely an option was passed in the wrong place, so print a helpful error message. - Change the default output path to be ".", i.e. the directory of the caller. Previously it was the plugin working directory which is buried in '.build' so it would appear as if nothing was generated. - Improve the formatting of the error message if the protoc invocation failed. The error message now includes a better formatted command that the user can invoke directly. Remove the error description because it was always "NSTaskTermination" without any useful information attached. Result: Command plugin is a little easier to use.
1 parent 183418a commit 44a0230

File tree

3 files changed

+79
-55
lines changed

3 files changed

+79
-55
lines changed

Plugins/GRPCProtobufGeneratorCommand/CommandConfig.swift

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,8 @@ struct CommandConfig {
4242
}
4343

4444
extension CommandConfig {
45-
static func parse(
46-
argumentExtractor argExtractor: inout ArgumentExtractor,
47-
pluginWorkDirectory: URL
48-
) throws -> CommandConfig {
45+
static func parse(args: [String]) throws -> CommandConfig {
46+
var argExtractor = ArgumentExtractor(args)
4947
var config = CommandConfig.defaults
5048

5149
for flag in OptionsAndFlags.allCases {
@@ -131,9 +129,7 @@ extension CommandConfig {
131129
config.common.protocPath = argExtractor.extractSingleOption(named: flag.rawValue)
132130

133131
case .outputPath:
134-
config.common.outputPath =
135-
argExtractor.extractSingleOption(named: flag.rawValue)
136-
?? pluginWorkDirectory.absoluteStringNoScheme
132+
config.common.outputPath = argExtractor.extractSingleOption(named: flag.rawValue) ?? "."
137133

138134
case .verbose:
139135
let verbose = argExtractor.extractFlag(named: flag.rawValue)

Plugins/GRPCProtobufGeneratorCommand/CommandPluginError.swift

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,12 @@ enum CommandPluginError: Error {
2121
case unknownAccessLevel(String)
2222
case unknownFileNamingStrategy(String)
2323
case conflictingFlags(String, String)
24+
case invalidInputFiles([String])
2425
case generationFailure(
25-
errorDescription: String,
2626
executable: String,
2727
arguments: [String],
2828
stdErr: String?
2929
)
30-
case tooManyParameterSeparators
3130
}
3231

3332
extension CommandPluginError: CustomStringConvertible {
@@ -45,21 +44,52 @@ extension CommandPluginError: CustomStringConvertible {
4544
return "Provided file naming strategy is unknown: \(value)."
4645
case .conflictingFlags(let flag1, let flag2):
4746
return "Provided flags conflict: '\(flag1)' and '\(flag2)'."
48-
case .generationFailure(let errorDescription, let executable, let arguments, let stdErr):
49-
var message = """
50-
Code generation failed with: \(errorDescription).
51-
\tExecutable: \(executable)
52-
\tArguments: \(arguments.joined(separator: " "))
53-
"""
47+
48+
case .invalidInputFiles(let files):
49+
var lines: [String] = []
50+
lines.append("Invalid input file(s)")
51+
lines.append("")
52+
lines.append("Found \(files.count) input(s) not ending in '.proto':")
53+
for file in files {
54+
lines.append("- \(file)")
55+
}
56+
lines.append("")
57+
lines.append("All options must be before '--', and all input files must be")
58+
lines.append("after '--'. Input files must end in '.proto'.")
59+
lines.append("")
60+
lines.append("See --help for more information.")
61+
return lines.joined(separator: "\n")
62+
63+
case .generationFailure(let executable, let arguments, let stdErr):
64+
var lines: [String] = []
65+
lines.append("protoc failed to generate code")
66+
lines.append("")
67+
lines.append(String(repeating: "-", count: 80))
68+
lines.append("Command run:")
69+
lines.append("")
70+
lines.append("\(executable) \\")
71+
var iterator = arguments.makeIterator()
72+
var current = iterator.next()
73+
while let currentArg = current {
74+
var nextArg = iterator.next()
75+
defer { current = nextArg }
76+
77+
if nextArg != nil {
78+
lines.append(" \(currentArg) \\")
79+
} else {
80+
lines.append(" \(currentArg)")
81+
}
82+
}
83+
5484
if let stdErr {
55-
message += """
56-
\n\tprotoc error output:
57-
\t\(stdErr)
58-
"""
85+
lines.append("")
86+
lines.append(String(repeating: "-", count: 80))
87+
lines.append("Error output (stderr):")
88+
lines.append("")
89+
lines.append(stdErr)
5990
}
60-
return message
61-
case .tooManyParameterSeparators:
62-
return "Unexpected parameter structure, too many '--' separators."
91+
92+
return lines.joined(separator: "\n")
6393
}
6494
}
6595
}

Plugins/GRPCProtobufGeneratorCommand/Plugin.swift

Lines changed: 31 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -50,45 +50,29 @@ struct GRPCProtobufGeneratorCommandPlugin {
5050
tool: (String) throws -> PluginContext.Tool,
5151
pluginWorkDirectoryURL: URL
5252
) throws {
53-
let flagsAndOptions: [String]
54-
let inputFiles: [String]
55-
56-
let separatorCount = arguments.count { $0 == CommandConfig.parameterGroupSeparator }
57-
switch separatorCount {
58-
case 0:
59-
var argExtractor = ArgumentExtractor(arguments)
60-
// check if help requested
61-
if argExtractor.extractFlag(named: OptionsAndFlags.help.rawValue) > 0 {
62-
OptionsAndFlags.printHelp(requested: true)
63-
return
64-
}
65-
66-
inputFiles = arguments
67-
flagsAndOptions = []
53+
// Check for help.
54+
if arguments.isEmpty || arguments.contains(where: { $0 == "--help" || $0 == "-h" }) {
55+
OptionsAndFlags.printHelp(requested: true)
56+
return
57+
}
6858

69-
case 1:
70-
let splitIndex = arguments.firstIndex(of: CommandConfig.parameterGroupSeparator)!
71-
flagsAndOptions = Array(arguments[..<splitIndex])
72-
inputFiles = Array(arguments[splitIndex.advanced(by: 1)...])
59+
// Split the input into options and input files.
60+
let (flagsAndOptions, inputFiles) = self.splitArgs(arguments)
7361

74-
default:
75-
throw CommandPluginError.tooManyParameterSeparators
62+
// Check the input files all look like protos
63+
let nonProtoInputs = inputFiles.filter { !$0.hasSuffix(".proto") }
64+
if !nonProtoInputs.isEmpty {
65+
throw CommandPluginError.invalidInputFiles(nonProtoInputs)
7666
}
7767

78-
var argExtractor = ArgumentExtractor(flagsAndOptions)
79-
// help requested
80-
if argExtractor.extractFlag(named: OptionsAndFlags.help.rawValue) > 0 {
81-
OptionsAndFlags.printHelp(requested: true)
82-
return
68+
if inputFiles.isEmpty {
69+
throw CommandPluginError.missingInputFile
8370
}
8471

85-
// MARK: Configuration
72+
// Parse the config.
8673
let commandConfig: CommandConfig
8774
do {
88-
commandConfig = try CommandConfig.parse(
89-
argumentExtractor: &argExtractor,
90-
pluginWorkDirectory: pluginWorkDirectoryURL
91-
)
75+
commandConfig = try CommandConfig.parse(args: flagsAndOptions)
9276
} catch {
9377
throw error
9478
}
@@ -159,6 +143,22 @@ struct GRPCProtobufGeneratorCommandPlugin {
159143
}
160144
}
161145
}
146+
147+
private func splitArgs(_ args: [String]) -> (options: [String], inputs: [String]) {
148+
let inputs: [String]
149+
let options: [String]
150+
151+
if let index = args.firstIndex(of: "--") {
152+
let nextIndex = args.index(after: index)
153+
inputs = Array(args[nextIndex...])
154+
options = Array(args[..<index])
155+
} else {
156+
options = []
157+
inputs = args
158+
}
159+
160+
return (options, inputs)
161+
}
162162
}
163163

164164
/// Execute a single invocation of `protoc`, printing output and if in verbose mode the invocation
@@ -202,7 +202,6 @@ func executeProtocInvocation(
202202
stdErr = nil
203203
}
204204
throw CommandPluginError.generationFailure(
205-
errorDescription: "\(error)",
206205
executable: executableURL.absoluteStringNoScheme,
207206
arguments: arguments,
208207
stdErr: stdErr
@@ -224,7 +223,6 @@ func executeProtocInvocation(
224223
}
225224
let problem = "\(process.terminationReason):\(process.terminationStatus)"
226225
throw CommandPluginError.generationFailure(
227-
errorDescription: problem,
228226
executable: executableURL.absoluteStringNoScheme,
229227
arguments: arguments,
230228
stdErr: stdErr

0 commit comments

Comments
 (0)