diff --git a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift index d5db618a7..f725dd9d0 100644 --- a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift @@ -98,6 +98,17 @@ public struct CompletionShell: RawRepresentable, Hashable, CaseIterable { /// /// The environment variable is set in generated completion scripts. static let shellVersionEnvironmentVariableName = "SAP_SHELL_VERSION" + + func format(completions: [String]) -> String { + var completions = completions + if self == .zsh { + // This pseudo-completion is removed by the zsh completion script. + // It allows trailing empty string completions to work in zsh. + // zsh completion scripts generated by older SAP versions ignore spaces. + completions.append(" ") + } + return completions.joined(separator: "\n") + } } struct CompletionsGenerator { @@ -129,7 +140,7 @@ struct CompletionsGenerator { CompletionShell._requesting.withLock { $0 = shell } switch shell { case .zsh: - return ZshCompletionsGenerator.generateCompletionScript(command) + return [command].zshCompletionScript case .bash: return BashCompletionsGenerator.generateCompletionScript(command) case .fish: @@ -164,6 +175,16 @@ extension ParsableCommand { } } +extension [ParsableCommand.Type] { + /// Include default 'help' subcommand in nonempty subcommand list if & only if + /// no help subcommand already exists. + mutating func addHelpSubcommandIfMissing() { + if !isEmpty && allSatisfy({ $0._commandName != "help" }) { + append(HelpCommand.self) + } + } +} + extension Sequence where Element == ParsableCommand.Type { func completionFunctionName() -> String { "_" @@ -171,4 +192,23 @@ extension Sequence where Element == ParsableCommand.Type { .uniquingAdjacentElements() .joined(separator: "_") } + + var shellVariableNamePrefix: String { + flatMap { $0.compositeCommandName } + .joined(separator: "_") + .shellEscapeForVariableName() + } +} + +extension String { + func shellEscapeForSingleQuotedString(iterationCount: UInt64 = 1) -> Self { + iterationCount == 0 + ? self + : replacingOccurrences(of: "'", with: "'\\''") + .shellEscapeForSingleQuotedString(iterationCount: iterationCount - 1) + } + + func shellEscapeForVariableName() -> Self { + replacingOccurrences(of: "-", with: "_") + } } diff --git a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift index 9c5469091..9807a2bea 100644 --- a/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift @@ -9,217 +9,245 @@ // //===----------------------------------------------------------------------===// -struct ZshCompletionsGenerator { +extension [ParsableCommand.Type] { /// Generates a Zsh completion script for the given command. - static func generateCompletionScript(_ type: ParsableCommand.Type) -> String { - let initialFunctionName = [type].completionFunctionName() - + var zshCompletionScript: String { + // swift-format-ignore: NeverForceUnwrap + // Preconditions: + // - first must be non-empty for a zsh completion script to be of use. + // - first is guaranteed non-empty in the one place where this computed var is used. + let commandName = first!._commandName return """ - #compdef \(type._commandName) - local context state state_descr line - _\(type._commandName.zshEscapingCommandName())_commandname=$words[1] - typeset -A opt_args - - \(generateCompletionFunction([type])) - _custom_completion() { - local completions=("${(@f)$($*)}") - _describe '' completions + #compdef \(commandName) + + \(completeFunctionName)() { + local -ar non_empty_completions=("${@:#(|:*)}") + local -ar empty_completions=("${(M)@:#(|:*)}") + _describe '' non_empty_completions -- empty_completions -P $'\\'\\'' } - \(initialFunctionName) + \(customCompleteFunctionName)() { + local -a completions + completions=("${(@f)"$("${@}")"}") + if [[ "${#completions[@]}" -gt 1 ]]; then + \(completeFunctionName) "${completions[@]:0:-1}" + fi + } + + \(completionFunctions)\ + \(completionFunctionName()) """ } - static func generateCompletionFunction(_ commands: [ParsableCommand.Type]) - -> String - { - guard let type = commands.last else { return "" } - let functionName = commands.completionFunctionName() - let isRootCommand = commands.count == 1 + private var completionFunctions: String { + guard let type = last else { return "" } + let functionName = completionFunctionName() + let isRootCommand = count == 1 + + let argumentSpecsAndSetupScripts = argumentsForHelp(visibility: .default) + .compactMap { argumentSpecAndSetupScript($0) } + var argumentSpecs = argumentSpecsAndSetupScripts.map(\.argumentSpec) + let setupScripts = argumentSpecsAndSetupScripts.compactMap(\.setupScript) - var args = generateCompletionArguments(commands) var subcommands = type.configuration.subcommands .filter { $0.configuration.shouldDisplay } - var subcommandHandler = "" - if !subcommands.isEmpty { - args.append("'(-): :->command'") - args.append("'(-)*:: :->arg'") - if isRootCommand { - subcommands.append(HelpCommand.self) - } + let subcommandHandler: String + if subcommands.isEmpty { + subcommandHandler = "" + } else { + argumentSpecs.append("'(-): :->command'") + argumentSpecs.append("'(-)*:: :->arg'") - let subcommandModes = subcommands.map { - """ - '\($0._commandName):\($0.configuration.abstract.zshEscaped())' - """ - .indentingEachLine(by: 12) - } - let subcommandArgs = subcommands.map { - """ - (\($0._commandName)) - \(functionName)_\($0._commandName) - ;; - """ - .indentingEachLine(by: 12) + if isRootCommand { + subcommands.addHelpSubcommandIfMissing() } subcommandHandler = """ - case $state in - (command) - local subcommands - subcommands=( - \(subcommandModes.joined(separator: "\n")) + case "${state}" in + command) + local -ar subcommands=( + \( + subcommands.map { """ + '\($0._commandName):\($0.configuration.abstract.zshEscapeForSingleQuotedExplanation())' + """ + } + .joined(separator: "\n") + ) ) _describe "subcommand" subcommands ;; - (arg) - case ${words[1]} in - \(subcommandArgs.joined(separator: "\n")) + arg) + case "${words[1]}" in + \(subcommands.map { $0._commandName }.joined(separator: "|"))) + "\(functionName)_${words[1]}" + ;; esac ;; - esac + esac """ - .indentingEachLine(by: 4) } - let functionText = """ - \(functionName)() {\(isRootCommand ? """ - - export \(CompletionShell.shellEnvironmentVariableName)=zsh - \(CompletionShell.shellVersionEnvironmentVariableName)="$(builtin emulate zsh -c 'printf %s "${ZSH_VERSION}"')" - export \(CompletionShell.shellVersionEnvironmentVariableName) - """ : "") - integer ret=1 - local -a args - args+=( - \(args.joined(separator: "\n").indentingEachLine(by: 8)) + return """ + \(functionName)() { + \(isRootCommand + ? """ + emulate -RL zsh -G + setopt extendedglob + unsetopt aliases banghist + + local -xr \(CompletionShell.shellEnvironmentVariableName)=zsh + local -x \(CompletionShell.shellVersionEnvironmentVariableName) + \(CompletionShell.shellVersionEnvironmentVariableName)="$(builtin emulate zsh -c 'printf %s "${ZSH_VERSION}"')" + local -r \(CompletionShell.shellVersionEnvironmentVariableName) + + local context state state_descr line + local -A opt_args + + local -r command_name="${words[1]}" + local -ar command_line=("${words[@]}") + + + """ + : "" + )\ + local -i ret=1 + \(setupScripts.map { "\($0)\n" }.joined().indentingEachLine(by: 4))\ + local -ar arg_specs=( + \(argumentSpecs.joined(separator: "\n").indentingEachLine(by: 8)) ) - _arguments -w -s -S $args[@] && ret=0 + _arguments -w -s -S : "${arg_specs[@]}" && ret=0 \(subcommandHandler) - return ret + return "${ret}" } - + \(subcommands.map { (self + [$0]).completionFunctions }.joined()) """ - - return functionText - + subcommands - .map { generateCompletionFunction(commands + [$0]) } - .joined() } - static func generateCompletionArguments(_ commands: [ParsableCommand.Type]) - -> [String] - { - commands - .argumentsForHelp(visibility: .default) - .compactMap { $0.zshCompletionString(commands) } - } -} - -extension String { - fileprivate func zshEscapingSingleQuotes() -> String { - self.replacingOccurrences(of: "'", with: #"'"'"'"#) - } - - fileprivate func zshEscapingMetacharacters() -> String { - self.replacingOccurrences( - of: #"[\\\[\]]"#, with: #"\\$0"#, options: .regularExpression) - } - - fileprivate func zshEscaped() -> String { - self.zshEscapingSingleQuotes().zshEscapingMetacharacters() - } - - fileprivate func zshEscapingCommandName() -> String { - self.replacingOccurrences(of: "-", with: "_") - } -} - -extension ArgumentDefinition { - var zshCompletionAbstract: String { - guard !help.abstract.isEmpty else { return "" } - return "[\(help.abstract.zshEscaped())]" - } - - func zshCompletionString(_ commands: [ParsableCommand.Type]) -> String? { - guard help.visibility.base == .default else { return nil } - - let inputs: String - switch update { - case .unary: - inputs = ":\(valueName):\(zshActionString(commands))" - case .nullary: - inputs = "" - } + private func argumentSpecAndSetupScript( + _ arg: ArgumentDefinition + ) -> (argumentSpec: String, setupScript: String?)? { + guard arg.help.visibility.base == .default else { return nil } let line: String - switch names.count { + switch arg.names.count { case 0: line = "" case 1: - let star = isRepeatableOption ? "*" : "" line = """ - \(star)\(names[0].synopsisString)\(zshCompletionAbstract) + \(arg.isRepeatableOption ? "*" : "")\(arg.names[0].synopsisString)\(arg.zshCompletionAbstract) """ default: - let synopses = names.map { $0.synopsisString } - let suppression = - isRepeatableOption ? "*" : "(\(synopses.joined(separator: " ")))" + let synopses = arg.names.map { $0.synopsisString } line = """ - \(suppression)'\ + \(arg.isRepeatableOption ? "*" : "(\(synopses.joined(separator: " ")))")'\ {\(synopses.joined(separator: ","))}\ - '\(zshCompletionAbstract) + '\(arg.zshCompletionAbstract) """ } - return "'\(line)\(inputs)'" - } - - /// - returns: `true` if I'm an option and can be tab-completed multiple times in one command line. For example, `ssh` allows the `-L` option to be given multiple times, to establish multiple port forwardings. - private var isRepeatableOption: Bool { - guard - case .named(_) = kind, - help.options.contains(.isRepeating) - else { return false } - - switch parsingStrategy { - case .default, .unconditional: return true - default: return false + switch arg.update { + case .unary: + let (argumentAction, setupScript) = argumentActionAndSetupScript(arg) + return ("'\(line):\(arg.valueName):\(argumentAction)'", setupScript) + case .nullary: + return ("'\(line)'", nil) } } /// Returns the zsh "action" for an argument completion string. - func zshActionString(_ commands: [ParsableCommand.Type]) -> String { - switch completion.kind { + private func argumentActionAndSetupScript( + _ arg: ArgumentDefinition + ) -> (argumentAction: String, setupScript: String?) { + switch arg.completion.kind { case .default: - return "" + return ("", nil) case .file(let extensions): - let pattern = + return extensions.isEmpty - ? "" - : " -g '\(extensions.map { "*." + $0 }.joined(separator: " "))'" - return "_files\(pattern.zshEscaped())" + ? ("_files", nil) + : ( + "_files -g '\\''\(extensions.map { "*.\($0.zshEscapeForSingleQuotedExplanation())" }.joined(separator: " "))'\\''", + nil + ) case .directory: - return "_files -/" + return ("_files -/", nil) case .list(let list): - return "(" + list.joined(separator: " ") + ")" + let variableName = variableName(arg) + return ( + "{\(completeFunctionName) \"${\(variableName)[@]}\"}", + "local -ar \(variableName)=(\(list.map { "'\($0.shellEscapeForSingleQuotedString())'" }.joined(separator: " ")))" + ) case .shellCommand(let command): - return - "{local -a list; list=(${(f)\"$(\(command))\"}); _describe '''' list}" + return ( + "{local -a list;list=(${(f)\"$(\(command.shellEscapeForSingleQuotedString()))\"});_describe \"\" list}", + nil + ) case .custom: - guard let type = commands.first else { return "" } - // Generate a call back into the command to retrieve a completions list - let commandName = type._commandName.zshEscapingCommandName() + return ( + "{\(customCompleteFunctionName) \"${command_name}\" \(arg.customCompletionCall(self)) \"${command_line[@]}\"}", + nil + ) + } + } + + private func variableName(_ arg: ArgumentDefinition) -> String { + guard let argName = arg.names.preferredName else { return - "{_custom_completion $_\(commandName)_commandname \(customCompletionCall(commands)) $words}" + "\(shellVariableNamePrefix)_\(arg.valueName.shellEscapeForVariableName())" } + return + "\(argName.case == .long ? "__" : "_")\(shellVariableNamePrefix)_\(argName.valueString.shellEscapeForVariableName())" + } + + private var completeFunctionName: String { + // swift-format-ignore: NeverForceUnwrap + // Precondition: first is guaranteed to be non-empty + "__\(first!._commandName)_complete" + } + + private var customCompleteFunctionName: String { + // swift-format-ignore: NeverForceUnwrap + // Precondition: first is guaranteed to be non-empty + "__\(first!._commandName)_custom_complete" + } +} + +extension String { + fileprivate func zshEscapeForSingleQuotedExplanation() -> String { + replacingOccurrences( + of: #"[\\\[\]]"#, + with: #"\\$0"#, + options: .regularExpression + ) + .shellEscapeForSingleQuotedString() + } +} + +extension ArgumentDefinition { + /// - returns: `true` if `self` is an option and can be tab-completed multiple times in one command line. + /// For example, `ssh` allows the `-L` option to be given multiple times, to establish multiple port forwardings. + fileprivate var isRepeatableOption: Bool { + guard + case .named(_) = kind, + help.options.contains(.isRepeating) + else { return false } + + switch parsingStrategy { + case .default, .unconditional: return true + default: return false + } + } + + fileprivate var zshCompletionAbstract: String { + guard !help.abstract.isEmpty else { return "" } + return "[\(help.abstract.zshEscapeForSingleQuotedExplanation())]" } } diff --git a/Sources/ArgumentParser/Documentation.docc/Articles/InstallingCompletionScripts.md b/Sources/ArgumentParser/Documentation.docc/Articles/InstallingCompletionScripts.md index 24265bcaf..e460ed623 100644 --- a/Sources/ArgumentParser/Documentation.docc/Articles/InstallingCompletionScripts.md +++ b/Sources/ArgumentParser/Documentation.docc/Articles/InstallingCompletionScripts.md @@ -1,6 +1,6 @@ # Generating and Installing Completion Scripts -Install shell completion scripts generated by your command-line tool. +Install shell completion scripts generated by your command-line tool. ## Overview @@ -9,13 +9,8 @@ Command-line tools that you build with `ArgumentParser` include a built-in optio ``` $ example --generate-completion-script bash #compdef example -local context state state_descr line -_example_commandname="example" -typeset -A opt_args _example() { - integer ret=1 - local -a args ... } diff --git a/Sources/ArgumentParser/Parsing/CommandParser.swift b/Sources/ArgumentParser/Parsing/CommandParser.swift index de0ce844a..89aca8db5 100644 --- a/Sources/ArgumentParser/Parsing/CommandParser.swift +++ b/Sources/ArgumentParser/Parsing/CommandParser.swift @@ -436,8 +436,11 @@ extension CommandParser { // Parsing and retrieval successful! We don't want to continue with any // other parsing here, so after printing the result of the completion // function, exit with a success code. - let output = completionFunction(completionValues).joined(separator: "\n") - throw ParserError.completionScriptCustomResponse(output) + let completions = completionFunction(completionValues) + throw ParserError.completionScriptCustomResponse( + CompletionShell.requesting?.format(completions: completions) + ?? completions.joined(separator: "\n") + ) } } diff --git a/Sources/ArgumentParserTestHelpers/TestHelpers.swift b/Sources/ArgumentParserTestHelpers/TestHelpers.swift index 8f0dc012f..507ab2de6 100644 --- a/Sources/ArgumentParserTestHelpers/TestHelpers.swift +++ b/Sources/ArgumentParserTestHelpers/TestHelpers.swift @@ -315,14 +315,17 @@ extension XCTest { expected: String? = nil, exitCode: ExitCode = .success, file: StaticString = #filePath, - line: UInt = #line + line: UInt = #line, + environment: [String: String] = [:] ) throws -> String { try AssertExecuteCommand( command: command.split(separator: " ").map(String.init), expected: expected, exitCode: exitCode, file: file, - line: line) + line: line, + environment: environment + ) } // swift-format-ignore: AlwaysUseLowerCamelCase @@ -332,7 +335,8 @@ extension XCTest { expected: String? = nil, exitCode: ExitCode = .success, file: StaticString = #filePath, - line: UInt = #line + line: UInt = #line, + environment: [String: String] = [:] ) throws -> String { #if os(Windows) throw XCTSkip("Unsupported on this platform") @@ -358,6 +362,15 @@ extension XCTest { let error = Pipe() process.standardError = error + if !environment.isEmpty { + if let existingEnvironment = process.environment { + process.environment = + existingEnvironment.merging(environment) { (_, new) in new } + } else { + process.environment = environment + } + } + guard (try? process.run()) != nil else { XCTFail("Couldn't run command process.", file: file, line: line) return "" @@ -366,11 +379,9 @@ extension XCTest { let outputData = output.fileHandleForReading.readDataToEndOfFile() let outputActual = String(data: outputData, encoding: .utf8)! - .trimmingCharacters(in: .whitespacesAndNewlines) let errorData = error.fileHandleForReading.readDataToEndOfFile() let errorActual = String(data: errorData, encoding: .utf8)! - .trimmingCharacters(in: .whitespacesAndNewlines) if let expected = expected { AssertEqualStrings( @@ -394,8 +405,8 @@ extension XCTest { file: StaticString = #filePath, line: UInt = #line ) throws { AssertEqualStrings( - actual: actual.trimmingCharacters(in: .whitespacesAndNewlines), - expected: expected.trimmingCharacters(in: .whitespacesAndNewlines), + actual: actual, + expected: expected, file: file, line: line) @@ -450,7 +461,7 @@ extension XCTest { let expected = try String(contentsOf: snapshotFileURL, encoding: .utf8) AssertEqualStrings( actual: actual, - expected: expected.trimmingCharacters(in: .newlines), + expected: expected, file: file, line: line) return expected diff --git a/Tests/ArgumentParserExampleTests/CountLinesExampleTests.swift b/Tests/ArgumentParserExampleTests/CountLinesExampleTests.swift index eb4758c3f..424b56f27 100644 --- a/Tests/ArgumentParserExampleTests/CountLinesExampleTests.swift +++ b/Tests/ArgumentParserExampleTests/CountLinesExampleTests.swift @@ -26,9 +26,9 @@ final class CountLinesExampleTests: XCTestCase { let testFile = try XCTUnwrap( Bundle.module.url(forResource: "CountLinesTest", withExtension: "txt")) try AssertExecuteCommand( - command: "count-lines \(testFile.path)", expected: "20") + command: "count-lines \(testFile.path)", expected: "20\n") try AssertExecuteCommand( - command: "count-lines \(testFile.path) --prefix al", expected: "4") + command: "count-lines \(testFile.path) --prefix al", expected: "4\n") } func testCountLinesHelp() throws { @@ -44,6 +44,8 @@ final class CountLinesExampleTests: XCTestCase { --prefix Only count lines with this prefix. --verbose Include extra information in the output. -h, --help Show help information. + + """ try AssertExecuteCommand(command: "count-lines -h", expected: helpText) } diff --git a/Tests/ArgumentParserExampleTests/MathExampleTests.swift b/Tests/ArgumentParserExampleTests/MathExampleTests.swift index 12273cb79..aa1fa7df7 100644 --- a/Tests/ArgumentParserExampleTests/MathExampleTests.swift +++ b/Tests/ArgumentParserExampleTests/MathExampleTests.swift @@ -13,6 +13,12 @@ import ArgumentParser import ArgumentParserTestHelpers import XCTest +#if swift(>=6.0) +@testable internal import struct ArgumentParser.CompletionShell +#else +@testable import struct ArgumentParser.CompletionShell +#endif + final class MathExampleTests: XCTestCase { override func setUp() { #if !os(Windows) && !os(WASI) @@ -21,9 +27,9 @@ final class MathExampleTests: XCTestCase { } func testMath_Simple() throws { - try AssertExecuteCommand(command: "math 1 2 3 4 5", expected: "15") + try AssertExecuteCommand(command: "math 1 2 3 4 5", expected: "15\n") try AssertExecuteCommand( - command: "math multiply 1 2 3 4 5", expected: "120") + command: "math multiply 1 2 3 4 5", expected: "120\n") } func testMath_Help() throws { @@ -42,6 +48,7 @@ final class MathExampleTests: XCTestCase { stats Calculate descriptive statistics. See 'math help ' for detailed help. + """ try AssertExecuteCommand(command: "math -h", expected: helpText) @@ -62,6 +69,8 @@ final class MathExampleTests: XCTestCase { -x, --hex-output Use hexadecimal notation for the result. --version Show the version. -h, --help Show help information. + + """ try AssertExecuteCommand(command: "math add -h", expected: helpText) @@ -89,6 +98,8 @@ final class MathExampleTests: XCTestCase { median, mode; default: mean) --version Show the version. -h, --help Show help information. + + """ try AssertExecuteCommand( @@ -117,6 +128,8 @@ final class MathExampleTests: XCTestCase { --custom --version Show the version. -h, --help Show help information. + + """ // The "quantiles" subcommand's run() method is unimplemented, so it @@ -139,6 +152,7 @@ final class MathExampleTests: XCTestCase { Error: Please provide at least one value to calculate the mode. Usage: math stats average [--kind ] [ ...] See 'math stats average --help' for more information. + """, exitCode: .validationFailure) } @@ -146,13 +160,13 @@ final class MathExampleTests: XCTestCase { func testMath_Versions() throws { try AssertExecuteCommand( command: "math --version", - expected: "1.0.0") + expected: "1.0.0\n") try AssertExecuteCommand( command: "math stats --version", - expected: "1.0.0") + expected: "1.0.0\n") try AssertExecuteCommand( command: "math stats average --version", - expected: "1.5.0-alpha") + expected: "1.5.0-alpha\n") } func testMath_ExitCodes() throws { @@ -181,6 +195,7 @@ final class MathExampleTests: XCTestCase { Error: Unknown option '--foo' Usage: math add [--hex-output] [ ...] See 'math add --help' for more information. + """, exitCode: .validationFailure) @@ -191,6 +206,7 @@ final class MathExampleTests: XCTestCase { Help: A group of integers to operate on. Usage: math add [--hex-output] [ ...] See 'math add --help' for more information. + """, exitCode: .validationFailure) } @@ -219,28 +235,54 @@ extension MathExampleTests { try assertSnapshot(actual: script, extension: "fish") } - func testMath_CustomCompletion() throws { + func testMath_BashCustomCompletion() throws { + try testMath_CustomCompletion(forShell: .bash) + } + + func testMath_FishCustomCompletion() throws { + try testMath_CustomCompletion(forShell: .fish) + } + + func testMath_ZshCustomCompletion() throws { + try testMath_CustomCompletion(forShell: .zsh) + } + + private func testMath_CustomCompletion( + forShell shell: CompletionShell + ) throws { try AssertExecuteCommand( command: "math ---completion stats quantiles -- --custom", - expected: """ - hello - helicopter - heliotrope - """) + expected: shell.format(completions: [ + "hello", + "helicopter", + "heliotrope", + ]) + "\n", + environment: [ + CompletionShell.shellEnvironmentVariableName: shell.rawValue + ] + ) try AssertExecuteCommand( command: "math ---completion stats quantiles -- --custom h", - expected: """ - hello - helicopter - heliotrope - """) + expected: shell.format(completions: [ + "hello", + "helicopter", + "heliotrope", + ]) + "\n", + environment: [ + CompletionShell.shellEnvironmentVariableName: shell.rawValue + ] + ) try AssertExecuteCommand( command: "math ---completion stats quantiles -- --custom a", - expected: """ - aardvark - aaaaalbert - """) + expected: shell.format(completions: [ + "aardvark", + "aaaaalbert", + ]) + "\n", + environment: [ + CompletionShell.shellEnvironmentVariableName: shell.rawValue + ] + ) } } diff --git a/Tests/ArgumentParserExampleTests/RepeatExampleTests.swift b/Tests/ArgumentParserExampleTests/RepeatExampleTests.swift index dcf0a74ca..ea6185129 100644 --- a/Tests/ArgumentParserExampleTests/RepeatExampleTests.swift +++ b/Tests/ArgumentParserExampleTests/RepeatExampleTests.swift @@ -25,6 +25,7 @@ final class RepeatExampleTests: XCTestCase { expected: """ hello hello + """) } @@ -34,6 +35,7 @@ final class RepeatExampleTests: XCTestCase { expected: """ 1: hello 2: hello + """) } @@ -47,6 +49,7 @@ final class RepeatExampleTests: XCTestCase { hello hello hello + """) } @@ -61,6 +64,8 @@ final class RepeatExampleTests: XCTestCase { --count The number of times to repeat 'phrase'. --include-counter Include a counter with each repetition. -h, --help Show help information. + + """ try AssertExecuteCommand(command: "repeat -h", expected: helpText) @@ -82,6 +87,8 @@ final class RepeatExampleTests: XCTestCase { --count The number of times to repeat 'phrase'. --include-counter Include a counter with each repetition. -h, --help Show help information. + + """, exitCode: .validationFailure) @@ -92,6 +99,7 @@ final class RepeatExampleTests: XCTestCase { Help: --count The number of times to repeat 'phrase'. Usage: repeat [--count ] [--include-counter] See 'repeat --help' for more information. + """, exitCode: .validationFailure) @@ -102,6 +110,7 @@ final class RepeatExampleTests: XCTestCase { Help: --count The number of times to repeat 'phrase'. Usage: repeat [--count ] [--include-counter] See 'repeat --help' for more information. + """, exitCode: .validationFailure) @@ -111,6 +120,7 @@ final class RepeatExampleTests: XCTestCase { Error: Unknown option '--version' Usage: repeat [--count ] [--include-counter] See 'repeat --help' for more information. + """, exitCode: .validationFailure) } diff --git a/Tests/ArgumentParserExampleTests/RollDiceExampleTests.swift b/Tests/ArgumentParserExampleTests/RollDiceExampleTests.swift index dcfc72c4e..9567cc244 100644 --- a/Tests/ArgumentParserExampleTests/RollDiceExampleTests.swift +++ b/Tests/ArgumentParserExampleTests/RollDiceExampleTests.swift @@ -34,6 +34,8 @@ final class RollDiceExampleTests: XCTestCase { --seed A seed to use for repeatable random generation. -v, --verbose Show all roll results. -h, --help Show help information. + + """ try AssertExecuteCommand(command: "roll -h", expected: helpText) @@ -48,6 +50,7 @@ final class RollDiceExampleTests: XCTestCase { Help: --times Rolls the dice times. Usage: roll [--times ] [--sides ] [--seed ] [--verbose] See 'roll --help' for more information. + """, exitCode: .validationFailure) @@ -58,6 +61,7 @@ final class RollDiceExampleTests: XCTestCase { Help: --times Rolls the dice times. Usage: roll [--times ] [--sides ] [--seed ] [--verbose] See 'roll --help' for more information. + """, exitCode: .validationFailure) } diff --git a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh index ba5b1db74..cf7c3695a 100644 --- a/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh +++ b/Tests/ArgumentParserExampleTests/Snapshots/testMathZshCompletionScript().zsh @@ -1,181 +1,175 @@ #compdef math -local context state state_descr line -_math_commandname=$words[1] -typeset -A opt_args + +__math_complete() { + local -ar non_empty_completions=("${@:#(|:*)}") + local -ar empty_completions=("${(M)@:#(|:*)}") + _describe '' non_empty_completions -- empty_completions -P $'\'\'' +} + +__math_custom_complete() { + local -a completions + completions=("${(@f)"$("${@}")"}") + if [[ "${#completions[@]}" -gt 1 ]]; then + __math_complete "${completions[@]:0:-1}" + fi +} _math() { - export SAP_SHELL=zsh + emulate -RL zsh -G + setopt extendedglob + unsetopt aliases banghist + + local -xr SAP_SHELL=zsh + local -x SAP_SHELL_VERSION SAP_SHELL_VERSION="$(builtin emulate zsh -c 'printf %s "${ZSH_VERSION}"')" - export SAP_SHELL_VERSION - integer ret=1 - local -a args - args+=( + local -r SAP_SHELL_VERSION + + local context state state_descr line + local -A opt_args + + local -r command_name="${words[1]}" + local -ar command_line=("${words[@]}") + + local -i ret=1 + local -ar arg_specs=( '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' '(-): :->command' '(-)*:: :->arg' ) - _arguments -w -s -S $args[@] && ret=0 - case $state in - (command) - local subcommands - subcommands=( - 'add:Print the sum of the values.' - 'multiply:Print the product of the values.' - 'stats:Calculate descriptive statistics.' - 'help:Show subcommand help information.' - ) - _describe "subcommand" subcommands - ;; - (arg) - case ${words[1]} in - (add) - _math_add - ;; - (multiply) - _math_multiply - ;; - (stats) - _math_stats - ;; - (help) - _math_help - ;; - esac + _arguments -w -s -S : "${arg_specs[@]}" && ret=0 + case "${state}" in + command) + local -ar subcommands=( + 'add:Print the sum of the values.' + 'multiply:Print the product of the values.' + 'stats:Calculate descriptive statistics.' + 'help:Show subcommand help information.' + ) + _describe "subcommand" subcommands + ;; + arg) + case "${words[1]}" in + add|multiply|stats|help) + "_math_${words[1]}" ;; + esac + ;; esac - return ret + return "${ret}" } _math_add() { - integer ret=1 - local -a args - args+=( + local -i ret=1 + local -ar arg_specs=( '(--hex-output -x)'{--hex-output,-x}'[Use hexadecimal notation for the result.]' ':values:' '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' ) - _arguments -w -s -S $args[@] && ret=0 + _arguments -w -s -S : "${arg_specs[@]}" && ret=0 - return ret + return "${ret}" } _math_multiply() { - integer ret=1 - local -a args - args+=( + local -i ret=1 + local -ar arg_specs=( '(--hex-output -x)'{--hex-output,-x}'[Use hexadecimal notation for the result.]' ':values:' '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' ) - _arguments -w -s -S $args[@] && ret=0 + _arguments -w -s -S : "${arg_specs[@]}" && ret=0 - return ret + return "${ret}" } _math_stats() { - integer ret=1 - local -a args - args+=( + local -i ret=1 + local -ar arg_specs=( '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' '(-): :->command' '(-)*:: :->arg' ) - _arguments -w -s -S $args[@] && ret=0 - case $state in - (command) - local subcommands - subcommands=( - 'average:Print the average of the values.' - 'stdev:Print the standard deviation of the values.' - 'quantiles:Print the quantiles of the values (TBD).' - ) - _describe "subcommand" subcommands - ;; - (arg) - case ${words[1]} in - (average) - _math_stats_average - ;; - (stdev) - _math_stats_stdev - ;; - (quantiles) - _math_stats_quantiles - ;; - esac + _arguments -w -s -S : "${arg_specs[@]}" && ret=0 + case "${state}" in + command) + local -ar subcommands=( + 'average:Print the average of the values.' + 'stdev:Print the standard deviation of the values.' + 'quantiles:Print the quantiles of the values (TBD).' + ) + _describe "subcommand" subcommands + ;; + arg) + case "${words[1]}" in + average|stdev|quantiles) + "_math_stats_${words[1]}" ;; + esac + ;; esac - return ret + return "${ret}" } _math_stats_average() { - integer ret=1 - local -a args - args+=( - '--kind[The kind of average to provide.]:kind:(mean median mode)' + local -i ret=1 + local -ar __math_stats_average_kind=('mean' 'median' 'mode') + local -ar arg_specs=( + '--kind[The kind of average to provide.]:kind:{__math_complete "${__math_stats_average_kind[@]}"}' ':values:' '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' ) - _arguments -w -s -S $args[@] && ret=0 + _arguments -w -s -S : "${arg_specs[@]}" && ret=0 - return ret + return "${ret}" } _math_stats_stdev() { - integer ret=1 - local -a args - args+=( + local -i ret=1 + local -ar arg_specs=( ':values:' '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' ) - _arguments -w -s -S $args[@] && ret=0 + _arguments -w -s -S : "${arg_specs[@]}" && ret=0 - return ret + return "${ret}" } _math_stats_quantiles() { - integer ret=1 - local -a args - args+=( - ':one-of-four:(alphabet alligator branch braggart)' - ':custom-arg:{_custom_completion $_math_commandname ---completion stats quantiles -- customArg $words}' + local -i ret=1 + local -ar math_stats_quantiles_one_of_four=('alphabet' 'alligator' 'branch' 'braggart') + local -ar arg_specs=( + ':one-of-four:{__math_complete "${math_stats_quantiles_one_of_four[@]}"}' + ':custom-arg:{__math_custom_complete "${command_name}" ---completion stats quantiles -- customArg "${command_line[@]}"}' ':values:' - '--file:file:_files -g '"'"'*.txt *.md'"'"'' + '--file:file:_files -g '\''*.txt *.md'\''' '--directory:directory:_files -/' - '--shell:shell:{local -a list; list=(${(f)"$(head -100 /usr/share/dict/words | tail -50)"}); _describe '''' list}' - '--custom:custom:{_custom_completion $_math_commandname ---completion stats quantiles -- --custom $words}' + '--shell:shell:{local -a list;list=(${(f)"$(head -100 /usr/share/dict/words | tail -50)"});_describe "" list}' + '--custom:custom:{__math_custom_complete "${command_name}" ---completion stats quantiles -- --custom "${command_line[@]}"}' '--version[Show the version.]' '(-h --help)'{-h,--help}'[Show help information.]' ) - _arguments -w -s -S $args[@] && ret=0 + _arguments -w -s -S : "${arg_specs[@]}" && ret=0 - return ret + return "${ret}" } _math_help() { - integer ret=1 - local -a args - args+=( + local -i ret=1 + local -ar arg_specs=( ':subcommands:' '--version[Show the version.]' ) - _arguments -w -s -S $args[@] && ret=0 - - return ret -} - + _arguments -w -s -S : "${arg_specs[@]}" && ret=0 -_custom_completion() { - local completions=("${(@f)$($*)}") - _describe '' completions + return "${ret}" } _math diff --git a/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testColorDoccReference().md b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testColorDoccReference().md index f0c92a1f3..966fd9cdd 100644 --- a/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testColorDoccReference().md +++ b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testColorDoccReference().md @@ -32,3 +32,8 @@ color help [...] ``` **subcommands:** + + + + + diff --git a/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testCountLinesDoccReference().md b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testCountLinesDoccReference().md index d92527216..88b97b538 100644 --- a/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testCountLinesDoccReference().md +++ b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testCountLinesDoccReference().md @@ -35,3 +35,8 @@ count-lines help [...] ``` **subcommands:** + + + + + diff --git a/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testMathDoccReference().md b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testMathDoccReference().md index 762138958..adcaac132 100644 --- a/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testMathDoccReference().md +++ b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testMathDoccReference().md @@ -205,3 +205,8 @@ math help [...] ``` **subcommands:** + + + + + diff --git a/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testRepeatDoccReference().md b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testRepeatDoccReference().md index 5becb0f5c..8d0714b48 100644 --- a/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testRepeatDoccReference().md +++ b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testRepeatDoccReference().md @@ -35,3 +35,8 @@ repeat help [...] ``` **subcommands:** + + + + + diff --git a/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testRollDoccReference().md b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testRollDoccReference().md index 15f2b0bb7..d84e3e2b0 100644 --- a/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testRollDoccReference().md +++ b/Tests/ArgumentParserGenerateDoccReferenceTests/Snapshots/testRollDoccReference().md @@ -42,3 +42,8 @@ roll help [...] ``` **subcommands:** + + + + + diff --git a/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift b/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift index 833623894..83bf347af 100644 --- a/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift +++ b/Tests/ArgumentParserUnitTests/CompletionScriptTests.swift @@ -164,15 +164,15 @@ extension CompletionScriptTests { func assertCustomCompletion( _ arg: String, - shell: String, + shell: CompletionShell, prefix: String = "", file: StaticString = #filePath, line: UInt = #line ) throws { #if !os(Windows) && !os(WASI) do { - setenv("SAP_SHELL", shell, 1) - defer { unsetenv("SAP_SHELL") } + setenv(CompletionShell.shellEnvironmentVariableName, shell.rawValue, 1) + defer { unsetenv(CompletionShell.shellEnvironmentVariableName) } _ = try Custom.parse(["---completion", "--", arg]) XCTFail("Didn't error as expected", file: file, line: line) } catch let error as CommandError { @@ -182,11 +182,11 @@ extension CompletionScriptTests { } AssertEqualStrings( actual: output, - expected: """ - \(prefix)1_\(shell) - \(prefix)2_\(shell) - \(prefix)3_\(shell) - """, + expected: shell.format(completions: [ + "\(prefix)1_\(shell.rawValue)", + "\(prefix)2_\(shell.rawValue)", + "\(prefix)3_\(shell.rawValue)", + ]), file: file, line: line) } @@ -194,7 +194,7 @@ extension CompletionScriptTests { } func assertCustomCompletions( - shell: String, + shell: CompletionShell, file: StaticString = #filePath, line: UInt = #line ) throws { @@ -218,14 +218,14 @@ extension CompletionScriptTests { } func testBashCustomCompletions() throws { - try assertCustomCompletions(shell: "bash") + try assertCustomCompletions(shell: .bash) } func testFishCustomCompletions() throws { - try assertCustomCompletions(shell: "fish") + try assertCustomCompletions(shell: .fish) } func testZshCustomCompletions() throws { - try assertCustomCompletions(shell: "zsh") + try assertCustomCompletions(shell: .zsh) } } diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testADumpHelp().json b/Tests/ArgumentParserUnitTests/Snapshots/testADumpHelp().json index ca23cdfe2..7e180a263 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testADumpHelp().json +++ b/Tests/ArgumentParserUnitTests/Snapshots/testADumpHelp().json @@ -213,4 +213,4 @@ ] }, "serializationVersion" : 0 -} +} \ No newline at end of file diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBDumpHelp().json b/Tests/ArgumentParserUnitTests/Snapshots/testBDumpHelp().json index ceb7b826b..1fe07a4e9 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBDumpHelp().json +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBDumpHelp().json @@ -108,4 +108,4 @@ ] }, "serializationVersion" : 0 -} +} \ No newline at end of file diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash index 64b7a47e8..599a68f4f 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Bash().bash @@ -105,4 +105,4 @@ _base_test_help() { } -complete -F _base_test base-test +complete -F _base_test base-test \ No newline at end of file diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Fish().fish b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Fish().fish index 86510cbab..5636f921a 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Fish().fish +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Fish().fish @@ -58,4 +58,4 @@ complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-comman complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command escaped-command help"' -s h -l help -d 'Show help information.' complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command escaped-command help"' -f -a 'sub-command' -d '' complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command escaped-command help"' -f -a 'escaped-command' -d '' -complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command escaped-command help"' -f -a 'help' -d 'Show subcommand help information.' +complete -c base-test -n '_swift_base-test_using_command "base-test" "sub-command escaped-command help"' -f -a 'help' -d 'Show subcommand help information.' \ No newline at end of file diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh index 36c522302..ef2da963f 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh +++ b/Tests/ArgumentParserUnitTests/Snapshots/testBase_Zsh().zsh @@ -1,101 +1,110 @@ #compdef base-test -local context state state_descr line -_base_test_commandname=$words[1] -typeset -A opt_args + +__base-test_complete() { + local -ar non_empty_completions=("${@:#(|:*)}") + local -ar empty_completions=("${(M)@:#(|:*)}") + _describe '' non_empty_completions -- empty_completions -P $'\'\'' +} + +__base-test_custom_complete() { + local -a completions + completions=("${(@f)"$("${@}")"}") + if [[ "${#completions[@]}" -gt 1 ]]; then + __base-test_complete "${completions[@]:0:-1}" + fi +} _base-test() { - export SAP_SHELL=zsh + emulate -RL zsh -G + setopt extendedglob + unsetopt aliases banghist + + local -xr SAP_SHELL=zsh + local -x SAP_SHELL_VERSION SAP_SHELL_VERSION="$(builtin emulate zsh -c 'printf %s "${ZSH_VERSION}"')" - export SAP_SHELL_VERSION - integer ret=1 - local -a args - args+=( - '--name[The user'"'"'s name.]:name:' - '--kind:kind:(one two custom-three)' - '--other-kind:other-kind:(b1_zsh b2_zsh b3_zsh)' + local -r SAP_SHELL_VERSION + + local context state state_descr line + local -A opt_args + + local -r command_name="${words[1]}" + local -ar command_line=("${words[@]}") + + local -i ret=1 + local -ar __base_test_kind=('one' 'two' 'custom-three') + local -ar __base_test_other_kind=('b1_zsh' 'b2_zsh' 'b3_zsh') + local -ar __base_test_path3=('c1_zsh' 'c2_zsh' 'c3_zsh') + local -ar arg_specs=( + '--name[The user'\''s name.]:name:' + '--kind:kind:{__base-test_complete "${__base_test_kind[@]}"}' + '--other-kind:other-kind:{__base-test_complete "${__base_test_other_kind[@]}"}' '--path1:path1:_files' '--path2:path2:_files' - '--path3:path3:(c1_zsh c2_zsh c3_zsh)' + '--path3:path3:{__base-test_complete "${__base_test_path3[@]}"}' '--one' '--two' '--three' '*--kind-counter' '*--rep1:rep1:' '*'{-r,--rep2}':rep2:' - ':argument:{_custom_completion $_base_test_commandname ---completion -- argument $words}' - ':nested-argument:{_custom_completion $_base_test_commandname ---completion -- nested.nestedArgument $words}' + ':argument:{__base-test_custom_complete "${command_name}" ---completion -- argument "${command_line[@]}"}' + ':nested-argument:{__base-test_custom_complete "${command_name}" ---completion -- nested.nestedArgument "${command_line[@]}"}' '(-h --help)'{-h,--help}'[Show help information.]' '(-): :->command' '(-)*:: :->arg' ) - _arguments -w -s -S $args[@] && ret=0 - case $state in - (command) - local subcommands - subcommands=( - 'sub-command:' - 'escaped-command:' - 'help:Show subcommand help information.' - ) - _describe "subcommand" subcommands - ;; - (arg) - case ${words[1]} in - (sub-command) - _base-test_sub-command - ;; - (escaped-command) - _base-test_escaped-command - ;; - (help) - _base-test_help - ;; - esac + _arguments -w -s -S : "${arg_specs[@]}" && ret=0 + case "${state}" in + command) + local -ar subcommands=( + 'sub-command:' + 'escaped-command:' + 'help:Show subcommand help information.' + ) + _describe "subcommand" subcommands + ;; + arg) + case "${words[1]}" in + sub-command|escaped-command|help) + "_base-test_${words[1]}" ;; + esac + ;; esac - return ret + return "${ret}" } _base-test_sub-command() { - integer ret=1 - local -a args - args+=( + local -i ret=1 + local -ar arg_specs=( '(-h --help)'{-h,--help}'[Show help information.]' ) - _arguments -w -s -S $args[@] && ret=0 + _arguments -w -s -S : "${arg_specs[@]}" && ret=0 - return ret + return "${ret}" } _base-test_escaped-command() { - integer ret=1 - local -a args - args+=( - '--one[Escaped chars: '"'"'\[\]\\.]:one:' - ':two:{_custom_completion $_base_test_commandname ---completion escaped-command -- two $words}' + local -i ret=1 + local -ar arg_specs=( + '--one[Escaped chars: '\''\[\]\\.]:one:' + ':two:{__base-test_custom_complete "${command_name}" ---completion escaped-command -- two "${command_line[@]}"}' '(-h --help)'{-h,--help}'[Show help information.]' ) - _arguments -w -s -S $args[@] && ret=0 + _arguments -w -s -S : "${arg_specs[@]}" && ret=0 - return ret + return "${ret}" } _base-test_help() { - integer ret=1 - local -a args - args+=( + local -i ret=1 + local -ar arg_specs=( ':subcommands:' ) - _arguments -w -s -S $args[@] && ret=0 - - return ret -} - + _arguments -w -s -S : "${arg_specs[@]}" && ret=0 -_custom_completion() { - local completions=("${(@f)$($*)}") - _describe '' completions + return "${ret}" } -_base-test +_base-test \ No newline at end of file diff --git a/Tests/ArgumentParserUnitTests/Snapshots/testCDumpHelp().json b/Tests/ArgumentParserUnitTests/Snapshots/testCDumpHelp().json index 5fa599a65..2e115c010 100644 --- a/Tests/ArgumentParserUnitTests/Snapshots/testCDumpHelp().json +++ b/Tests/ArgumentParserUnitTests/Snapshots/testCDumpHelp().json @@ -239,4 +239,4 @@ ] }, "serializationVersion" : 0 -} +} \ No newline at end of file