├── 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 | [](https://developer.apple.com/osx/)
4 | [](https://www.ubuntu.com/)
5 | [](https://developer.apple.com/swift/)
6 | [](https://developer.apple.com/xcode/)
7 | [](https://github.com/Carthage/Carthage)
8 | [](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 [