Skip to content

Commit e7765e1

Browse files
authored
Replace createHelp and includeHidden (#405)
- Replaces `ArgumentSet.init(_:creatingHelp:includeHidden:)` with `ArgumentSet.init(_:visibility:)`. `visibility` intentionally does not have a default value to ensure that callers only have the correct arguments. As part of this change `includeHidden` has been replaced throughout the codebase with `visibility`. This change also fixes a bug where arguments with hidden `visibility` were being displayed in the generated command usage string.
1 parent e344426 commit e7765e1

14 files changed

+94
-43
lines changed

Sources/ArgumentParser/Completions/BashCompletionsGenerator.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -122,14 +122,16 @@ struct BashCompletionsGenerator {
122122

123123
/// Returns the option and flag names that can be top-level completions.
124124
fileprivate static func generateArgumentWords(_ commands: [ParsableCommand.Type]) -> [String] {
125-
commands.argumentsForHelp().flatMap { $0.bashCompletionWords() }
125+
commands
126+
.argumentsForHelp(visibility: .default)
127+
.flatMap { $0.bashCompletionWords() }
126128
}
127129

128130
/// Returns additional top-level completions from positional arguments.
129131
///
130132
/// These consist of completions that are defined as `.list` or `.custom`.
131133
fileprivate static func generateArgumentCompletions(_ commands: [ParsableCommand.Type]) -> [String] {
132-
ArgumentSet(commands.last!)
134+
ArgumentSet(commands.last!, visibility: .default)
133135
.compactMap { arg -> String? in
134136
guard arg.isPositional else { return nil }
135137

@@ -156,7 +158,7 @@ struct BashCompletionsGenerator {
156158

157159
/// Returns the case-matching statements for supplying completions after an option or flag.
158160
fileprivate static func generateOptionHandlers(_ commands: [ParsableCommand.Type]) -> String {
159-
ArgumentSet(commands.last!)
161+
ArgumentSet(commands.last!, visibility: .default)
160162
.compactMap { arg -> String? in
161163
let words = arg.bashCompletionWords()
162164
if words.isEmpty { return nil }

Sources/ArgumentParser/Completions/FishCompletionsGenerator.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ struct FishCompletionsGenerator {
5656
}
5757

5858
let argumentCompletions = commands
59-
.argumentsForHelp()
59+
.argumentsForHelp(visibility: .default)
6060
.flatMap { $0.argumentSegments(commandChain) }
6161
.map { complete(ancestors: $0, suggestion: $1) }
6262

Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,9 @@ struct ZshCompletionsGenerator {
103103
}
104104

105105
static func generateCompletionArguments(_ commands: [ParsableCommand.Type]) -> [String] {
106-
commands.argumentsForHelp().compactMap { $0.zshCompletionString(commands) }
106+
commands
107+
.argumentsForHelp(visibility: .default)
108+
.compactMap { $0.zshCompletionString(commands) }
107109
}
108110
}
109111

Sources/ArgumentParser/Parsable Properties/ArgumentVisibility.swift

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,27 @@ public enum ArgumentVisibility {
2020
/// Never show help for this argument.
2121
case `private`
2222
}
23+
24+
extension ArgumentVisibility {
25+
/// A raw Integer value that represents each visibility level.
26+
///
27+
/// `_comparableLevel` can be used to test if a Visibility case is more or
28+
/// less visible than another, without committing this behavior to API.
29+
/// A lower `_comparableLevel` indicates that the case is less visible (more
30+
/// secret).
31+
private var _comparableLevel: Int {
32+
switch self {
33+
case .default:
34+
return 2
35+
case .hidden:
36+
return 1
37+
case .private:
38+
return 0
39+
}
40+
}
41+
42+
/// - Returns: true if `self` is at least as visible as the supplied argument.
43+
func isAtLeastAsVisible(as other: Self) -> Bool {
44+
self._comparableLevel >= other._comparableLevel
45+
}
46+
}

Sources/ArgumentParser/Parsable Properties/OptionGroup.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ public struct OptionGroup<Value: ParsableArguments>: Decodable, ParsedWrapper {
6464
/// Creates a property that represents another parsable type.
6565
public init() {
6666
self.init(_parsedValue: .init { _ in
67-
ArgumentSet(Value.self)
67+
ArgumentSet(Value.self, visibility: .private)
6868
})
6969
}
7070

Sources/ArgumentParser/Parsable Types/ParsableArguments.swift

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -163,7 +163,7 @@ extension ParsableArguments {
163163
includeHidden: Bool = false,
164164
columns: Int? = nil
165165
) -> String {
166-
HelpGenerator(self, includeHidden: includeHidden)
166+
HelpGenerator(self, visibility: includeHidden ? .hidden : .default)
167167
.rendered(screenWidth: columns)
168168
}
169169

@@ -269,7 +269,7 @@ extension ArgumentSetProvider {
269269
}
270270

271271
extension ArgumentSet {
272-
init(_ type: ParsableArguments.Type, creatingHelp: Bool = false, includeHidden: Bool = false) {
272+
init(_ type: ParsableArguments.Type, visibility: ArgumentVisibility) {
273273

274274
#if DEBUG
275275
do {
@@ -285,13 +285,8 @@ extension ArgumentSet {
285285
guard var codingKey = child.label else { return nil }
286286

287287
if let parsed = child.value as? ArgumentSetProvider {
288-
if creatingHelp {
289-
if includeHidden {
290-
guard parsed._visibility != .private else { return nil }
291-
} else {
292-
guard parsed._visibility == .default else { return nil }
293-
}
294-
}
288+
guard parsed._visibility.isAtLeastAsVisible(as: visibility)
289+
else { return nil }
295290

296291
// Property wrappers have underscore-prefixed names
297292
codingKey = String(codingKey.first == "_"

Sources/ArgumentParser/Parsable Types/ParsableCommand.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ extension ParsableCommand {
108108
) -> String {
109109
HelpGenerator(
110110
commandStack: CommandParser(self).commandStack(for: subcommand),
111-
includeHidden: includeHidden)
111+
visibility: includeHidden ? .hidden : .default)
112112
.rendered(screenWidth: columns)
113113
}
114114

@@ -141,7 +141,7 @@ extension ParsableCommand {
141141
/// `true` if this command contains any array arguments that are declared
142142
/// with `.unconditionalRemaining`.
143143
internal static var includesUnconditionalArguments: Bool {
144-
ArgumentSet(self).contains(where: {
144+
ArgumentSet(self, visibility: .private).contains(where: {
145145
$0.isRepeatingPositional && $0.parsingStrategy == .allRemainingInput
146146
})
147147
}

Sources/ArgumentParser/Parsing/CommandParser.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,7 +130,7 @@ extension CommandParser {
130130
/// possible.
131131
fileprivate mutating func parseCurrent(_ split: inout SplitArguments) throws -> ParsableCommand {
132132
// Build the argument set (i.e. information on how to parse):
133-
let commandArguments = ArgumentSet(currentNode.element)
133+
let commandArguments = ArgumentSet(currentNode.element, visibility: .private)
134134

135135
// Parse the arguments, ignoring anything unexpected
136136
let values = try commandArguments.lenientParse(
@@ -315,7 +315,7 @@ extension CommandParser {
315315
let completionValues = Array(args)
316316

317317
// Generate the argument set and parse the argument to find in the set
318-
let argset = ArgumentSet(current.element)
318+
let argset = ArgumentSet(current.element, visibility: .private)
319319
let parsedArgument = try! parseIndividualArg(argToMatch, at: 0).first!
320320

321321
// Look up the specified argument and retrieve its custom completion function

Sources/ArgumentParser/Usage/DumpHelpGenerator.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ fileprivate extension BidirectionalCollection where Element == ParsableCommand.T
3838
/// Returns the ArgumentSet for the last command in this stack, including
3939
/// help and version flags, when appropriate.
4040
func allArguments() -> ArgumentSet {
41-
guard var arguments = self.last.map({ ArgumentSet($0, creatingHelp: false) })
41+
guard var arguments = self.last.map({ ArgumentSet($0, visibility: .private) })
4242
else { return ArgumentSet() }
4343
self.versionArgumentDefinition().map { arguments.append($0) }
4444
self.helpArgumentDefinition().map { arguments.append($0) }

Sources/ArgumentParser/Usage/HelpCommand.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ struct HelpCommand: ParsableCommand {
1919
@Argument var subcommands: [String] = []
2020

2121
/// Capture and ignore any extra help flags given by the user.
22-
@Flag(name: [.short, .long, .customLong("help", withSingleDash: true)], help: .hidden)
22+
@Flag(name: [.short, .long, .customLong("help", withSingleDash: true)], help: .private)
2323
var help = false
2424

2525
private(set) var commandStack: [ParsableCommand.Type] = []
@@ -39,7 +39,10 @@ struct HelpCommand: ParsableCommand {
3939

4040
/// Used for testing.
4141
func generateHelp(screenWidth: Int) -> String {
42-
HelpGenerator(commandStack: commandStack).rendered(screenWidth: screenWidth)
42+
HelpGenerator(
43+
commandStack: commandStack,
44+
visibility: visibility)
45+
.rendered(screenWidth: screenWidth)
4346
}
4447

4548
enum CodingKeys: CodingKey {

Sources/ArgumentParser/Usage/HelpGenerator.swift

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -89,12 +89,12 @@ internal struct HelpGenerator {
8989
var sections: [Section]
9090
var discussionSections: [DiscussionSection]
9191

92-
init(commandStack: [ParsableCommand.Type], includeHidden: Bool = false) {
92+
init(commandStack: [ParsableCommand.Type], visibility: ArgumentVisibility) {
9393
guard let currentCommand = commandStack.last else {
9494
fatalError()
9595
}
9696

97-
let currentArgSet = ArgumentSet(currentCommand)
97+
let currentArgSet = ArgumentSet(currentCommand, visibility: visibility)
9898
self.commandStack = commandStack
9999

100100
// Build the tool name and subcommand name from the command configuration
@@ -121,25 +121,24 @@ internal struct HelpGenerator {
121121
}
122122
self.abstract += "\n\(currentCommand.configuration.discussion)"
123123
}
124-
125-
self.sections = HelpGenerator.generateSections(commandStack: commandStack, includeHidden: includeHidden)
124+
125+
self.sections = HelpGenerator.generateSections(commandStack: commandStack, visibility: visibility)
126126
self.discussionSections = []
127127
}
128128

129-
init(_ type: ParsableArguments.Type, includeHidden: Bool = false) {
130-
self.init(commandStack: [type.asCommand], includeHidden: includeHidden)
129+
init(_ type: ParsableArguments.Type, visibility: ArgumentVisibility) {
130+
self.init(commandStack: [type.asCommand], visibility: visibility)
131131
}
132132

133-
private static func generateSections(commandStack: [ParsableCommand.Type], includeHidden: Bool) -> [Section] {
133+
private static func generateSections(commandStack: [ParsableCommand.Type], visibility: ArgumentVisibility) -> [Section] {
134134
guard !commandStack.isEmpty else { return [] }
135135

136136
var positionalElements: [Section.Element] = []
137137
var optionElements: [Section.Element] = []
138138

139139
/// Start with a full slice of the ArgumentSet so we can peel off one or
140140
/// more elements at a time.
141-
var args = commandStack.argumentsForHelp(includeHidden: includeHidden)[...]
142-
141+
var args = commandStack.argumentsForHelp(visibility: visibility)[...]
143142
while let arg = args.popFirst() {
144143
guard arg.help.visibility == .default else { continue }
145144

@@ -300,7 +299,7 @@ internal extension BidirectionalCollection where Element == ParsableCommand.Type
300299
.map { $0.configuration.helpNames!.generateHelpNames(visibility: visibility) }
301300
?? CommandConfiguration.defaultHelpNames.generateHelpNames(visibility: visibility)
302301
}
303-
302+
304303
func getPrimaryHelpName() -> Name? {
305304
getHelpNames(visibility: .default).preferredName
306305
}
@@ -340,8 +339,8 @@ internal extension BidirectionalCollection where Element == ParsableCommand.Type
340339

341340
/// Returns the ArgumentSet for the last command in this stack, including
342341
/// help and version flags, when appropriate.
343-
func argumentsForHelp(includeHidden: Bool = false) -> ArgumentSet {
344-
guard var arguments = self.last.map({ ArgumentSet($0, creatingHelp: true, includeHidden: includeHidden) })
342+
func argumentsForHelp(visibility: ArgumentVisibility) -> ArgumentSet {
343+
guard var arguments = self.last.map({ ArgumentSet($0, visibility: visibility) })
345344
else { return ArgumentSet() }
346345
self.versionArgumentDefinition().map { arguments.append($0) }
347346
self.helpArgumentDefinition().map { arguments.append($0) }

Sources/ArgumentParser/Usage/MessageInfo.swift

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ enum MessageInfo {
2828
// Exit early on built-in requests
2929
switch e.parserError {
3030
case .helpRequested(let visibility):
31-
self = .help(text: HelpGenerator(commandStack: e.commandStack, includeHidden: visibility != .default).rendered())
31+
self = .help(text: HelpGenerator(commandStack: e.commandStack, visibility: visibility).rendered())
3232
return
3333

3434
case .dumpHelpRequested:
@@ -73,7 +73,7 @@ enum MessageInfo {
7373
parserError = .userValidationError(error)
7474
}
7575

76-
var usage = HelpGenerator(commandStack: commandStack).usageMessage()
76+
var usage = HelpGenerator(commandStack: commandStack, visibility: .default).usageMessage()
7777

7878
let commandNames = commandStack.map { $0._commandName }.joined(separator: " ")
7979
if let helpName = commandStack.getPrimaryHelpName() {
@@ -96,7 +96,7 @@ enum MessageInfo {
9696
if let command = command {
9797
commandStack = CommandParser(type.asCommand).commandStack(for: command)
9898
}
99-
self = .help(text: HelpGenerator(commandStack: commandStack).rendered())
99+
self = .help(text: HelpGenerator(commandStack: commandStack, visibility: .default).rendered())
100100
case .dumpRequest(let command):
101101
if let command = command {
102102
commandStack = CommandParser(type.asCommand).commandStack(for: command)
@@ -119,9 +119,9 @@ enum MessageInfo {
119119
} else if let parserError = parserError {
120120
let usage: String = {
121121
guard case ParserError.noArguments = parserError else { return usage }
122-
return "\n" + HelpGenerator(commandStack: [type.asCommand]).rendered()
122+
return "\n" + HelpGenerator(commandStack: [type.asCommand], visibility: .default).rendered()
123123
}()
124-
let argumentSet = ArgumentSet(commandStack.last!)
124+
let argumentSet = ArgumentSet(commandStack.last!, visibility: .default)
125125
let message = argumentSet.errorDescription(error: parserError) ?? ""
126126
let helpAbstract = argumentSet.helpDescription(error: parserError) ?? ""
127127
self = .validation(message: message, usage: usage, help: helpAbstract)

Sources/ArgumentParser/Usage/UsageGenerator.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ extension UsageGenerator {
2323
}
2424

2525
init(toolName: String, parsable: ParsableArguments) {
26-
self.init(toolName: toolName, definition: ArgumentSet(type(of: parsable)))
26+
self.init(
27+
toolName: toolName,
28+
definition: ArgumentSet(type(of: parsable), visibility: .default))
2729
}
2830

2931
init(toolName: String, definition: [ArgumentSet]) {

Tests/ArgumentParserUnitTests/HelpGenerationTests.swift

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,29 @@ extension HelpGenerationTests {
7171
-h, --help Show help information.
7272
7373
""")
74+
75+
#if !os(Linux)
76+
XCTExpectFailure("""
77+
The following test fails to properly generate the help-hidden
78+
message properly because help-hidden is not fully supported yet.
79+
""")
80+
AssertHelp(.hidden, for: B.self, equals: """
81+
USAGE: b --name <name> [--title <title>] [<hidden-name>] [--hidden-title <hidden-title>] [--hidden-flag] [--hidden-inverted-flag] [--no-hidden-inverted-flag]
82+
83+
ARGUMENTS:
84+
<hidden-name>
85+
86+
OPTIONS:
87+
--name <name> Your name
88+
--title <title> Your title
89+
--hidden-title <hidden-title>
90+
--hidden-flag
91+
--hidden-inverted-flag/--no-hidden-inverted-flag
92+
(default: true)
93+
-h, --help Show help information.
94+
95+
""")
96+
#endif
7497
}
7598

7699
struct C: ParsableArguments {
@@ -322,7 +345,8 @@ extension HelpGenerationTests {
322345
}
323346

324347
func testOverviewButNoAbstractSpacing() {
325-
let renderedHelp = HelpGenerator(J.self).rendered()
348+
let renderedHelp = HelpGenerator(J.self, visibility: .default)
349+
.rendered()
326350
AssertEqualStringsIgnoringTrailingWhitespace(renderedHelp, """
327351
OVERVIEW:
328352
test
@@ -508,7 +532,7 @@ extension HelpGenerationTests {
508532
let helpMessage = """
509533
OVERVIEW: Demo hiding option groups
510534
511-
USAGE: driver [--verbose] [--custom-name <custom-name>] [--timeout <timeout>]
535+
USAGE: driver [--timeout <timeout>]
512536
513537
OPTIONS:
514538
--timeout <timeout> Time to wait before timeout (in seconds)
@@ -569,7 +593,7 @@ extension HelpGenerationTests {
569593
}
570594

571595
func testAllValues() {
572-
let opts = ArgumentSet(AllValues.self)
596+
let opts = ArgumentSet(AllValues.self, visibility: .private)
573597
XCTAssertEqual(AllValues.Manual.allValueStrings, opts[0].help.allValues)
574598
XCTAssertEqual(AllValues.Manual.allValueStrings, opts[1].help.allValues)
575599

0 commit comments

Comments
 (0)