Skip to content
Merged
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
1 change: 1 addition & 0 deletions Sources/ArgumentParser/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ add_library(ArgumentParser

Utilities/CollectionExtensions.swift
Utilities/Mutex.swift
Utilities/Foundation.swift
Utilities/Platform.swift
Utilities/SequenceExtensions.swift
Utilities/StringExtensions.swift
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,10 +190,10 @@ extension CommandInfoV0 {
shopt -s extglob
set +o history +o posix

local -xr \(CompletionShell.shellEnvironmentVariableName)=bash
local -x \(CompletionShell.shellVersionEnvironmentVariableName)
\(CompletionShell.shellVersionEnvironmentVariableName)="$(IFS='.';printf %s "${BASH_VERSINFO[*]}")"
local -r \(CompletionShell.shellVersionEnvironmentVariableName)
local -xr \(Platform.Environment.Key.shellName.rawValue)=bash
local -x \(Platform.Environment.Key.shellVersion.rawValue)
\(Platform.Environment.Key.shellVersion.rawValue)="$(IFS='.';printf %s "${BASH_VERSINFO[*]}")"
local -r \(Platform.Environment.Key.shellVersion.rawValue)

local -r cur="${2}"
local -r prev="${3}"
Expand Down
94 changes: 75 additions & 19 deletions Sources/ArgumentParser/Completions/CompletionsGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@

#if compiler(>=6.0)
internal import ArgumentParserToolInfo
internal import Foundation
#else
import ArgumentParserToolInfo
import Foundation
#endif

/// A shell for which the parser can generate a completion script.
public struct CompletionShell: RawRepresentable, Hashable, CaseIterable {
public struct CompletionShell: RawRepresentable, Hashable, CaseIterable,
Sendable
{
public var rawValue: String

/// Creates a new instance from the given string.
Expand Down Expand Up @@ -87,20 +87,6 @@ public struct CompletionShell: RawRepresentable, Hashable, CaseIterable {
Self._requestingVersion.withLock { $0 }
}

/// The name of the environment variable whose value is the name of the shell
/// for which completions are being requested from a custom completion
/// handler.
///
/// The environment variable is set in generated completion scripts.
static let shellEnvironmentVariableName = "SAP_SHELL"

/// The name of the environment variable whose value is the version of the
/// shell for which completions are being requested from a custom completion
/// handler.
///
/// 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 {
Expand Down Expand Up @@ -157,12 +143,82 @@ extension String {
func shellEscapeForSingleQuotedString(iterationCount: UInt64 = 1) -> Self {
iterationCount == 0
? self
: replacingOccurrences(of: "'", with: "'\\''")
: self
.replacing("'", with: "'\\''")
.shellEscapeForSingleQuotedString(iterationCount: iterationCount - 1)
}

func shellEscapeForVariableName() -> Self {
replacingOccurrences(of: "-", with: "_")
self.replacing("-", with: "_")
}

func replacing(_ old: Self, with new: Self) -> Self {
guard !old.isEmpty else { return self }

var result = ""
var startIndex = self.startIndex

// Look for occurrences of the old string.
while let matchRange = self.firstMatch(of: old, at: startIndex) {
// Add the substring before the match.
result.append(contentsOf: self[startIndex..<matchRange.start])

// Add the replacement string.
result.append(contentsOf: new)

// Move past the matched portion.
startIndex = matchRange.end
}

// No more matches found, add the rest of the string.
result.append(contentsOf: self[startIndex..<self.endIndex])

return result
}

func firstMatch(
of match: Self,
at startIndex: Self.Index
) -> (start: Self.Index, end: Self.Index)? {
guard !match.isEmpty else { return nil }
guard match.count <= self.count else { return nil }

var startIndex = startIndex
while startIndex < self.endIndex {
// Check if theres a match.
if let endIndex = self.matches(match, at: startIndex) {
// Return the match.
return (startIndex, endIndex)
}

// Move to the next of index.
self.formIndex(after: &startIndex)
}

return nil
}

func matches(
_ match: Self,
at startIndex: Self.Index
) -> Self.Index? {
var selfIndex = startIndex
var matchIndex = match.startIndex

while true {
// Only continue checking if there is more match to check
guard matchIndex < match.endIndex else { return selfIndex }

// Exit early if there is no more "self" to check.
guard selfIndex < self.endIndex else { return nil }

// Check match and self are the the same.
guard self[selfIndex] == match[matchIndex] else { return nil }

// Move to the next pair of indices.
self.formIndex(after: &selfIndex)
match.formIndex(after: &matchIndex)
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@

#if compiler(>=6.0)
internal import ArgumentParserToolInfo
internal import Foundation
#else
import ArgumentParserToolInfo
import Foundation
#endif

extension ToolInfoV0 {
Expand Down Expand Up @@ -75,8 +73,8 @@ extension CommandInfoV0 {
end

function \(customCompletionFunctionName)
set -x \(CompletionShell.shellEnvironmentVariableName) fish
set -x \(CompletionShell.shellVersionEnvironmentVariableName) $FISH_VERSION
set -x \(Platform.Environment.Key.shellName.rawValue) fish
set -x \(Platform.Environment.Key.shellVersion.rawValue) $FISH_VERSION

set -l tokens (\(tokensFunctionName) -p)
if test -z (\(tokensFunctionName) -t)
Expand Down Expand Up @@ -315,8 +313,9 @@ extension String {
) -> Self {
iterationCount == 0
? self
: replacingOccurrences(of: "\\", with: "\\\\")
.replacingOccurrences(of: "'", with: "\\'")
: self
.replacing("\\", with: "\\\\")
.replacing("'", with: "\\'")
.fishEscapeForSingleQuotedString(iterationCount: iterationCount - 1)
}
}
Expand Down
32 changes: 14 additions & 18 deletions Sources/ArgumentParser/Completions/ZshCompletionsGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@

#if compiler(>=6.0)
internal import ArgumentParserToolInfo
internal import Foundation
#else
import ArgumentParserToolInfo
import Foundation
#endif

extension ToolInfoV0 {
Expand Down Expand Up @@ -111,10 +109,10 @@ extension CommandInfoV0 {
setopt extendedglob nullglob numericglobsort
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 -xr \(Platform.Environment.Key.shellName.rawValue)=zsh
local -x \(Platform.Environment.Key.shellVersion.rawValue)
\(Platform.Environment.Key.shellVersion.rawValue)="$(builtin emulate zsh -c 'printf %s "${ZSH_VERSION}"')"
local -r \(Platform.Environment.Key.shellVersion.rawValue)

local context state state_descr line
local -A opt_args
Expand Down Expand Up @@ -271,19 +269,17 @@ extension ArgumentInfoV0 {

extension String {
fileprivate func zshEscapeForSingleQuotedDescribeCompletion() -> String {
replacingOccurrences(
of: #"[:\\]"#,
with: #"\\$0"#,
options: .regularExpression
)
.shellEscapeForSingleQuotedString()
self
.replacing("\\", with: "\\\\")
.replacing(":", with: "\\:")
.shellEscapeForSingleQuotedString()
}
fileprivate func zshEscapeForSingleQuotedOptionSpec() -> String {
replacingOccurrences(
of: #"[:\\\[\]]"#,
with: #"\\$0"#,
options: .regularExpression
)
.shellEscapeForSingleQuotedString()
self
.replacing("\\", with: "\\\\")
.replacing(":", with: "\\:")
.replacing("[", with: "\\[")
.replacing("]", with: "\\]")
.shellEscapeForSingleQuotedString()
}
}
17 changes: 6 additions & 11 deletions Sources/ArgumentParser/Parsable Types/ParsableArguments.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,6 @@
//
//===----------------------------------------------------------------------===//

#if compiler(>=6.0)
internal import Foundation
#else
import Foundation
#endif

/// A type that can be parsed from a program's command-line arguments.
///
/// When you implement a `ParsableArguments` type, all properties must be declared with
Expand All @@ -41,13 +35,14 @@ struct _WrappedParsableCommand<P: ParsableArguments>: ParsableCommand {

// If the type is named something like "TransformOptions", we only want
// to use "transform" as the command name.
if let optionsRange = name.range(of: "_options"),
optionsRange.upperBound == name.endIndex
{
return String(name[..<optionsRange.lowerBound])
} else {
guard
let matchRange = name.firstMatch(of: "_options", at: name.startIndex),
matchRange.end == name.endIndex
else {
return name
}

return String(name[..<matchRange.start])
}

@OptionGroup var options: P
Expand Down
3 changes: 2 additions & 1 deletion Sources/ArgumentParser/Parsing/ArgumentDefinition.swift
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,8 @@ extension ArgumentDefinition {
help: ArgumentHelp?,
defaultValueDescription: String?,
parsingStrategy: ParsingStrategy,
parser: @escaping (InputKey, InputOrigin, Name?, String) throws ->
parser:
@escaping (InputKey, InputOrigin, Name?, String) throws ->
Container.Contained,
initial: Container.Initial?,
completion: CompletionKind?
Expand Down
44 changes: 7 additions & 37 deletions Sources/ArgumentParser/Parsing/CommandParser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,10 @@
#if canImport(Dispatch)
@preconcurrency private import class Dispatch.DispatchSemaphore
#endif
internal import class Foundation.NSLock
internal import class Foundation.ProcessInfo
#else
#if canImport(Dispatch)
@preconcurrency import class Dispatch.DispatchSemaphore
#endif
import class Foundation.NSLock
import class Foundation.ProcessInfo
#endif

struct CommandError: Error {
Expand Down Expand Up @@ -455,16 +451,13 @@ extension CommandParser {
_ argument: ArgumentDefinition,
forArguments args: [String]
) throws {
let environment = ProcessInfo.processInfo.environment
if let completionShellName = environment[
CompletionShell.shellEnvironmentVariableName]
{
if let completionShellName = Platform.Environment[.shellName] {
let shell = CompletionShell(rawValue: completionShellName)
CompletionShell._requesting.withLock { $0 = shell }
}

CompletionShell._requestingVersion.withLock {
$0 = environment[CompletionShell.shellVersionEnvironmentVariableName]
$0 = Platform.Environment[.shellVersion]
}

let completions: [String]
Expand Down Expand Up @@ -550,46 +543,23 @@ private func asyncCustomCompletions(
let (args, completingArgumentIndex, completingPrefix) =
try parseCustomCompletionArguments(from: args)

let completionsBox = SendableBox<[String]>([])
let completionsBox = Mutex<[String]>([])
let semaphore = DispatchSemaphore(value: 0)

Task {
completionsBox.value = await complete(
let completion = await complete(
args,
completingArgumentIndex,
completingPrefix
)
completingPrefix)
completionsBox.withLock { $0 = completion }
semaphore.signal()
}

semaphore.wait()
return completionsBox.value
return completionsBox.withLock { $0 }
#endif
}

// Helper class to make values sendable across concurrency boundaries
private final class SendableBox<T>: @unchecked Sendable {
private let lock = NSLock()
private var _value: T

init(_ value: T) {
self._value = value
}

var value: T {
get {
lock.lock()
defer { lock.unlock() }
return _value
}
set {
lock.lock()
defer { lock.unlock() }
_value = newValue
}
}
}

// MARK: Building Command Stacks

extension CommandParser {
Expand Down
8 changes: 1 addition & 7 deletions Sources/ArgumentParser/Usage/DumpHelpGenerator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@

#if compiler(>=6.0)
internal import ArgumentParserToolInfo
internal import class Foundation.JSONEncoder
#else
import ArgumentParserToolInfo
import class Foundation.JSONEncoder
#endif

internal struct DumpHelpGenerator {
Expand All @@ -29,11 +27,7 @@ internal struct DumpHelpGenerator {
}

func rendered() -> String {
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
encoder.outputFormatting.insert(.sortedKeys)
guard let encoded = try? encoder.encode(self.toolInfo) else { return "" }
return String(data: encoded, encoding: .utf8) ?? ""
JSONEncoder.encode(self.toolInfo)
}
}

Expand Down
Loading
Loading