├── .gitignore ├── .swift-version ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE.txt ├── Moderator.podspec ├── Moderator.xcodeproj ├── ModeratorTests_Info.plist ├── Moderator_Info.plist ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcshareddata │ └── xcschemes │ ├── Moderator Swift 3.xcscheme │ ├── Moderator.xcscheme │ └── xcschememanagement.plist ├── Package.swift ├── Package@swift-4.swift ├── README.md ├── Sources ├── Moderator.swift ├── Parsers.swift └── SwiftCompat.swift └── Tests ├── LinuxMain.swift └── ModeratorTests └── Moderator_Tests.swift /.gitignore: -------------------------------------------------------------------------------- 1 | # Xcode 2 | # 3 | .DS_Store 4 | build/ 5 | .build 6 | *.pbxuser 7 | !default.pbxuser 8 | *.mode1v3 9 | !default.mode1v3 10 | *.mode2v3 11 | !default.mode2v3 12 | *.perspectivev3 13 | !default.perspectivev3 14 | xcuserdata 15 | *.xccheckout 16 | *.moved-aside 17 | DerivedData 18 | *.hmap 19 | *.ipa 20 | -------------------------------------------------------------------------------- /.swift-version: -------------------------------------------------------------------------------- 1 | 4.1.2 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | include: 3 | - name: "Linux Swift 3.1.1" 4 | os: linux 5 | language: generic 6 | dist: trusty 7 | sudo: required 8 | env: 9 | - SWIFT_BRANCH=swift-3.1.1-release 10 | - SWIFT_VERSION=swift-3.1.1-RELEASE 11 | install: 12 | - mkdir swift 13 | - curl https://swift.org/builds/$SWIFT_BRANCH/ubuntu1404/$SWIFT_VERSION/$SWIFT_VERSION-ubuntu14.04.tar.gz -s | tar -xz -C swift 14 | - export PATH="$(pwd)/swift/$SWIFT_VERSION-ubuntu14.04/usr/bin:$PATH" 15 | 16 | - name: "Linux Swift 4.1.3" 17 | os: linux 18 | language: generic 19 | dist: trusty 20 | sudo: required 21 | env: 22 | - SWIFT_BRANCH=swift-4.1.3-release 23 | - SWIFT_VERSION=swift-4.1.3-RELEASE 24 | install: 25 | - mkdir swift 26 | - curl https://swift.org/builds/$SWIFT_BRANCH/ubuntu1404/$SWIFT_VERSION/$SWIFT_VERSION-ubuntu14.04.tar.gz -s | tar -xz -C swift 27 | - export PATH="$(pwd)/swift/$SWIFT_VERSION-ubuntu14.04/usr/bin:$PATH" 28 | 29 | - name: "Linux Swift 4.2.3" 30 | os: linux 31 | language: generic 32 | dist: trusty 33 | sudo: required 34 | env: 35 | - SWIFT_BRANCH=swift-4.2.3-release 36 | - SWIFT_VERSION=swift-4.2.3-RELEASE 37 | install: 38 | - mkdir swift 39 | - curl https://swift.org/builds/$SWIFT_BRANCH/ubuntu1404/$SWIFT_VERSION/$SWIFT_VERSION-ubuntu14.04.tar.gz -s | tar -xz -C swift 40 | - export PATH="$(pwd)/swift/$SWIFT_VERSION-ubuntu14.04/usr/bin:$PATH" 41 | 42 | - name: "Linux Swift 5.0" 43 | os: linux 44 | language: generic 45 | dist: trusty 46 | sudo: required 47 | env: 48 | - SWIFT_BRANCH=swift-5.0-release 49 | - SWIFT_VERSION=swift-5.0-RELEASE 50 | install: 51 | - mkdir swift 52 | - curl https://swift.org/builds/$SWIFT_BRANCH/ubuntu1404/$SWIFT_VERSION/$SWIFT_VERSION-ubuntu14.04.tar.gz -s | tar -xz -C swift 53 | - export PATH="$(pwd)/swift/$SWIFT_VERSION-ubuntu14.04/usr/bin:$PATH" 54 | 55 | - name: "Mac Xcode 9" 56 | os: osx 57 | osx_image: xcode9.4 58 | language: generic 59 | sudo: required 60 | 61 | - name: "Mac Xcode 10" 62 | os: osx 63 | osx_image: xcode10 64 | language: generic 65 | sudo: required 66 | 67 | script: 68 | - swift package reset 69 | - swift build 70 | - swift test 71 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## How to contribute 2 | 3 | Issues and suggestions [are always welcome](https://github.com/kareman/Moderator.swift/issues). 4 | 5 | If you want to make changes yourself please follow the standard [pull request guidelines](http://help.github.com/pull-requests/). In short: fork, create topic branch, one commit per atomic change, make sure all unit tests pass, and create the pull request. 6 | 7 | If it's a sizeable change or will break backwards compatibility it's probably best if you create an issue first so we can discuss the changes beforehand. 8 | 9 | #### Testing 10 | 11 | - Unit tests are awesome. Please create new ones to test the changes you make. 12 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Kåre Morstøl, NotTooBad Software (nottoobadsoftware.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Moderator.podspec: -------------------------------------------------------------------------------- 1 | Pod::Spec.new do |s| 2 | s.name = 'Moderator' 3 | s.version = '0.5.1' 4 | s.summary = 'A simple, modular command line argument parser in Swift.' 5 | s.description = 'Moderator is a simple Swift library for parsing commandline arguments.' 6 | s.homepage = 'https://github.com/kareman/Moderator' 7 | s.license = { type: 'MIT', file: 'LICENSE.txt' } 8 | s.author = { 'Kare Morstol' => 'kare@nottoobadsoftware.com' } 9 | s.source = { git: 'https://github.com/kareman/Moderator.git', tag: s.version.to_s } 10 | s.source_files = 'Sources/*.swift' 11 | s.osx.deployment_target = '10.10' 12 | end 13 | -------------------------------------------------------------------------------- /Moderator.xcodeproj/ModeratorTests_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | BNDL 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Moderator.xcodeproj/Moderator_Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | CFBundleDevelopmentRegion 5 | en 6 | CFBundleExecutable 7 | $(EXECUTABLE_NAME) 8 | CFBundleIdentifier 9 | $(PRODUCT_BUNDLE_IDENTIFIER) 10 | CFBundleInfoDictionaryVersion 11 | 6.0 12 | CFBundleName 13 | $(PRODUCT_NAME) 14 | CFBundlePackageType 15 | FMWK 16 | CFBundleShortVersionString 17 | 1.0 18 | CFBundleSignature 19 | ???? 20 | CFBundleVersion 21 | $(CURRENT_PROJECT_VERSION) 22 | NSPrincipalClass 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Moderator.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | BA002F8121261C2C00ECA66A /* SwiftCompat.swift in Sources */ = {isa = PBXBuildFile; fileRef = BA002F8021261C2C00ECA66A /* SwiftCompat.swift */; }; 11 | OBJ_22 /* Moderator.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_9 /* Moderator.swift */; }; 12 | OBJ_23 /* Parsers.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_10 /* Parsers.swift */; }; 13 | OBJ_30 /* Moderator_Tests.swift in Sources */ = {isa = PBXBuildFile; fileRef = OBJ_13 /* Moderator_Tests.swift */; }; 14 | OBJ_32 /* Moderator.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = OBJ_15 /* Moderator.framework */; }; 15 | /* End PBXBuildFile section */ 16 | 17 | /* Begin PBXContainerItemProxy section */ 18 | BAB4C2131DDE1618001201AE /* PBXContainerItemProxy */ = { 19 | isa = PBXContainerItemProxy; 20 | containerPortal = OBJ_1 /* Project object */; 21 | proxyType = 1; 22 | remoteGlobalIDString = OBJ_17; 23 | remoteInfo = Moderator; 24 | }; 25 | /* End PBXContainerItemProxy section */ 26 | 27 | /* Begin PBXFileReference section */ 28 | BA002F8021261C2C00ECA66A /* SwiftCompat.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SwiftCompat.swift; sourceTree = ""; }; 29 | OBJ_10 /* Parsers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Parsers.swift; sourceTree = ""; }; 30 | OBJ_13 /* Moderator_Tests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Moderator_Tests.swift; sourceTree = ""; }; 31 | OBJ_15 /* Moderator.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Moderator.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 32 | OBJ_16 /* ModeratorTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; path = ModeratorTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 33 | OBJ_6 /* Package.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; path = Package.swift; sourceTree = ""; }; 34 | OBJ_9 /* Moderator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Moderator.swift; sourceTree = ""; }; 35 | /* End PBXFileReference section */ 36 | 37 | /* Begin PBXFrameworksBuildPhase section */ 38 | OBJ_24 /* Frameworks */ = { 39 | isa = PBXFrameworksBuildPhase; 40 | buildActionMask = 0; 41 | files = ( 42 | ); 43 | runOnlyForDeploymentPostprocessing = 0; 44 | }; 45 | OBJ_31 /* Frameworks */ = { 46 | isa = PBXFrameworksBuildPhase; 47 | buildActionMask = 0; 48 | files = ( 49 | OBJ_32 /* Moderator.framework in Frameworks */, 50 | ); 51 | runOnlyForDeploymentPostprocessing = 0; 52 | }; 53 | /* End PBXFrameworksBuildPhase section */ 54 | 55 | /* Begin PBXGroup section */ 56 | OBJ_11 /* Tests */ = { 57 | isa = PBXGroup; 58 | children = ( 59 | OBJ_12 /* ModeratorTests */, 60 | ); 61 | path = Tests; 62 | sourceTree = ""; 63 | }; 64 | OBJ_12 /* ModeratorTests */ = { 65 | isa = PBXGroup; 66 | children = ( 67 | OBJ_13 /* Moderator_Tests.swift */, 68 | ); 69 | name = ModeratorTests; 70 | path = Tests/ModeratorTests; 71 | sourceTree = SOURCE_ROOT; 72 | }; 73 | OBJ_14 /* Products */ = { 74 | isa = PBXGroup; 75 | children = ( 76 | OBJ_15 /* Moderator.framework */, 77 | OBJ_16 /* ModeratorTests.xctest */, 78 | ); 79 | name = Products; 80 | sourceTree = BUILT_PRODUCTS_DIR; 81 | }; 82 | OBJ_5 = { 83 | isa = PBXGroup; 84 | children = ( 85 | OBJ_6 /* Package.swift */, 86 | OBJ_7 /* Sources */, 87 | OBJ_11 /* Tests */, 88 | OBJ_14 /* Products */, 89 | ); 90 | sourceTree = ""; 91 | }; 92 | OBJ_7 /* Sources */ = { 93 | isa = PBXGroup; 94 | children = ( 95 | OBJ_8 /* Moderator */, 96 | ); 97 | path = Sources; 98 | sourceTree = ""; 99 | }; 100 | OBJ_8 /* Moderator */ = { 101 | isa = PBXGroup; 102 | children = ( 103 | BA002F8021261C2C00ECA66A /* SwiftCompat.swift */, 104 | OBJ_9 /* Moderator.swift */, 105 | OBJ_10 /* Parsers.swift */, 106 | ); 107 | name = Moderator; 108 | path = Sources; 109 | sourceTree = SOURCE_ROOT; 110 | }; 111 | /* End PBXGroup section */ 112 | 113 | /* Begin PBXNativeTarget section */ 114 | OBJ_17 /* Moderator */ = { 115 | isa = PBXNativeTarget; 116 | buildConfigurationList = OBJ_18 /* Build configuration list for PBXNativeTarget "Moderator" */; 117 | buildPhases = ( 118 | OBJ_21 /* Sources */, 119 | OBJ_24 /* Frameworks */, 120 | ); 121 | buildRules = ( 122 | ); 123 | dependencies = ( 124 | ); 125 | name = Moderator; 126 | productName = Moderator; 127 | productReference = OBJ_15 /* Moderator.framework */; 128 | productType = "com.apple.product-type.framework"; 129 | }; 130 | OBJ_25 /* ModeratorTests */ = { 131 | isa = PBXNativeTarget; 132 | buildConfigurationList = OBJ_26 /* Build configuration list for PBXNativeTarget "ModeratorTests" */; 133 | buildPhases = ( 134 | OBJ_29 /* Sources */, 135 | OBJ_31 /* Frameworks */, 136 | ); 137 | buildRules = ( 138 | ); 139 | dependencies = ( 140 | OBJ_33 /* PBXTargetDependency */, 141 | ); 142 | name = ModeratorTests; 143 | productName = ModeratorTests; 144 | productReference = OBJ_16 /* ModeratorTests.xctest */; 145 | productType = "com.apple.product-type.bundle.unit-test"; 146 | }; 147 | /* End PBXNativeTarget section */ 148 | 149 | /* Begin PBXProject section */ 150 | OBJ_1 /* Project object */ = { 151 | isa = PBXProject; 152 | attributes = { 153 | LastUpgradeCheck = 9999; 154 | TargetAttributes = { 155 | OBJ_17 = { 156 | LastSwiftMigration = ""; 157 | }; 158 | OBJ_25 = { 159 | LastSwiftMigration = ""; 160 | }; 161 | }; 162 | }; 163 | buildConfigurationList = OBJ_2 /* Build configuration list for PBXProject "Moderator" */; 164 | compatibilityVersion = "Xcode 3.2"; 165 | developmentRegion = English; 166 | hasScannedForEncodings = 0; 167 | knownRegions = ( 168 | en, 169 | ); 170 | mainGroup = OBJ_5; 171 | productRefGroup = OBJ_14 /* Products */; 172 | projectDirPath = ""; 173 | projectRoot = ""; 174 | targets = ( 175 | OBJ_17 /* Moderator */, 176 | OBJ_25 /* ModeratorTests */, 177 | ); 178 | }; 179 | /* End PBXProject section */ 180 | 181 | /* Begin PBXSourcesBuildPhase section */ 182 | OBJ_21 /* Sources */ = { 183 | isa = PBXSourcesBuildPhase; 184 | buildActionMask = 0; 185 | files = ( 186 | OBJ_22 /* Moderator.swift in Sources */, 187 | OBJ_23 /* Parsers.swift in Sources */, 188 | BA002F8121261C2C00ECA66A /* SwiftCompat.swift in Sources */, 189 | ); 190 | runOnlyForDeploymentPostprocessing = 0; 191 | }; 192 | OBJ_29 /* Sources */ = { 193 | isa = PBXSourcesBuildPhase; 194 | buildActionMask = 0; 195 | files = ( 196 | OBJ_30 /* Moderator_Tests.swift in Sources */, 197 | ); 198 | runOnlyForDeploymentPostprocessing = 0; 199 | }; 200 | /* End PBXSourcesBuildPhase section */ 201 | 202 | /* Begin PBXTargetDependency section */ 203 | OBJ_33 /* PBXTargetDependency */ = { 204 | isa = PBXTargetDependency; 205 | target = OBJ_17 /* Moderator */; 206 | targetProxy = BAB4C2131DDE1618001201AE /* PBXContainerItemProxy */; 207 | }; 208 | /* End PBXTargetDependency section */ 209 | 210 | /* Begin XCBuildConfiguration section */ 211 | BA002F82212647D700ECA66A /* Debug Swift 3 */ = { 212 | isa = XCBuildConfiguration; 213 | buildSettings = { 214 | COMBINE_HIDPI_IMAGES = YES; 215 | COPY_PHASE_STRIP = NO; 216 | DEBUG_INFORMATION_FORMAT = dwarf; 217 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 218 | ENABLE_NS_ASSERTIONS = YES; 219 | GCC_OPTIMIZATION_LEVEL = 0; 220 | MACOSX_DEPLOYMENT_TARGET = 10.10; 221 | ONLY_ACTIVE_ARCH = YES; 222 | OTHER_SWIFT_FLAGS = "-DXcode"; 223 | PRODUCT_NAME = "$(TARGET_NAME)"; 224 | SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; 225 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 226 | SWIFT_VERSION = 3.0; 227 | USE_HEADERMAP = NO; 228 | }; 229 | name = "Debug Swift 3"; 230 | }; 231 | BA002F83212647D700ECA66A /* Debug Swift 3 */ = { 232 | isa = XCBuildConfiguration; 233 | buildSettings = { 234 | ENABLE_TESTABILITY = YES; 235 | FRAMEWORK_SEARCH_PATHS = "$(PLATFORM_DIR)/Developer/Library/Frameworks"; 236 | HEADER_SEARCH_PATHS = ""; 237 | INFOPLIST_FILE = Moderator.xcodeproj/Moderator_Info.plist; 238 | LD_RUNPATH_SEARCH_PATHS = "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; 239 | OTHER_LDFLAGS = "$(inherited)"; 240 | OTHER_SWIFT_FLAGS = "$(inherited)"; 241 | PRODUCT_BUNDLE_IDENTIFIER = Moderator; 242 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 243 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 244 | SUPPORTED_PLATFORMS = macosx; 245 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = SWIFT_PACKAGE; 246 | TARGET_NAME = Moderator; 247 | }; 248 | name = "Debug Swift 3"; 249 | }; 250 | BA002F84212647D700ECA66A /* Debug Swift 3 */ = { 251 | isa = XCBuildConfiguration; 252 | buildSettings = { 253 | EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; 254 | FRAMEWORK_SEARCH_PATHS = "$(PLATFORM_DIR)/Developer/Library/Frameworks"; 255 | HEADER_SEARCH_PATHS = ""; 256 | INFOPLIST_FILE = Moderator.xcodeproj/ModeratorTests_Info.plist; 257 | LD_RUNPATH_SEARCH_PATHS = "@loader_path/../Frameworks"; 258 | OTHER_LDFLAGS = "$(inherited)"; 259 | OTHER_SWIFT_FLAGS = "$(inherited)"; 260 | SUPPORTED_PLATFORMS = macosx; 261 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = SWIFT_PACKAGE; 262 | TARGET_NAME = ModeratorTests; 263 | }; 264 | name = "Debug Swift 3"; 265 | }; 266 | OBJ_19 /* Debug */ = { 267 | isa = XCBuildConfiguration; 268 | buildSettings = { 269 | ENABLE_TESTABILITY = YES; 270 | FRAMEWORK_SEARCH_PATHS = "$(PLATFORM_DIR)/Developer/Library/Frameworks"; 271 | HEADER_SEARCH_PATHS = ""; 272 | INFOPLIST_FILE = Moderator.xcodeproj/Moderator_Info.plist; 273 | LD_RUNPATH_SEARCH_PATHS = "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; 274 | OTHER_LDFLAGS = "$(inherited)"; 275 | OTHER_SWIFT_FLAGS = "$(inherited)"; 276 | PRODUCT_BUNDLE_IDENTIFIER = Moderator; 277 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 278 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 279 | SUPPORTED_PLATFORMS = macosx; 280 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = SWIFT_PACKAGE; 281 | TARGET_NAME = Moderator; 282 | }; 283 | name = Debug; 284 | }; 285 | OBJ_20 /* Release */ = { 286 | isa = XCBuildConfiguration; 287 | buildSettings = { 288 | ENABLE_TESTABILITY = YES; 289 | FRAMEWORK_SEARCH_PATHS = "$(PLATFORM_DIR)/Developer/Library/Frameworks"; 290 | HEADER_SEARCH_PATHS = ""; 291 | INFOPLIST_FILE = Moderator.xcodeproj/Moderator_Info.plist; 292 | LD_RUNPATH_SEARCH_PATHS = "$(TOOLCHAIN_DIR)/usr/lib/swift/macosx"; 293 | OTHER_LDFLAGS = "$(inherited)"; 294 | OTHER_SWIFT_FLAGS = "$(inherited)"; 295 | PRODUCT_BUNDLE_IDENTIFIER = Moderator; 296 | PRODUCT_MODULE_NAME = "$(TARGET_NAME:c99extidentifier)"; 297 | PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; 298 | SUPPORTED_PLATFORMS = macosx; 299 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = SWIFT_PACKAGE; 300 | TARGET_NAME = Moderator; 301 | }; 302 | name = Release; 303 | }; 304 | OBJ_27 /* Debug */ = { 305 | isa = XCBuildConfiguration; 306 | buildSettings = { 307 | EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; 308 | FRAMEWORK_SEARCH_PATHS = "$(PLATFORM_DIR)/Developer/Library/Frameworks"; 309 | HEADER_SEARCH_PATHS = ""; 310 | INFOPLIST_FILE = Moderator.xcodeproj/ModeratorTests_Info.plist; 311 | LD_RUNPATH_SEARCH_PATHS = "@loader_path/../Frameworks"; 312 | OTHER_LDFLAGS = "$(inherited)"; 313 | OTHER_SWIFT_FLAGS = "$(inherited)"; 314 | SUPPORTED_PLATFORMS = macosx; 315 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = SWIFT_PACKAGE; 316 | TARGET_NAME = ModeratorTests; 317 | }; 318 | name = Debug; 319 | }; 320 | OBJ_28 /* Release */ = { 321 | isa = XCBuildConfiguration; 322 | buildSettings = { 323 | EMBEDDED_CONTENT_CONTAINS_SWIFT = YES; 324 | FRAMEWORK_SEARCH_PATHS = "$(PLATFORM_DIR)/Developer/Library/Frameworks"; 325 | HEADER_SEARCH_PATHS = ""; 326 | INFOPLIST_FILE = Moderator.xcodeproj/ModeratorTests_Info.plist; 327 | LD_RUNPATH_SEARCH_PATHS = "@loader_path/../Frameworks"; 328 | OTHER_LDFLAGS = "$(inherited)"; 329 | OTHER_SWIFT_FLAGS = "$(inherited)"; 330 | SUPPORTED_PLATFORMS = macosx; 331 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = SWIFT_PACKAGE; 332 | TARGET_NAME = ModeratorTests; 333 | }; 334 | name = Release; 335 | }; 336 | OBJ_3 /* Debug */ = { 337 | isa = XCBuildConfiguration; 338 | buildSettings = { 339 | COMBINE_HIDPI_IMAGES = YES; 340 | COPY_PHASE_STRIP = NO; 341 | DEBUG_INFORMATION_FORMAT = dwarf; 342 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 343 | ENABLE_NS_ASSERTIONS = YES; 344 | GCC_OPTIMIZATION_LEVEL = 0; 345 | MACOSX_DEPLOYMENT_TARGET = 10.10; 346 | ONLY_ACTIVE_ARCH = YES; 347 | OTHER_SWIFT_FLAGS = "-DXcode"; 348 | PRODUCT_NAME = "$(TARGET_NAME)"; 349 | SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; 350 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 351 | SWIFT_VERSION = 4.0; 352 | USE_HEADERMAP = NO; 353 | }; 354 | name = Debug; 355 | }; 356 | OBJ_4 /* Release */ = { 357 | isa = XCBuildConfiguration; 358 | buildSettings = { 359 | COMBINE_HIDPI_IMAGES = YES; 360 | COPY_PHASE_STRIP = YES; 361 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 362 | DYLIB_INSTALL_NAME_BASE = "@rpath"; 363 | GCC_OPTIMIZATION_LEVEL = s; 364 | MACOSX_DEPLOYMENT_TARGET = 10.10; 365 | OTHER_SWIFT_FLAGS = "-DXcode"; 366 | PRODUCT_NAME = "$(TARGET_NAME)"; 367 | SUPPORTED_PLATFORMS = "macosx iphoneos iphonesimulator appletvos appletvsimulator watchos watchsimulator"; 368 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 369 | SWIFT_VERSION = 4.0; 370 | USE_HEADERMAP = NO; 371 | }; 372 | name = Release; 373 | }; 374 | /* End XCBuildConfiguration section */ 375 | 376 | /* Begin XCConfigurationList section */ 377 | OBJ_18 /* Build configuration list for PBXNativeTarget "Moderator" */ = { 378 | isa = XCConfigurationList; 379 | buildConfigurations = ( 380 | OBJ_19 /* Debug */, 381 | BA002F83212647D700ECA66A /* Debug Swift 3 */, 382 | OBJ_20 /* Release */, 383 | ); 384 | defaultConfigurationIsVisible = 0; 385 | defaultConfigurationName = Debug; 386 | }; 387 | OBJ_2 /* Build configuration list for PBXProject "Moderator" */ = { 388 | isa = XCConfigurationList; 389 | buildConfigurations = ( 390 | OBJ_3 /* Debug */, 391 | BA002F82212647D700ECA66A /* Debug Swift 3 */, 392 | OBJ_4 /* Release */, 393 | ); 394 | defaultConfigurationIsVisible = 0; 395 | defaultConfigurationName = Debug; 396 | }; 397 | OBJ_26 /* Build configuration list for PBXNativeTarget "ModeratorTests" */ = { 398 | isa = XCConfigurationList; 399 | buildConfigurations = ( 400 | OBJ_27 /* Debug */, 401 | BA002F84212647D700ECA66A /* Debug Swift 3 */, 402 | OBJ_28 /* Release */, 403 | ); 404 | defaultConfigurationIsVisible = 0; 405 | defaultConfigurationName = Debug; 406 | }; 407 | /* End XCConfigurationList section */ 408 | }; 409 | rootObject = OBJ_1 /* Project object */; 410 | } 411 | -------------------------------------------------------------------------------- /Moderator.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Moderator.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Moderator.xcodeproj/xcshareddata/xcschemes/Moderator Swift 3.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 35 | 41 | 42 | 43 | 44 | 45 | 51 | 52 | 53 | 54 | 57 | 58 | 59 | 60 | 61 | 62 | 72 | 73 | 79 | 80 | 81 | 82 | 83 | 84 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /Moderator.xcodeproj/xcshareddata/xcschemes/Moderator.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 32 | 33 | 35 | 41 | 42 | 43 | 44 | 45 | 51 | 52 | 53 | 54 | 55 | 56 | 66 | 67 | 73 | 74 | 75 | 76 | 77 | 78 | 84 | 85 | 87 | 88 | 91 | 92 | 93 | -------------------------------------------------------------------------------- /Moderator.xcodeproj/xcshareddata/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | SchemeUserState 5 | 6 | Moderator.xcscheme 7 | 8 | 9 | SuppressBuildableAutocreation 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:3.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Moderator" 8 | ) 9 | -------------------------------------------------------------------------------- /Package@swift-4.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "Moderator", 8 | products: [ 9 | // Products define the executables and libraries produced by a package, and make them visible to other packages. 10 | .library( 11 | name: "Moderator", 12 | targets: ["Moderator"]), 13 | ], 14 | targets: [ 15 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 16 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 17 | .target( 18 | name: "Moderator", 19 | path: "Sources"), 20 | 21 | // Test Targets 22 | .testTarget( 23 | name: "ModeratorTests", 24 | dependencies: ["Moderator"] 25 | ) 26 | ] 27 | ) 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |  [Run shell commands](https://github.com/kareman/SwiftShell)   |   Parse command line arguments | [Handle files and directories](https://github.com/kareman/FileSmith) 2 | 3 | --- 4 | 5 | [![Build Status](https://travis-ci.org/kareman/Moderator.svg?branch=master)](https://travis-ci.org/kareman/Moderator) ![Platforms](https://img.shields.io/badge/platforms-macOS%20%7C%20Linux-lightgrey.svg) ![Version](https://img.shields.io/badge/Swift-3%20%7C%204%20%7C%205-orange.svg) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-4BC51D.svg?style=flat)](https://github.com/Carthage/Carthage) 6 | 7 | # Moderator 8 | 9 | Moderator is a simple Swift library for parsing commandline arguments. 10 | 11 | ## Features 12 | 13 | - [x] Modular, easy to extend. 14 | - [x] Supports a [cross platform syntax](https://nottoobadsoftware.com/blog/uncategorized/cross-platform-command-line-arguments-syntax/). 15 | - [x] Generates help text automatically. 16 | - [x] Handles arguments of the type '--option=value'. 17 | - [x] Optional strict parsing, where an error is thrown if there are any unrecognised arguments. 18 | - [x] Any arguments after an "\--" argument are taken literally, they are not parsed as options and any '=' are left untouched. 19 | 20 | See also [why Moderator was created](https://nottoobadsoftware.com/blog/swift/moderator-parsing-commandline-arguments-in-swift/). 21 | 22 | ## Example 23 | 24 | from [linuxmain-generator](https://github.com/kareman/linuxmain-generator): 25 | 26 | ```Swift 27 | import Moderator 28 | import FileSmith 29 | 30 | let arguments = Moderator(description: "Automatically add code to Swift Package Manager projects to run unit tests on Linux.") 31 | let overwrite = arguments.add(.option("o","overwrite", description: "Replace /LinuxMain.swift if it already exists.")) 32 | let testdirarg = arguments.add(Argument 33 | .optionWithValue("testdir", name: "test directory", description: "The path to the directory with the unit tests.") 34 | .default("Tests")) 35 | _ = arguments.add(Argument 36 | .singleArgument(name: "directory", description: "The project root directory.") 37 | .default("./") 38 | .map { (projectpath: String) in 39 | let projectdir = try Directory(open: projectpath) 40 | try projectdir.verifyContains("Package.swift") 41 | Directory.current = projectdir 42 | }) 43 | 44 | do { 45 | try arguments.parse() 46 | 47 | let testdir = try Directory(open: testdirarg.value) 48 | if !overwrite.value && testdir.contains("LinuxMain.swift") { 49 | throw ArgumentError(errormessage: "\(testdir.path)/LinuxMain.swift already exists. Use -o/--overwrite to replace it.") 50 | } 51 | ... 52 | } catch { 53 | WritableFile.stderror.print(error) 54 | exit(Int32(error._code)) 55 | } 56 | ``` 57 | 58 | Automatically generated help text: 59 | 60 | ```text 61 | Automatically add code to Swift Package Manager projects to run unit tests on Linux. 62 | 63 | Usage: linuxmain-generator 64 | -o,--overwrite: 65 | Replace /LinuxMain.swift if it already exists. 66 | --testdir : 67 | The path to the directory with the unit tests. Default = 'Tests'. 68 | : 69 | The project root directory. Default = './'. 70 | ``` 71 | 72 | ## Introduction 73 | 74 | Moderator works by having a single Moderator object which you add individual argument parsers to. When you start parsing it goes through each argument parser _in the order they were added_. Each parser takes the array of string arguments from the command line, finds the arguments it is responsible for, processes them and throws any errors if anything is wrong, _removes the arguments from the array_, returns its output (which for some parsers may be nil if the argument was not found) and passes the modified array to the next parser. 75 | 76 | This keeps the code simple and each parser only has to take care of its own arguments. The built-in parsers can easily be customised, and you can create your own parsers from scratch. 77 | 78 | ## Built-in parsers 79 | 80 | ### Option 81 | 82 | ```swift 83 | func option(_ names: String..., description: String? = nil) -> Argument 84 | ``` 85 | 86 | Handles option arguments like `-h` and `--help`. Returns true if the argument is present and false otherwise. 87 | 88 | ### Option with value 89 | 90 | ```swift 91 | func optionWithValue(_ names: String..., name valuename: String? = nil, description: String? = nil) 92 | -> Argument 93 | ``` 94 | 95 | Handles option arguments with a following value, like `--help `. It returns the value as a String, or nil if the option is not present. 96 | 97 | ### Single argument 98 | 99 | ```swift 100 | func singleArgument(name: String, description: String? = nil) -> Argument 101 | ``` 102 | 103 | Returns the next argument, or nil if there are no more arguments or the next argument is an option. Must be added after any option parsers. 104 | 105 | ## Customise 106 | 107 | ### `default` 108 | 109 | Can be used on parsers returning optionals, to replace nil with a default value. 110 | 111 | ### `required` 112 | 113 | Can be used on parsers returning optionals, to throw an error on nil. 114 | 115 | ### `map` 116 | 117 | Takes the output of any argument parser and converts it to something else. 118 | 119 | ### `repeat` 120 | 121 | Looks for multiple occurrences of an argument, by repeating an optional parser until it returns nil. 122 | 123 | ```swift 124 | let m = Moderator() 125 | let options = m.add(Argument.optionWithValue("b").repeat()) 126 | let multiple = m.add(Argument.singleArgument(name: "multiple").repeat()) 127 | 128 | try m.parse(["-b", "b1", "-b", "b2", "-b", "b3", "one", "two", "three"]) 129 | ``` 130 | 131 | `options.value` is now `["b1", "b2", "b3"]` and `multiple.value` is `["one", "two", "three"]`.  132 | 133 | ### `count` 134 | 135 | Counts the number of times an option argument occurs. 136 | 137 | ```swift 138 | 139 | let m = Moderator() 140 | let option = m.add(Argument.option("b").count()) 141 | 142 | try m.parse(["-b", "-b", "some", "other", "-b", "arguments"]) 143 | ``` 144 | 145 | `option.value` returns `3` 146 |   147 | ## Add new parsers 148 | 149 | If the built in parsers and customisations are not enough, you can easily create your own parsers. As an example here is the implementation of the singleArgument parser: 150 | 151 | ```swift 152 | extension Argument { 153 | public static func singleArgument (name: String, description: String? = nil) -> Argument { 154 | return Argument(usage: description.map { ("<"+name+">", $0) }) { args in 155 | let index = args.first == "--" ? args.index(after: args.startIndex) : args.startIndex 156 | guard index != args.endIndex, !isOption(index: index, args: args) else { return (nil, args) } 157 | var args = args 158 | return (args.remove(at: index), args) 159 | } 160 | } 161 | } 162 | ``` 163 | 164 | In the Argument initialiser you return a tuple with the output of the parser and the arguments array without the processed argument(s). 165 | 166 | ## Installation 167 | 168 | ### [Swift Package Manager](https://github.com/apple/swift-package-manager) 169 | 170 | Add `.Package(url: "https://github.com/kareman/Moderator", "0.5.0")` to your Package.swift: 171 | 172 | ```swift 173 | import PackageDescription 174 | 175 | let package = Package( 176 | name: "somename", 177 | dependencies: [ 178 | .Package(url: "https://github.com/kareman/Moderator", "0.5.0") 179 | ] 180 | ) 181 | ``` 182 | 183 | and run `swift build`. 184 | 185 | ### [Carthage](https://github.com/Carthage/Carthage) 186 | 187 | Add `github "kareman/Moderator"` to your Cartfile, then run `carthage update` and add the resulting framework to the "Embedded Binaries" section of the application. See [Carthage's README][carthage-installation] for further instructions. 188 | 189 | [carthage-installation]: https://github.com/Carthage/Carthage/blob/master/README.md#adding-frameworks-to-an-application 190 | 191 | ### [CocoaPods](https://cocoapods.org/) 192 | 193 | Add `Moderator` to your `Podfile`. 194 | 195 | ```ruby 196 | pod "Moderator", git: "https://github.com/kareman/Moderator.git" 197 | ``` 198 | 199 | Then run `pod install` to install it. 200 | 201 | ## License 202 | 203 | Released under the MIT License (MIT), http://opensource.org/licenses/MIT 204 | 205 | Kåre Morstøl, [NotTooBad Software](https://nottoobadsoftware.com) 206 | -------------------------------------------------------------------------------- /Sources/Moderator.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Moderator.swift 3 | // 4 | // Created by Kåre Morstøl. 5 | // Copyright (c) 2016 NotTooBad Software. All rights reserved. 6 | // 7 | 8 | import Foundation 9 | 10 | public final class Moderator { 11 | fileprivate var parsers: [Argument] = [] 12 | public fileprivate(set) var remaining: [String] = [] 13 | let description: String 14 | 15 | /// Creates a Moderator object 16 | /// 17 | /// - Parameter description: The description of this executable, printed at the beginning of the help text. Empty by default. 18 | public init (description: String = "") { 19 | self.description = description.isEmpty ? description : description + "\n\n" 20 | _ = add(.joinedOptionAndArgumentParser()) 21 | } 22 | 23 | public func add (_ p: Argument) -> FutureValue { 24 | let b = FutureValue() 25 | parsers.append(p.map {b.value = $0}) 26 | return b 27 | } 28 | 29 | public func parse (_ args: [String], strict: Bool = true) throws { 30 | do { 31 | remaining = try parsers.reduce(args) { (args, parser) in try parser.parse(args).remainder } 32 | if remaining.count == 1 && remaining.first == "--" { 33 | remaining = [] 34 | } 35 | if strict && !remaining.isEmpty { 36 | throw ArgumentError(errormessage: "Unknown arguments: " + self.remaining.joined(separator: " ")) 37 | } 38 | } catch var error as ArgumentError { 39 | error.usagetext = error.usagetext ?? self.usagetext 40 | throw error 41 | } 42 | } 43 | 44 | public func parse (strict: Bool = true) throws { 45 | try parse(Array(CommandLine.arguments.dropFirst()), strict: strict) 46 | } 47 | 48 | static func commandName() -> String { 49 | return URL(fileURLWithPath: CommandLine.arguments.first ?? "").lastPathComponent 50 | } 51 | 52 | public var usagetext: String { 53 | let usagetexts = parsers.compactMap { $0.usage } 54 | guard !usagetexts.isEmpty else {return ""} 55 | return usagetexts.reduce(description + "Usage: \(Moderator.commandName())\n") { 56 | (acc:String, usagetext:UsageText) -> String in 57 | return acc + format(usagetext: usagetext) + "\n" 58 | } 59 | } 60 | } 61 | 62 | func format(usagetext: UsageText) -> String { 63 | guard let usagetext = usagetext else { return "" } 64 | return " " + usagetext.title + ":\n " + usagetext.description 65 | } 66 | 67 | // https://github.com/robrix/Box 68 | // Copyright (c) 2014 Rob Rix. All rights reserved. 69 | 70 | /// A value that will be set sometime in the future. 71 | public final class FutureValue: CustomStringConvertible { 72 | /// Initializes a `FutureValue` with the given value. 73 | public init(_ value: T) { 74 | self.value = value 75 | } 76 | 77 | /// Initializes an empty `FutureValue`. 78 | public init() { 79 | self._value = nil 80 | } 81 | 82 | private var _value: T! 83 | 84 | /// The (mutable) value. 85 | public var value: T { 86 | get { 87 | precondition(_value != nil, "Remember to call Argument.parse() before accessing value of arguments.") 88 | return _value! 89 | } 90 | set { 91 | _value = newValue 92 | } 93 | } 94 | 95 | /// Constructs a new FutureValue by transforming `value` by `f`. 96 | public func map(_ f: (T) -> U) -> FutureValue { 97 | return FutureValue(f(value)) 98 | } 99 | 100 | // MARK: Printable 101 | 102 | public var description: String { 103 | return String(describing: value) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /Sources/Parsers.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Moderator.swift 3 | // 4 | // Created by Kåre Morstøl. 5 | // Copyright (c) 2016 NotTooBad Software. All rights reserved. 6 | // 7 | 8 | // Should ideally and eventually be compatible with http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap12.html , 9 | // with the addition of "--longname". For more, see http://blog.nottoobadsoftware.com/uncategorized/cross-platform-command-line-arguments-syntax/ . 10 | 11 | public typealias UsageText = (title: String, description: String)? 12 | 13 | public struct Argument { 14 | public let usage: UsageText 15 | public let parse: ([String]) throws -> (value: Value, remainder: [String]) 16 | 17 | public init (usage: UsageText = nil, p: @escaping ([String]) throws -> (value: Value, remainder: [String])) { 18 | self.parse = p 19 | self.usage = usage 20 | } 21 | 22 | public init (usage: UsageText = nil, value: Value) { 23 | self.parse = { args in (value, args) } 24 | self.usage = usage 25 | } 26 | } 27 | 28 | extension Argument { 29 | public func map (_ f: @escaping (Value) throws -> Outvalue) -> Argument { 30 | return Argument(usage: self.usage) { args in 31 | let result = try self.parse(args) 32 | return (value: try f(result.value), remainder: result.remainder) 33 | } 34 | } 35 | } 36 | 37 | public struct ArgumentError: Error, CustomStringConvertible { 38 | public let errormessage: String 39 | public internal(set) var usagetext: String? = nil 40 | 41 | public init (errormessage: String, usagetext: String? = nil) { 42 | self.errormessage = errormessage 43 | self.usagetext = usagetext 44 | } 45 | 46 | public var description: String { return errormessage + (usagetext.map { "\n" + $0 } ?? "") } 47 | } 48 | 49 | extension Argument { 50 | static func isOption (index: Array.Index, args: [String]) -> Bool { 51 | if let i = args.index(of: "--"), i < index { return false } 52 | let argument = args[index] 53 | if argument.first == "-", 54 | let second = argument.dropFirst().first, !("0"..."9").contains(second) { 55 | return true 56 | } 57 | return false 58 | } 59 | 60 | static func option(names: [String], description: String? = nil) -> Argument { 61 | for illegalcharacter in [" ","-","="] { 62 | precondition(!names.contains(where: {$0.contains(illegalcharacter)}), "Option names cannot contain '\(illegalcharacter)'") 63 | } 64 | for digit in 0...9 { 65 | precondition(!names.contains(where: {$0.hasPrefix(String(digit))}), "Option names cannot begin with a number.") 66 | } 67 | precondition(!names.contains("W"), "Option '-W' is reserved for system use.") 68 | 69 | let names = names.map { ($0.count==1 ? "-" : "--") + $0 } 70 | let usage = description.map { (names.joined(separator: ","), $0) } 71 | return Argument(usage: usage) { args in 72 | var args = args 73 | guard let index = args.index(where: names.contains), isOption(index: index, args: args) else { 74 | return (false, args) 75 | } 76 | args.remove(at: index) 77 | return (true, args) 78 | } 79 | } 80 | 81 | public static func option(_ names: String..., description: String? = nil) -> Argument { 82 | return option(names: names, description: description) 83 | } 84 | 85 | /// Parses arguments like '--opt=value' into '--opt value'. 86 | internal static func joinedOptionAndArgumentParser() -> Argument { 87 | return Argument() { args in 88 | return ((), args.enumerated().flatMap { (index, arg) in 89 | isOption(index: index, args: args) ? 90 | arg.split(separator: "=", maxSplits: 1, omittingEmptySubsequences: true).map(String.init) : 91 | [arg] 92 | }) 93 | } 94 | } 95 | } 96 | 97 | 98 | extension Array where Element: Equatable { 99 | public func indexOfFirstDifference (_ other: Array) -> Index? { 100 | for i in self.indices { 101 | if i >= other.endIndex || self[i] != other[i] { return i } 102 | } 103 | return nil 104 | } 105 | } 106 | 107 | extension Argument { 108 | public static func optionWithValue 109 | (_ names: String..., name valuename: String? = nil, description: String? = nil) 110 | -> Argument { 111 | 112 | let option = Argument.option(names: names, description: description) 113 | let usage = option.usage.map { usage in 114 | return (usage.title + " <\(valuename ?? "arg")>", usage.description) 115 | } 116 | 117 | return Argument(usage: usage) { args in 118 | var optionresult = try option.parse(args) 119 | guard optionresult.value else { 120 | return (nil, args) 121 | } 122 | guard let firstchange = optionresult.remainder.indexOfFirstDifference(args) else { 123 | throw ArgumentError(errormessage: "Expected value for '\(args.last!)'.", 124 | usagetext: format(usagetext: usage)) 125 | } 126 | guard !isOption(index: firstchange, args: optionresult.remainder) else { 127 | throw ArgumentError( 128 | errormessage: "Expected value for '\(args[firstchange])', got option '\(optionresult.remainder[firstchange])'.", 129 | usagetext: format(usagetext: usage)) 130 | } 131 | let value = optionresult.remainder.remove(at: firstchange) 132 | return (value, optionresult.remainder) 133 | } 134 | } 135 | 136 | /// Parses the next argument, if it is not an option. 137 | /// 138 | /// - Parameters: 139 | /// - name: The placeholder in the help text. 140 | /// - description: The description of this argument. 141 | /// - Returns: The next argument, or nil if there are no more arguments or the next argument is an option. 142 | public static func singleArgument (name: String, description: String? = nil) -> Argument { 143 | return Argument(usage: description.map { ("<"+name+">", $0) }) { args in 144 | let index = args.first == "--" ? args.index(after: args.startIndex) : args.startIndex 145 | guard index != args.endIndex, !isOption(index: index, args: args) else { return (nil, args) } 146 | var args = args 147 | return (args.remove(at: index), args) 148 | } 149 | } 150 | } 151 | 152 | 153 | public protocol OptionalType { 154 | associatedtype Wrapped 155 | func toOptional() -> Wrapped? 156 | } 157 | 158 | extension Optional: OptionalType { 159 | public func toOptional() -> Optional { 160 | return self 161 | } 162 | } 163 | 164 | extension Argument where Value: OptionalType { 165 | public func `default`(_ defaultvalue: Value.Wrapped) -> Argument { 166 | let newusage = self.usage.map { ($0.title, $0.description + " Default = '\(defaultvalue)'.") } 167 | return Argument(usage: newusage) { args in 168 | let result = try self.parse(args) 169 | return (result.value.toOptional() ?? defaultvalue, result.remainder) 170 | } 171 | } 172 | 173 | /// Makes this optional argument required. An error is thrown during argument parsing if it is missing. 174 | /// 175 | /// - Parameter errormessage: The error message to display if the argument is missing. 176 | /// If no error message is provided one will be automatically generated. 177 | /// - Returns: A new argument parser with a non-optional value. 178 | public func required(errormessage: String? = nil) -> Argument { 179 | return Argument(usage: self.usage) { args in 180 | let result = try self.parse(args) 181 | guard let value = result.value.toOptional() else { 182 | let errormessage = errormessage ?? "Missing argument" + (self.usage == nil ? "." : ":") 183 | throw ArgumentError(errormessage: errormessage, usagetext: format(usagetext: self.usage)) 184 | } 185 | return (value, result.remainder) 186 | } 187 | } 188 | 189 | /// Looks for multiple occurrences of an argument, 190 | /// by repeating an optional parser until it returns nil. 191 | /// 192 | /// - Returns: An array of the values the parser returned. 193 | public func `repeat`() -> Argument<[Value.Wrapped]> { 194 | return Argument<[Value.Wrapped]>(usage: self.usage) { args in 195 | var args = args 196 | var values = Array() 197 | while true { 198 | let result = try self.parse(args) 199 | guard let value = result.value.toOptional() else { 200 | return (values, result.remainder) 201 | } 202 | values.append(value) 203 | args = result.remainder 204 | } 205 | } 206 | } 207 | } 208 | 209 | extension Argument where Value == Bool { 210 | /// Counts the number of times an option argument occurs. 211 | public func count() -> Argument { 212 | return Argument(usage: self.usage) { args in 213 | let result = try self.map { $0 ? true : nil }.repeat().parse(args) 214 | return (result.value.count, result.remainder) 215 | } 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /Sources/SwiftCompat.swift: -------------------------------------------------------------------------------- 1 | #if !swift(>=4.0) 2 | extension String { 3 | func dropFirst(_ n: Int = 1) -> String.CharacterView { 4 | return self.characters.dropFirst(n) 5 | } 6 | 7 | var first: Character? { 8 | return self.characters.first 9 | } 10 | 11 | var count: Int { 12 | return self.characters.count 13 | } 14 | 15 | func split(separator: Character, maxSplits: Int = Int.max, omittingEmptySubsequences: Bool = true) -> [String.CharacterView] { 16 | return self.characters.split(separator: separator, maxSplits: maxSplits, omittingEmptySubsequences: omittingEmptySubsequences) 17 | } 18 | } 19 | 20 | extension Sequence { 21 | func compactMap(_ transform: (Iterator.Element) throws -> ElementOfResult?) rethrows -> [ElementOfResult] { 22 | return try flatMap(transform) 23 | } 24 | } 25 | #endif 26 | 27 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | 2 | import XCTest 3 | 4 | import ModeratorTests 5 | 6 | let tests: [XCTestCaseEntry] = [ 7 | testCase(Moderator_Tests.allTests), 8 | ] 9 | 10 | XCTMain(tests) 11 | -------------------------------------------------------------------------------- /Tests/ModeratorTests/Moderator_Tests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Moderator_Tests 3 | // 4 | // Created by Kåre Morstøl on 03.11.15. 5 | // Copyright 2015 NotTooBad Software. All rights reserved. 6 | // 7 | 8 | import XCTest 9 | import Moderator 10 | import Foundation 11 | 12 | extension Array { 13 | var toStrings: [String] { 14 | return map {String(describing: $0)} 15 | } 16 | } 17 | 18 | public class Moderator_Tests: XCTestCase { 19 | 20 | func testOptionAndArgumentJoinedWithEqualSign () { 21 | let m = Moderator() 22 | let arguments = ["lskdfj", "--verbose", "--this=that=", "-b", "--", "--c=1"] 23 | 24 | do { 25 | try m.parse(arguments, strict: false) 26 | XCTAssertEqual(m.remaining, ["lskdfj", "--verbose", "--this", "that=", "-b", "--", "--c=1"]) 27 | } catch { 28 | XCTFail(String(describing: error)) 29 | } 30 | } 31 | 32 | /* 33 | func testPreprocessorHandlesJoinedFlags () { 34 | let arguments = ["-abc", "delta", "--echo", "-f"] 35 | 36 | let result = Argument().preprocess(arguments) 37 | XCTAssertEqual(result.toStrings, ["-a", "-b", "-c", "delta", "--echo", "-f"]) 38 | } 39 | */ 40 | 41 | func testParsingOption () { 42 | let m = Moderator() 43 | let arguments = ["--ignored", "-a", "b", "bravo", "--charlie"] 44 | let parsedlong = m.add(Argument.option("c", "charlie")) 45 | let parsedshort = m.add(Argument.option("a", "alpha")) 46 | let unparsed = m.add(Argument.option("b", "bravo")) 47 | 48 | do { 49 | try m.parse(arguments, strict: false) 50 | XCTAssertEqual(parsedshort.value, true) 51 | XCTAssertEqual(unparsed.value, false) 52 | XCTAssertEqual(parsedlong.value, true) 53 | XCTAssertEqual(m.remaining, ["--ignored", "b", "bravo"]) 54 | } catch { 55 | XCTFail(String(describing: error)) 56 | } 57 | } 58 | 59 | func testParsingOptionWithValue () { 60 | let m = Moderator() 61 | let arguments = ["--charlie", "sheen", "ignored", "-a", "alphasvalue"] 62 | let parsedshort = m.add(Argument.optionWithValue("a", "alpha")) 63 | let unparsed = m.add(Argument.option("b", "bravo")) 64 | let parsedlong = m.add(Argument.optionWithValue("c", "charlie")) 65 | 66 | do { 67 | try m.parse(arguments, strict: false) 68 | XCTAssertEqual(parsedshort.value, "alphasvalue") 69 | XCTAssertEqual(parsedlong.value, "sheen") 70 | XCTAssertEqual(unparsed.value, false) 71 | XCTAssertEqual(m.remaining, ["ignored"]) 72 | } catch { 73 | XCTFail(String(describing: error)) 74 | } 75 | } 76 | 77 | func testParsingOptionWithMissingValueThrows () { 78 | let m = Moderator() 79 | let arguments = ["--verbose", "--alpha"] 80 | _ = m.add(Argument.optionWithValue("a", "alpha")) 81 | 82 | XCTAssertThrowsError( try m.parse(arguments) ) { error in 83 | XCTAssertTrue(String(describing: error).contains("--alpha")) 84 | } 85 | } 86 | 87 | func testParsingMissingOptionWithValue () { 88 | let m = Moderator() 89 | let arguments = ["arg1", "arg2", "arg3"] 90 | let parsed = m.add(Argument.optionWithValue("a", "alpha").default("default")) 91 | 92 | do { 93 | try m.parse(arguments, strict: false) 94 | XCTAssertEqual(parsed.value, "default") 95 | } catch { 96 | XCTFail(String(describing: error)) 97 | } 98 | } 99 | 100 | func testParsingStringArgumentWithOptionValueThrows () { 101 | let m = Moderator() 102 | let arguments = ["--verbose", "-a", "-b"] 103 | _ = m.add(Argument.optionWithValue("a", "alpha")) 104 | 105 | XCTAssertThrowsError( try m.parse(arguments) ) { error in 106 | XCTAssert(String(describing: error).contains("-a")) 107 | } 108 | } 109 | 110 | func testSingleArgument () { 111 | let m = Moderator() 112 | let arguments = ["-a", "argument", "--ignored", "--charlie"] 113 | let parsedlong = m.add(Argument.option("c", "charlie")) 114 | let parsedshort = m.add(Argument.option("a", "alpha")) 115 | let single = m.add(Argument.singleArgument(name: "argumentname")) 116 | 117 | do { 118 | try m.parse(arguments, strict: false) 119 | XCTAssertEqual(parsedshort.value, true) 120 | XCTAssertEqual(parsedlong.value, true) 121 | XCTAssertEqual(single.value, "argument") 122 | XCTAssertEqual(m.remaining, ["--ignored"]) 123 | 124 | try m.parse(["-a", "--charlie", "argument2", "--ignored"], strict: false) 125 | XCTAssertEqual(single.value, "argument2") 126 | 127 | try m.parse(["-a", "--charlie", "--", "--argument3", "--ignored"], strict: false) 128 | XCTAssertEqual(single.value, "--argument3") 129 | 130 | try m.parse(["-a", "--charlie", "--"], strict: false) 131 | XCTAssertEqual(single.value, nil) 132 | 133 | try m.parse(["-a", "--charlie"], strict: false) 134 | XCTAssertEqual(single.value, nil) 135 | } catch { 136 | XCTFail(String(describing: error)) 137 | } 138 | } 139 | 140 | func testMissingSingleArgument() { 141 | let m = Moderator() 142 | _ = m.add(Argument.option("c", "charlie")) 143 | _ = m.add(Argument.option("a", "alpha")) 144 | let single = m.add(Argument.singleArgument(name: "argumentname")) 145 | 146 | do { 147 | try m.parse(["-a", "-b"], strict: false) 148 | XCTAssertNil(single.value) 149 | } catch { 150 | XCTFail(String(describing: error)) 151 | } 152 | } 153 | 154 | func testDefaultValue() { 155 | let m = Moderator() 156 | _ = m.add(Argument.option("c", "charlie")) 157 | _ = m.add(Argument.option("a", "alpha")) 158 | let defaultsingle = m.add(Argument.singleArgument(name: "argumentname").default("defaultvalue")) 159 | 160 | do { 161 | try m.parse(["-a", "-b"], strict: false) 162 | XCTAssertEqual(defaultsingle.value, "defaultvalue") 163 | try m.parse(["-a", "notdefaultvalue"], strict: false) 164 | XCTAssertEqual(defaultsingle.value, "notdefaultvalue") 165 | } catch { 166 | XCTFail(String(describing: error)) 167 | } 168 | } 169 | 170 | func testMissingRequiredValueThrows() { 171 | let m = Moderator() 172 | _ = m.add(Argument.option("c", "charlie")) 173 | _ = m.add(Argument.optionWithValue("v", name: "optionv", description: "the value for v.").required()) 174 | _ = m.add(Argument.singleArgument(name: "Argumentname", description: "Argumentname's description").required(errormessage: "Argumentname is required.")) 175 | 176 | XCTAssertThrowsError( try m.parse(["-a", "-b"]) ) 177 | 178 | do { try m.parse(["-v", "-b"]) } catch { print(error) } 179 | do { try m.parse(["-a", "-b"]) } catch { print(error) } 180 | do { try m.parse(["-a", "-v"]) } catch { print(error) } 181 | do { try m.parse(["-a", "-v", "vvvv"]) } catch { print(error) } 182 | } 183 | 184 | func testRepeat () { 185 | let m = Moderator() 186 | let options = m.add(Argument.optionWithValue("b").repeat()) 187 | let multiple = m.add(Argument.singleArgument(name: "multiple").repeat()) 188 | 189 | do { 190 | try m.parse(["-b", "b1", "-b", "b2", "notb", "-b", "b3"], strict: false) 191 | XCTAssertEqual(options.value, ["b1", "b2", "b3"]) 192 | try m.parse(["one", "two", "three"], strict: true) 193 | XCTAssertEqual(multiple.value, ["one", "two", "three"]) 194 | try m.parse(["one", "-a", "two", "three"], strict: false) 195 | XCTAssertEqual(multiple.value, ["one"]) 196 | try m.parse([], strict: true) 197 | XCTAssertEqual(multiple.value, []) 198 | } catch { 199 | XCTFail(String(describing: error)) 200 | } 201 | } 202 | 203 | func testCount() { 204 | let m = Moderator() 205 | let option = m.add(Argument.option("b").count()) 206 | 207 | do { 208 | try m.parse(["-b", "-b", "b2", "notb", "-b", "b3"], strict: false) 209 | XCTAssertEqual(option.value, 3) 210 | try m.parse(["one", "two", "three"], strict: false) 211 | XCTAssertEqual(option.value, 0) 212 | try m.parse(["-b", "-b"], strict: true) 213 | XCTAssertEqual(option.value, 2) 214 | try m.parse(["one", "-b", "two", "three"], strict: false) 215 | XCTAssertEqual(option.value, 1) 216 | try m.parse([], strict: true) 217 | XCTAssertEqual(option.value, 0) 218 | } catch { 219 | XCTFail(String(describing: error)) 220 | } 221 | } 222 | 223 | func testStrictParsingThrowsErrorOnUnknownArguments () { 224 | let m = Moderator() 225 | let arguments = ["--alpha", "-c"] 226 | _ = m.add(Argument.option("a", "alpha", description: "The leader.")) 227 | _ = m.add(Argument.option("b", "bravo", description: "Well done!")) 228 | 229 | XCTAssertThrowsError( try m.parse(arguments, strict: true) ) { error in 230 | XCTAssertTrue(String(describing: error).contains("Unknown arguments")) 231 | XCTAssertTrue(String(describing: error).contains("The leader."), "Error should have contained usage text.") 232 | XCTAssertTrue(String(describing: error).contains("Well done!"), "Error should have contained usage text.") 233 | } 234 | } 235 | 236 | func testStrictParsing () { 237 | let m = Moderator() 238 | let arguments = ["--alpha", "-b"] 239 | _ = m.add(Argument.option("a", "alpha", description: "The leader.")) 240 | _ = m.add(Argument.option("b", "bravo", description: "Well done!")) 241 | 242 | do { 243 | try m.parse(arguments, strict: true) 244 | } catch { 245 | XCTFail(String(describing: error)) 246 | } 247 | } 248 | 249 | func testRemoveDoubleDashIfAlone () { 250 | let m = Moderator() 251 | let arguments = ["--"] 252 | 253 | do { 254 | try m.parse(arguments, strict: true) 255 | try m.parse(arguments, strict: false) 256 | XCTAssert(m.remaining.isEmpty) 257 | } catch { 258 | XCTFail(String(describing: error)) 259 | } 260 | } 261 | 262 | func testUsageText () { 263 | let m = Moderator(description: "A very thorough and informative description.") 264 | _ = m.add(.option("a", "alpha", description: "The leader.")) 265 | _ = m.add(Argument.optionWithValue("b", "bravo", description: "Well done!").default("default value")) 266 | _ = m.add(Argument.option("x", "hasnohelptext")) 267 | 268 | let usagetext = m.usagetext 269 | print(usagetext) 270 | XCTAssert(usagetext.contains("alpha")) 271 | XCTAssert(usagetext.contains("The leader")) 272 | XCTAssert(usagetext.contains("bravo")) 273 | XCTAssert(usagetext.contains("Well done")) 274 | XCTAssert(usagetext.contains("default value")) 275 | 276 | XCTAssertFalse(m.usagetext.contains("hasnohelptext")) 277 | } 278 | } 279 | 280 | extension Moderator_Tests { 281 | public static var allTests = [ 282 | ("testOptionAndArgumentJoinedWithEqualSign", testOptionAndArgumentJoinedWithEqualSign), 283 | //("testPreprocessorHandlesJoinedFlags", testPreprocessorHandlesJoinedFlags), 284 | ("testParsingOption", testParsingOption), 285 | ("testParsingOptionWithValue", testParsingOptionWithValue), 286 | ("testParsingOptionWithMissingValueThrows", testParsingOptionWithMissingValueThrows), 287 | ("testParsingMissingOptionWithValue", testParsingMissingOptionWithValue), 288 | ("testParsingStringArgumentWithOptionValueThrows", testParsingStringArgumentWithOptionValueThrows), 289 | ("testSingleArgument", testSingleArgument), 290 | ("testMissingSingleArgument", testMissingSingleArgument), 291 | ("testDefaultValue", testDefaultValue), 292 | ("testMissingRequiredValueThrows", testMissingRequiredValueThrows), 293 | ("testRepeat", testRepeat), 294 | ("testCount", testCount), 295 | ("testStrictParsingThrowsErrorOnUnknownArguments", testStrictParsingThrowsErrorOnUnknownArguments), 296 | ("testStrictParsing", testStrictParsing), 297 | ("testRemoveDoubleDashIfAlone", testRemoveDoubleDashIfAlone), 298 | ("testUsageText", testUsageText), 299 | ] 300 | } 301 | --------------------------------------------------------------------------------