├── CommandLineKit.xcodeproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── CommandLineKit.xcscheme │ └── CommandLineKitDemo.xcscheme ├── CommandLineKitDemo └── main.swift ├── CHANGELOG.md ├── Tests ├── CommandLineKitTests │ ├── Info.plist │ ├── AnsiCodesTests.swift │ ├── LineReaderHistoryTests.swift │ ├── EditStateTests.swift │ └── FlagTests.swift └── LinuxMain.swift ├── Sources ├── CommandLineKit │ ├── Info.plist │ ├── LineReaderError.swift │ ├── CommandLineKit.h │ ├── TextStyle.swift │ ├── ControlCharacters.swift │ ├── FlagError.swift │ ├── BackgroundColor.swift │ ├── ConvertibleFromString.swift │ ├── LineReaderHistory.swift │ ├── AnsiCodes.swift │ ├── TextColor.swift │ ├── Command.swift │ ├── TextProperties.swift │ ├── Terminal.swift │ ├── FlagWrapper.swift │ ├── EditState.swift │ ├── Flag.swift │ ├── Flags.swift │ └── LineReader.swift └── CommandLineKitDemo │ ├── Info.plist │ ├── LinuxMain.swift │ └── main.swift ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── Package.swift └── README.md /CommandLineKit.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /CommandLineKitDemo/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // CommandLineKitDemo 4 | // 5 | // Created by Matthias Zenger on 26/10/2023. 6 | // Copyright © 2023 Matthias Zenger. All rights reserved. 7 | // 8 | 9 | import Foundation 10 | 11 | print("Hello, World!") 12 | 13 | -------------------------------------------------------------------------------- /CommandLineKit.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | # 0.3.5 (2023-01-29) 4 | - Be more clever about detecting color terminals 5 | - Migrate project to Xcode 14.2 6 | 7 | ## 0.3.4 (2021-05-12) 8 | - Minor bug fixes 9 | - Migrated project to Xcode 12.5 10 | 11 | ## 0.3.3 (2020-10-04) 12 | - Ported code to Swift 5.3 13 | - Migrated project to Xcode 12.0 14 | 15 | ## 0.3.2 (2020-02-01) 16 | - Fixed line reader to handle buffered output 17 | - Migrated project to Xcode 11.3 18 | 19 | ## 0.3.1 (2019-09-23) 20 | - Migrated project to Xcode 11.0 21 | - Ported code to Swift 5.1 22 | 23 | ## 0.3 (2019-03-30) 24 | - Migrated library to Xcode 10.2 25 | - Ported code to Swift 5 26 | 27 | ## 0.2 (2018-06-10) 28 | - Bug fixes 29 | - Support of usage descriptions 30 | - Initial documentation 31 | 32 | ## 0.1 (2018-05-14) 33 | - Initial version 34 | -------------------------------------------------------------------------------- /Tests/CommandLineKitTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 0.3.4 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /Sources/CommandLineKit/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 0.3.4 19 | CFBundleVersion 20 | $(CURRENT_PROJECT_VERSION) 21 | NSHumanReadableCopyright 22 | Copyright © 2018-2021 Google LLC 23 | NSPrincipalClass 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /Sources/CommandLineKitDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | Copyright © 2018-2023 Google LLC 27 | 28 | 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 4 | 5 | ## Build generated 6 | build/ 7 | DerivedData 8 | 9 | ## Various settings 10 | *.pbxuser 11 | !default.pbxuser 12 | *.mode1v3 13 | !default.mode1v3 14 | *.mode2v3 15 | !default.mode2v3 16 | *.perspectivev3 17 | !default.perspectivev3 18 | xcuserdata 19 | 20 | ## Other 21 | *.xccheckout 22 | *.moved-aside 23 | *.xcuserstate 24 | *.xcscmblueprint 25 | 26 | ## Obj-C/Swift specific 27 | *.hmap 28 | *.ipa 29 | 30 | ## Playgrounds 31 | timeline.xctimeline 32 | playground.xcworkspace 33 | 34 | # Swift Package Manager 35 | .build 36 | Packages 37 | 38 | # CocoaPods 39 | # 40 | # We recommend against adding the Pods directory to your .gitignore. However 41 | # you should judge for yourself, the pros and cons are mentioned at: 42 | # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control 43 | # 44 | # Pods/ 45 | 46 | # Carthage 47 | # 48 | # Add this line if you want to avoid checking in source code from Carthage dependencies. 49 | # Carthage/Checkouts 50 | 51 | Carthage/Build 52 | 53 | # macOS 54 | .DS_Store 55 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We'd love to accept your patches and contributions to this project. There are 4 | just a few small guidelines you need to follow. 5 | 6 | ## Contributor License Agreement 7 | 8 | Contributions to this project must be accompanied by a Contributor License 9 | Agreement. You (or your employer) retain the copyright to your contribution, 10 | this simply gives us permission to use and redistribute your contributions as 11 | part of the project. Head over to to see 12 | your current agreements on file or to sign a new one. 13 | 14 | You generally only need to submit a CLA once, so if you've already submitted one 15 | (even if it was for a different project), you probably don't need to do it 16 | again. 17 | 18 | ## Code reviews 19 | 20 | All submissions, including submissions by project members, require review. We 21 | use GitHub pull requests for this purpose. Consult 22 | [GitHub Help](https://help.github.com/articles/about-pull-requests/) for more 23 | information on using pull requests. 24 | 25 | ## Community Guidelines 26 | 27 | This project follows [Google's Open Source Community 28 | Guidelines](https://opensource.google.com/conduct/). 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright © 2018-2019 Google LLC 2 | Copyright © 2017 Andy Best 3 | Copyright © 2010-2014 Salvatore Sanfilippo 4 | Copyright © 2010-2013 Pieter Noordhuis 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, 10 | this list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its contributors 17 | may be used to endorse or promote products derived from this software without 18 | specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 21 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 22 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 24 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 25 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 26 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 27 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 29 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Sources/CommandLineKitDemo/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LinuxMain.swift 3 | // CommandLineKitDemo 4 | // 5 | // Created by Matthias Zenger on 02/06/2018. 6 | // Copyright © 2018-2019 Google LLC 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions are met: 10 | // 11 | // * Redistributions of source code must retain the above copyright notice, 12 | // this list of conditions and the following disclaimer. 13 | // 14 | // * Redistributions in binary form must reproduce the above copyright notice, 15 | // this list of conditions and the following disclaimer in the documentation 16 | // and/or other materials provided with the distribution. 17 | // 18 | // * Neither the name of the copyright holder nor the names of its contributors 19 | // may be used to endorse or promote products derived from this software without 20 | // specific prior written permission. 21 | // 22 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 26 | // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 29 | // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | // 33 | 34 | #if os(Linux) 35 | @main struct CommandLineKitDemo { 36 | static func main() { 37 | demo() 38 | } 39 | } 40 | #endif 41 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LinuxMain.swift 3 | // CommandLineKitTests 4 | // 5 | // Created by Matthias Zenger on 02/06/2018. 6 | // Copyright © 2018-2019 Google LLC 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions are met: 10 | // 11 | // * Redistributions of source code must retain the above copyright notice, 12 | // this list of conditions and the following disclaimer. 13 | // 14 | // * Redistributions in binary form must reproduce the above copyright notice, 15 | // this list of conditions and the following disclaimer in the documentation 16 | // and/or other materials provided with the distribution. 17 | // 18 | // * Neither the name of the copyright holder nor the names of its contributors 19 | // may be used to endorse or promote products derived from this software without 20 | // specific prior written permission. 21 | // 22 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 26 | // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 29 | // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | // 33 | 34 | #if os(Linux) 35 | 36 | import XCTest 37 | @testable import CommandLineKitTests 38 | 39 | XCTMain( 40 | [ 41 | testCase(FlagTests.allTests), 42 | testCase(LineReaderHistoryTests.allTests), 43 | testCase(EditStateTests.allTests), 44 | testCase(AnsiCodesTests.allTests), 45 | ] 46 | ) 47 | 48 | #endif 49 | 50 | -------------------------------------------------------------------------------- /Sources/CommandLineKit/LineReaderError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LineReaderError.swift 3 | // CommandLineKit 4 | // 5 | // Created by Matthias Zenger on 07/04/2018. 6 | // Copyright © 2018-2019 Google LLC 7 | // Copyright © 2017 Andy Best 8 | // Copyright © 2010-2014 Salvatore Sanfilippo 9 | // Copyright © 2010-2013 Pieter Noordhuis 10 | // 11 | // Redistribution and use in source and binary forms, with or without 12 | // modification, are permitted provided that the following conditions are met: 13 | // 14 | // * Redistributions of source code must retain the above copyright notice, 15 | // this list of conditions and the following disclaimer. 16 | // 17 | // * Redistributions in binary form must reproduce the above copyright notice, 18 | // this list of conditions and the following disclaimer in the documentation 19 | // and/or other materials provided with the distribution. 20 | // 21 | // * Neither the name of the copyright holder nor the names of its contributors 22 | // may be used to endorse or promote products derived from this software without 23 | // specific prior written permission. 24 | // 25 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 26 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 27 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 28 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 29 | // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 30 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 31 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 32 | // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 33 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 34 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 35 | // 36 | 37 | import Foundation 38 | 39 | public enum LineReaderError: Error { 40 | case generalError(String) 41 | case EOF 42 | case CTRLC 43 | } 44 | -------------------------------------------------------------------------------- /Sources/CommandLineKit/CommandLineKit.h: -------------------------------------------------------------------------------- 1 | // 2 | // CommandLineKit.h 3 | // CommandLineKit 4 | // 5 | // Created by Matthias Zenger on 08/04/2018. 6 | // Copyright © 2018-2019 Google LLC 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions are met: 10 | // 11 | // * Redistributions of source code must retain the above copyright notice, 12 | // this list of conditions and the following disclaimer. 13 | // 14 | // * Redistributions in binary form must reproduce the above copyright notice, 15 | // this list of conditions and the following disclaimer in the documentation 16 | // and/or other materials provided with the distribution. 17 | // 18 | // * Neither the name of the copyright holder nor the names of its contributors 19 | // may be used to endorse or promote products derived from this software without 20 | // specific prior written permission. 21 | // 22 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 26 | // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 29 | // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | // 33 | 34 | #import 35 | 36 | //! Project version number for CommandLineKit. 37 | FOUNDATION_EXPORT double CommandLineKitVersionNumber; 38 | 39 | //! Project version string for CommandLineKit. 40 | FOUNDATION_EXPORT const unsigned char CommandLineKitVersionString[]; 41 | 42 | // In this header, you should import all the public headers of your framework using statements like #import 43 | 44 | 45 | -------------------------------------------------------------------------------- /Sources/CommandLineKit/TextStyle.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextStyle.swift 3 | // CommandLineKit 4 | // 5 | // Created by Matthias Zenger on 18/04/2018. 6 | // Copyright © 2018-2019 Google LLC 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions are met: 10 | // 11 | // * Redistributions of source code must retain the above copyright notice, 12 | // this list of conditions and the following disclaimer. 13 | // 14 | // * Redistributions in binary form must reproduce the above copyright notice, 15 | // this list of conditions and the following disclaimer in the documentation 16 | // and/or other materials provided with the distribution. 17 | // 18 | // * Neither the name of the copyright holder nor the names of its contributors 19 | // may be used to endorse or promote products derived from this software without 20 | // specific prior written permission. 21 | // 22 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 26 | // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 29 | // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | // 33 | 34 | import Foundation 35 | 36 | /// 37 | /// Enumeration of all supported text styles. 38 | /// 39 | public enum TextStyle: UInt8, Hashable { 40 | case `default` = 0 41 | case bold = 1 42 | case dim = 2 43 | case italic = 3 44 | case underline = 4 45 | case blink = 5 46 | case swap = 7 47 | 48 | public var code: UInt8 { 49 | return self.rawValue 50 | } 51 | 52 | public var properties: TextProperties { 53 | return TextProperties(textStyles: [self]) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /Sources/CommandLineKit/ControlCharacters.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ControlCharacters.swift 3 | // CommandLineKit 4 | // 5 | // Created by Matthias Zenger on 07/04/2018. 6 | // Copyright © 2018-2019 Google LLC 7 | // Copyright © 2017 Andy Best 8 | // Copyright © 2010-2014, Salvatore Sanfilippo 9 | // Copyright © 2010-2013, Pieter Noordhuis 10 | // 11 | // Redistribution and use in source and binary forms, with or without 12 | // modification, are permitted provided that the following conditions are met: 13 | // 14 | // * Redistributions of source code must retain the above copyright notice, 15 | // this list of conditions and the following disclaimer. 16 | // 17 | // * Redistributions in binary form must reproduce the above copyright notice, 18 | // this list of conditions and the following disclaimer in the documentation 19 | // and/or other materials provided with the distribution. 20 | // 21 | // * Neither the name of the copyright holder nor the names of its contributors 22 | // may be used to endorse or promote products derived from this software without 23 | // specific prior written permission. 24 | // 25 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 26 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 27 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 28 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 29 | // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 30 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 31 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 32 | // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 33 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 34 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 35 | // 36 | 37 | import Foundation 38 | 39 | public enum ControlCharacters: UInt8 { 40 | case Null = 0 41 | case CtrlA = 1 42 | case CtrlB = 2 43 | case CtrlC = 3 44 | case CtrlD = 4 45 | case CtrlE = 5 46 | case CtrlF = 6 47 | case Bell = 7 48 | case CtrlH = 8 49 | case Tab = 9 50 | case CtrlK = 11 51 | case CtrlL = 12 52 | case Enter = 13 53 | case CtrlN = 14 54 | case CtrlP = 16 55 | case CtrlT = 20 56 | case CtrlU = 21 57 | case CtrlW = 23 58 | case Esc = 27 59 | case Backspace = 127 60 | 61 | var character: Character { 62 | return Character(UnicodeScalar(Int(self.rawValue))!) 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Sources/CommandLineKit/FlagError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlagError.swift 3 | // CommandLineKit 4 | // 5 | // Created by Matthias Zenger on 25/03/2017. 6 | // Copyright © 2018-2019 Google LLC 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions are met: 10 | // 11 | // * Redistributions of source code must retain the above copyright notice, 12 | // this list of conditions and the following disclaimer. 13 | // 14 | // * Redistributions in binary form must reproduce the above copyright notice, 15 | // this list of conditions and the following disclaimer in the documentation 16 | // and/or other materials provided with the distribution. 17 | // 18 | // * Neither the name of the copyright holder nor the names of its contributors 19 | // may be used to endorse or promote products derived from this software without 20 | // specific prior written permission. 21 | // 22 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 26 | // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 29 | // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | // 33 | 34 | import Foundation 35 | 36 | /// 37 | /// Enum `FlagError` defines all the errors potentially returned when parsing command-line 38 | /// arguments. Each flag error consists of a `kind` defining the type of the error as well 39 | /// as an optional `flag` referring to the flag to which the error belongs. 40 | /// 41 | public struct FlagError: Error { 42 | 43 | /// The description of the error. 44 | public enum Kind { 45 | case unknownFlag(String) 46 | case missingValue 47 | case malformedValue(String) 48 | case illegalFlagCombination(String) 49 | case tooManyValues(String) 50 | } 51 | 52 | /// The error kind. 53 | public let kind: Kind 54 | 55 | /// The flag to which the error belongs. If `flag` is set to nil, the error is related to 56 | /// parsing the command-line as a whole. 57 | public let flag: Flag? 58 | 59 | /// Initializes a new flag error from an error kind and a corresponding flag. 60 | public init(_ kind: Kind, _ flag: Flag? = nil) { 61 | self.kind = kind 62 | self.flag = flag 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.4 2 | // 3 | // Package.swift 4 | // CommandLineKit 5 | // 6 | // Build targets by calling the Swift Package Manager in the following way for debug purposes: 7 | // swift build 8 | // 9 | // A release can be built with these options: 10 | // swift build -c release 11 | // 12 | // Created by Matthias Zenger on 06/05/2017. 13 | // Copyright © 2018-2023 Google LLC 14 | // 15 | // Redistribution and use in source and binary forms, with or without 16 | // modification, are permitted provided that the following conditions are met: 17 | // 18 | // * Redistributions of source code must retain the above copyright notice, 19 | // this list of conditions and the following disclaimer. 20 | // 21 | // * Redistributions in binary form must reproduce the above copyright notice, 22 | // this list of conditions and the following disclaimer in the documentation 23 | // and/or other materials provided with the distribution. 24 | // 25 | // * Neither the name of the copyright holder nor the names of its contributors 26 | // may be used to endorse or promote products derived from this software without 27 | // specific prior written permission. 28 | // 29 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 30 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 31 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 32 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 33 | // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 34 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 35 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 36 | // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 37 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 38 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 39 | // 40 | 41 | import PackageDescription 42 | 43 | let package = Package( 44 | name: "CommandLineKit", 45 | platforms: [ 46 | .macOS(.v11) 47 | ], 48 | products: [ 49 | .library(name: "CommandLineKit", targets: ["CommandLineKit"]), 50 | .executable(name: "CommandLineKitDemo", targets: ["CommandLineKitDemo"]) 51 | ], 52 | dependencies: [ 53 | ], 54 | targets: [ 55 | .target(name: "CommandLineKit", 56 | dependencies: [], 57 | exclude: ["Info.plist"]), 58 | .executableTarget(name: "CommandLineKitDemo", 59 | dependencies: ["CommandLineKit"], 60 | exclude: []), 61 | .testTarget(name: "CommandLineKitTests", 62 | dependencies: ["CommandLineKit"], 63 | exclude: ["Info.plist"]) 64 | ], 65 | swiftLanguageVersions: [.v5] 66 | ) 67 | -------------------------------------------------------------------------------- /Tests/CommandLineKitTests/AnsiCodesTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnsiCodesTests.swift 3 | // CommandLineKitTests 4 | // 5 | // Created by Matthias Zenger on 08/04/2018. 6 | // Copyright © 2018-2019 Google LLC 7 | // Copyright © 2017 Andy Best 8 | // Copyright © 2010-2014 Salvatore Sanfilippo 9 | // Copyright © 2010-2013 Pieter Noordhuis 10 | // 11 | // Redistribution and use in source and binary forms, with or without 12 | // modification, are permitted provided that the following conditions are met: 13 | // 14 | // * Redistributions of source code must retain the above copyright notice, 15 | // this list of conditions and the following disclaimer. 16 | // 17 | // * Redistributions in binary form must reproduce the above copyright notice, 18 | // this list of conditions and the following disclaimer in the documentation 19 | // and/or other materials provided with the distribution. 20 | // 21 | // * Neither the name of the copyright holder nor the names of its contributors 22 | // may be used to endorse or promote products derived from this software without 23 | // specific prior written permission. 24 | // 25 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 26 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 27 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 28 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 29 | // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 30 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 31 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 32 | // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 33 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 34 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 35 | // 36 | 37 | import XCTest 38 | @testable import CommandLineKit 39 | 40 | class AnsiCodesTests: XCTestCase { 41 | 42 | func testGenerateEscapeCode() { 43 | XCTAssertEqual(AnsiCodes.escapeCode("foo"), "\u{001B}[foo") 44 | } 45 | 46 | func testEraseRight() { 47 | XCTAssertEqual(AnsiCodes.eraseRight, "\u{001B}[0K") 48 | } 49 | 50 | func testCursorForward() { 51 | XCTAssertEqual(AnsiCodes.cursorForward(10), "\u{001B}[10C") 52 | } 53 | 54 | func testClearScreen() { 55 | XCTAssertEqual(AnsiCodes.clearScreen, "\u{001B}[2J") 56 | } 57 | 58 | func testHomeCursor() { 59 | XCTAssertEqual(AnsiCodes.homeCursor, "\u{001B}[H") 60 | } 61 | 62 | static let allTests = [ 63 | ("testGenerateEscapeCode", testGenerateEscapeCode), 64 | ("testEraseRight", testEraseRight), 65 | ("testCursorForward", testCursorForward), 66 | ("testClearScreen", testClearScreen), 67 | ("testHomeCursor", testHomeCursor), 68 | ] 69 | } 70 | -------------------------------------------------------------------------------- /Sources/CommandLineKitDemo/main.swift: -------------------------------------------------------------------------------- 1 | // 2 | // main.swift 3 | // CommandLineKitDemo 4 | // 5 | // Created by Matthias Zenger on 08/04/2018. 6 | // Copyright © 2018-2019 Google LLC 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions are met: 10 | // 11 | // * Redistributions of source code must retain the above copyright notice, 12 | // this list of conditions and the following disclaimer. 13 | // 14 | // * Redistributions in binary form must reproduce the above copyright notice, 15 | // this list of conditions and the following disclaimer in the documentation 16 | // and/or other materials provided with the distribution. 17 | // 18 | // * Neither the name of the copyright holder nor the names of its contributors 19 | // may be used to endorse or promote products derived from this software without 20 | // specific prior written permission. 21 | // 22 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 26 | // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 29 | // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | // 33 | 34 | import Foundation 35 | import CommandLineKit 36 | 37 | func demo() { 38 | print("Detected terminal: \(Terminal.current)") 39 | print(Terminal.fullColorSupport ? "Full color support" : "No color support") 40 | print(LineReader.supportedByTerminal ? "LineReader support" : "No LineReader support") 41 | 42 | if let ln = LineReader() { 43 | ln.setCompletionCallback { currentBuffer in 44 | let completions = [ 45 | "Hello, world!", 46 | "Hello, Linenoise!", 47 | "Swift is Awesome!" 48 | ] 49 | return completions.filter { $0.hasPrefix(currentBuffer) } 50 | } 51 | ln.setHintsCallback { currentBuffer in 52 | let hints = [ 53 | "Carpe Diem", 54 | "Lorem Ipsum", 55 | "Swift is Awesome!" 56 | ] 57 | let filtered = hints.filter { $0.hasPrefix(currentBuffer) } 58 | if let hint = filtered.first { 59 | let hintText = String(hint.dropFirst(currentBuffer.count)) 60 | return (hintText, TextColor.grey.properties) 61 | } else { 62 | return nil 63 | } 64 | } 65 | print("Type 'exit' to quit") 66 | var done = false 67 | while !done { 68 | do { 69 | let output = try ln.readLine(prompt: "> ", 70 | maxCount: 200, 71 | strippingNewline: true, 72 | promptProperties: TextProperties(.green, nil, .bold), 73 | readProperties: TextProperties(.blue, nil), 74 | parenProperties: TextProperties(.red, nil, .bold)) 75 | print("Entered: \(output)") 76 | ln.addHistory(output) 77 | if output == "exit" { 78 | break 79 | } 80 | } catch LineReaderError.CTRLC { 81 | print("\nCaptured CTRL+C. Quitting.") 82 | done = true 83 | } catch { 84 | print(error) 85 | } 86 | } 87 | } 88 | } 89 | 90 | demo() 91 | -------------------------------------------------------------------------------- /CommandLineKit.xcodeproj/xcshareddata/xcschemes/CommandLineKit.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 63 | 69 | 70 | 71 | 72 | 78 | 79 | 85 | 86 | 87 | 88 | 90 | 91 | 94 | 95 | 96 | -------------------------------------------------------------------------------- /Sources/CommandLineKit/BackgroundColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // BackgroundColor.swift 3 | // CommandLineKit 4 | // 5 | // Created by Matthias Zenger on 18/04/2018. 6 | // Copyright © 2018-2019 Google LLC 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions are met: 10 | // 11 | // * Redistributions of source code must retain the above copyright notice, 12 | // this list of conditions and the following disclaimer. 13 | // 14 | // * Redistributions in binary form must reproduce the above copyright notice, 15 | // this list of conditions and the following disclaimer in the documentation 16 | // and/or other materials provided with the distribution. 17 | // 18 | // * Neither the name of the copyright holder nor the names of its contributors 19 | // may be used to endorse or promote products derived from this software without 20 | // specific prior written permission. 21 | // 22 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 26 | // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 29 | // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | // 33 | 34 | import Foundation 35 | 36 | public enum BackgroundColor: Hashable { 37 | case black 38 | case red 39 | case green 40 | case yellow 41 | case blue 42 | case magenta 43 | case cyan 44 | case white 45 | case `default` 46 | case extended(UInt8) 47 | 48 | public init?(colorCode: UInt8, fullColorSupport all256: Bool = false) { 49 | if all256 { 50 | self = .extended(colorCode) 51 | } else { 52 | switch colorCode { 53 | case 40: 54 | self = .black 55 | case 41: 56 | self = .red 57 | case 42: 58 | self = .green 59 | case 43: 60 | self = .yellow 61 | case 44: 62 | self = .blue 63 | case 45: 64 | self = .magenta 65 | case 46: 66 | self = .cyan 67 | case 47: 68 | self = .white 69 | case 49: 70 | self = .default 71 | default: 72 | return nil 73 | } 74 | } 75 | } 76 | 77 | public init(color: (UInt8, UInt8, UInt8), fullColorSupport all256: Bool = false) { 78 | let code = Terminal.closestColor(to: color, fullColorSupport: all256) 79 | if all256 { 80 | self = .extended(code) 81 | } else { 82 | let color: BackgroundColor? = code < 8 ? BackgroundColor(colorCode: code + 40) : nil 83 | self = color ?? .default 84 | } 85 | } 86 | 87 | public var code: UInt8 { 88 | switch self { 89 | case .black: 90 | return 40 91 | case .red: 92 | return 41 93 | case .green: 94 | return 42 95 | case .yellow: 96 | return 43 97 | case .blue: 98 | return 44 99 | case .magenta: 100 | return 45 101 | case .cyan: 102 | return 46 103 | case .white: 104 | return 47 105 | case .default: 106 | return 49 107 | case .extended(let c): 108 | return c 109 | } 110 | } 111 | 112 | public var isExtended: Bool { 113 | guard case .extended(_) = self else { 114 | return false 115 | } 116 | return true 117 | } 118 | 119 | public var properties: TextProperties { 120 | return TextProperties(backgroundColor: self) 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Sources/CommandLineKit/ConvertibleFromString.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ConvertibleFromString.swift 3 | // CommandLineKit 4 | // 5 | // Created by Matthias Zenger on 26/03/2017. 6 | // Copyright © 2018-2019 Google LLC 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions are met: 10 | // 11 | // * Redistributions of source code must retain the above copyright notice, 12 | // this list of conditions and the following disclaimer. 13 | // 14 | // * Redistributions in binary form must reproduce the above copyright notice, 15 | // this list of conditions and the following disclaimer in the documentation 16 | // and/or other materials provided with the distribution. 17 | // 18 | // * Neither the name of the copyright holder nor the names of its contributors 19 | // may be used to endorse or promote products derived from this software without 20 | // specific prior written permission. 21 | // 22 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 26 | // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 29 | // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | // 33 | 34 | import Foundation 35 | 36 | /// 37 | /// Classes and structs implementing protocol `ConvertibleFromString` come with a default 38 | /// initializer which initializes the object/struct by parsing a string. 39 | /// 40 | public protocol ConvertibleFromString { 41 | init?(fromString: String) 42 | } 43 | 44 | extension ConvertibleFromString { 45 | public static func from(string str: String) -> Self? { 46 | return Self.init(fromString: str) 47 | } 48 | } 49 | 50 | extension String: ConvertibleFromString { 51 | public init?(fromString other: String) { 52 | self.init(other) 53 | } 54 | } 55 | 56 | extension Int: ConvertibleFromString { 57 | public init?(fromString str: String) { 58 | self.init(str) 59 | } 60 | } 61 | 62 | extension Int64: ConvertibleFromString { 63 | public init?(fromString str: String) { 64 | self.init(str) 65 | } 66 | } 67 | 68 | extension UInt: ConvertibleFromString { 69 | public init?(fromString str: String) { 70 | self.init(str) 71 | } 72 | } 73 | 74 | extension UInt64: ConvertibleFromString { 75 | public init?(fromString str: String) { 76 | self.init(str) 77 | } 78 | } 79 | 80 | extension Float: ConvertibleFromString { 81 | public init?(fromString str: String) { 82 | self.init(str) 83 | } 84 | } 85 | 86 | extension Double: ConvertibleFromString { 87 | public init?(fromString str: String) { 88 | self.init(str) 89 | } 90 | } 91 | 92 | extension Bool: ConvertibleFromString { 93 | public init?(fromString str: String) { 94 | switch str.lowercased() { 95 | case "true", "t", "yes", "y", "1": 96 | self.init(true) 97 | case "false", "f", "no", "n", "0": 98 | self.init(false) 99 | default: 100 | return nil 101 | } 102 | } 103 | } 104 | 105 | extension RawRepresentable where RawValue: ConvertibleFromString { 106 | public init?(fromString str: String) { 107 | guard let value = RawValue.from(string: str) else { 108 | return nil 109 | } 110 | self.init(rawValue: value) 111 | } 112 | 113 | public static func from(string str: String) -> Self? { 114 | return Self.init(fromString: str) 115 | } 116 | } 117 | 118 | extension Optional: ConvertibleFromString where Wrapped: ConvertibleFromString { 119 | public init?(fromString str: String) { 120 | guard let value = Wrapped.from(string: str) else { 121 | return nil 122 | } 123 | self.init(value) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /Sources/CommandLineKit/LineReaderHistory.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LineReaderHistory.swift 3 | // CommandLineKit 4 | // 5 | // Created by Matthias Zenger on 07/04/2018. 6 | // Copyright © 2018-2021 Google LLC 7 | // Copyright © 2017 Andy Best 8 | // Copyright © 2010-2014 Salvatore Sanfilippo 9 | // Copyright © 2010-2013 Pieter Noordhuis 10 | // 11 | // Redistribution and use in source and binary forms, with or without 12 | // modification, are permitted provided that the following conditions are met: 13 | // 14 | // * Redistributions of source code must retain the above copyright notice, 15 | // this list of conditions and the following disclaimer. 16 | // 17 | // * Redistributions in binary form must reproduce the above copyright notice, 18 | // this list of conditions and the following disclaimer in the documentation 19 | // and/or other materials provided with the distribution. 20 | // 21 | // * Neither the name of the copyright holder nor the names of its contributors 22 | // may be used to endorse or promote products derived from this software without 23 | // specific prior written permission. 24 | // 25 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 26 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 27 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 28 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 29 | // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 30 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 31 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 32 | // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 33 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 34 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 35 | // 36 | 37 | import Foundation 38 | 39 | class LineReaderHistory { 40 | 41 | public enum HistoryDirection: Int { 42 | case previous = -1 43 | case next = 1 44 | } 45 | 46 | var maxLength: UInt = 0 { 47 | didSet { 48 | if history.count > maxLength && maxLength > 0 { 49 | history.removeFirst(history.count - Int(maxLength)) 50 | } 51 | } 52 | } 53 | private var index: Int = 0 54 | 55 | var currentIndex: Int { 56 | return index 57 | } 58 | 59 | private var hasTempItem: Bool = false 60 | 61 | private var history: [String] = [String]() 62 | var historyItems: [String] { 63 | return history 64 | } 65 | 66 | public func add(_ item: String) { 67 | // Don't add a duplicate if the last item is equal to this one 68 | if let lastItem = history.last { 69 | if lastItem == item { 70 | index = history.endIndex 71 | return 72 | } 73 | } 74 | // Remove an item if we have reached maximum length 75 | if maxLength > 0 && history.count >= maxLength { 76 | _ = history.removeFirst() 77 | } 78 | history.append(item) 79 | index = history.endIndex 80 | } 81 | 82 | func replaceCurrent(_ item: String) { 83 | history[index] = item 84 | } 85 | 86 | internal func navigateHistory(direction: HistoryDirection) -> String? { 87 | if history.count == 0 { 88 | return nil 89 | } 90 | switch direction { 91 | case .next: 92 | index += HistoryDirection.next.rawValue 93 | case .previous: 94 | index += HistoryDirection.previous.rawValue 95 | } 96 | // Stop at the beginning and end of history 97 | if index < 0 { 98 | index = 0 99 | return nil 100 | } else if index >= history.count { 101 | index = history.count 102 | return nil 103 | } 104 | return history[index] 105 | } 106 | 107 | internal func save(toFile path: String) throws { 108 | let output = history.joined(separator: "\n") 109 | try output.write(toFile: path, atomically: true, encoding: .utf8) 110 | } 111 | 112 | internal func load(fromFile path: String) throws { 113 | let input = try String(contentsOfFile: path, encoding: .utf8) 114 | 115 | input.split(separator: "\n").forEach { 116 | add(String($0)) 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /CommandLineKit.xcodeproj/xcshareddata/xcschemes/CommandLineKitDemo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 42 | 48 | 49 | 50 | 51 | 52 | 62 | 64 | 70 | 71 | 72 | 73 | 77 | 78 | 79 | 80 | 86 | 88 | 94 | 95 | 96 | 97 | 99 | 100 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /Sources/CommandLineKit/AnsiCodes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AnsiCodes.swift 3 | // CommandLineKit 4 | // 5 | // Created by Matthias Zenger on 07/04/2018. 6 | // Copyright © 2018-2019 Google LLC 7 | // Copyright © 2017 Andy Best 8 | // Copyright © 2010-2014 Salvatore Sanfilippo 9 | // Copyright © 2010-2013 Pieter Noordhuis 10 | // 11 | // Redistribution and use in source and binary forms, with or without 12 | // modification, are permitted provided that the following conditions are met: 13 | // 14 | // * Redistributions of source code must retain the above copyright notice, 15 | // this list of conditions and the following disclaimer. 16 | // 17 | // * Redistributions in binary form must reproduce the above copyright notice, 18 | // this list of conditions and the following disclaimer in the documentation 19 | // and/or other materials provided with the distribution. 20 | // 21 | // * Neither the name of the copyright holder nor the names of its contributors 22 | // may be used to endorse or promote products derived from this software without 23 | // specific prior written permission. 24 | // 25 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 26 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 27 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 28 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 29 | // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 30 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 31 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 32 | // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 33 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 34 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 35 | // 36 | 37 | import Foundation 38 | 39 | public struct AnsiCodes { 40 | public static let CSI: String = "\u{001B}[" 41 | 42 | public static let eraseRight: String = escapeCode("0K") 43 | public static let homeCursor: String = escapeCode("H") 44 | 45 | /// The following line is a hack. It is the only reliable way I could get "Terminal.app" on 46 | /// macOS to return to the beginning of the line 47 | public static let beginningOfLine: String = cursorBackward(3000) + cursorBackward(999) 48 | public static let endOfLine: String = cursorForward(3000) + cursorForward(999) 49 | 50 | /// Clear screen 51 | public static let clearScreen: String = escapeCode("2J") 52 | public static let clearCursorToBottom: String = escapeCode("0J") 53 | public static let clearTopToCursor: String = escapeCode("1J") 54 | 55 | /// Clear line 56 | public static let clearLine: String = escapeCode("2K") 57 | public static let clearCursorToEnd: String = escapeCode("0K") 58 | public static let clearBeginningToCursor: String = escapeCode("1K") 59 | 60 | /// Save/restore cursor position 61 | public static let savePos: String = escapeCode("s") 62 | public static let restorePos: String = escapeCode("u") 63 | 64 | public static let cursorLocation: String = escapeCode("6n") 65 | public static let origTermColor: String = escapeCode("0m") 66 | 67 | public static func escapeCode(_ input: String) -> String { 68 | return AnsiCodes.CSI + input 69 | } 70 | 71 | public static func setCursorColumn(_ column: Int) -> String { 72 | return escapeCode("\(column)G") 73 | } 74 | 75 | public static func setCursorPos(_ row: Int, _ column: Int) -> String { 76 | return escapeCode("\(row);\(column)H") 77 | } 78 | 79 | public static func cursorUp(_ rows: Int) -> String { 80 | guard rows > 0 else { 81 | return "" 82 | } 83 | return escapeCode("\(rows)A") 84 | } 85 | 86 | public static func cursorDown(_ rows: Int) -> String { 87 | guard rows > 0 else { 88 | return "" 89 | } 90 | return escapeCode("\(rows)B") 91 | } 92 | 93 | public static func cursorForward(_ columns: Int) -> String { 94 | guard columns > 0 else { 95 | return "" 96 | } 97 | return escapeCode("\(columns)C") 98 | } 99 | 100 | public static func cursorBackward(_ columns: Int) -> String { 101 | guard columns > 0 else { 102 | return "" 103 | } 104 | return escapeCode("\(columns)D") 105 | } 106 | 107 | public static func nextLine(_ lines: Int) -> String { 108 | return escapeCode("\(lines)E") 109 | } 110 | 111 | public static func previousLine(_ lines: Int) -> String { 112 | return escapeCode("\(lines)F") 113 | } 114 | 115 | public static func termColor(color: Int, bold: Bool) -> String { 116 | return escapeCode("\(color);\(bold ? 1 : 0);49m") 117 | } 118 | 119 | public static func termColor256(color: Int) -> String { 120 | return escapeCode("38;5;\(color)m") 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /Sources/CommandLineKit/TextColor.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextColor.swift 3 | // CommandLineKit 4 | // 5 | // Created by Matthias Zenger on 07/04/2018. 6 | // Copyright © 2018-2019 Google LLC 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions are met: 10 | // 11 | // * Redistributions of source code must retain the above copyright notice, 12 | // this list of conditions and the following disclaimer. 13 | // 14 | // * Redistributions in binary form must reproduce the above copyright notice, 15 | // this list of conditions and the following disclaimer in the documentation 16 | // and/or other materials provided with the distribution. 17 | // 18 | // * Neither the name of the copyright holder nor the names of its contributors 19 | // may be used to endorse or promote products derived from this software without 20 | // specific prior written permission. 21 | // 22 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 26 | // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 29 | // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | // 33 | 34 | import Foundation 35 | 36 | /// 37 | /// Enumeration of all supported text colors. 38 | /// 39 | public enum TextColor: Hashable { 40 | case black 41 | case maroon 42 | case green 43 | case olive 44 | case navy 45 | case purple 46 | case teal 47 | case silver 48 | case `default` 49 | case grey 50 | case red 51 | case lime 52 | case yellow 53 | case blue 54 | case fuchsia 55 | case aqua 56 | case white 57 | case extended(UInt8) 58 | 59 | public init?(colorCode: UInt8, fullColorSupport all256: Bool = false) { 60 | if all256 { 61 | self = .extended(colorCode) 62 | } else { 63 | switch colorCode { 64 | case 30: 65 | self = .black 66 | case 31: 67 | self = .maroon 68 | case 32: 69 | self = .green 70 | case 33: 71 | self = .olive 72 | case 34: 73 | self = .navy 74 | case 35: 75 | self = .purple 76 | case 36: 77 | self = .teal 78 | case 37: 79 | self = .silver 80 | case 39: 81 | self = .default 82 | case 90: 83 | self = .grey 84 | case 91: 85 | self = .red 86 | case 92: 87 | self = .lime 88 | case 93: 89 | self = .yellow 90 | case 94: 91 | self = .blue 92 | case 95: 93 | self = .fuchsia 94 | case 96: 95 | self = .aqua 96 | case 97: 97 | self = .white 98 | default: 99 | return nil 100 | } 101 | } 102 | } 103 | 104 | public init(color: (UInt8, UInt8, UInt8), fullColorSupport all256: Bool = false) { 105 | let code = Terminal.closestColor(to: color, fullColorSupport: all256) 106 | if all256 { 107 | self = .extended(code) 108 | } else { 109 | let color: TextColor? 110 | if code < 8 { 111 | color = TextColor(colorCode: code + 30) 112 | } else if code < 16 { 113 | color = TextColor(colorCode: code + 90) 114 | } else { 115 | color = nil 116 | } 117 | self = color ?? .default 118 | } 119 | } 120 | 121 | public var code: UInt8 { 122 | switch self { 123 | case .black: 124 | return 30 125 | case .maroon: 126 | return 31 127 | case .green: 128 | return 32 129 | case .olive: 130 | return 33 131 | case .navy: 132 | return 34 133 | case .purple: 134 | return 35 135 | case .teal: 136 | return 36 137 | case .silver: 138 | return 37 139 | case .default: 140 | return 39 141 | case .grey: 142 | return 90 143 | case .red: 144 | return 91 145 | case .lime: 146 | return 92 147 | case .yellow: 148 | return 93 149 | case .blue: 150 | return 94 151 | case .fuchsia: 152 | return 95 153 | case .aqua: 154 | return 96 155 | case .white: 156 | return 97 157 | case .extended(let c): 158 | return c 159 | } 160 | } 161 | 162 | public var isExtended: Bool { 163 | guard case .extended(_) = self else { 164 | return false 165 | } 166 | return true 167 | } 168 | 169 | public var properties: TextProperties { 170 | return TextProperties(textColor: self) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /Sources/CommandLineKit/Command.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Command.swift 3 | // CommandLineKit 4 | // 5 | // Created by Matthias Zenger on 20/08/2023. 6 | // Copyright © 2023 Matthias Zenger. All rights reserved. 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions are met: 10 | // 11 | // * Redistributions of source code must retain the above copyright notice, 12 | // this list of conditions and the following disclaimer. 13 | // 14 | // * Redistributions in binary form must reproduce the above copyright notice, 15 | // this list of conditions and the following disclaimer in the documentation 16 | // and/or other materials provided with the distribution. 17 | // 18 | // * Neither the name of the copyright holder nor the names of its contributors 19 | // may be used to endorse or promote products derived from this software without 20 | // specific prior written permission. 21 | // 22 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 26 | // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 29 | // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | // 33 | 34 | import Foundation 35 | 36 | /// 37 | /// The command protocol implements an API for command-line tools. A `Flags` object 38 | /// gets automatically instantiated and flags, declared with flag property wrappers, 39 | /// are automatically registered with the `Flags` object. If flag parsing results in 40 | /// an error, the `fail(with: String)` method is called. Otherwise, `run()` gets 41 | /// executed. 42 | /// 43 | public protocol Command { 44 | static var name: String { get } 45 | static var arguments: [String] { get } 46 | init() 47 | mutating func run() throws 48 | mutating func fail(with: String) throws 49 | } 50 | 51 | extension Command { 52 | 53 | public mutating func fail(with reason: String) { 54 | print(reason) 55 | exit(1) 56 | } 57 | 58 | public static var name: String { 59 | if let toolPath = CommandLine.arguments.first { 60 | return URL(fileURLWithPath: toolPath).lastPathComponent 61 | } else { 62 | let str = String(describing: self) 63 | if let i = str.firstIndex(of: "("), i > str.startIndex, i < str.endIndex { 64 | return String(str[str.startIndex.. Flags { 78 | return Flags(toolName: Self.name, arguments: Self.arguments) 79 | } 80 | 81 | public static func argumentNameConverter() -> ArgumentNameConverter { 82 | return ArgumentNameConverter(Self.argumentNamingStrategy()) 83 | } 84 | 85 | public static func argumentNamingStrategy() -> ArgumentNameConverter.Strategy { 86 | return .lowercase 87 | } 88 | 89 | public static func main() throws { 90 | var command = Self() 91 | let flags = Self.newFlags() 92 | let converter = Self.argumentNameConverter() 93 | let children = Mirror(reflecting: command).children 94 | for child in children { 95 | if let wrapper = child.value as? FlagWrapper { 96 | if let label = child.label { 97 | if label.hasPrefix("_") { 98 | wrapper.register(as: converter.convert(String(label.dropFirst())), with: flags) 99 | } else { 100 | wrapper.register(as: converter.convert(label), with: flags) 101 | } 102 | } else { 103 | wrapper.register(as: nil, with: flags) 104 | } 105 | } 106 | } 107 | if let reason = flags.parsingFailure() { 108 | try command.fail(with: reason) 109 | } else { 110 | try command.run() 111 | } 112 | } 113 | } 114 | 115 | public class ArgumentNameConverter { 116 | 117 | public enum Strategy { 118 | case camelcase 119 | case lowercase 120 | case separate(Character) 121 | } 122 | 123 | public let strategy: Strategy 124 | public let prefix: String 125 | 126 | public init(_ strategy: Strategy = .lowercase, prefix: String = "") { 127 | self.strategy = strategy 128 | self.prefix = prefix 129 | } 130 | 131 | public func convert(_ name: String) -> String { 132 | guard !name.isEmpty else { 133 | return "" 134 | } 135 | switch self.strategy { 136 | case .camelcase: 137 | if prefix.isEmpty { 138 | return name 139 | } else { 140 | return prefix + name.prefix(1).capitalized + name.dropFirst() 141 | } 142 | case .lowercase: 143 | return (prefix + name).lowercased() 144 | case .separate(let separator): 145 | var index = name.startIndex 146 | var separate = true 147 | var res = "" 148 | while index < name.endIndex { 149 | let character = name[index] 150 | if character.isUppercase { 151 | if separate && !res.isEmpty { 152 | res.append(separator) 153 | } 154 | let next = name.index(after: index) 155 | separate = next < name.endIndex && 156 | name[next].isUppercase && 157 | name.index(after: next) < name.endIndex && 158 | name[name.index(after: next)].isLowercase 159 | } else { 160 | separate = character != separator 161 | } 162 | res += character.lowercased() 163 | index = name.index(after: index) 164 | } 165 | return res 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /Sources/CommandLineKit/TextProperties.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TextProperties.swift 3 | // CommandLineKit 4 | // 5 | // Created by Matthias Zenger on 18/04/2018. 6 | // Copyright © 2018-2019 Google LLC 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions are met: 10 | // 11 | // * Redistributions of source code must retain the above copyright notice, 12 | // this list of conditions and the following disclaimer. 13 | // 14 | // * Redistributions in binary form must reproduce the above copyright notice, 15 | // this list of conditions and the following disclaimer in the documentation 16 | // and/or other materials provided with the distribution. 17 | // 18 | // * Neither the name of the copyright holder nor the names of its contributors 19 | // may be used to endorse or promote products derived from this software without 20 | // specific prior written permission. 21 | // 22 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 26 | // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 29 | // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | // 33 | 34 | import Foundation 35 | 36 | /// 37 | /// A `TextProperties` struct bundles a text color, a background color and a text style 38 | /// in one object. Text properties can be merged with the `with(:)` functions and applied to 39 | /// a string with the `apply(to:)` function. 40 | /// 41 | public struct TextProperties: Hashable { 42 | let textColor: TextColor? 43 | let backgroundColor: BackgroundColor? 44 | let textStyles: Set 45 | 46 | public static let none = TextProperties() 47 | 48 | public init(textColor: TextColor? = nil, 49 | backgroundColor: BackgroundColor? = nil, 50 | textStyles: Set = []) { 51 | self.textColor = textColor 52 | self.backgroundColor = backgroundColor 53 | self.textStyles = textStyles 54 | } 55 | 56 | public init(_ tcolor: TextColor? = nil, 57 | _ bgcolor: BackgroundColor? = nil, 58 | _ tstyles: TextStyle...) { 59 | self.textColor = tcolor 60 | self.textStyles = Set(tstyles) 61 | self.backgroundColor = bgcolor 62 | } 63 | 64 | public var isEmpty: Bool { 65 | return self.textColor == nil && self.backgroundColor == nil && self.textStyles.isEmpty 66 | } 67 | 68 | public func with(_ properties: TextProperties) -> TextProperties { 69 | var styles = self.textStyles 70 | for style in properties.textStyles { 71 | styles.insert(style) 72 | } 73 | return TextProperties(textColor: properties.textColor ?? self.textColor, 74 | backgroundColor: properties.backgroundColor ?? self.backgroundColor, 75 | textStyles: styles) 76 | } 77 | 78 | public func with(_ tcolor: TextColor) -> TextProperties { 79 | return TextProperties(textColor: tcolor, 80 | backgroundColor: self.backgroundColor, 81 | textStyles: self.textStyles) 82 | } 83 | 84 | public func with(_ bgcolor: BackgroundColor) -> TextProperties { 85 | return TextProperties(textColor: self.textColor, 86 | backgroundColor: bgcolor, 87 | textStyles: self.textStyles) 88 | } 89 | 90 | public func with(_ tstyle: TextStyle) -> TextProperties { 91 | var styles = self.textStyles 92 | styles.insert(tstyle) 93 | return TextProperties(textColor: self.textColor, 94 | backgroundColor: self.backgroundColor, 95 | textStyles: styles) 96 | } 97 | 98 | public func apply(to text: String) -> String { 99 | if self.isEmpty { 100 | return text 101 | } 102 | var codes: [UInt8] = [] 103 | if let color = self.textColor { 104 | if color.isExtended { 105 | codes.append(38) 106 | codes.append(5) 107 | } 108 | codes.append(color.code) 109 | } 110 | if let backgroundColor = self.backgroundColor { 111 | if backgroundColor.isExtended { 112 | codes.append(48) 113 | codes.append(5) 114 | } 115 | codes.append(backgroundColor.code) 116 | } 117 | codes += self.textStyles.map { $0.code } 118 | return codes.isEmpty ? 119 | text : 120 | "\(AnsiCodes.CSI)\(codes.map{String($0)}.joined(separator: ";"))m\(text)\(AnsiCodes.CSI)0m" 121 | } 122 | 123 | public static func extract(from str: String) -> (TextProperties, String) { 124 | let (codes, res) = TextProperties.extractCodes(from: str) 125 | return (TextProperties.interpret(codes: codes), res) 126 | } 127 | 128 | private static func extractCodes(from string: String) -> ([UInt8], String) { 129 | var index = string.index(string.startIndex, offsetBy: AnsiCodes.CSI.count) 130 | var codesString = "" 131 | while string[index] != "m" { 132 | codesString.append(string[index]) 133 | index = string.index(after: index) 134 | } 135 | let codes = codesString.split(separator: ";", 136 | omittingEmptySubsequences: false).compactMap { UInt8($0) } 137 | let startIndex = string.index(after: index) 138 | let endIndex = string.index(string.endIndex, offsetBy: -"\(AnsiCodes.CSI)0m".count) 139 | let text = String(string[startIndex.. TextProperties { 144 | var color: TextColor? = nil 145 | var backgroundColor: BackgroundColor? = nil 146 | var styles: Set = [] 147 | var i = 0 148 | while i < codes.count { 149 | if (i + 2) < codes.count { 150 | if codes[i] == 38 && codes[i + 1] == 5 { 151 | color = TextColor(colorCode: codes[i + 2], fullColorSupport: true) 152 | i += 2 153 | continue 154 | } else if codes[i] == 48 && codes[i + 1] == 5 { 155 | backgroundColor = BackgroundColor(colorCode: codes[i + 2], fullColorSupport: true) 156 | i += 2 157 | continue 158 | } 159 | } 160 | if let c = TextColor(colorCode: codes[i]) { 161 | color = c 162 | } else if let bg = BackgroundColor(colorCode: codes[i]) { 163 | backgroundColor = bg 164 | } else if let style = TextStyle(rawValue: codes[i]) { 165 | styles.insert(style) 166 | } 167 | i += 1 168 | } 169 | return TextProperties(textColor: color, 170 | backgroundColor: backgroundColor, 171 | textStyles: styles) 172 | } 173 | 174 | public func hash(into hasher: inout Hasher) { 175 | hasher.combine(self.textColor) 176 | hasher.combine(self.textStyles) 177 | hasher.combine(self.backgroundColor) 178 | } 179 | 180 | public static func == (lhs: TextProperties, rhs: TextProperties) -> Bool { 181 | return lhs.textColor == rhs.textColor && 182 | lhs.backgroundColor == rhs.backgroundColor && 183 | lhs.textStyles == rhs.textStyles 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /Tests/CommandLineKitTests/LineReaderHistoryTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LineReaderHistoryTests.swift 3 | // CommandLineKitTests 4 | // 5 | // Created by Matthias Zenger on 08/04/2018. 6 | // Copyright © 2018-2019 Google LLC 7 | // Copyright © 2017 Andy Best 8 | // Copyright © 2010-2014 Salvatore Sanfilippo 9 | // Copyright © 2010-2013 Pieter Noordhuis 10 | // 11 | // Redistribution and use in source and binary forms, with or without 12 | // modification, are permitted provided that the following conditions are met: 13 | // 14 | // * Redistributions of source code must retain the above copyright notice, 15 | // this list of conditions and the following disclaimer. 16 | // 17 | // * Redistributions in binary form must reproduce the above copyright notice, 18 | // this list of conditions and the following disclaimer in the documentation 19 | // and/or other materials provided with the distribution. 20 | // 21 | // * Neither the name of the copyright holder nor the names of its contributors 22 | // may be used to endorse or promote products derived from this software without 23 | // specific prior written permission. 24 | // 25 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 26 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 27 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 28 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 29 | // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 30 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 31 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 32 | // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 33 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 34 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 35 | // 36 | 37 | import XCTest 38 | @testable import CommandLineKit 39 | 40 | class LineReaderHistoryTests: XCTestCase { 41 | 42 | func testHistoryAddItem() { 43 | let h = LineReaderHistory() 44 | h.add("Test") 45 | XCTAssertEqual(h.historyItems, ["Test"]) 46 | } 47 | 48 | func testHistoryDoesNotAddDuplicatedLines() { 49 | let h = LineReaderHistory() 50 | h.add("Test") 51 | h.add("Test") 52 | XCTAssertEqual(h.historyItems.count, 1) 53 | // Test adding a new item in-between doesn't de-dupe the newest line 54 | h.add("Test 2") 55 | h.add("Test") 56 | XCTAssertEqual(h.historyItems.count, 3) 57 | } 58 | 59 | func testHistoryHonorsMaxLength() { 60 | let h = LineReaderHistory() 61 | h.maxLength = 2 62 | h.add("Test 1") 63 | h.add("Test 2") 64 | h.add("Test 3") 65 | XCTAssertEqual(h.historyItems.count, 2) 66 | XCTAssertEqual(h.historyItems, ["Test 2", "Test 3"]) 67 | } 68 | 69 | func testHistoryRemovesEntriesWhenMaxLengthIsSet() { 70 | let h = LineReaderHistory() 71 | h.add("Test 1") 72 | h.add("Test 2") 73 | h.add("Test 3") 74 | XCTAssertEqual(h.historyItems.count, 3) 75 | h.maxLength = 2 76 | XCTAssertEqual(h.historyItems.count, 2) 77 | XCTAssertEqual(h.historyItems, ["Test 2", "Test 3"]) 78 | } 79 | 80 | func testHistoryNavigationReturnsNilWhenHistoryEmpty() { 81 | let h = LineReaderHistory() 82 | XCTAssertNil(h.navigateHistory(direction: .next)) 83 | XCTAssertNil(h.navigateHistory(direction: .previous)) 84 | } 85 | 86 | func testHistoryNavigationReturnsSingleItemWhenHistoryHasOneItem() { 87 | let h = LineReaderHistory() 88 | h.add("Test") 89 | XCTAssertNil(h.navigateHistory(direction: .next)) 90 | guard let previousItem = h.navigateHistory(direction: .previous) else { 91 | XCTFail("Expected previous item to not be nil") 92 | return 93 | } 94 | XCTAssertEqual(previousItem, "Test") 95 | } 96 | 97 | func testHistoryStopsAtBeginning() { 98 | let h = LineReaderHistory() 99 | h.add("1") 100 | h.add("2") 101 | h.add("3") 102 | XCTAssertEqual(h.navigateHistory(direction: .previous), "3") 103 | XCTAssertEqual(h.navigateHistory(direction: .previous), "2") 104 | XCTAssertEqual(h.navigateHistory(direction: .previous), "1") 105 | XCTAssertNil(h.navigateHistory(direction: .previous)) 106 | } 107 | 108 | func testHistoryNavigationStopsAtEnd() { 109 | let h = LineReaderHistory() 110 | h.add("1") 111 | h.add("2") 112 | h.add("3") 113 | XCTAssertNil(h.navigateHistory(direction: .next)) 114 | } 115 | 116 | func testHistorySavesToFile() { 117 | let h = LineReaderHistory() 118 | h.add("Test 1") 119 | h.add("Test 2") 120 | h.add("Test 3") 121 | let tempFile = "/tmp/linereaderhistory_save_test.txt" 122 | do { 123 | try h.save(toFile: tempFile) 124 | } catch { 125 | XCTFail("Saving file should not throw exception") 126 | } 127 | let fileContents: String 128 | do { 129 | fileContents = try String(contentsOfFile: tempFile, encoding: .utf8) 130 | } catch { 131 | XCTFail("Loading file should not throw exception") 132 | return 133 | } 134 | // Reading the file should yield the same lines as input 135 | let items = fileContents.split(separator: "\n") 136 | XCTAssertEqual(items, ["Test 1", "Test 2", "Test 3"]) 137 | } 138 | 139 | func testHistoryLoadsFromFile() { 140 | let h = LineReaderHistory() 141 | let tempFile = "/tmp/linereaderhistory_load_test.txt" 142 | do { 143 | try "Test 1\nTest 2\nTest 3".write(toFile: tempFile, atomically: true, encoding: .utf8) 144 | } catch { 145 | XCTFail("Writing file should not throw exception") 146 | } 147 | do { 148 | try h.load(fromFile: tempFile) 149 | } catch { 150 | XCTFail("Loading file should not throw exception") 151 | return 152 | } 153 | XCTAssertEqual(h.historyItems.count, 3) 154 | XCTAssertEqual(h.historyItems, ["Test 1", "Test 2", "Test 3"]) 155 | } 156 | 157 | func testHistoryLoadingRespectsMaxLength() { 158 | let h = LineReaderHistory() 159 | h.maxLength = 2 160 | let tempFile = "/tmp/linereaderhistory_load_test.txt" 161 | do { 162 | try "Test 1\nTest 2\nTest 3".write(toFile: tempFile, atomically: true, encoding: .utf8) 163 | } catch { 164 | XCTFail("Writing file should not throw exception") 165 | } 166 | do { 167 | try h.load(fromFile: tempFile) 168 | } catch { 169 | XCTFail("Loading file should not throw exception") 170 | return 171 | } 172 | XCTAssertEqual(h.historyItems.count, 2) 173 | XCTAssertEqual(h.historyItems, ["Test 2", "Test 3"]) 174 | } 175 | 176 | static let allTests = [ 177 | ("testHistoryAddItem", testHistoryAddItem), 178 | ("testHistoryDoesNotAddDuplicatedLines", testHistoryDoesNotAddDuplicatedLines), 179 | ("testHistoryHonorsMaxLength", testHistoryHonorsMaxLength), 180 | ("testHistoryRemovesEntriesWhenMaxLengthIsSet", testHistoryRemovesEntriesWhenMaxLengthIsSet), 181 | ("testHistoryNavigationReturnsNilWhenHistoryEmpty", 182 | testHistoryNavigationReturnsNilWhenHistoryEmpty), 183 | ("testHistoryNavigationReturnsSingleItemWhenHistoryHasOneItem", 184 | testHistoryNavigationReturnsSingleItemWhenHistoryHasOneItem), 185 | ("testHistoryStopsAtBeginning", testHistoryStopsAtBeginning), 186 | ("testHistoryNavigationStopsAtEnd", testHistoryNavigationStopsAtEnd), 187 | ("testHistorySavesToFile", testHistorySavesToFile), 188 | ("testHistoryLoadsFromFile", testHistoryLoadsFromFile), 189 | ("testHistoryLoadingRespectsMaxLength", testHistoryLoadingRespectsMaxLength), 190 | ] 191 | } 192 | -------------------------------------------------------------------------------- /Tests/CommandLineKitTests/EditStateTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EditStateTests.swift 3 | // CommandLineKitTests 4 | // 5 | // Created by Matthias Zenger on 08/04/2018. 6 | // Copyright © 2018-2019 Google LLC 7 | // Copyright © 2017 Andy Best 8 | // Copyright © 2010-2014 Salvatore Sanfilippo 9 | // Copyright © 2010-2013 Pieter Noordhuis 10 | // 11 | // Redistribution and use in source and binary forms, with or without 12 | // modification, are permitted provided that the following conditions are met: 13 | // 14 | // * Redistributions of source code must retain the above copyright notice, 15 | // this list of conditions and the following disclaimer. 16 | // 17 | // * Redistributions in binary form must reproduce the above copyright notice, 18 | // this list of conditions and the following disclaimer in the documentation 19 | // and/or other materials provided with the distribution. 20 | // 21 | // * Neither the name of the copyright holder nor the names of its contributors 22 | // may be used to endorse or promote products derived from this software without 23 | // specific prior written permission. 24 | // 25 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 26 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 27 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 28 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 29 | // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 30 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 31 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 32 | // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 33 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 34 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 35 | // 36 | 37 | import XCTest 38 | @testable import CommandLineKit 39 | 40 | class EditStateTests: XCTestCase { 41 | 42 | func testInitEmptyBuffer() { 43 | let s = EditState(prompt: "$ ") 44 | XCTAssertEqual(s.buffer, "") 45 | XCTAssertEqual(s.location, s.buffer.startIndex) 46 | XCTAssertEqual(s.prompt, "$ ") 47 | } 48 | 49 | func testInsertCharacter() { 50 | let s = EditState(prompt: "") 51 | XCTAssert(s.insertCharacter("A"["A".startIndex])) 52 | XCTAssertEqual(s.buffer, "A") 53 | XCTAssertEqual(s.location, s.buffer.endIndex) 54 | XCTAssertEqual(s.cursorPosition, 1) 55 | } 56 | 57 | func testBackspace() { 58 | let s = EditState(prompt: "") 59 | XCTAssert(s.insertCharacter("A"["A".startIndex])) 60 | XCTAssertTrue(s.backspace()) 61 | XCTAssertEqual(s.buffer, "") 62 | XCTAssertEqual(s.location, s.buffer.startIndex) 63 | // No more characters left, so backspace should return false 64 | XCTAssertFalse(s.backspace()) 65 | } 66 | 67 | func testMoveLeft() { 68 | let s = EditState(prompt: "") 69 | s.buffer = "Hello" 70 | s.location = s.buffer.endIndex 71 | XCTAssertTrue(s.moveLeft()) 72 | XCTAssertEqual(s.cursorPosition, 4) 73 | s.location = s.buffer.startIndex 74 | XCTAssertFalse(s.moveLeft()) 75 | } 76 | 77 | func testMoveRight() { 78 | let s = EditState(prompt: "") 79 | s.buffer = "Hello" 80 | s.location = s.buffer.startIndex 81 | XCTAssertTrue(s.moveRight()) 82 | XCTAssertEqual(s.cursorPosition, 1) 83 | s.location = s.buffer.endIndex 84 | XCTAssertFalse(s.moveRight()) 85 | } 86 | 87 | func testMoveHome() { 88 | let s = EditState(prompt: "") 89 | s.buffer = "Hello" 90 | s.location = s.buffer.endIndex 91 | XCTAssertTrue(s.moveHome()) 92 | XCTAssertEqual(s.cursorPosition, 0) 93 | XCTAssertFalse(s.moveHome()) 94 | } 95 | 96 | func testMoveEnd() { 97 | let s = EditState(prompt: "") 98 | s.buffer = "Hello" 99 | s.location = s.buffer.startIndex 100 | XCTAssertTrue(s.moveEnd()) 101 | XCTAssertEqual(s.cursorPosition, 5) 102 | XCTAssertFalse(s.moveEnd()) 103 | } 104 | 105 | func testRemovePreviousWord() { 106 | let s = EditState(prompt: "") 107 | s.buffer = "Hello world" 108 | s.location = s.buffer.endIndex 109 | XCTAssertTrue(s.deletePreviousWord()) 110 | XCTAssertEqual(s.buffer, "Hello ") 111 | XCTAssertEqual(s.location, "Hello ".endIndex) 112 | s.buffer = "" 113 | s.location = s.buffer.endIndex 114 | XCTAssertFalse(s.deletePreviousWord()) 115 | // Test with cursor location in the middle of the text 116 | s.buffer = "This is a test" 117 | s.location = s.buffer.index(s.buffer.startIndex, offsetBy: 8) 118 | XCTAssertTrue(s.deletePreviousWord()) 119 | XCTAssertEqual(s.buffer, "This a test") 120 | } 121 | 122 | func testDeleteToEndOfLine() { 123 | let s = EditState(prompt: "") 124 | s.buffer = "Hello world" 125 | s.location = s.buffer.endIndex 126 | XCTAssertFalse(s.deleteToEndOfLine()) 127 | s.location = s.buffer.index(s.buffer.startIndex, offsetBy: 5) 128 | XCTAssertTrue(s.deleteToEndOfLine()) 129 | XCTAssertEqual(s.buffer, "Hello") 130 | } 131 | 132 | func testDeleteCharacter() { 133 | let s = EditState(prompt: "") 134 | s.buffer = "Hello world" 135 | s.location = s.buffer.endIndex 136 | XCTAssertFalse(s.deleteCharacter()) 137 | s.location = s.buffer.startIndex 138 | XCTAssertTrue(s.deleteCharacter()) 139 | XCTAssertEqual(s.buffer, "ello world") 140 | s.location = s.buffer.index(s.buffer.startIndex, offsetBy: 5) 141 | XCTAssertTrue(s.deleteCharacter()) 142 | XCTAssertEqual(s.buffer, "ello orld") 143 | } 144 | 145 | func testEraseCharacterRight() { 146 | let s = EditState(prompt: "") 147 | s.buffer = "Hello" 148 | s.location = s.buffer.endIndex 149 | XCTAssertFalse(s.eraseCharacterRight()) 150 | s.location = s.buffer.startIndex 151 | XCTAssertTrue(s.eraseCharacterRight()) 152 | XCTAssertEqual(s.buffer, "ello") 153 | // Test empty buffer 154 | s.buffer = "" 155 | s.location = s.buffer.startIndex 156 | XCTAssertFalse(s.eraseCharacterRight()) 157 | } 158 | 159 | func testSwapCharacters() { 160 | let s = EditState(prompt: "") 161 | s.buffer = "Hello" 162 | s.location = s.buffer.endIndex 163 | // Cursor at the end of the text 164 | XCTAssertTrue(s.swapCharacterWithPrevious()) 165 | XCTAssertEqual(s.buffer, "Helol") 166 | XCTAssertEqual(s.location, s.buffer.endIndex) 167 | // Cursor in the middle of the text 168 | s.location = s.buffer.index(before: s.buffer.endIndex) 169 | XCTAssertTrue(s.swapCharacterWithPrevious()) 170 | XCTAssertEqual(s.buffer, "Hello") 171 | XCTAssertEqual(s.location, s.buffer.endIndex) 172 | // Cursor at the start of the text 173 | s.location = s.buffer.startIndex 174 | XCTAssertTrue(s.swapCharacterWithPrevious()) 175 | XCTAssertEqual(s.buffer, "eHllo") 176 | XCTAssertEqual(s.location, s.buffer.index(s.buffer.startIndex, offsetBy: 2)) 177 | } 178 | 179 | static let allTests = [ 180 | ("testInitEmptyBuffer", testInitEmptyBuffer), 181 | ("testInsertCharacter", testInsertCharacter), 182 | ("testBackspace", testBackspace), 183 | ("testMoveLeft", testMoveLeft), 184 | ("testMoveRight", testMoveRight), 185 | ("testMoveHome", testMoveHome), 186 | ("testMoveEnd", testMoveEnd), 187 | ("testRemovePreviousWord", testRemovePreviousWord), 188 | ("testDeleteToEndOfLine", testDeleteToEndOfLine), 189 | ("testDeleteCharacter", testDeleteCharacter), 190 | ("testEraseCharacterRight", testEraseCharacterRight), 191 | ("testSwapCharacters", testSwapCharacters), 192 | ] 193 | } 194 | -------------------------------------------------------------------------------- /Sources/CommandLineKit/Terminal.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Terminal.swift 3 | // CommandLineKit 4 | // 5 | // Created by Matthias Zenger on 19/04/2018. 6 | // Copyright © 2018-2019 Google LLC 7 | // Copyright © 2017 Andy Best 8 | // Copyright © 2010-2014 Salvatore Sanfilippo 9 | // Copyright © 2010-2013 Pieter Noordhuis 10 | // 11 | // Redistribution and use in source and binary forms, with or without 12 | // modification, are permitted provided that the following conditions are met: 13 | // 14 | // * Redistributions of source code must retain the above copyright notice, 15 | // this list of conditions and the following disclaimer. 16 | // 17 | // * Redistributions in binary form must reproduce the above copyright notice, 18 | // this list of conditions and the following disclaimer in the documentation 19 | // and/or other materials provided with the distribution. 20 | // 21 | // * Neither the name of the copyright holder nor the names of its contributors 22 | // may be used to endorse or promote products derived from this software without 23 | // specific prior written permission. 24 | // 25 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 26 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 27 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 28 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 29 | // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 30 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 31 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 32 | // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 33 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 34 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 35 | // 36 | 37 | import Foundation 38 | 39 | public struct Terminal { 40 | 41 | public static let current: String = ProcessInfo.processInfo.environment["TERM"] ?? "" 42 | 43 | public static var fullColorSupport: Bool { 44 | // First make sure we are not running within Xcode 45 | guard ProcessInfo.processInfo.environment["__XCODE_BUILT_PRODUCTS_DIR_PATHS"] == nil else { 46 | return false 47 | } 48 | // Next, check if there is an environment variable COLORTERM set to "truecolor" 49 | if let cterm = ProcessInfo.processInfo.environment["COLORTERM"], cterm == "truecolor" { 50 | return true 51 | } 52 | // Finally, apply some heuristics based on the current terminal 53 | return Terminal.fullColorSupport(terminal: Terminal.current) 54 | } 55 | 56 | public static func fullColorSupport(terminal: String) -> Bool { 57 | // A rather dumb way of detecting colour support 58 | return terminal.contains("256") 59 | } 60 | 61 | // Colour tables from https://jonasjacek.github.io/colors/ 62 | // Format: (r, g, b) 63 | 64 | private static let colors: [(UInt8, UInt8, UInt8)] = [ 65 | // Standard 66 | (0, 0, 0), (128, 0, 0), (0, 128, 0), (128, 128, 0), (0, 0, 128), (128, 0, 128), 67 | (0, 128, 128), (192, 192, 192), 68 | // High intensity 69 | (128, 128, 128), (255, 0, 0), (0, 255, 0), (255, 255, 0), (0, 0, 255), (255, 0, 255), 70 | (0, 255, 255), (255, 255, 255), 71 | // 256 color extended 72 | (0, 0, 0), (0, 0, 95), (0, 0, 135), (0, 0, 175), (0, 0, 215), (0, 0, 255), (0, 95, 0), 73 | (0, 95, 95), (0, 95, 135), (0, 95, 175), (0, 95, 215), (0, 95, 255), (0, 135, 0), 74 | (0, 135, 95), (0, 135, 135), (0, 135, 175), (0, 135, 215), (0, 135, 255), (0, 175, 0), 75 | (0, 175, 95), (0, 175, 135), (0, 175, 175), (0, 175, 215), (0, 175, 255), (0, 215, 0), 76 | (0, 215, 95), (0, 215, 135), (0, 215, 175), (0, 215, 215), (0, 215, 255), (0, 255, 0), 77 | (0, 255, 95), (0, 255, 135), (0, 255, 175), (0, 255, 215), (0, 255, 255), (95, 0, 0), 78 | (95, 0, 95), (95, 0, 135), (95, 0, 175), (95, 0, 215), (95, 0, 255), (95, 95, 0), 79 | (95, 95, 95), (95, 95, 135), (95, 95, 175), (95, 95, 215), (95, 95, 255), (95, 135, 0), 80 | (95, 135, 95), (95, 135, 135), (95, 135, 175), (95, 135, 215), (95, 135, 255), (95, 175, 0), 81 | (95, 175, 95), (95, 175, 135), (95, 175, 175), (95, 175, 215), (95, 175, 255), (95, 215, 0), 82 | (95, 215, 95), (95, 215, 135), (95, 215, 175), (95, 215, 215), (95, 215, 255), (95, 255, 0), 83 | (95, 255, 95), (95, 255, 135), (95, 255, 175), (95, 255, 215), (95, 255, 255), (135, 0, 0), 84 | (135, 0, 95), (135, 0, 135), (135, 0, 175), (135, 0, 215), (135, 0, 255), (135, 95, 0), 85 | (135, 95, 95), (135, 95, 135), (135, 95, 175), (135, 95, 215), (135, 95, 255), (135, 135, 0), 86 | (135, 135, 95), (135, 135, 135), (135, 135, 175), (135, 135, 215), (135, 135, 255), 87 | (135, 175, 0), (135, 175, 95), (135, 175, 135), (135, 175, 175), (135, 175, 215), 88 | (135, 175, 255), (135, 215, 0), (135, 215, 95), (135, 215, 135), (135, 215, 175), 89 | (135, 215, 215), (135, 215, 255), (135, 255, 0), (135, 255, 95), (135, 255, 135), 90 | (135, 255, 175), (135, 255, 215), (135, 255, 255), (175, 0, 0), (175, 0, 95), (175, 0, 135), 91 | (175, 0, 175), (175, 0, 215), (175, 0, 255), (175, 95, 0), (175, 95, 95), (175, 95, 135), 92 | (175, 95, 175), (175, 95, 215), (175, 95, 255), (175, 135, 0), (175, 135, 95), 93 | (175, 135, 135), (175, 135, 175), (175, 135, 215), (175, 135, 255), (175, 175, 0), 94 | (175, 175, 95), (175, 175, 135), (175, 175, 175), (175, 175, 215), (175, 175, 255), 95 | (175, 215, 0), (175, 215, 95), (175, 215, 135), (175, 215, 175), (175, 215, 215), 96 | (175, 215, 255), (175, 255, 0), (175, 255, 95), (175, 255, 135), (175, 255, 175), 97 | (175, 255, 215), (175, 255, 255), (215, 0, 0), (215, 0, 95), (215, 0, 135), (215, 0, 175), 98 | (215, 0, 215), (215, 0, 255), (215, 95, 0), (215, 95, 95), (215, 95, 135), (215, 95, 175), 99 | (215, 95, 215), (215, 95, 255), (215, 135, 0), (215, 135, 95), (215, 135, 135), 100 | (215, 135, 175), (215, 135, 215), (215, 135, 255), (215, 175, 0), (215, 175, 95), 101 | (215, 175, 135), (215, 175, 175), (215, 175, 215), (215, 175, 255), (215, 215, 0), 102 | (215, 215, 95), (215, 215, 135), (215, 215, 175), (215, 215, 215), (215, 215, 255), 103 | (215, 255, 0), (215, 255, 95), (215, 255, 135), (215, 255, 175), (215, 255, 215), 104 | (215, 255, 255), (255, 0, 0), (255, 0, 95), (255, 0, 135), (255, 0, 175), (255, 0, 215), 105 | (255, 0, 255), (255, 95, 0), (255, 95, 95), (255, 95, 135), (255, 95, 175), (255, 95, 215), 106 | (255, 95, 255), (255, 135, 0), (255, 135, 95), (255, 135, 135), (255, 135, 175), 107 | (255, 135, 215), (255, 135, 255), (255, 175, 0), (255, 175, 95), (255, 175, 135), 108 | (255, 175, 175), (255, 175, 215), (255, 175, 255), (255, 215, 0), (255, 215, 95), 109 | (255, 215, 135), (255, 215, 175), (255, 215, 215), (255, 215, 255), (255, 255, 0), 110 | (255, 255, 95), (255, 255, 135), (255, 255, 175), (255, 255, 215), (255, 255, 255), 111 | (8, 8, 8), (18, 18, 18), (28, 28, 28), (38, 38, 38), (48, 48, 48), (58, 58, 58), 112 | (68, 68, 68), (78, 78, 78), (88, 88, 88), (98, 98, 98), (108, 108, 108), (118, 118, 118), 113 | (128, 128, 128), (138, 138, 138), (148, 148, 148), (158, 158, 158), (168, 168, 168), 114 | (178, 178, 178), (188, 188, 188), (198, 198, 198), (208, 208, 208), (218, 218, 218), 115 | (228, 228, 228), (238, 238, 238) 116 | ] 117 | 118 | internal static func closestColor(to targetColor: (UInt8, UInt8, UInt8), 119 | fullColorSupport all256: Bool = false) -> UInt8 { 120 | let colorTable: [(UInt8, UInt8, UInt8)] = all256 ? colors : Array(colors[0..<8]) 121 | let distances = colorTable.map { 122 | sqrt(pow(Double(Int($0.0) - Int(targetColor.0)), 2) + 123 | pow(Double(Int($0.1) - Int(targetColor.1)), 2) + 124 | pow(Double(Int($0.2) - Int(targetColor.2)), 2)) 125 | } 126 | var closest = Double.greatestFiniteMagnitude 127 | var closestIdx = 0 128 | for i in 0.. { 45 | 46 | public enum State { 47 | case config(shortName: Character?, longName: String?, description: String, value: V) 48 | case flag(F) 49 | } 50 | 51 | public var state: State 52 | 53 | public var projectedValue: Flag { 54 | guard case .flag(let flag) = self.state else { 55 | preconditionFailure("accessing flags accessor before initialization") 56 | } 57 | return flag 58 | } 59 | 60 | public init(short: Character? = nil, 61 | long: String? = nil, 62 | description: String? = nil, 63 | value: Value) { 64 | self.state = .config(shortName: short, 65 | longName: long, 66 | description: description ?? "undocumented", 67 | value: value) 68 | } 69 | } 70 | 71 | /// Inject the flags object into a command with the `@CommandFlags` property wrapper. 72 | @propertyWrapper 73 | public class CommandFlags: FlagWrapper { 74 | private var flags: Flags? 75 | 76 | public var wrappedValue: Flags { 77 | get { 78 | guard let flags = self.flags else { 79 | preconditionFailure("accessing flags accessor before initialization") 80 | } 81 | return flags 82 | } 83 | } 84 | 85 | public init() { 86 | self.flags = nil 87 | } 88 | 89 | public func register(as: String?, with flags: Flags) { 90 | self.flags = flags 91 | } 92 | } 93 | 94 | /// Declare an option flag 95 | @propertyWrapper 96 | public class CommandOption: CommandFlag, FlagWrapper { 97 | 98 | public var wrappedValue: Bool { 99 | get { 100 | guard case .flag(let flag) = self.state else { 101 | preconditionFailure("accessing flags accessor before initialization") 102 | } 103 | return flag.wasSet 104 | } 105 | } 106 | 107 | public init(short: Character? = nil, 108 | long: String? = nil, 109 | description: String? = nil) { 110 | super.init(short: short, long: long, description: description, value: false) 111 | } 112 | 113 | public func register(as optName: String?, with flags: Flags) { 114 | guard case .config(let short, let long, let descr, _) = self.state else { 115 | preconditionFailure("initializing flag twice") 116 | } 117 | var longName: String? = long 118 | if short == nil && long == nil { 119 | longName = optName 120 | } 121 | let flag = flags.option(short, longName, description: descr) 122 | self.state = .flag(flag) 123 | } 124 | } 125 | 126 | /// Declare an argument flag 127 | @propertyWrapper 128 | public class CommandArgument: CommandFlag>, FlagWrapper 129 | where Value: ConvertibleFromString { 130 | 131 | public var wrappedValue: Value { 132 | get { 133 | guard case .flag(let flag) = self.state else { 134 | preconditionFailure("accessing flags accessor before initialization") 135 | } 136 | return flag.value! 137 | } 138 | } 139 | 140 | public init(short: Character? = nil, 141 | long: String? = nil, 142 | description: String? = nil) where Value == Optional { 143 | super.init(short: short, long: long, description: description, value: nil) 144 | } 145 | 146 | public init(wrappedValue: Value, 147 | short: Character? = nil, 148 | long: String? = nil, 149 | description: String? = nil) { 150 | super.init(short: short, long: long, description: description, value: wrappedValue) 151 | } 152 | 153 | public func register(as argName: String?, with flags: Flags) { 154 | guard case .config(let short, let long, let descr, let value) = self.state else { 155 | preconditionFailure("initializing flag twice") 156 | } 157 | var longName: String? = long 158 | if short == nil && long == nil { 159 | longName = argName 160 | } 161 | let flag = SingletonArgument(shortName: short, 162 | longName: longName, 163 | description: descr, 164 | value: value) 165 | flags.register(flag) 166 | self.state = .flag(flag) 167 | } 168 | } 169 | 170 | /// Declare a repeated argument flag 171 | @propertyWrapper 172 | public class CommandArguments: CommandFlag>, FlagWrapper 173 | where Value: ConvertibleFromString { 174 | 175 | public var wrappedValue: [Value] { 176 | get { 177 | guard case .flag(let flag) = self.state else { 178 | preconditionFailure("accessing flags accessor before initialization") 179 | } 180 | return flag.value 181 | } 182 | } 183 | 184 | public init(short: Character? = nil, 185 | long: String? = nil, 186 | description: String? = nil, 187 | maxCount: Int = Int.max) { 188 | super.init(short: short, long: long, description: description, value: maxCount) 189 | } 190 | 191 | public func register(as argName: String?, with flags: Flags) { 192 | guard case .config(let short, let long, let descr, let value) = self.state else { 193 | preconditionFailure("initializing flag twice") 194 | } 195 | var longName: String? = long 196 | if short == nil && long == nil { 197 | longName = argName 198 | } 199 | let flag = RepeatedArgument(shortName: short, 200 | longName: longName, 201 | description: descr, 202 | maxCount: value) 203 | flags.register(flag) 204 | self.state = .flag(flag) 205 | } 206 | } 207 | 208 | /// Inject the remaining/unparsed parameters into a command with the `@CommandParameters` 209 | /// property wrapper. 210 | @propertyWrapper 211 | public class CommandParameters: FlagWrapper { 212 | private var flags: Flags? 213 | 214 | public var wrappedValue: [String] { 215 | get { 216 | guard let flags = self.flags else { 217 | preconditionFailure("accessing flags accessor before initialization") 218 | } 219 | return flags.parameters 220 | } 221 | } 222 | 223 | public init() { 224 | self.flags = nil 225 | } 226 | 227 | public func register(as: String?, with flags: Flags) { 228 | self.flags = flags 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /Tests/CommandLineKitTests/FlagTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // FlagTests.swift 3 | // CommandLineKitTests 4 | // 5 | // Created by Matthias Zenger on 25/03/2017. 6 | // Copyright © 2018-2023 Google LLC 7 | // 8 | // Redistribution and use in source and binary forms, with or without 9 | // modification, are permitted provided that the following conditions are met: 10 | // 11 | // * Redistributions of source code must retain the above copyright notice, 12 | // this list of conditions and the following disclaimer. 13 | // 14 | // * Redistributions in binary form must reproduce the above copyright notice, 15 | // this list of conditions and the following disclaimer in the documentation 16 | // and/or other materials provided with the distribution. 17 | // 18 | // * Neither the name of the copyright holder nor the names of its contributors 19 | // may be used to endorse or promote products derived from this software without 20 | // specific prior written permission. 21 | // 22 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 23 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 24 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 25 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 26 | // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 27 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 28 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 29 | // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 30 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 31 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 32 | // 33 | 34 | import XCTest 35 | @testable import CommandLineKit 36 | 37 | class FlagTests: XCTestCase { 38 | 39 | func testLongFlagNames2() throws { 40 | let flags = Flags(arguments: ["--one", "--four", "912", "--three", "--five", "-3.141", 41 | "--six", "six", "seven"]) 42 | let one = flags.option(nil, "one", description: "the one option") 43 | let two = flags.option(nil, "two", description: "the two option") 44 | let three = flags.option(nil, "three", description: "the three option") 45 | let four = flags.argument(nil, "four", description: "the four option for ints", value: 0) 46 | let five = flags.double(nil, "five", description: "the five option for doubles") 47 | let six = flags.string(nil, "six", description: "the six option for strings") 48 | let seven = flags.string(nil, "seven", description: "the seven option for strings") 49 | try flags.parse() 50 | XCTAssert(one.wasSet) 51 | XCTAssert(!two.wasSet) 52 | XCTAssert(three.wasSet) 53 | XCTAssert(four.value != nil && four.value! == 912) 54 | XCTAssert(five.value != nil && five.value! == -3.141) 55 | XCTAssert(six.value != nil && six.value! == "six") 56 | XCTAssertNil(seven.value) 57 | XCTAssert(flags.parameters.count == 1 && flags.parameters[0] == "seven") 58 | } 59 | 60 | func testLongFlagNames() throws { 61 | let flags = Flags(arguments: ["--one", "--four", "912", "--three", "--five", "-3.141", 62 | "--six", "six", "seven"]) 63 | let one = flags.option(nil, "one", description: "the one option") 64 | let two = flags.option(nil, "two", description: "the two option") 65 | let three = flags.option(nil, "three", description: "the three option") 66 | let four = flags.int(nil, "four", description: "the four option for ints") 67 | let five = flags.double(nil, "five", description: "the five option for doubles") 68 | let six = flags.string(nil, "six", description: "the six option for strings") 69 | let seven = flags.string(nil, "seven", description: "the seven option for strings") 70 | try flags.parse() 71 | XCTAssert(one.wasSet) 72 | XCTAssert(!two.wasSet) 73 | XCTAssert(three.wasSet) 74 | XCTAssert(four.value != nil && four.value! == 912) 75 | XCTAssert(five.value != nil && five.value! == -3.141) 76 | XCTAssert(six.value != nil && six.value! == "six") 77 | XCTAssertNil(seven.value) 78 | XCTAssert(flags.parameters.count == 1 && flags.parameters[0] == "seven") 79 | } 80 | 81 | func testShortFlagNames() throws { 82 | let flags = Flags(arguments: ["-a", "-d", "912", "-c", "-e", "-3.141", 83 | "-f", "six", "seven"]) 84 | let one = flags.option("a", description: "the one option") 85 | let two = flags.option("b", description: "the two option") 86 | let three = flags.option("c", description: "the three option") 87 | let four = flags.int("d", description: "the four option for ints") 88 | let five = flags.double("e", description: "the five option for doubles") 89 | let six = flags.string("f", description: "the six option for strings") 90 | let seven = flags.string("g", description: "the seven option for strings") 91 | try flags.parse() 92 | XCTAssert(one.wasSet) 93 | XCTAssert(!two.wasSet) 94 | XCTAssert(three.wasSet) 95 | XCTAssert(four.value != nil && four.value! == 912) 96 | XCTAssert(five.value != nil && five.value! == -3.141) 97 | XCTAssert(six.value != nil && six.value! == "six") 98 | XCTAssertNil(seven.value) 99 | XCTAssert(flags.parameters.count == 1 && flags.parameters[0] == "seven") 100 | } 101 | 102 | func testCommandFlags() throws { 103 | struct TestCommand: Command { 104 | static var executed = false 105 | static var flagsOk = false 106 | static var name: String { 107 | return "TestTool" 108 | } 109 | static var arguments: [String] { 110 | return ["--size", "23", "-h", "--repeated", "hello", "world", "!"] 111 | } 112 | @CommandOption(short: "h", description: "help") var help: Bool 113 | @CommandArgument(description: "count") var count: Int = 7 114 | @CommandArgument(description: "size") var size: Int? 115 | @CommandArguments(description: "repeated", maxCount: 3) var repeated: [String] 116 | @CommandParameters var params: [String] 117 | @CommandFlags var flags: Flags 118 | mutating func run() { 119 | TestCommand.executed = true 120 | TestCommand.flagsOk = self.size == 23 && self.help && self.repeated.count == 3 && 121 | self.flags.toolName == "TestTool" 122 | } 123 | } 124 | try TestCommand.main() 125 | XCTAssert(TestCommand.executed) 126 | XCTAssert(TestCommand.flagsOk) 127 | } 128 | 129 | func testCommandFlagsFailure() throws { 130 | struct TestCommand: Command { 131 | static var executed = false 132 | static var arguments: [String] { 133 | return ["--foo", "23"] 134 | } 135 | @CommandOption(short: "h", description: "help") var help: Bool 136 | @CommandArgument(description: "count") var count: Int = 7 137 | @CommandArgument(description: "size") var size: Int? 138 | @CommandArguments(description: "repeated", maxCount: 3) var repeated: [String] 139 | @CommandParameters var params: [String] 140 | @CommandFlags var flags: Flags 141 | mutating func fail(with: String) { 142 | TestCommand.executed = false 143 | } 144 | mutating func run() { 145 | TestCommand.executed = true 146 | } 147 | } 148 | try TestCommand.main() 149 | XCTAssert(!TestCommand.executed) 150 | } 151 | 152 | func testMoreCommandFlags() throws { 153 | struct TestCommand: Command { 154 | static var executed = false 155 | static var arguments: [String] { 156 | return [] 157 | } 158 | @CommandArguments(short: "f", description: "Adds file path in which programs are searched for.") 159 | var filePath: [String] 160 | @CommandArguments(short: "l", description: "Adds file path in which libraries are searched for.") 161 | var libPaths: [String] 162 | @CommandArgument(short: "x", description: "Initial capacity of the heap") 163 | var heapSize: Int = 1234 164 | @CommandOption(short: "h", description: "Show description of usage and options of this tools.") 165 | var help: Bool 166 | @CommandFlags 167 | var flags: Flags 168 | mutating func run() { 169 | TestCommand.executed = true 170 | } 171 | } 172 | try TestCommand.main() 173 | XCTAssert(TestCommand.executed) 174 | } 175 | 176 | static let allTests = [ 177 | ("testLongFlagNames2", testLongFlagNames2), 178 | ("testLongFlagNames", testLongFlagNames), 179 | ("testShortFlagNames", testShortFlagNames), 180 | ("testCommandFlags", testCommandFlags), 181 | ("testCommandFlagsFailure", testCommandFlagsFailure), 182 | ("testMoreCommandFlags", testMoreCommandFlags), 183 | ] 184 | } 185 | -------------------------------------------------------------------------------- /Sources/CommandLineKit/EditState.swift: -------------------------------------------------------------------------------- 1 | // 2 | // EditState.swift 3 | // CommandLineKit 4 | // 5 | // Created by Matthias Zenger on 07/04/2018. 6 | // Copyright © 2018-2019 Google LLC 7 | // Copyright © 2017 Andy Best 8 | // Copyright © 2010-2014 Salvatore Sanfilippo 9 | // Copyright © 2010-2013 Pieter Noordhuis 10 | // 11 | // Redistribution and use in source and binary forms, with or without 12 | // modification, are permitted provided that the following conditions are met: 13 | // 14 | // * Redistributions of source code must retain the above copyright notice, 15 | // this list of conditions and the following disclaimer. 16 | // 17 | // * Redistributions in binary form must reproduce the above copyright notice, 18 | // this list of conditions and the following disclaimer in the documentation 19 | // and/or other materials provided with the distribution. 20 | // 21 | // * Neither the name of the copyright holder nor the names of its contributors 22 | // may be used to endorse or promote products derived from this software without 23 | // specific prior written permission. 24 | // 25 | // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 26 | // ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 27 | // WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 28 | // DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 29 | // ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 30 | // (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 31 | // LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 32 | // ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 33 | // (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 34 | // SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 35 | // 36 | 37 | import Foundation 38 | 39 | internal class EditState { 40 | let prompt: String 41 | let promptProperties: TextProperties 42 | let readProperties: TextProperties 43 | let parenProperties: TextProperties 44 | let maxCount: Int? 45 | var buffer: String 46 | var location: String.Index 47 | 48 | init(prompt: String, 49 | maxCount: Int? = nil, 50 | promptProperties: TextProperties = TextProperties.none, 51 | readProperties: TextProperties = TextProperties.none, 52 | parenProperties: TextProperties = TextProperties.none) { 53 | self.prompt = prompt 54 | self.promptProperties = promptProperties 55 | self.readProperties = readProperties 56 | self.parenProperties = parenProperties 57 | self.maxCount = maxCount 58 | self.buffer = "" 59 | self.location = buffer.endIndex 60 | } 61 | 62 | var cursorWidth: Int { 63 | return self.cursorPosition + self.prompt.count 64 | } 65 | 66 | var cursorPosition: Int { 67 | return self.buffer.distance(from: self.buffer.startIndex, to: self.location) 68 | } 69 | 70 | var cursorAtEnd: Bool { 71 | return self.location == self.buffer.endIndex 72 | } 73 | 74 | func insertCharacter(_ char: Character) -> Bool { 75 | if let max = self.maxCount, self.buffer.count >= max { 76 | return false 77 | } 78 | let origLoc = self.location 79 | let origEnd = self.buffer.endIndex 80 | self.buffer.insert(char, at: location) 81 | self.location = self.buffer.index(after: location) 82 | if origLoc == origEnd { 83 | self.location = self.buffer.endIndex 84 | } 85 | return true 86 | } 87 | 88 | func setBuffer(_ newBuffer: String, truncateIfNeeded: Bool = false) -> Bool { 89 | if let max = self.maxCount, newBuffer.count >= max { 90 | if truncateIfNeeded { 91 | self.buffer = String(newBuffer.prefix(max)) 92 | return true 93 | } else { 94 | return false 95 | } 96 | } else { 97 | self.buffer = newBuffer 98 | return true 99 | } 100 | } 101 | 102 | func backspace() -> Bool { 103 | if self.location != self.buffer.startIndex { 104 | if self.location != self.buffer.startIndex { 105 | self.location = self.buffer.index(before: self.location) 106 | } 107 | self.buffer.remove(at: self.location) 108 | return true 109 | } else { 110 | return false 111 | } 112 | } 113 | 114 | func moveLeft() -> Bool { 115 | if self.location == self.buffer.startIndex { 116 | return false 117 | } else { 118 | self.location = self.buffer.index(before: self.location) 119 | return true 120 | } 121 | } 122 | 123 | func moveRight() -> Bool { 124 | if self.location == self.buffer.endIndex { 125 | return false 126 | } else { 127 | self.location = self.buffer.index(after: self.location) 128 | return true 129 | } 130 | } 131 | 132 | func moveHome() -> Bool { 133 | if self.location == self.buffer.startIndex { 134 | return false 135 | } else { 136 | self.location = self.buffer.startIndex 137 | return true 138 | } 139 | } 140 | 141 | func moveEnd() -> Bool { 142 | if self.location == self.buffer.endIndex { 143 | return false 144 | } else { 145 | self.location = self.buffer.endIndex 146 | return true 147 | } 148 | } 149 | 150 | func deleteCharacter() -> Bool { 151 | if self.location >= self.buffer.endIndex || self.buffer.isEmpty { 152 | return false 153 | } else { 154 | self.buffer.remove(at: self.location) 155 | return true 156 | } 157 | } 158 | 159 | func eraseCharacterRight() -> Bool { 160 | if self.buffer.count == 0 || self.location >= self.buffer.endIndex { 161 | return false 162 | } else { 163 | self.buffer.remove(at: self.location) 164 | if self.location > self.buffer.endIndex { 165 | self.location = self.buffer.endIndex 166 | } 167 | return true 168 | } 169 | } 170 | 171 | func deletePreviousWord() -> Bool { 172 | let oldLocation = self.location 173 | // Go backwards to find the first non space character 174 | while self.location > self.buffer.startIndex && 175 | self.buffer[self.buffer.index(before: self.location)] == " " { 176 | self.location = self.buffer.index(before: self.location) 177 | } 178 | // Go backwards to find the next space character (start of the word) 179 | while self.location > self.buffer.startIndex && 180 | self.buffer[self.buffer.index(before: self.location)] != " " { 181 | self.location = self.buffer.index(before: self.location) 182 | } 183 | if self.buffer.distance(from: oldLocation, to: self.location) == 0 { 184 | return false 185 | } else { 186 | self.buffer.removeSubrange(self.location.. Bool { 192 | let oldLocation = self.location 193 | // Go backwards to find the first non space character 194 | while self.location > self.buffer.startIndex && 195 | self.buffer[self.buffer.index(before: self.location)] == " " { 196 | self.location = self.buffer.index(before: self.location) 197 | } 198 | // Go backwards to find the next space character (start of the word) 199 | while self.location > self.buffer.startIndex && 200 | self.buffer[self.buffer.index(before: self.location)] != " " { 201 | self.location = self.buffer.index(before: self.location) 202 | } 203 | return self.buffer.distance(from: oldLocation, to: self.location) != 0 204 | } 205 | 206 | func moveToWordEnd() -> Bool { 207 | let oldLocation = self.location 208 | // Go forward to find the first non space character 209 | while self.location < self.buffer.endIndex && self.buffer[self.location] == " " { 210 | self.location = self.buffer.index(after: self.location) 211 | } 212 | // Go forward to find the next space character (end of the word) 213 | while self.location < self.buffer.endIndex && self.buffer[self.location] != " " { 214 | self.location = self.buffer.index(after: self.location) 215 | } 216 | return self.buffer.distance(from: oldLocation, to: self.location) != 0 217 | } 218 | 219 | func deleteToEndOfLine() -> Bool { 220 | if self.location == self.buffer.endIndex || self.buffer.isEmpty { 221 | return false 222 | } else { 223 | self.buffer.removeLast(self.buffer.count - self.cursorPosition) 224 | return true 225 | } 226 | } 227 | 228 | func swapCharacterWithPrevious() -> Bool { 229 | if buffer.count < 2 { 230 | return false 231 | } else if self.location == self.buffer.endIndex { 232 | // Swap the two previous characters if at end index 233 | let temp = self.buffer.remove(at: self.buffer.index(self.location, offsetBy: -2)) 234 | self.buffer.insert(temp, at: self.buffer.endIndex) 235 | self.location = self.buffer.endIndex 236 | return true 237 | } else if self.location > self.buffer.startIndex { 238 | // If the characters are in the middle of the string, swap character under cursor with 239 | // previous, then move the cursor to the right 240 | let temp = self.buffer.remove(at: self.buffer.index(before: self.location)) 241 | self.buffer.insert(temp, at: self.location) 242 | if self.location < self.buffer.endIndex { 243 | self.location = buffer.index(after: self.location) 244 | } 245 | return true 246 | } else if self.location == self.buffer.startIndex { 247 | // If the character is at the start of the string, swap the first two characters, then 248 | // put the cursor after them 249 | let temp = self.buffer.remove(at: self.location) 250 | self.buffer.insert(temp, at: self.buffer.index(after: self.location)) 251 | if self.location < self.buffer.endIndex { 252 | self.location = self.buffer.index(self.buffer.startIndex, offsetBy: 2) 253 | } 254 | return true 255 | } else { 256 | return false 257 | } 258 | } 259 | 260 | func requiresMatching() -> Bool { 261 | guard self.location > self.buffer.startIndex, !self.parenProperties.isEmpty else { 262 | return false 263 | } 264 | switch self.buffer[self.buffer.index(before: self.location)] { 265 | case "(", ")", "[", "]", "{", "}": 266 | return true 267 | default: 268 | return false 269 | } 270 | } 271 | 272 | func matchingParen() -> String.Index? { 273 | guard self.location > self.buffer.startIndex, !self.parenProperties.isEmpty else { 274 | return nil 275 | } 276 | var idx = self.buffer.index(before: self.location) 277 | let this: Character = self.buffer[idx] 278 | let other: Character 279 | let forward: Bool 280 | switch this { 281 | case "(": 282 | other = ")" 283 | forward = true 284 | case ")": 285 | other = "(" 286 | forward = false 287 | case "[": 288 | other = "]" 289 | forward = true 290 | case "]": 291 | other = "[" 292 | forward = false 293 | case "{": 294 | other = "}" 295 | forward = true 296 | case "}": 297 | other = "{" 298 | forward = false 299 | default: 300 | return nil 301 | } 302 | var open = 0 303 | if forward { 304 | idx = self.buffer.index(after: idx) 305 | while idx < self.buffer.endIndex && (open > 0 || self.buffer[idx] != other) { 306 | if self.buffer[idx] == this { 307 | open += 1 308 | } else if self.buffer[idx] == other { 309 | open -= 1 310 | } 311 | idx = self.buffer.index(after: idx) 312 | } 313 | if idx < self.buffer.endIndex && open == 0 { 314 | return idx 315 | } 316 | } else if idx > self.buffer.startIndex { 317 | idx = self.buffer.index(before: idx) 318 | while idx >= self.buffer.startIndex && (open > 0 || self.buffer[idx] != other) { 319 | if self.buffer[idx] == this { 320 | open += 1 321 | } else if self.buffer[idx] == other { 322 | open -= 1 323 | } 324 | guard idx > self.buffer.startIndex else { 325 | return nil 326 | } 327 | idx = self.buffer.index(before: idx) 328 | } 329 | if idx >= self.buffer.startIndex && open == 0 { 330 | return idx 331 | } 332 | } 333 | return nil 334 | } 335 | 336 | func withTemporaryState(_ body: () throws -> () ) throws { 337 | let originalBuffer = self.buffer 338 | let originalLocation = self.location 339 | try body() 340 | self.buffer = originalBuffer 341 | self.location = originalLocation 342 | } 343 | } 344 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Swift CommandLineKit 2 | 3 | [![Platform: macOS](https://img.shields.io/badge/Platform-macOS-blue.svg?style=flat)](https://developer.apple.com/osx/) 4 | [![Platform: Linux](https://img.shields.io/badge/Platform-Linux-blue.svg?style=flat)](https://www.ubuntu.com/) 5 | [![Language: Swift 5.8](https://img.shields.io/badge/Language-Swift%205.8-green.svg?style=flat)](https://developer.apple.com/swift/) 6 | [![IDE: Xcode 14](https://img.shields.io/badge/IDE-Xcode%2014-orange.svg?style=flat)](https://developer.apple.com/xcode/) 7 | [![Carthage: compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 8 | [![License: BSD](https://img.shields.io/badge/License-BSD-lightgrey.svg?style=flat)](https://developers.google.com/open-source/licenses/bsd) 9 | 10 | ## Overview 11 | 12 | This is a library supporting the development of command-line tools in 13 | the programming language Swift on macOS. It also compiles under Linux. 14 | The library provides the following functionality: 15 | 16 | - Management of command-line arguments, 17 | - Usage of escape sequences on terminals, and 18 | - Reading strings on terminals using a lineread-inspired implementation 19 | based on the library [Linenoise-Swift](https://github.com/andybest/linenoise-swift), 20 | but supporting unicode input, multiple lines, and styled text. 21 | 22 | ## Command-line arguments 23 | 24 | ### Basics 25 | 26 | CommandLineKit handles command-line arguments with the following protocol: 27 | 28 | 1. A new [Flags](https://github.com/objecthub/swift-commandlinekit/blob/master/Sources/CommandLineKit/Flags.swift) 29 | object gets created either for the system-provided command-line arguments or for a 30 | custom sequence of arguments. 31 | 2. For every flag, a [Flag](https://github.com/objecthub/swift-commandlinekit/blob/master/Sources/CommandLineKit/Flag.swift) 32 | object is being created and registered in the `Flags` object. 33 | 3. Once all flag objects are declared and registered, the command-line gets parsed. After parsing 34 | is complete, the flag objects can be used to access the extracted options and arguments. 35 | 36 | CommandLineKit defines different types of 37 | [Flag](https://github.com/objecthub/swift-commandlinekit/blob/master/Sources/CommandLineKit/Flag.swift) 38 | subclasses for handling _options_ (i.e. flags without 39 | parameters) and _arguments_ (i.e. flags with parameters). Arguments are either _singleton arguments_ (i.e. they 40 | have exactly one value) or they are _repeated arguments_ (i.e. they have many values). Arguments are 41 | parameterized with a type which defines how to parse values. The framework natively supports _int_, 42 | _double_, _string_, and _enum_ types, which means that in practice, just using the built-in flag classes 43 | are almost always sufficient. Nevertheless, 44 | [the framework is extensible](https://github.com/objecthub/swift-commandlinekit/tree/master/Sources/CommandLineKit) 45 | and supports arbitrary argument types. 46 | 47 | A flag is identified by a _short name_ character and a _long name_ string. At least one of the two needs to be 48 | defined. For instance, the "help" option could be defined by the short name "h" and the long name "help". 49 | On the command-line, a user could either use `-h` or `--help` to refer to this option; i.e. short names are 50 | prefixed with a single dash, long names are prefixed with a double dash. 51 | 52 | An argument is a parameterized flag. The parameters follow directly the flag identifier (typically separated by 53 | a space). For instance, an integer argument with long name "size" could be defined as: `--size 64`. If the 54 | argument is repeated, then multiple parameters may follow the flag identifier, as in this 55 | example: `--size 2 4 8 16`. The sequence is terminated by either the end of the command-line arguments, 56 | another flag, or the terminator "---". All command-line arguments following the terminator are not being parsed 57 | and are returned in the `parameters` field of the `Flags` object. 58 | 59 | ### Programmatic API 60 | 61 | Here is an [example](https://github.com/objecthub/swift-lispkit/blob/master/Sources/LispKitRepl/main.swift) 62 | from the [LispKit](https://github.com/objecthub/swift-lispkit) project. It uses factory methods (like `flags.string`, 63 | `flags.int`, `flags.option`, `flags.strings`, etc.) provided by the 64 | [Flags](https://github.com/objecthub/swift-commandlinekit/blob/master/Sources/CommandLineKit/Flags.swift) 65 | class to create and register individual flags. 66 | 67 | ```swift 68 | // Create a new flags object for the system-provided command-line arguments 69 | var flags = Flags() 70 | 71 | // Define the various flags 72 | let filePaths = flags.strings("f", "filepath", 73 | description: "Adds file path in which programs are searched for.") 74 | let libPaths = flags.strings("l", "libpath", 75 | description: "Adds file path in which libraries are searched for.") 76 | let heapSize = flags.int("x", "heapsize", 77 | description: "Initial capacity of the heap", value: 1000) 78 | let importLibs = flags.strings("i", "import", 79 | description: "Imports library automatically after startup.") 80 | let prelude = flags.string("p", "prelude", 81 | description: "Path to prelude file which gets executed after " + 82 | "loading all provided libraries.") 83 | let prompt = flags.string("r", "prompt", 84 | description: "String used as prompt in REPL.", value: "> ") 85 | let quiet = flags.option("q", "quiet", 86 | description: "In quiet mode, optional messages are not printed.") 87 | let help = flags.option("h", "help", 88 | description: "Show description of usage and options of this tools.") 89 | 90 | // Parse the command-line arguments and return error message if parsing fails 91 | if let failure = flags.parsingFailure() { 92 | print(failure) 93 | exit(1) 94 | } 95 | ``` 96 | 97 | The framework supports printing the supported options via the `Flags.usageDescription` function. For the 98 | command-line flags as defined above, this function returns the following usage description: 99 | 100 | ``` 101 | usage: LispKitRepl [