Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
308 changes: 308 additions & 0 deletions Sources/ArgumentParser/Parsable Properties/FlagOption.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,308 @@
//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift Argument Parser open source project
//
// Copyright (c) 2025 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
//
//===----------------------------------------------------------------------===//

/// A property wrapper that represents a command-line argument that can work as both a flag and an option.
///
/// Use the `@FlagOption` wrapper to define a property that can be used in two ways:
/// 1. As a flag (without a value): `--show-bin-path` - uses the default value
/// 2. As an option (with a value): `--show-bin-path json` - uses the provided value
///
/// This provides backward compatibility for flags while allowing optional values.
///
/// For example, the following program declares a flag-option that defaults to "text"
/// when used as a flag, but accepts custom values when used as an option:
///
/// ```swift
/// @main
/// struct Tool: ParsableCommand {
/// @FlagOption(defaultValue: "text")
/// var showBinPath: String
///
/// mutating func run() {
/// print("Format: \(showBinPath)")
/// }
/// }
/// ```
///
/// This allows both usage patterns:
/// - `tool --show-bin-path` outputs "Format: text"
/// - `tool --show-bin-path json` outputs "Format: json"
///
@propertyWrapper
public struct FlagOption<Value>: Decodable, ParsedWrapper {
internal var _parsedValue: Parsed<Value>

internal init(_parsedValue: Parsed<Value>) {
self._parsedValue = _parsedValue
}

public init(from _decoder: Decoder) throws {
try self.init(_decoder: _decoder)
}

/// This initializer works around a quirk of property wrappers, where the
/// compiler will not see no-argument initializers in extensions.
@available(
*, unavailable,
message: "A default value must be provided for FlagOption."
)
public init() {
fatalError("unavailable")
}

/// The value presented by this property wrapper.
public var wrappedValue: Value {
get {
switch _parsedValue {
case .value(let v):
return v
case .definition:
configurationFailure(directlyInitializedError)
}
}
set {
_parsedValue = .value(newValue)
}
}
}

extension FlagOption: CustomStringConvertible {
public var description: String {
switch _parsedValue {
case .value(let v):
return String(describing: v)
case .definition:
return "FlagOption(*definition*)"
}
}
}

extension FlagOption: Sendable where Value: Sendable {}
extension FlagOption: DecodableParsedWrapper where Value: Decodable {}

// MARK: - @FlagOption T: ExpressibleByArgument Initializers

extension FlagOption where Value: ExpressibleByArgument {
/// Creates a flag-option property with a default value that is used when
/// the argument is specified as a flag without a value.
///
/// - Parameters:
/// - defaultValue: The value to use when the argument is specified as a flag
/// - wrappedValue: The initial value for the property
/// - name: A specification for what names are allowed for this flag-option
/// - help: Information about how to use this flag-option
/// - completion: The type of command-line completion provided for this option
public init(
defaultValue: Value,
wrappedValue: Value,
name: NameSpecification = .long,
help: ArgumentHelp? = nil,
completion: CompletionKind? = nil
) {
self.init(
_parsedValue: .init { key in
let arg = ArgumentDefinition(
container: Bare<Value>.self,
key: key,
kind: .name(key: key, specification: name),
help: .init(
help?.abstract ?? "",
discussion: help?.discussion,
valueName: help?.valueName,
visibility: help?.visibility ?? .default,
argumentType: Value.self
),
parsingStrategy: .scanningForValue,
initial: wrappedValue,
completion: completion)

return ArgumentSet(arg)
})
}

/// Creates a flag-option property with a default value.
///
/// - Parameters:
/// - defaultValue: The value to use when the argument is specified as a flag
/// - name: A specification for what names are allowed for this flag-option
/// - help: Information about how to use this flag-option
/// - completion: The type of command-line completion provided for this option
public init(
defaultValue: Value,
name: NameSpecification = .long,
help: ArgumentHelp? = nil,
completion: CompletionKind? = nil
) {
self.init(
_parsedValue: .init { key in
let arg = ArgumentDefinition(
container: Bare<Value>.self,
key: key,
kind: .name(key: key, specification: name),
help: .init(
help?.abstract ?? "",
discussion: help?.discussion,
valueName: help?.valueName,
visibility: help?.visibility ?? .default,
argumentType: Value.self
),
parsingStrategy: .scanningForValue,
initial: nil,
completion: completion)

return ArgumentSet(arg)
})
}
}

// MARK: - @FlagOption T Initializers (with transform)

extension FlagOption {
/// Creates a flag-option property with a transform closure and default value.
///
/// - Parameters:
/// - defaultValue: The value to use when the argument is specified as a flag
/// - wrappedValue: The initial value for the property
/// - name: A specification for what names are allowed for this flag-option
/// - help: Information about how to use this flag-option
/// - completion: The type of command-line completion provided for this option
/// - transform: A closure that converts a string into this property's type
@preconcurrency
public init(
defaultValue: Value,
wrappedValue: Value,
name: NameSpecification = .long,
help: ArgumentHelp? = nil,
completion: CompletionKind? = nil,
transform: @Sendable @escaping (String) throws -> Value
) {
self.init(
_parsedValue: .init { key in
let arg = ArgumentDefinition(
container: Bare<Value>.self,
key: key,
kind: .name(key: key, specification: name),
help: help,
parsingStrategy: .scanningForValue,
transform: transform,
initial: wrappedValue,
completion: completion)

return ArgumentSet(arg)
})
}

/// Creates a flag-option property with a transform closure and default value.
///
/// - Parameters:
/// - defaultValue: The value to use when the argument is specified as a flag
/// - name: A specification for what names are allowed for this flag-option
/// - help: Information about how to use this flag-option
/// - completion: The type of command-line completion provided for this option
/// - transform: A closure that converts a string into this property's type
@preconcurrency
public init(
defaultValue: Value,
name: NameSpecification = .long,
help: ArgumentHelp? = nil,
completion: CompletionKind? = nil,
transform: @Sendable @escaping (String) throws -> Value
) {
self.init(
_parsedValue: .init { key in
let arg = ArgumentDefinition(
container: Bare<Value>.self,
key: key,
kind: .name(key: key, specification: name),
help: help,
parsingStrategy: .scanningForValue,
transform: transform,
initial: nil,
completion: completion)

return ArgumentSet(arg)
})
}
}

// MARK: - @FlagOption Optional<T> Initializers

extension FlagOption {
/// Creates an optional flag-option property with a default value.
///
/// - Parameters:
/// - defaultValue: The value to use when the argument is specified as a flag
/// - wrappedValue: The initial value for the property
/// - name: A specification for what names are allowed for this flag-option
/// - help: Information about how to use this flag-option
/// - completion: The type of command-line completion provided for this option
public init<T>(
defaultValue: T,
wrappedValue: T?,
name: NameSpecification = .long,
help: ArgumentHelp? = nil,
completion: CompletionKind? = nil
) where T: ExpressibleByArgument, Value == T? {
self.init(
_parsedValue: .init { key in
let arg = ArgumentDefinition(
container: Optional<T>.self,
key: key,
kind: .name(key: key, specification: name),
help: .init(
help?.abstract ?? "",
discussion: help?.discussion,
valueName: help?.valueName,
visibility: help?.visibility ?? .default,
argumentType: T.self
),
parsingStrategy: .scanningForValue,
initial: wrappedValue,
completion: completion)

return ArgumentSet(arg)
})
}

/// Creates an optional flag-option property with a default value.
///
/// - Parameters:
/// - defaultValue: The value to use when the argument is specified as a flag
/// - name: A specification for what names are allowed for this flag-option
/// - help: Information about how to use this flag-option
/// - completion: The type of command-line completion provided for this option
public init<T>(
defaultValue: T,
name: NameSpecification = .long,
help: ArgumentHelp? = nil,
completion: CompletionKind? = nil
) where T: ExpressibleByArgument, Value == T? {
self.init(
_parsedValue: .init { key in
let arg = ArgumentDefinition(
container: Optional<T>.self,
key: key,
kind: .name(key: key, specification: name),
help: .init(
help?.abstract ?? "",
discussion: help?.discussion,
valueName: help?.valueName,
visibility: help?.visibility ?? .default,
argumentType: T.self
),
parsingStrategy: .scanningForValue,
initial: nil,
completion: completion)

return ArgumentSet(arg)
})
}
}
Loading
Loading