From 3d6df7cf3747187f0fd29643c1165f5de5057567 Mon Sep 17 00:00:00 2001 From: Nate Cook Date: Mon, 10 Oct 2022 17:50:20 -0500 Subject: [PATCH] [NFC] Sequester platform-specific code (#504) --- Sources/ArgumentParser/CMakeLists.txt | 1 + .../Completions/CompletionsGenerator.swift | 19 +-- .../Parsable Properties/Errors.swift | 26 +-- .../Parsable Types/ParsableArguments.swift | 27 +--- .../ArgumentParser/Usage/HelpGenerator.swift | 42 +---- .../ArgumentParser/Utilities/Platform.swift | 150 ++++++++++++++++++ 6 files changed, 159 insertions(+), 106 deletions(-) create mode 100644 Sources/ArgumentParser/Utilities/Platform.swift diff --git a/Sources/ArgumentParser/CMakeLists.txt b/Sources/ArgumentParser/CMakeLists.txt index 1ad96931b..557873c78 100644 --- a/Sources/ArgumentParser/CMakeLists.txt +++ b/Sources/ArgumentParser/CMakeLists.txt @@ -41,6 +41,7 @@ add_library(ArgumentParser Usage/UsageGenerator.swift Utilities/CollectionExtensions.swift + Utilities/Platform.swift Utilities/SequenceExtensions.swift Utilities/StringExtensions.swift Utilities/Tree.swift) diff --git a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift index 5150f69a8..d411cff4a 100644 --- a/Sources/ArgumentParser/Completions/CompletionsGenerator.swift +++ b/Sources/ArgumentParser/Completions/CompletionsGenerator.swift @@ -9,16 +9,6 @@ // //===----------------------------------------------------------------------===// -#if canImport(Glibc) -import Glibc -#elseif canImport(Darwin) -import Darwin -#elseif canImport(CRT) -import CRT -#elseif canImport(WASILibc) -import WASILibc -#endif - /// A shell for which the parser can generate a completion script. public struct CompletionShell: RawRepresentable, Hashable, CaseIterable { public var rawValue: String @@ -44,14 +34,7 @@ public struct CompletionShell: RawRepresentable, Hashable, CaseIterable { /// Returns an instance representing the current shell, if recognized. public static func autodetected() -> CompletionShell? { -#if os(Windows) - return nil -#else - // FIXME: This retrieves the user's preferred shell, not necessarily the one currently in use. - guard let shellVar = getenv("SHELL") else { return nil } - let shellParts = String(cString: shellVar).split(separator: "/") - return CompletionShell(rawValue: String(shellParts.last ?? "")) -#endif + Platform.shellName.flatMap(CompletionShell.init(rawValue:)) } /// An array of all supported shells for completion scripts. diff --git a/Sources/ArgumentParser/Parsable Properties/Errors.swift b/Sources/ArgumentParser/Parsable Properties/Errors.swift index c08b008e7..28070d1f5 100644 --- a/Sources/ArgumentParser/Parsable Properties/Errors.swift +++ b/Sources/ArgumentParser/Parsable Properties/Errors.swift @@ -9,20 +9,6 @@ // //===----------------------------------------------------------------------===// -#if canImport(Glibc) -import Glibc -#elseif canImport(Darwin) -import Darwin -#elseif canImport(CRT) -import CRT -#elseif canImport(WASILibc) -import WASILibc -#endif - -#if os(Windows) -import let WinSDK.ERROR_BAD_ARGUMENTS -#endif - /// An error type that is presented to the user as an error with parsing their /// command-line input. public struct ValidationError: Error, CustomStringConvertible { @@ -60,19 +46,13 @@ public struct ExitCode: Error, RawRepresentable, Hashable { } /// An exit code that indicates successful completion of a command. - public static let success = ExitCode(EXIT_SUCCESS) + public static let success = ExitCode(Platform.exitCodeSuccess) /// An exit code that indicates that the command failed. - public static let failure = ExitCode(EXIT_FAILURE) + public static let failure = ExitCode(Platform.exitCodeFailure) /// An exit code that indicates that the user provided invalid input. -#if os(Windows) - public static let validationFailure = ExitCode(ERROR_BAD_ARGUMENTS) -#elseif os(WASI) - public static let validationFailure = ExitCode(EXIT_FAILURE) -#else - public static let validationFailure = ExitCode(EX_USAGE) -#endif + public static let validationFailure = ExitCode(Platform.exitCodeValidationFailure) /// A Boolean value indicating whether this exit code represents the /// successful completion of a command. diff --git a/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift b/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift index 22657e2ca..b2e572a6e 100644 --- a/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift +++ b/Sources/ArgumentParser/Parsable Types/ParsableArguments.swift @@ -9,19 +9,6 @@ // //===----------------------------------------------------------------------===// -#if canImport(Glibc) -import Glibc -let _exit: (Int32) -> Never = Glibc.exit -#elseif canImport(Darwin) -import Darwin -let _exit: (Int32) -> Never = Darwin.exit -#elseif canImport(CRT) -import CRT -let _exit: (Int32) -> Never = ucrt._exit -#elseif canImport(WASILibc) -import WASILibc -#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 @@ -60,14 +47,6 @@ struct _WrappedParsableCommand: ParsableCommand { @OptionGroup var options: P } -struct StandardError: TextOutputStream { - mutating func write(_ string: String) { - for byte in string.utf8 { putc(numericCast(byte), stderr) } - } -} - -var standardError = StandardError() - extension ParsableArguments { public mutating func validate() throws {} @@ -208,7 +187,7 @@ extension ParsableArguments { withError error: Error? = nil ) -> Never { guard let error = error else { - _exit(ExitCode.success.rawValue) + Platform.exit(ExitCode.success.rawValue) } let messageInfo = MessageInfo(error: error, type: self) @@ -217,10 +196,10 @@ extension ParsableArguments { if messageInfo.shouldExitCleanly { print(fullText) } else { - print(fullText, to: &standardError) + print(fullText, to: &Platform.standardError) } } - _exit(messageInfo.exitCode.rawValue) + Platform.exit(messageInfo.exitCode.rawValue) } /// Parses a new instance of this type from command-line arguments or exits diff --git a/Sources/ArgumentParser/Usage/HelpGenerator.swift b/Sources/ArgumentParser/Usage/HelpGenerator.swift index 26ff88118..7bc6020c6 100644 --- a/Sources/ArgumentParser/Usage/HelpGenerator.swift +++ b/Sources/ArgumentParser/Usage/HelpGenerator.swift @@ -12,7 +12,7 @@ internal struct HelpGenerator { static var helpIndent = 2 static var labelColumnWidth = 26 - static var systemScreenWidth: Int { _terminalSize().width } + static var systemScreenWidth: Int { Platform.terminalWidth } struct Section { struct Element: Hashable { @@ -386,43 +386,3 @@ internal extension BidirectionalCollection where Element == ParsableCommand.Type return arguments } } - -#if canImport(Glibc) -import Glibc -func ioctl(_ a: Int32, _ b: Int32, _ p: UnsafeMutableRawPointer) -> Int32 { - ioctl(CInt(a), UInt(b), p) -} -#elseif canImport(Darwin) -import Darwin -#elseif canImport(CRT) -import CRT -import WinSDK -#endif - -func _terminalSize() -> (width: Int, height: Int) { -#if os(WASI) - // WASI doesn't yet support terminal size - return (80, 25) -#elseif os(Windows) - var csbi: CONSOLE_SCREEN_BUFFER_INFO = CONSOLE_SCREEN_BUFFER_INFO() - guard GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi) else { - return (80, 25) - } - return (width: Int(csbi.srWindow.Right - csbi.srWindow.Left) + 1, - height: Int(csbi.srWindow.Bottom - csbi.srWindow.Top) + 1) -#else - var w = winsize() -#if os(OpenBSD) - // TIOCGWINSZ is a complex macro, so we need the flattened value. - let tiocgwinsz = Int32(0x40087468) - let err = ioctl(STDOUT_FILENO, tiocgwinsz, &w) -#else - let err = ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) -#endif - let width = Int(w.ws_col) - let height = Int(w.ws_row) - guard err == 0 else { return (80, 25) } - return (width: width > 0 ? width : 80, - height: height > 0 ? height : 25) -#endif -} diff --git a/Sources/ArgumentParser/Utilities/Platform.swift b/Sources/ArgumentParser/Utilities/Platform.swift new file mode 100644 index 000000000..b0d20cd0d --- /dev/null +++ b/Sources/ArgumentParser/Utilities/Platform.swift @@ -0,0 +1,150 @@ +//===----------------------------------------------------------*- 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 +// +//===----------------------------------------------------------------------===// + +#if canImport(Glibc) +import Glibc +#elseif canImport(Darwin) +import Darwin +#elseif canImport(CRT) +import CRT +#elseif canImport(WASILibc) +import WASILibc +#endif + +enum Platform {} + +// MARK: Shell + +extension Platform { + /// The name of the user's preferred shell, if detectable from the + /// environment. + static var shellName: String? { +#if os(Windows) + return nil +#else + // FIXME: This retrieves the user's preferred shell, not necessarily the one currently in use. + guard let shellVar = getenv("SHELL") else { return nil } + let shellParts = String(cString: shellVar).split(separator: "/") + return shellParts.last.map(String.init) +#endif + } +} + +// MARK: Exit codes + +#if os(Windows) +import let WinSDK.ERROR_BAD_ARGUMENTS +#endif + +extension Platform { + /// The code for successful exit. + static var exitCodeSuccess: Int32 { + EXIT_SUCCESS + } + + /// The code for exit with a general failure. + static var exitCodeFailure: Int32 { + EXIT_FAILURE + } + + /// The code for exit with a validation failure. + static var exitCodeValidationFailure: Int32 { +#if os(Windows) + return ERROR_BAD_ARGUMENTS +#elseif os(WASI) + return EXIT_FAILURE +#else + return EX_USAGE +#endif + } +} + +// MARK: Exit function + +extension Platform { + /// Complete execution with the given exit code. + static func exit(_ code: Int32) -> Never { +#if canImport(Glibc) + Glibc.exit(code) +#elseif canImport(Darwin) + Darwin.exit(code) +#elseif canImport(CRT) + ucrt._exit(code) +#elseif canImport(WASILibc) + exit(code) +#endif + } +} + +// MARK: Standard error + +extension Platform { + /// A type that represents the `stderr` output stream. + struct StandardError: TextOutputStream { + mutating func write(_ string: String) { + for byte in string.utf8 { putc(numericCast(byte), stderr) } + } + } + + /// The `stderr` output stream. + static var standardError = StandardError() +} + +// MARK: Terminal size + +#if canImport(Glibc) +func ioctl(_ a: Int32, _ b: Int32, _ p: UnsafeMutableRawPointer) -> Int32 { + ioctl(CInt(a), UInt(b), p) +} +#endif + +extension Platform { + /// The default terminal size. + static var defaultTerminalSize: (width: Int, height: Int) { + (80, 25) + } + + /// Returns the current terminal size, or the default if the size is + /// unavailable. + static func terminalSize() -> (width: Int, height: Int) { +#if os(WASI) + // WASI doesn't yet support terminal size + return defaultTerminalSize +#elseif os(Windows) + var csbi: CONSOLE_SCREEN_BUFFER_INFO = CONSOLE_SCREEN_BUFFER_INFO() + guard GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi) else { + return defaultTerminalSize + } + return (width: Int(csbi.srWindow.Right - csbi.srWindow.Left) + 1, + height: Int(csbi.srWindow.Bottom - csbi.srWindow.Top) + 1) +#else + var w = winsize() +#if os(OpenBSD) + // TIOCGWINSZ is a complex macro, so we need the flattened value. + let tiocgwinsz = Int32(0x40087468) + let err = ioctl(STDOUT_FILENO, tiocgwinsz, &w) +#else + let err = ioctl(STDOUT_FILENO, TIOCGWINSZ, &w) +#endif + let width = Int(w.ws_col) + let height = Int(w.ws_row) + guard err == 0 else { return defaultTerminalSize } + return (width: width > 0 ? width : defaultTerminalSize.width, + height: height > 0 ? height : defaultTerminalSize.height) +#endif + } + + /// The current terminal size, or the default if the width is unavailable. + static var terminalWidth: Int { + terminalSize().width + } +} +