diff --git a/Package@swift-5.6.swift b/Package@swift-5.6.swift index 9072fabb7..b70ee3a04 100644 --- a/Package@swift-5.6.swift +++ b/Package@swift-5.6.swift @@ -19,8 +19,8 @@ var package = Package( name: "ArgumentParser", targets: ["ArgumentParser"]), .plugin( - name: "GenerateManualPlugin", - targets: ["Generate Manual"]), + name: "GenerateManual", + targets: ["GenerateManual"]), ], dependencies: [], targets: [ @@ -40,13 +40,12 @@ var package = Package( // Plugins .plugin( - name: "Generate Manual", + name: "GenerateManual", capability: .command( intent: .custom( verb: "generate-manual", description: "Generate a manual entry for a specified target.")), - dependencies: ["generate-manual"], - path: "Plugins/GenerateManualPlugin"), + dependencies: ["generate-manual"]), // Examples .executableTarget( diff --git a/Plugins/GenerateManualPlugin/GenerateManualPlugin.swift b/Plugins/GenerateManual/GenerateManualPlugin.swift similarity index 100% rename from Plugins/GenerateManualPlugin/GenerateManualPlugin.swift rename to Plugins/GenerateManual/GenerateManualPlugin.swift diff --git a/Plugins/GenerateManualPlugin/GenerateManualPluginError.swift b/Plugins/GenerateManual/GenerateManualPluginError.swift similarity index 100% rename from Plugins/GenerateManualPlugin/GenerateManualPluginError.swift rename to Plugins/GenerateManual/GenerateManualPluginError.swift diff --git a/Plugins/GenerateManualPlugin/PackagePlugin+Helpers.swift b/Plugins/GenerateManual/PackagePlugin+Helpers.swift similarity index 100% rename from Plugins/GenerateManualPlugin/PackagePlugin+Helpers.swift rename to Plugins/GenerateManual/PackagePlugin+Helpers.swift diff --git a/Sources/ArgumentParser/CMakeLists.txt b/Sources/ArgumentParser/CMakeLists.txt index 557873c78..c4f872e87 100644 --- a/Sources/ArgumentParser/CMakeLists.txt +++ b/Sources/ArgumentParser/CMakeLists.txt @@ -37,6 +37,7 @@ add_library(ArgumentParser Usage/DumpHelpGenerator.swift Usage/HelpCommand.swift Usage/HelpGenerator.swift + Usage/HelpOptions.swift Usage/MessageInfo.swift Usage/UsageGenerator.swift diff --git a/Sources/ArgumentParser/Documentation.docc/Articles/CustomizingCommandHelp.md b/Sources/ArgumentParser/Documentation.docc/Articles/CustomizingCommandHelp.md index fb847d8c7..f8b6afbb8 100644 --- a/Sources/ArgumentParser/Documentation.docc/Articles/CustomizingCommandHelp.md +++ b/Sources/ArgumentParser/Documentation.docc/Articles/CustomizingCommandHelp.md @@ -1,6 +1,6 @@ # Customizing Help for Commands -Define your command's abstract, extended discussion, or usage string, and set the flags used to invoke the help display. +Define your command's abstract, discussion, or usage string, and set the flags used to invoke the help display. ## Overview diff --git a/Sources/ArgumentParser/Parsable Properties/ArgumentHelp.swift b/Sources/ArgumentParser/Parsable Properties/ArgumentHelp.swift index b4b5643cb..d59404585 100644 --- a/Sources/ArgumentParser/Parsable Properties/ArgumentHelp.swift +++ b/Sources/ArgumentParser/Parsable Properties/ArgumentHelp.swift @@ -16,7 +16,13 @@ public struct ArgumentHelp { /// An expanded description of the argument, in plain text form. public var discussion: String = "" - + + /// Additional detailed description of the argument, in plain text form. + /// + /// This discussion is shown in the detailed help display and supplemental + /// content such as generated manuals. + public var detailedDiscussion: String = "" + /// An alternative name to use for the argument's value when showing usage /// information. /// @@ -28,41 +34,17 @@ public struct ArgumentHelp { /// the extended help display. public var visibility: ArgumentVisibility = .default - /// A Boolean value indicating whether this argument should be shown in - /// the extended help display. - @available(*, deprecated, message: "Use visibility level instead.") - public var shouldDisplay: Bool { - get { - return visibility.base == .default - } - set { - visibility = newValue ? .default : .hidden - } - } - - /// Creates a new help instance. - @available(*, deprecated, message: "Use init(_:discussion:valueName:visibility:) instead.") - public init( - _ abstract: String = "", - discussion: String = "", - valueName: String? = nil, - shouldDisplay: Bool) - { - self.abstract = abstract - self.discussion = discussion - self.valueName = valueName - self.shouldDisplay = shouldDisplay - } - /// Creates a new help instance. public init( _ abstract: String = "", discussion: String = "", + detailedDiscussion: String = "", valueName: String? = nil, visibility: ArgumentVisibility = .default) { self.abstract = abstract self.discussion = discussion + self.detailedDiscussion = detailedDiscussion self.valueName = valueName self.visibility = visibility } @@ -83,3 +65,34 @@ extension ArgumentHelp: ExpressibleByStringInterpolation { self.abstract = value } } + +// MARK: - Deprecated API +extension ArgumentHelp { + /// A Boolean value indicating whether this argument should be shown in + /// the extended help display. + @available(*, deprecated, message: "Use visibility level instead.") + public var shouldDisplay: Bool { + get { + return visibility.base == .default + } + set { + visibility = newValue ? .default : .hidden + } + } + + /// Creates a new help instance. + @available(*, deprecated, message: "Use init(_:discussion:detailedDiscussion:valueName:visibility:) instead.") + public init( + _ abstract: String = "", + discussion: String = "", + valueName: String? = nil, + shouldDisplay: Bool) + { + self.init( + abstract, + discussion: discussion, + detailedDiscussion: "", + valueName: valueName, + visibility: shouldDisplay ? .default : .hidden) + } +} diff --git a/Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift b/Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift index 5c6614fc3..0c23a34da 100644 --- a/Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift +++ b/Sources/ArgumentParser/Parsable Types/CommandConfiguration.swift @@ -38,6 +38,10 @@ public struct CommandConfiguration { /// A longer description of this command, to be shown in the extended help /// display. public var discussion: String + + /// Additional detailed description of the argument to be shown in the + /// detailed help display and supplemental content such as generated manuals. + public var detailedDiscussion: String /// Version information for this command. public var version: String @@ -67,6 +71,9 @@ public struct CommandConfiguration { /// automatically generating a usage description. Passing an empty string /// hides the usage string altogether. /// - discussion: A longer description of the command. + /// - detailedDiscussion: Additional detailed description to be shown in the + /// detailed help display and supplemental content such as generated + /// manuals. /// - version: The version number for this command. When you provide a /// non-empty string, the argument parser prints it if the user provides /// a `--version` flag. @@ -85,6 +92,7 @@ public struct CommandConfiguration { abstract: String = "", usage: String? = nil, discussion: String = "", + detailedDiscussion: String = "", version: String = "", shouldDisplay: Bool = true, subcommands: [ParsableCommand.Type] = [], @@ -95,6 +103,7 @@ public struct CommandConfiguration { self.abstract = abstract self.usage = usage self.discussion = discussion + self.detailedDiscussion = detailedDiscussion self.version = version self.shouldDisplay = shouldDisplay self.subcommands = subcommands @@ -110,6 +119,7 @@ public struct CommandConfiguration { abstract: String = "", usage: String? = nil, discussion: String = "", + detailedDiscussion: String = "", version: String = "", shouldDisplay: Bool = true, subcommands: [ParsableCommand.Type] = [], @@ -121,6 +131,7 @@ public struct CommandConfiguration { self.abstract = abstract self.usage = usage self.discussion = discussion + self.detailedDiscussion = "" self.version = version self.shouldDisplay = shouldDisplay self.subcommands = subcommands @@ -130,7 +141,7 @@ public struct CommandConfiguration { } extension CommandConfiguration { - @available(*, deprecated, message: "Use the memberwise initializer with the usage parameter.") + @available(*, deprecated, message: "Use the member-wise initializer with the usage parameter.") public init( commandName: String?, abstract: String, @@ -146,6 +157,32 @@ extension CommandConfiguration { abstract: abstract, usage: "", discussion: discussion, + detailedDiscussion: "", + version: version, + shouldDisplay: shouldDisplay, + subcommands: subcommands, + defaultSubcommand: defaultSubcommand, + helpNames: helpNames) + } + + @available(*, deprecated, message: "Use the member-wise initializer with the detailedDiscussion parameter.") + public init( + commandName: String?, + abstract: String, + usage: String, + discussion: String, + version: String, + shouldDisplay: Bool, + subcommands: [ParsableCommand.Type], + defaultSubcommand: ParsableCommand.Type?, + helpNames: NameSpecification? + ) { + self.init( + commandName: commandName, + abstract: abstract, + usage: usage, + discussion: discussion, + detailedDiscussion: "", version: version, shouldDisplay: shouldDisplay, subcommands: subcommands, diff --git a/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift b/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift index b2e572a6e..b45543446 100644 --- a/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift +++ b/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift @@ -75,7 +75,7 @@ extension ParsableArguments { // Parse the command and unwrap the result if necessary. switch try self.asCommand.parseAsRoot(arguments) { case let helpCommand as HelpCommand: - throw ParserError.helpRequested(visibility: helpCommand.visibility) + throw ParserError.helpRequested(options: helpCommand.options) case let result as _WrappedParsableCommand: return result.options case var result as Self: @@ -142,7 +142,11 @@ extension ParsableArguments { includeHidden: Bool = false, columns: Int? = nil ) -> String { - HelpGenerator(self, visibility: includeHidden ? .hidden : .default) + HelpGenerator( + self, + options: .init( + visibility: includeHidden ? .hidden : .default, + detailed: false)) .rendered(screenWidth: columns) } diff --git a/Sources/ArgumentParser/Parsable Types/ParsableCommand.swift b/Sources/ArgumentParser/Parsable Types/ParsableCommand.swift index a69f268d2..889a122b7 100644 --- a/Sources/ArgumentParser/Parsable Types/ParsableCommand.swift +++ b/Sources/ArgumentParser/Parsable Types/ParsableCommand.swift @@ -109,7 +109,9 @@ extension ParsableCommand { ) -> String { HelpGenerator( commandStack: CommandParser(self).commandStack(for: subcommand), - visibility: includeHidden ? .hidden : .default) + options: .init( + visibility: includeHidden ? .hidden : .default, + detailed: false)) .rendered(screenWidth: columns) } diff --git a/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift b/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift index bdbda9a26..014977596 100644 --- a/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift +++ b/Sources/ArgumentParser/Parsing/ArgumentDefinition.swift @@ -52,6 +52,7 @@ struct ArgumentDefinition { var isComposite: Bool var abstract: String var discussion: String + var detailedDiscussion: String var valueName: String var visibility: ArgumentVisibility var parentTitle: String @@ -71,6 +72,7 @@ struct ArgumentDefinition { self.isComposite = isComposite self.abstract = help?.abstract ?? "" self.discussion = help?.discussion ?? "" + self.detailedDiscussion = help?.detailedDiscussion ?? "" self.valueName = help?.valueName ?? "" self.visibility = help?.visibility ?? .default self.parentTitle = "" diff --git a/Sources/ArgumentParser/Parsing/CommandParser.swift b/Sources/ArgumentParser/Parsing/CommandParser.swift index ee327c41f..a446c99a9 100644 --- a/Sources/ArgumentParser/Parsing/CommandParser.swift +++ b/Sources/ArgumentParser/Parsing/CommandParser.swift @@ -15,7 +15,7 @@ struct CommandError: Error { } struct HelpRequested: Error { - var visibility: ArgumentVisibility + var options: HelpOptions } struct CommandParser { @@ -85,15 +85,16 @@ extension CommandParser { requireSoloArgument: Bool = false ) throws { guard !requireSoloArgument || split.count == 1 else { return } - - // Look for help flags - guard !split.contains(anyOf: self.commandStack.getHelpNames(visibility: .default)) else { - throw HelpRequested(visibility: .default) - } - // Look for help-hidden flags - guard !split.contains(anyOf: self.commandStack.getHelpNames(visibility: .hidden)) else { - throw HelpRequested(visibility: .hidden) + // Search for various help flags [.default, .hidden] x [standard, detailed] + for visibility in [ArgumentVisibility.default, .hidden] { + for detailed in [false, true] { + let options = HelpOptions(visibility: visibility, detailed: detailed) + let helpNames = self.commandStack.getHelpNames(options: options) + if split.contains(anyOf: helpNames) { + throw HelpRequested(options: options) + } + } } // Look for dump-help flag @@ -254,7 +255,7 @@ extension CommandParser { } catch let helpRequest as HelpRequested { return .success(HelpCommand( commandStack: commandStack, - visibility: helpRequest.visibility)) + options: helpRequest.options)) } catch { return .failure(CommandError(commandStack: commandStack, parserError: .invalidState)) } diff --git a/Sources/ArgumentParser/Parsing/ParserError.swift b/Sources/ArgumentParser/Parsing/ParserError.swift index ed104983b..1667bf9e8 100644 --- a/Sources/ArgumentParser/Parsing/ParserError.swift +++ b/Sources/ArgumentParser/Parsing/ParserError.swift @@ -11,7 +11,7 @@ /// Gets thrown while parsing and will be handled by the error output generation. enum ParserError: Error { - case helpRequested(visibility: ArgumentVisibility) + case helpRequested(options: HelpOptions) case versionRequested case dumpHelpRequested diff --git a/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift b/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift index 27c301888..98e0133b9 100644 --- a/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift +++ b/Sources/ArgumentParser/Usage/DumpHelpGenerator.swift @@ -114,6 +114,7 @@ fileprivate extension CommandInfoV0 { commandName: command._commandName, abstract: command.configuration.abstract, discussion: command.configuration.discussion, + detailedDiscussion: command.configuration.detailedDiscussion, defaultSubcommand: defaultSubcommand, subcommands: subcommands, arguments: arguments) @@ -135,7 +136,8 @@ fileprivate extension ArgumentInfoV0 { defaultValue: argument.help.defaultValue, allValues: argument.help.allValues, abstract: argument.help.abstract, - discussion: argument.help.discussion) + discussion: argument.help.discussion, + detailedDiscussion: argument.help.detailedDiscussion) } } diff --git a/Sources/ArgumentParser/Usage/HelpCommand.swift b/Sources/ArgumentParser/Usage/HelpCommand.swift index c79d37983..cfbcb1e43 100644 --- a/Sources/ArgumentParser/Usage/HelpCommand.swift +++ b/Sources/ArgumentParser/Usage/HelpCommand.swift @@ -10,6 +10,11 @@ //===----------------------------------------------------------------------===// struct HelpCommand: ParsableCommand { + enum CodingKeys: CodingKey { + case subcommands + case help + } + static var configuration = CommandConfiguration( commandName: "help", abstract: "Show subcommand help information.", @@ -23,16 +28,32 @@ struct HelpCommand: ParsableCommand { var help = false private(set) var commandStack: [ParsableCommand.Type] = [] - private(set) var visibility: ArgumentVisibility = .default + private(set) var options = HelpOptions.plain - init() {} + init() { } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.subcommands = try container.decode([String].self, forKey: .subcommands) + self.help = try container.decode(Bool.self, forKey: .help) + } + init( + commandStack: [ParsableCommand.Type], + options: HelpOptions + ) { + self.commandStack = commandStack + self.options = options + self.subcommands = commandStack.map { $0._commandName } + self.help = false + } + mutating func run() throws { throw CommandError( commandStack: commandStack, - parserError: .helpRequested(visibility: visibility)) + parserError: .helpRequested(options: options)) } - + mutating func buildCommandStack(with parser: CommandParser) throws { commandStack = parser.commandStack(for: subcommands) } @@ -41,25 +62,7 @@ struct HelpCommand: ParsableCommand { func generateHelp(screenWidth: Int) -> String { HelpGenerator( commandStack: commandStack, - visibility: visibility) + options: options) .rendered(screenWidth: screenWidth) } - - enum CodingKeys: CodingKey { - case subcommands - case help - } - - init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.subcommands = try container.decode([String].self, forKey: .subcommands) - self.help = try container.decode(Bool.self, forKey: .help) - } - - init(commandStack: [ParsableCommand.Type], visibility: ArgumentVisibility) { - self.commandStack = commandStack - self.visibility = visibility - self.subcommands = commandStack.map { $0._commandName } - self.help = false - } } diff --git a/Sources/ArgumentParser/Usage/HelpGenerator.swift b/Sources/ArgumentParser/Usage/HelpGenerator.swift index 7bc6020c6..002ece52f 100644 --- a/Sources/ArgumentParser/Usage/HelpGenerator.swift +++ b/Sources/ArgumentParser/Usage/HelpGenerator.swift @@ -17,8 +17,9 @@ internal struct HelpGenerator { struct Section { struct Element: Hashable { var label: String - var abstract: String = "" - var discussion: String = "" + var abstract: String + var discussion: String + var detailedDiscussion: String var paddedLabel: String { String(repeating: " ", count: HelpGenerator.helpIndent) + label @@ -28,9 +29,12 @@ internal struct HelpGenerator { let paddedLabel = self.paddedLabel let wrappedAbstract = self.abstract .wrapped(to: screenWidth, wrappingIndent: HelpGenerator.labelColumnWidth) - let wrappedDiscussion = self.discussion.isEmpty - ? "" - : self.discussion.wrapped(to: screenWidth, wrappingIndent: HelpGenerator.helpIndent * 4) + "\n" + let wrappedDiscussion = self.discussion + .nonEmpty? + .wrapped(to: screenWidth, wrappingIndent: HelpGenerator.helpIndent * 4) + let wrappedDetailedDiscussion = self.detailedDiscussion + .nonEmpty? + .wrapped(to: screenWidth, wrappingIndent: HelpGenerator.helpIndent * 4) let renderedAbstract: String = { guard !abstract.isEmpty else { return "" } if paddedLabel.count < HelpGenerator.labelColumnWidth { @@ -41,9 +45,15 @@ internal struct HelpGenerator { return "\n" + wrappedAbstract } }() - return paddedLabel - + renderedAbstract + "\n" - + wrappedDiscussion + let labelAndAbstract = paddedLabel + renderedAbstract + + return [ + labelAndAbstract, + wrappedDiscussion, + wrappedDetailedDiscussion, + ""] // Included so the rendered section always ends with a newline. + .compactMap { $0 } + .joined(separator: "\n") } } @@ -52,7 +62,8 @@ internal struct HelpGenerator { case subcommands case options case title(String) - + case detailedDiscussion + var description: String { switch self { case .positionalArguments: @@ -63,6 +74,8 @@ internal struct HelpGenerator { return "Options" case .title(let name): return name + case .detailedDiscussion: + return "Discussion" } } } @@ -92,12 +105,13 @@ internal struct HelpGenerator { var sections: [Section] var discussionSections: [DiscussionSection] - init(commandStack: [ParsableCommand.Type], visibility: ArgumentVisibility) { + init(commandStack: [ParsableCommand.Type], options: HelpOptions) { guard let currentCommand = commandStack.last else { fatalError() } - let currentArgSet = ArgumentSet(currentCommand, visibility: visibility, parent: .root) + let currentArgSet = ArgumentSet( + currentCommand, visibility: options.visibility, parent: .root) self.commandStack = commandStack // Build the tool name and subcommand name from the command configuration @@ -126,15 +140,16 @@ internal struct HelpGenerator { self.abstract += "\n\(currentCommand.configuration.discussion)" } - self.sections = HelpGenerator.generateSections(commandStack: commandStack, visibility: visibility) + self.sections = HelpGenerator.generateSections( + commandStack: commandStack, options: options) self.discussionSections = [] } - init(_ type: ParsableArguments.Type, visibility: ArgumentVisibility) { - self.init(commandStack: [type.asCommand], visibility: visibility) + init(_ type: ParsableArguments.Type, options: HelpOptions) { + self.init(commandStack: [type.asCommand], options: options) } - private static func generateSections(commandStack: [ParsableCommand.Type], visibility: ArgumentVisibility) -> [Section] { + private static func generateSections(commandStack: [ParsableCommand.Type], options: HelpOptions) -> [Section] { guard !commandStack.isEmpty else { return [] } var positionalElements: [Section.Element] = [] @@ -146,9 +161,10 @@ internal struct HelpGenerator { /// Start with a full slice of the ArgumentSet so we can peel off one or /// more elements at a time. - var args = commandStack.argumentsForHelp(visibility: visibility)[...] + var args = commandStack.argumentsForHelp( + visibility: options.visibility)[...] while let arg = args.popFirst() { - assert(arg.help.visibility.isAtLeastAsVisible(as: visibility)) + assert(arg.help.visibility.isAtLeastAsVisible(as: options.visibility)) let synopsis: String let description: String @@ -156,7 +172,9 @@ internal struct HelpGenerator { if arg.help.isComposite { // If this argument is composite, we have a group of arguments to // output together. - let groupEnd = args.firstIndex(where: { $0.help.keys != arg.help.keys }) ?? args.endIndex + let groupEnd = args + .firstIndex { $0.help.keys != arg.help.keys } + ?? args.endIndex let groupedArgs = [arg] + args[.. [Name] { + /// If a non-default visibility is used the long names (both single and + /// double dash) are appended with the name of the visibility level. + /// + /// If a detailed is used the long names (both single and double dash) are + /// appended with "detailed". + /// + /// After the optional name modification step, the name are returned in + /// descending order. + func generateHelpNames( + options: HelpOptions + ) -> [Name] { self .makeNames(InputKey(name: "help", parent: .root)) .compactMap { name in - guard visibility.base != .default else { return name } + let suffix: String + switch (options.visibility != .default, options.detailed) { + case (true, true): + suffix = "\(options.visibility.base)-detailed" + case (true, false): + suffix = "\(options.visibility.base)" + case (false, true): + suffix = "detailed" + case (false, false): + // If default visibility or not detailed help, then return the names + // unmodified. + return name + } + switch name { case .long(let helpName): - return .long("\(helpName)-\(visibility.base)") + return .long("\(helpName)-\(suffix)") case .longWithSingleDash(let helpName): - return .longWithSingleDash("\(helpName)-\(visibility)") + return .longWithSingleDash("\(helpName)-\(suffix)") case .short: // Cannot create a non-default help flag from a short name. return nil @@ -313,14 +361,15 @@ internal extension BidirectionalCollection where Element == ParsableCommand.Type /// Returns a list of help names at the request visibility level for the top /// most ParsableCommand in the command stack with custom helpNames. If the /// command stack contains no custom help names the default help names. - func getHelpNames(visibility: ArgumentVisibility) -> [Name] { - self.last(where: { $0.configuration.helpNames != nil }) - .map { $0.configuration.helpNames!.generateHelpNames(visibility: visibility) } - ?? CommandConfiguration.defaultHelpNames.generateHelpNames(visibility: visibility) + func getHelpNames(options: HelpOptions) -> [Name] { + self + .last { $0.configuration.helpNames != nil } + .map { $0.configuration.helpNames!.generateHelpNames(options: options) } + ?? CommandConfiguration.defaultHelpNames.generateHelpNames(options: options) } func getPrimaryHelpName() -> Name? { - getHelpNames(visibility: .default).preferredName + getHelpNames(options: .plain).preferredName } func versionArgumentDefinition() -> ArgumentDefinition? { @@ -341,7 +390,7 @@ internal extension BidirectionalCollection where Element == ParsableCommand.Type } func helpArgumentDefinition() -> ArgumentDefinition? { - let names = getHelpNames(visibility: .default) + let names = getHelpNames(options: .plain) guard !names.isEmpty else { return nil } return ArgumentDefinition( kind: .named(names), diff --git a/Sources/ArgumentParser/Usage/HelpOptions.swift b/Sources/ArgumentParser/Usage/HelpOptions.swift new file mode 100644 index 000000000..bd56de5fa --- /dev/null +++ b/Sources/ArgumentParser/Usage/HelpOptions.swift @@ -0,0 +1,19 @@ +//===----------------------------------------------------------*- swift -*-===// +// +// This source file is part of the Swift Argument Parser open source project +// +// Copyright (c) 2020 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +internal struct HelpOptions { + internal var visibility: ArgumentVisibility + internal var detailed: Bool +} + +extension HelpOptions { + internal static let plain = HelpOptions(visibility: .default, detailed: false) +} diff --git a/Sources/ArgumentParser/Usage/MessageInfo.swift b/Sources/ArgumentParser/Usage/MessageInfo.swift index 2e95c96a9..995e411a1 100644 --- a/Sources/ArgumentParser/Usage/MessageInfo.swift +++ b/Sources/ArgumentParser/Usage/MessageInfo.swift @@ -27,8 +27,8 @@ enum MessageInfo { // Exit early on built-in requests switch e.parserError { - case .helpRequested(let visibility): - self = .help(text: HelpGenerator(commandStack: e.commandStack, visibility: visibility).rendered()) + case .helpRequested(let options): + self = .help(text: HelpGenerator(commandStack: e.commandStack, options: options).rendered()) return case .dumpHelpRequested: @@ -73,8 +73,9 @@ enum MessageInfo { parserError = .userValidationError(error) } - var usage = HelpGenerator(commandStack: commandStack, visibility: .default).usageMessage() - + var usage = HelpGenerator( + commandStack: commandStack, options: .plain).usageMessage() + let commandNames = commandStack.map { $0._commandName }.joined(separator: " ") if let helpName = commandStack.getPrimaryHelpName() { if !usage.isEmpty { @@ -96,7 +97,8 @@ enum MessageInfo { if let command = command { commandStack = CommandParser(type.asCommand).commandStack(for: command) } - self = .help(text: HelpGenerator(commandStack: commandStack, visibility: .default).rendered()) + self = .help(text: HelpGenerator( + commandStack: commandStack, options: .plain).rendered()) case .dumpRequest(let command): if let command = command { commandStack = CommandParser(type.asCommand).commandStack(for: command) @@ -119,7 +121,8 @@ enum MessageInfo { } else if let parserError = parserError { let usage: String = { guard case ParserError.noArguments = parserError else { return usage } - return "\n" + HelpGenerator(commandStack: [type.asCommand], visibility: .default).rendered() + return "\n" + HelpGenerator( + commandStack: [type.asCommand], options: .plain).rendered() }() let argumentSet = ArgumentSet(commandStack.last!, visibility: .default, parent: .root) let message = argumentSet.errorDescription(error: parserError) ?? "" diff --git a/Sources/ArgumentParserToolInfo/ToolInfo.swift b/Sources/ArgumentParserToolInfo/ToolInfo.swift index daecd28d4..47d44a368 100644 --- a/Sources/ArgumentParserToolInfo/ToolInfo.swift +++ b/Sources/ArgumentParserToolInfo/ToolInfo.swift @@ -51,6 +51,9 @@ public struct CommandInfoV0: Codable, Hashable { public var abstract: String? /// Extended description of the command's functionality. public var discussion: String? + /// Additional description of the command's functionality for supplemental + /// content. + public var detailedDiscussion: String? /// Optional name of the subcommand invoked when the command is invoked with /// no arguments. @@ -65,6 +68,7 @@ public struct CommandInfoV0: Codable, Hashable { commandName: String, abstract: String, discussion: String, + detailedDiscussion: String, defaultSubcommand: String?, subcommands: [CommandInfoV0], arguments: [ArgumentInfoV0] @@ -74,6 +78,7 @@ public struct CommandInfoV0: Codable, Hashable { self.commandName = commandName self.abstract = abstract.nonEmpty self.discussion = discussion.nonEmpty + self.detailedDiscussion = detailedDiscussion.nonEmpty self.defaultSubcommand = defaultSubcommand?.nonEmpty self.subcommands = subcommands.nonEmpty @@ -146,6 +151,11 @@ public struct ArgumentInfoV0: Codable, Hashable { public var abstract: String? /// Extended description of the argument's functionality. public var discussion: String? + /// Additional detailed description of the argument's functionality. + /// + /// This discussion is shown in the detailed help display and supplemental + /// content such as generated manuals. + public var detailedDiscussion: String? public init( kind: KindV0, @@ -159,7 +169,8 @@ public struct ArgumentInfoV0: Codable, Hashable { defaultValue: String?, allValues: [String]?, abstract: String?, - discussion: String? + discussion: String?, + detailedDiscussion: String? ) { self.kind = kind @@ -178,5 +189,6 @@ public struct ArgumentInfoV0: Codable, Hashable { self.abstract = abstract?.nonEmpty self.discussion = discussion?.nonEmpty + self.detailedDiscussion = detailedDiscussion?.nonEmpty } } diff --git a/Tests/ArgumentParserPackageManagerTests/HelpTests.swift b/Tests/ArgumentParserPackageManagerTests/HelpTests.swift index dd6ad2d7c..39a17fd5e 100644 --- a/Tests/ArgumentParserPackageManagerTests/HelpTests.swift +++ b/Tests/ArgumentParserPackageManagerTests/HelpTests.swift @@ -196,9 +196,9 @@ struct CustomHelp: ParsableCommand { extension HelpTests { func testCustomHelpNames() { - let helpNames = [CustomHelp.self].getHelpNames(visibility: .default) + let helpNames = [CustomHelp.self].getHelpNames(options: .plain) XCTAssertEqual(helpNames, [.short("?"), .long("show-help")]) - let helpHiddenNames = [CustomHelp.self].getHelpNames(visibility: .hidden) + let helpHiddenNames = [CustomHelp.self].getHelpNames(options: .init(visibility: .hidden, detailed: false)) XCTAssertEqual(helpHiddenNames, [.long("show-help-hidden")]) AssertFullErrorMessage(CustomHelp.self, ["--error"], """ @@ -219,9 +219,9 @@ struct NoHelp: ParsableCommand { extension HelpTests { func testNoHelpNames() { - let helpNames = [NoHelp.self].getHelpNames(visibility: .default) + let helpNames = [NoHelp.self].getHelpNames(options: .plain) XCTAssertEqual(helpNames, []) - let helpHiddenNames = [NoHelp.self].getHelpNames(visibility: .hidden) + let helpHiddenNames = [NoHelp.self].getHelpNames(options: .init(visibility: .hidden, detailed: false)) XCTAssertEqual(helpHiddenNames, []) AssertFullErrorMessage(NoHelp.self, ["--error"], """ @@ -267,7 +267,7 @@ extension HelpTests { let names = [ SubCommandCustomHelp.self, SubCommandCustomHelp.InheritHelp.self, - ].getHelpNames(visibility: .default) + ].getHelpNames(options: .plain) XCTAssertEqual(names, [.short("p"), .long("parent-help")]) } @@ -275,7 +275,7 @@ extension HelpTests { let names = [ SubCommandCustomHelp.self, SubCommandCustomHelp.ModifiedHelp.self - ].getHelpNames(visibility: .default) + ].getHelpNames(options: .plain) XCTAssertEqual(names, [.short("s"), .long("subcommand-help")]) } @@ -284,7 +284,7 @@ extension HelpTests { SubCommandCustomHelp.self, SubCommandCustomHelp.ModifiedHelp.self, SubCommandCustomHelp.ModifiedHelp.InheritImmediateParentdHelp.self - ].getHelpNames(visibility: .default) + ].getHelpNames(options: .plain) XCTAssertEqual(names, [.short("s"), .long("subcommand-help")]) } } diff --git a/Tests/ArgumentParserUnitTests/HelpGenerationTests+AtArgument.swift b/Tests/ArgumentParserUnitTests/HelpGenerationTests+AtArgument.swift index e34a7e255..730260ac2 100644 --- a/Tests/ArgumentParserUnitTests/HelpGenerationTests+AtArgument.swift +++ b/Tests/ArgumentParserUnitTests/HelpGenerationTests+AtArgument.swift @@ -221,6 +221,7 @@ extension HelpGenerationTests { var arg0: A? = nil } + @available(*, deprecated, message: "Included for test coverage") struct OptionalDefault: ParsableCommand { @Argument(help: "example") var arg0: A? = A() diff --git a/Tests/ArgumentParserUnitTests/HelpGenerationTests+AtOption.swift b/Tests/ArgumentParserUnitTests/HelpGenerationTests+AtOption.swift index a09b4a9a5..edb003d1d 100644 --- a/Tests/ArgumentParserUnitTests/HelpGenerationTests+AtOption.swift +++ b/Tests/ArgumentParserUnitTests/HelpGenerationTests+AtOption.swift @@ -179,6 +179,7 @@ extension HelpGenerationTests { var arg0: A? = nil } + @available(*, deprecated, message: "Included for test coverage") struct OptionalDefault: ParsableCommand { @Option(help: "example") var arg0: A? = A() diff --git a/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift b/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift index c1b3a7c13..7f4994294 100644 --- a/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift +++ b/Tests/ArgumentParserUnitTests/HelpGenerationTests.swift @@ -361,7 +361,7 @@ extension HelpGenerationTests { } func testOverviewButNoAbstractSpacing() { - let renderedHelp = HelpGenerator(J.self, visibility: .default) + let renderedHelp = HelpGenerator(J.self, options: .plain) .rendered() AssertEqualStringsIgnoringTrailingWhitespace(renderedHelp, """ OVERVIEW: diff --git a/Tools/generate-manual/DSL/SinglePageDescription.swift b/Tools/generate-manual/DSL/Description.swift similarity index 60% rename from Tools/generate-manual/DSL/SinglePageDescription.swift rename to Tools/generate-manual/DSL/Description.swift index dbbe947a2..0eed6c895 100644 --- a/Tools/generate-manual/DSL/SinglePageDescription.swift +++ b/Tools/generate-manual/DSL/Description.swift @@ -12,7 +12,8 @@ import ArgumentParser import ArgumentParserToolInfo -struct SinglePageDescription: MDocComponent { +struct Description: MDocComponent { + var multipage: Bool var command: CommandInfoV0 var body: MDocComponent { @@ -27,6 +28,14 @@ struct SinglePageDescription: MDocComponent { discussion } + if command.discussion != nil, command.detailedDiscussion != nil { + MDocMacro.ParagraphBreak() + } + + if let detailedDiscussion = command.detailedDiscussion { + detailedDiscussion + } + List { for argument in command.arguments ?? [] { MDocMacro.ListItem(title: argument.manualPageDescription) @@ -42,11 +51,23 @@ struct SinglePageDescription: MDocComponent { if let discussion = argument.discussion { discussion } + + if argument.discussion != nil, argument.detailedDiscussion != nil { + MDocMacro.ParagraphBreak() + } + + if let detailedDiscussion = argument.detailedDiscussion { + detailedDiscussion + } } - for subcommand in command.subcommands ?? [] { - MDocMacro.ListItem(title: MDocMacro.Emphasis(arguments: [subcommand.commandName])) - SinglePageDescription(command: subcommand).core + if !multipage { + for subcommand in command.subcommands ?? [] { + MDocMacro.ListItem(title: MDocMacro.Emphasis(arguments: [subcommand.commandName])) + Description( + multipage: multipage, + command: subcommand).core + } } } } diff --git a/Tools/generate-manual/DSL/Document.swift b/Tools/generate-manual/DSL/Document.swift index fe80d42e7..d4cbdf4bb 100644 --- a/Tools/generate-manual/DSL/Document.swift +++ b/Tools/generate-manual/DSL/Document.swift @@ -24,11 +24,7 @@ struct Document: MDocComponent { Preamble(date: date, section: section, command: command) Name(command: command) Synopsis(command: command) - if multiPage { - MultiPageDescription(command: command) - } else { - SinglePageDescription(command: command) - } + Description(multipage: multiPage, command: command) Exit(section: section) if multiPage { SeeAlso(section: section, command: command) diff --git a/Tools/generate-manual/GenerateManual.swift b/Tools/generate-manual/GenerateManual.swift index 55bdaaa86..85d20cac9 100644 --- a/Tools/generate-manual/GenerateManual.swift +++ b/Tools/generate-manual/GenerateManual.swift @@ -24,7 +24,22 @@ struct GenerateManual: ParsableCommand { static let configuration = CommandConfiguration( commandName: "generate-manual", - abstract: "Generate a manual for the provided tool.") + abstract: "Generate a manual for the provided tool.", + discussion: """ + The generate-manual tool powers the generate-manual plugin and typically \ + should not be directly invoked. Instead manuals should be generated \ + using the plugin which can be invoked via \ + `swift package generate-manual`. + """, + detailedDiscussion: """ + The generate-manual tool invokes provided executable with the \ + `--experimental-dump-help` argument and decoding the output into a \ + `ToolInfo` structure provided by the `ArgumentParserToolInfo` library. \ + The executable's `ToolInfo` is then transformed into to an \ + `MDocComponent` tree using a SwiftUI-esk resultBuilder DSL. Next, the \ + `MDocComponent` tree is lowered into an `Array` of `MDocASTNode` trees. \ + Lastly, the `MDocASTNode` trees are serialized to a manual page on disk. + """) @Argument(help: "Tool to generate manual for.") var tool: String @@ -32,16 +47,41 @@ struct GenerateManual: ParsableCommand { @Flag(help: "Generate a separate manual for each subcommand.") var multiPage = false - @Option(name: .long, help: "Override the creation date of the manual. Format: 'yyyy-mm-dd'.") + @Option( + name: .long, + help: "Override the creation date of the manual. Format: 'yyyy-mm-dd'.") var date: Date = Date() - @Option(name: .long, help: "Section of the manual.") + @Option( + name: .long, + help: .init( + "The manual section.", + discussion: """ + Manuals for executables are typically included in section 1, but may \ + also be found in other sections such as section 8. + """, + detailedDiscussion: """ + A description of manual sections is included below: + 1) General Commands + 2) System Calls + 3) Library Functions + 4) Device Drivers + 5) File Formats + 6) Games + 7) Miscellaneous Information + 8) System Manager's Manual + 9) Kernel Developer's Manual + """)) var section: Int = 1 - @Option(name: .long, help: "Names and/or emails of the tool's authors. Format: 'name'.") + @Option( + name: .long, + help: "Names and/or emails of the tool's authors. Format: 'name'.") var authors: [AuthorArgument] = [] - @Option(name: .shortAndLong, help: "Directory to save generated manual. Use '-' for stdout.") + @Option( + name: .shortAndLong, + help: "Directory to save generated manual. Use '-' for stdout.") var outputDirectory: String func validate() throws { @@ -51,14 +91,18 @@ struct GenerateManual: ParsableCommand { } if outputDirectory != "-" { - // outputDirectory must already exist, `GenerateManual` will not create it. + // `outputDirectory` must already exist, `GenerateManual` will not create + // it. var objcBool: ObjCBool = true - guard FileManager.default.fileExists(atPath: outputDirectory, isDirectory: &objcBool) else { - throw ValidationError("Output directory \(outputDirectory) does not exist") + guard FileManager.default.fileExists( + atPath: outputDirectory, isDirectory: &objcBool) else { + throw ValidationError( + "Output directory \(outputDirectory) does not exist") } guard objcBool.boolValue else { - throw ValidationError("Output directory \(outputDirectory) is not a directory") + throw ValidationError( + "Output directory \(outputDirectory) is not a directory") } } } @@ -67,18 +111,20 @@ struct GenerateManual: ParsableCommand { let data: Data do { let tool = URL(fileURLWithPath: tool) - let output = try executeCommand(executable: tool, arguments: ["--experimental-dump-help"]) + let output = try executeCommand( + executable: tool, arguments: ["--experimental-dump-help"]) data = output.data(using: .utf8) ?? Data() } catch { throw Error.failedToRunSubprocess(error: error) } do { - let toolInfoThin = try JSONDecoder().decode(ToolInfoHeader.self, from: data) - guard toolInfoThin.serializationVersion == 0 else { + let decoder = JSONDecoder() + let toolInfoHeader = try decoder.decode(ToolInfoHeader.self, from: data) + guard toolInfoHeader.serializationVersion == 0 else { throw Error.unsupportedDumpHelpVersion( expected: 0, - found: toolInfoThin.serializationVersion) + found: toolInfoHeader.serializationVersion) } } catch { throw Error.unableToParseToolOutput(error: error) @@ -104,7 +150,9 @@ struct GenerateManual: ParsableCommand { } } - func generatePages(from command: CommandInfoV0, savingTo directory: URL?) throws { + func generatePages( + from command: CommandInfoV0, savingTo directory: URL? + ) throws { let document = Document( multiPage: multiPage, date: date,