├── settings.gradle ├── logo.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradle.properties ├── .travis.yml ├── .gitignore ├── src └── main │ └── kotlin │ └── com │ └── xenomachina │ └── argparser │ ├── ParsingDelegate.kt │ ├── WrappingDelegate.kt │ ├── HelpFormatter.kt │ ├── PosixNaming.kt │ ├── PositionalDelegate.kt │ ├── OptionDelegate.kt │ ├── Default.kt │ ├── SystemExitException.kt │ ├── Exceptions.kt │ ├── DefaultHelpFormatter.kt │ └── ArgParser.kt ├── gradlew.bat ├── gradlew ├── CHANGELOG.md ├── detekt.yml ├── logo.svg ├── README.md ├── COPYING └── codecov.sh /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = "kotlin-argparser" 2 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xenomachina/kotlin-argparser/HEAD/logo.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/xenomachina/kotlin-argparser/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | detekt_version = 1.0.0.RC7 2 | dokka_version = 0.9.17 3 | gradle_bintray_version = 1.8.0 4 | kotlintest_version = 3.1.0 5 | kotlin_version = 1.2.41 6 | slf4j_version = 1.7.25 7 | xenocom_version = 0.0.7 8 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.6-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | 3 | jdk: 4 | - openjdk8 5 | 6 | before_cache: 7 | - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock 8 | - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ 9 | cache: 10 | directories: 11 | - $HOME/.gradle/ 12 | 13 | after_success: 14 | - ./gradlew jacocoTestReport && ./codecov.sh 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Gradle ### 2 | .gradle 3 | /build/ 4 | 5 | # Ignore Gradle GUI config 6 | gradle-app.setting 7 | 8 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 9 | !gradle-wrapper.jar 10 | 11 | # Cache of project 12 | .gradletasknamecache 13 | 14 | # # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 15 | # gradle/wrapper/gradle-wrapper.propertiesgradletasknamecache 16 | 17 | 18 | ### IntelliJ IDEA ### 19 | 20 | .idea/ 21 | 22 | ## File-based project format: 23 | *.iws 24 | 25 | ## Plugin-specific files: 26 | 27 | # IntelliJ 28 | /out/ 29 | 30 | # mpeltonen/sbt-idea plugin 31 | .idea_modules/ 32 | 33 | # JIRA plugin 34 | atlassian-ide-plugin.xml 35 | 36 | # Crashlytics plugin (for Android Studio and IntelliJ) 37 | com_crashlytics_export_strings.xml 38 | crashlytics.properties 39 | crashlytics-build.properties 40 | fabric.properties 41 | -------------------------------------------------------------------------------- /src/main/kotlin/com/xenomachina/argparser/ParsingDelegate.kt: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Laurence Gonsalves 2 | // 3 | // This file is part of kotlin-argparser, a library which can be found at 4 | // http://github.com/xenomachina/kotlin-argparser 5 | // 6 | // This library is free software; you can redistribute it and/or modify it 7 | // under the terms of the GNU Lesser General Public License as published by the 8 | // Free Software Foundation; either version 2.1 of the License, or (at your 9 | // option) any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | // FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License 14 | // for more details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, see http://www.gnu.org/licenses/ 18 | 19 | package com.xenomachina.argparser 20 | 21 | import com.xenomachina.common.Holder 22 | 23 | internal abstract class ParsingDelegate( 24 | override val parser: ArgParser, 25 | override val errorName: String, 26 | override val help: String 27 | ) : ArgParser.Delegate() { 28 | 29 | protected var holder: Holder? = null 30 | 31 | override fun addValidator(validator: ArgParser.Delegate.() -> Unit): ArgParser.Delegate = apply { 32 | validators.add(validator) 33 | } 34 | 35 | override val hasValidators: Boolean 36 | get() = validators.isNotEmpty() 37 | 38 | override val value: T 39 | get() { 40 | parser.force() 41 | checkHasValue() 42 | return holder!!.value 43 | } 44 | 45 | override val hasValue: Boolean 46 | get() = holder != null 47 | 48 | override fun validate() { 49 | for (validator in validators) validator() 50 | } 51 | 52 | private val validators = mutableListOf.() -> Unit>() 53 | } 54 | -------------------------------------------------------------------------------- /src/main/kotlin/com/xenomachina/argparser/WrappingDelegate.kt: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Laurence Gonsalves 2 | // 3 | // This file is part of kotlin-argparser, a library which can be found at 4 | // http://github.com/xenomachina/kotlin-argparser 5 | // 6 | // This library is free software; you can redistribute it and/or modify it 7 | // under the terms of the GNU Lesser General Public License as published by the 8 | // Free Software Foundation; either version 2.1 of the License, or (at your 9 | // option) any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | // FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License 14 | // for more details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, see http://www.gnu.org/licenses/ 18 | 19 | package com.xenomachina.argparser 20 | 21 | internal class WrappingDelegate( 22 | private val inner: ArgParser.Delegate, 23 | private val wrap: (U) -> W 24 | ) : ArgParser.Delegate() { 25 | 26 | override val parser: ArgParser 27 | get() = inner.parser 28 | 29 | override val value: W 30 | get() = wrap(inner.value) 31 | 32 | override val hasValue: Boolean 33 | get() = inner.hasValue 34 | 35 | override val errorName: String 36 | get() = inner.errorName 37 | 38 | override val help: String 39 | get() = inner.help 40 | 41 | override fun validate() { 42 | inner.validate() 43 | } 44 | 45 | override fun toHelpFormatterValue(): HelpFormatter.Value = inner.toHelpFormatterValue() 46 | 47 | override fun addValidator(validator: ArgParser.Delegate.() -> Unit): ArgParser.Delegate = 48 | apply { inner.addValidator { validator(this@WrappingDelegate) } } 49 | 50 | override val hasValidators: Boolean 51 | get() = inner.hasValidators 52 | 53 | override fun registerLeaf(root: ArgParser.Delegate<*>) { 54 | inner.registerLeaf(root) 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/kotlin/com/xenomachina/argparser/HelpFormatter.kt: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Laurence Gonsalves 2 | // 3 | // This file is part of kotlin-argparser, a library which can be found at 4 | // http://github.com/xenomachina/kotlin-argparser 5 | // 6 | // This library is free software; you can redistribute it and/or modify it 7 | // under the terms of the GNU Lesser General Public License as published by the 8 | // Free Software Foundation; either version 2.1 of the License, or (at your 9 | // option) any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | // FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License 14 | // for more details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, see http://www.gnu.org/licenses/ 18 | 19 | package com.xenomachina.argparser 20 | 21 | /** 22 | * Formats help for an [ArgParser]. 23 | */ 24 | interface HelpFormatter { 25 | /** 26 | * Formats a help message. 27 | * 28 | * @param programName name of the program as it should appear in usage information, or null if 29 | * program name is unknown. 30 | * @param columns width of display help should be formatted for, measured in character cells, or 0 for infinite 31 | * width. 32 | * @param values [Value] objects describing the arguments types available. 33 | */ 34 | fun format(programName: String?, columns: Int, values: List): String 35 | 36 | /** 37 | * An option or positional argument type which should be formatted for help 38 | * 39 | * @param usages possible usage strings for this argument type 40 | * @param isRequired indicates whether this is required 41 | * @param isRepeating indicates whether it makes sense to repeat this argument 42 | * @param isPositional indicates whether this is a positional argument 43 | * @param help help text provided at Delegate construction time 44 | */ 45 | data class Value( 46 | val usages: List, 47 | val isRequired: Boolean, 48 | val isRepeating: Boolean, 49 | val isPositional: Boolean, 50 | val help: String 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /src/main/kotlin/com/xenomachina/argparser/PosixNaming.kt: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Laurence Gonsalves 2 | // 3 | // This file is part of kotlin-argparser, a library which can be found at 4 | // http://github.com/xenomachina/kotlin-argparser 5 | // 6 | // This library is free software; you can redistribute it and/or modify it 7 | // under the terms of the GNU Lesser General Public License as published by the 8 | // Free Software Foundation; either version 2.1 of the License, or (at your 9 | // option) any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | // FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License 14 | // for more details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, see http://www.gnu.org/licenses/ 18 | 19 | package com.xenomachina.argparser 20 | 21 | /** 22 | * Defines rules for POSIX-style argument and option naming. 23 | */ 24 | internal object PosixNaming { 25 | fun identifierToOptionName(identifier: String): String { 26 | return when (identifier.length) { 27 | 1 -> "-" + identifier 28 | else -> "--" + identifier.camelCaseToUnderscored() 29 | } 30 | } 31 | 32 | fun String.camelCaseToUnderscored(): String { 33 | return replace('_', '-') 34 | .replace(Regex("(\\p{javaLowerCase})(\\p{javaUpperCase})")) { m -> 35 | m.groups[1]!!.value + "-" + m.groups[2]!!.value.toLowerCase() 36 | } 37 | } 38 | 39 | fun identifierToArgName(identifier: String): String { 40 | return identifier.camelCaseToUnderscored().toUpperCase() 41 | } 42 | 43 | fun selectRepresentativeOptionName(names: Array): String { 44 | if (names.size < 1) 45 | throw IllegalArgumentException("need at least one option name") 46 | // Return first long option... 47 | for (name in names) { 48 | if (name.startsWith("--")) { 49 | return name 50 | } 51 | } 52 | // ... but failing that just return first option. 53 | return names[0] 54 | } 55 | 56 | fun optionNameToArgName(name: String) = 57 | LEADING_HYPHENS_REGEX.replace(name, "").toUpperCase().replace('-', '_') 58 | } 59 | -------------------------------------------------------------------------------- /src/main/kotlin/com/xenomachina/argparser/PositionalDelegate.kt: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Laurence Gonsalves 2 | // 3 | // This file is part of kotlin-argparser, a library which can be found at 4 | // http://github.com/xenomachina/kotlin-argparser 5 | // 6 | // This library is free software; you can redistribute it and/or modify it 7 | // under the terms of the GNU Lesser General Public License as published by the 8 | // Free Software Foundation; either version 2.1 of the License, or (at your 9 | // option) any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | // FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License 14 | // for more details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, see http://www.gnu.org/licenses/ 18 | 19 | package com.xenomachina.argparser 20 | 21 | import com.xenomachina.common.Holder 22 | 23 | internal class PositionalDelegate( 24 | parser: ArgParser, 25 | argName: String, 26 | val sizeRange: IntRange, 27 | help: String, 28 | val transform: String.() -> T 29 | ) : ParsingDelegate>(parser, argName, help) { 30 | 31 | init { 32 | require(ARG_NAME_RE.matches(argName)) { "$argName is not a valid argument name" } 33 | } 34 | 35 | override fun registerLeaf(root: ArgParser.Delegate<*>) { 36 | assert(holder == null) 37 | val hasDefault = root.hasValue 38 | if (hasDefault && sizeRange.first != 1) { 39 | throw IllegalStateException( 40 | "default value can only be applied to positional that requires a minimum of 1 arguments") 41 | } 42 | // TODO: this feels like a bit of a kludge. Consider making .default only work on positional and not 43 | // postionalList by having them return different types? 44 | parser.registerPositional(this, hasDefault) 45 | } 46 | 47 | fun parseArguments(args: List) { 48 | holder = Holder(args.map(transform)) 49 | } 50 | 51 | override fun toHelpFormatterValue(): HelpFormatter.Value { 52 | return HelpFormatter.Value( 53 | isRequired = sizeRange.first > 0, 54 | isRepeating = sizeRange.last > 1, 55 | usages = listOf(errorName), 56 | isPositional = true, 57 | help = help) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/main/kotlin/com/xenomachina/argparser/OptionDelegate.kt: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Laurence Gonsalves 2 | // 3 | // This file is part of kotlin-argparser, a library which can be found at 4 | // http://github.com/xenomachina/kotlin-argparser 5 | // 6 | // This library is free software; you can redistribute it and/or modify it 7 | // under the terms of the GNU Lesser General Public License as published by the 8 | // Free Software Foundation; either version 2.1 of the License, or (at your 9 | // option) any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | // FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License 14 | // for more details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, see http://www.gnu.org/licenses/ 18 | 19 | package com.xenomachina.argparser 20 | 21 | import com.xenomachina.common.Holder 22 | 23 | internal class OptionDelegate( 24 | parser: ArgParser, 25 | errorName: String, 26 | help: String, 27 | val optionNames: List, 28 | val argNames: List, 29 | val isRepeating: Boolean, 30 | val handler: ArgParser.OptionInvocation.() -> T 31 | ) : ParsingDelegate(parser, errorName, help) { 32 | init { 33 | for (optionName in optionNames) { 34 | if (!OPTION_NAME_RE.matches(optionName)) { 35 | throw IllegalArgumentException("$optionName is not a valid option name") 36 | } 37 | } 38 | for (argName in argNames) { 39 | if (!ARG_NAME_RE.matches(argName)) { 40 | throw IllegalArgumentException("$argName is not a valid argument name") 41 | } 42 | } 43 | } 44 | 45 | fun parseOption(name: String, firstArg: String?, index: Int, args: Array): Int { 46 | val arguments = mutableListOf() 47 | if (!argNames.isEmpty()) { 48 | if (firstArg != null) arguments.add(firstArg) 49 | val required = argNames.size - arguments.size 50 | if (required + index > args.size) { 51 | // Only pass an argName if more than one argument. 52 | // Naming it when there's just one seems unnecessarily verbose. 53 | val argName = if (argNames.size > 1) argNames[args.size - index] else null 54 | throw OptionMissingRequiredArgumentException(name, argName) 55 | } 56 | for (i in 0 until required) { 57 | arguments.add(args[index + i]) 58 | } 59 | } 60 | val input = ArgParser.OptionInvocation(holder, name, arguments) 61 | holder = Holder(handler(input)) 62 | return argNames.size 63 | } 64 | 65 | override fun toHelpFormatterValue(): HelpFormatter.Value { 66 | return HelpFormatter.Value( 67 | isRequired = (holder == null), 68 | isRepeating = isRepeating, 69 | usages = if (!argNames.isEmpty()) { 70 | optionNames.map { "$it ${argNames.joinToString(" ")}" } 71 | } else { 72 | optionNames 73 | }, 74 | isPositional = false, 75 | help = help) 76 | } 77 | 78 | override fun registerLeaf(root: ArgParser.Delegate<*>) { 79 | for (name in optionNames) { 80 | parser.registerOption(name, this) 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/kotlin/com/xenomachina/argparser/Default.kt: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Laurence Gonsalves 2 | // 3 | // This file is part of kotlin-argparser, a library which can be found at 4 | // http://github.com/xenomachina/kotlin-argparser 5 | // 6 | // This library is free software; you can redistribute it and/or modify it 7 | // under the terms of the GNU Lesser General Public License as published by the 8 | // Free Software Foundation; either version 2.1 of the License, or (at your 9 | // option) any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | // FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License 14 | // for more details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, see http://www.gnu.org/licenses/ 18 | 19 | package com.xenomachina.argparser 20 | 21 | /** 22 | * Returns a new `DelegateProvider` with the specified default value. 23 | * 24 | * @param newDefault the default value for the resulting [ArgParser.Delegate] 25 | */ 26 | fun ArgParser.DelegateProvider.default(newDefault: T): ArgParser.DelegateProvider { 27 | return ArgParser.DelegateProvider(ctor = ctor, default = { newDefault }) 28 | } 29 | 30 | /** 31 | * Returns a new `DelegateProvider` with the specified default value from a lambda. 32 | * 33 | * @param newDefault the default value for the resulting [ArgParser.Delegate] 34 | */ 35 | fun ArgParser.DelegateProvider.default(newDefault: () -> T): ArgParser.DelegateProvider { 36 | return ArgParser.DelegateProvider(ctor = ctor, default = newDefault) 37 | } 38 | 39 | /** 40 | * Returns a new `Delegate` with the specified default value. 41 | * 42 | * @param newDefault the default value for the resulting [ArgParser.Delegate] 43 | */ 44 | fun ArgParser.Delegate.default(defaultValue: T): ArgParser.Delegate = default { defaultValue } 45 | 46 | /** 47 | * Returns a new `Delegate` with the specified default value as a lambda. 48 | * 49 | * @param newDefault the default value for the resulting [ArgParser.Delegate] 50 | */ 51 | fun ArgParser.Delegate.default(defaultValue: () -> T): ArgParser.Delegate { 52 | if (hasValidators) { 53 | throw IllegalStateException("Cannot add default after adding validators") 54 | } 55 | val inner = this 56 | 57 | return object : ArgParser.Delegate() { 58 | 59 | override val hasValidators: Boolean 60 | get() = inner.hasValidators 61 | 62 | override fun toHelpFormatterValue(): HelpFormatter.Value = inner.toHelpFormatterValue().copy(isRequired = false) 63 | 64 | override fun validate() { 65 | inner.validate() 66 | } 67 | 68 | override val parser: ArgParser 69 | get() = inner.parser 70 | 71 | override val value: T 72 | get() { 73 | inner.parser.force() 74 | return if (inner.hasValue) inner.value else defaultValue() 75 | } 76 | 77 | override val hasValue: Boolean 78 | get() = true 79 | 80 | override val errorName: String 81 | get() = inner.errorName 82 | 83 | override val help: String 84 | get() = inner.help 85 | 86 | override fun addValidator(validator: ArgParser.Delegate.() -> Unit): ArgParser.Delegate = 87 | apply { inner.addValidator { validator(this@apply) } } 88 | 89 | override fun registerLeaf(root: ArgParser.Delegate<*>) { 90 | inner.registerLeaf(root) 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/kotlin/com/xenomachina/argparser/SystemExitException.kt: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Laurence Gonsalves 2 | // 3 | // This file is part of kotlin-argparser, a library which can be found at 4 | // http://github.com/xenomachina/kotlin-argparser 5 | // 6 | // This library is free software; you can redistribute it and/or modify it 7 | // under the terms of the GNU Lesser General Public License as published by the 8 | // Free Software Foundation; either version 2.1 of the License, or (at your 9 | // option) any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | // FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License 14 | // for more details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, see http://www.gnu.org/licenses/ 18 | 19 | package com.xenomachina.argparser 20 | 21 | import java.io.OutputStreamWriter 22 | import java.io.Writer 23 | import kotlin.system.exitProcess 24 | 25 | /** 26 | * An exception that wants the process to terminate with a specific status code, and also (optionally) wants to display 27 | * a message to [System.out] or [System.err]. 28 | * 29 | * @property returnCode the return code that this process should exit with 30 | */ 31 | open class SystemExitException(message: String, val returnCode: Int) : Exception(message) { 32 | /** 33 | * Prints a message for the user to either `System.err` or `System.out`, and then exits with the appropriate 34 | * return code. 35 | * 36 | * @param programName the name of this program as invoked, or null if not known 37 | * @param columns the number of columns to wrap at, or 0 if not to wrap at all 38 | */ 39 | fun printAndExit(programName: String? = null, columns: Int = 0): Nothing { 40 | val writer = OutputStreamWriter(if (returnCode == 0) System.out else System.err) 41 | printUserMessage(writer, programName, columns) 42 | writer.flush() 43 | exitProcess(returnCode) 44 | } 45 | 46 | /** 47 | * Prints a message for the user to the specified `Writer`. 48 | * 49 | * @param writer where to write message for the user 50 | * @param programName the name of this program as invoked, or null if not known 51 | * @param columns the number of columns to wrap at, or 0 if not to wrap at all 52 | */ 53 | open fun printUserMessage(writer: Writer, programName: String?, columns: Int) { 54 | val leader = if (programName == null) "" else "$programName: " 55 | writer.write("$leader$message\n") 56 | } 57 | } 58 | 59 | /** 60 | * Calls [SystemExitException.printAndExit] on any `SystemExitException` that 61 | * is caught. 62 | * 63 | * @param programName the name of the program. If null, the system property com.xenomachina.argparser.programName will 64 | * be used, if set. 65 | * @param columns the number of columns to wrap any caught 66 | * `SystemExitException` to. Specify null for reasonable defaults, or 0 to not 67 | * wrap at all. 68 | * @param body the code that may throw a `SystemExitException` 69 | */ 70 | fun mainBody(programName: String? = null, columns: Int? = null, body: () -> R): R { 71 | try { 72 | return body() 73 | } catch (e: SystemExitException) { 74 | e.printAndExit( 75 | programName ?: System.getProperty(PROGRAM_NAME_PROPERTY), 76 | columns ?: System.getenv("COLUMNS")?.toInt() ?: DEFAULT_COLUMNS) 77 | } 78 | } 79 | 80 | private const val PROGRAM_NAME_PROPERTY = "com.xenomachina.argparser.programName" 81 | private const val DEFAULT_COLUMNS = 80 82 | -------------------------------------------------------------------------------- /src/main/kotlin/com/xenomachina/argparser/Exceptions.kt: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Laurence Gonsalves 2 | // 3 | // This file is part of kotlin-argparser, a library which can be found at 4 | // http://github.com/xenomachina/kotlin-argparser 5 | // 6 | // This library is free software; you can redistribute it and/or modify it 7 | // under the terms of the GNU Lesser General Public License as published by the 8 | // Free Software Foundation; either version 2.1 of the License, or (at your 9 | // option) any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | // FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License 14 | // for more details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, see http://www.gnu.org/licenses/ 18 | 19 | package com.xenomachina.argparser 20 | 21 | import java.io.Writer 22 | 23 | /** 24 | * Indicates that the user requested that help should be shown (with the 25 | * `--help` option, for example). 26 | */ 27 | class ShowHelpException internal constructor( 28 | private val helpFormatter: HelpFormatter, 29 | private val delegates: List> 30 | ) : SystemExitException("Help was requested", 0) { 31 | override fun printUserMessage(writer: Writer, programName: String?, columns: Int) { 32 | writer.write(helpFormatter.format(programName, columns, delegates.map { it.toHelpFormatterValue() })) 33 | } 34 | } 35 | 36 | /** 37 | * Indicates that an unrecognized option was supplied. 38 | * 39 | * @property optName the name of the option 40 | */ 41 | open class UnrecognizedOptionException(val optName: String) : 42 | SystemExitException("unrecognized option '$optName'", 2) 43 | 44 | /** 45 | * Indicates that a value is missing after parsing has completed. 46 | * 47 | * @property valueName the name of the missing value 48 | */ 49 | open class MissingValueException(val valueName: String) : 50 | SystemExitException("missing $valueName", 2) 51 | 52 | /** 53 | * Indicates that the value of a supplied argument is invalid. 54 | */ 55 | open class InvalidArgumentException(message: String) : SystemExitException(message, 2) 56 | 57 | /** 58 | * Indicates that a required option argument was not supplied. 59 | * 60 | * @property optName the name of the option 61 | * @property argName the name of the missing argument, or null 62 | */ 63 | open class OptionMissingRequiredArgumentException(val optName: String, val argName: String? = null) : 64 | SystemExitException( 65 | "option '$optName' is missing " + ( 66 | if (argName == null) "a required argument" 67 | else "the required argument $argName"), 68 | 2) 69 | 70 | /** 71 | * Indicates that a required positional argument was not supplied. 72 | * 73 | * @property argName the name of the positional argument 74 | */ 75 | open class MissingRequiredPositionalArgumentException(val argName: String) : 76 | SystemExitException("missing $argName operand", 2) 77 | 78 | /** 79 | * Indicates that an argument was forced upon an option that does not take one. 80 | * 81 | * For example, if the arguments contained "--foo=bar" and the "--foo" option does not consume any arguments. 82 | * 83 | * @property optName the name of the option 84 | */ 85 | open class UnexpectedOptionArgumentException(val optName: String) : 86 | SystemExitException("option '$optName' doesn't allow an argument", 2) 87 | 88 | /** 89 | * Indicates that there is an unhandled positional argument. 90 | * 91 | * @property valueName the name of the missing value 92 | */ 93 | open class UnexpectedPositionalArgumentException(val valueName: String?) : 94 | SystemExitException("unexpected argument${if (valueName == null) "" else " after $valueName"}", 2) 95 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## 2.0.7 - 2018-05-08 9 | 10 | ### Fixed 11 | 12 | - [Issue #54](https://github.com/xenomachina/kotlin-argparser/issues/54) 13 | Allow the dot character in options and arguments. Thanks @tgockel! 14 | 15 | - [Issue #47](https://github.com/xenomachina/kotlin-argparser/issues/47) 16 | If an option looks at the value of another option it can see the 17 | current value. If no value has been set a `MissingValueException` is thrown. 18 | 19 | ## 2.0.6 - 2018-03-26 20 | 21 | ### Changed 22 | 23 | - Help text formatting now treats multi-newlines as paragraph separators, while 24 | single newlines are still treated like spaces. Thanks @leomillon! 25 | 26 | ## 2.0.5 - 2018-03-20 27 | 28 | ### Changed 29 | 30 | - Releasing to Maven Central in addition to Bintray. This is probably the only 31 | really externally visible change. 32 | 33 | - Upgraded a bunch of dependencies, including gradlew. 34 | - gradle -> 4.5.1 35 | - dokka -> = 0.9.16 36 | - gradle_bintray -> = 1.8.0 37 | - gradle_release -> = 2.6.0 38 | - kotlin -> 1.2.30 39 | - xenocom -> 0.0.6 40 | 41 | ## 2.0.4 - 2018-01-18 42 | 43 | ### Added 44 | 45 | - If the `programName` passed to `mainBody` is null, then the 46 | system property `com.xenomachina.argparser.programName` is used, if set. 47 | 48 | - The `parseInto` method can be used as an inline alternative to `force`. 49 | Thanks @shanethehat! 50 | 51 | - [Issue #24](https://github.com/xenomachina/kotlin-argparser/issues/18): 52 | `default` can now accept a lambda, making it possible to defer computation of 53 | defaults until actually required. 54 | Thanks @shanethehat! 55 | 56 | ### Changed 57 | 58 | - All instances of `progName` have been renamed to `programName`. 59 | 60 | - The gradle wrapper has been updated. 61 | Thanks @ColinHebert! 62 | 63 | ## 2.0.3 - 2017-06-12 64 | 65 | ### Fixed 66 | 67 | - [Issue #18](https://github.com/xenomachina/kotlin-argparser/issues/18) 68 | 69 | ## 2.0.2 - 2017-06-12 70 | 71 | ### Fixed 72 | 73 | - [Issue #19](https://github.com/xenomachina/kotlin-argparser/issues/19) by 74 | updating xenocom dependency to 0.0.5. Also updated kotlin to 1.1.2-5 for good 75 | measure. 76 | 77 | ## 2.0.1 - 2017-05-15 78 | 79 | ### Changed 80 | 81 | - [Issue #14](https://github.com/xenomachina/kotlin-argparser/issues/14) — 82 | previously, automatic option naming would turn "camelCase" into 83 | "--camelCase". Now it is converted to "--camel-case". 84 | 85 | - Likewise, positinal argument auto-naming used to convert "camelCase" into 86 | "CAMELCASE". Now it is converted to "CAMEL-CASE". 87 | 88 | - Improve help formatting w/long program names 89 | 90 | - README formatting improved. 91 | Thanks @konfilios! 92 | 93 | ### Fixed 94 | 95 | - [Issue #17](https://github.com/xenomachina/kotlin-argparser/issues/17) — 96 | specifying 0 for the columns should format help without line wrapping. 97 | Previously, this did not work as documented, and would instead wrap text in 98 | very narrow columns. 99 | 100 | - [Issue #15](https://github.com/xenomachina/kotlin-argparser/issues/15) 101 | — make it possible to specify 'argName' on all variants of 'storing' and 102 | `adding` 103 | 104 | 105 | ## 2.0.0 - 2017-04-21 106 | 107 | ### Added 108 | 109 | - `ArgParser.option` is now a public method, so it's possible to create many 110 | new option types that were not previously possible. The existing option types 111 | are all written in terms of `option`, so they can be used to get an idea of 112 | how it works. 113 | 114 | - More tests have been added. 115 | 116 | - Started using keepachangelog.com format for CHANGELOG.md 117 | 118 | - Made minor improvements to release process 119 | 120 | ### Changed 121 | 122 | - The `storing`, `adding` and `positionalList` methods of `ArgParser` have had 123 | their parameters slightly reordered to be consistent with the other methods. 124 | This is an incompatible change. The name(s) come first, if any, followed by 125 | `help`. Other parameters appear after `help`, with the `transform` function, 126 | if any, last. It is recommended that clients either pass the transform as a 127 | block (ie: with braces) or as a named parameter, as any future new parameters 128 | will necessarily change its position in the list. 129 | 130 | - Delegate and DelegateProvider are now abstract classes with internal 131 | constructors. This makes it much easier for me to separate internal and 132 | public parts of their API. This is an incompatible change, however it 133 | shouldn't really affect you unless you were trying to implement `Delegate`, 134 | which was't supported to begin with. 135 | 136 | - `default` methods on both `Delegate` and `DelegateProvider` are now extension 137 | methods. This makes it possible to generalize the type when adding a 138 | default. This is most noticable when using a nullable value (or `null` 139 | itself) for the default, though may also be useful in other cases (eg: a 140 | "storing" that always produces a `Rectangle`, but you want the default to be 141 | a `Circle`. The resulting delegate will be a `Delegate`.) 142 | 143 | - Registration of delegates now takes place at binding-time rather than 144 | construction time. This should be pretty indistinguishable from the old 145 | behavior unless you're creating delegates without binding them. 146 | 147 | - Help formatting has been improved so that it's far less likely to wrap option 148 | names in the usage table. 149 | 150 | - There have been numerous bugfixes, particularly around positionals 151 | 152 | 153 | ## 1.1.0 - 2017-03-09 154 | 155 | ### Added 156 | 157 | - Auto-naming of options and positionals. 158 | - Each of the ArgParser methods that takes names and returns a Delegate has 159 | an overload that takes no name, but returns a DelegateProvider. 160 | 161 | - A DelegateProvider has an `operator fun provideDelegate` that returns a 162 | Delegate, using a name derived from the name of the property the 163 | DelegateProvider is being bound to. 164 | 165 | ### Removed 166 | 167 | - `addValidtator` is now deprecated. Use `addValidator` instead. 168 | 169 | ### Fixed 170 | 171 | - Removed documentation of `option` from `README.md`, as it is internal 172 | 173 | - Corrected spelling of `addValidator`. `addValidtator` is still there, but 174 | deprecated. It'll probably be removed in the next release, barring the 175 | addition of potato functionality. 176 | 177 | ## 1.0.2 - 2017-03-07 178 | 179 | ### Changed 180 | 181 | - Upgrade to Kotlin 1.1, extract xenocom package. 182 | 183 | ## 1.0.1 - 2017-01-30 184 | 185 | ### Fixed 186 | 187 | - Fix small bug where `runMain` didn't have a default value for `columns` 188 | parameter. (Now defaults to null.) 189 | 190 | ## 1.0.0 - 2017-01-27 191 | 192 | ### Added 193 | 194 | - Initial release 195 | -------------------------------------------------------------------------------- /src/main/kotlin/com/xenomachina/argparser/DefaultHelpFormatter.kt: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Laurence Gonsalves 2 | // 3 | // This file is part of kotlin-argparser, a library which can be found at 4 | // http://github.com/xenomachina/kotlin-argparser 5 | // 6 | // This library is free software; you can redistribute it and/or modify it 7 | // under the terms of the GNU Lesser General Public License as published by the 8 | // Free Software Foundation; either version 2.1 of the License, or (at your 9 | // option) any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | // FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License 14 | // for more details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, see http://www.gnu.org/licenses/ 18 | 19 | package com.xenomachina.argparser 20 | 21 | import com.xenomachina.text.NBSP_CODEPOINT 22 | import com.xenomachina.text.term.codePointWidth 23 | import com.xenomachina.text.term.columnize 24 | import com.xenomachina.text.term.wrapText 25 | 26 | /** 27 | * Default implementation of [HelpFormatter]. Output is modelled after that of common UNIX utilities and looks 28 | * something like this: 29 | * 30 | * ``` 31 | * usage: program_name [-h] [-n] [-I INCLUDE]... -o OUTPUT 32 | * [-v]... SOURCE... DEST 33 | * 34 | * Does something really useful. 35 | * 36 | * required arguments: 37 | * -o OUTPUT, directory in which all output should 38 | * --output OUTPUT be generated 39 | * 40 | * optional arguments: 41 | * -h, --help show this help message and exit 42 | * 43 | * -n, --dry-run don't do anything 44 | * 45 | * -I INCLUDE, search in this directory for header 46 | * --include INCLUDE files 47 | * 48 | * -v, --verbose increase verbosity 49 | * 50 | * positional arguments: 51 | * SOURCE source file 52 | * 53 | * DEST destination file 54 | * 55 | * More info is available at http://program-name.example.com/ 56 | * ``` 57 | * 58 | * @property prologue Text that should appear near the beginning of the help, immediately after the usage summary. 59 | * @property epilogue Text that should appear at the end of the help. 60 | */ 61 | class DefaultHelpFormatter( 62 | val prologue: String? = null, 63 | val epilogue: String? = null 64 | ) : HelpFormatter { 65 | val indent = " " 66 | val indentWidth = indent.codePointWidth() 67 | 68 | override fun format( 69 | programName: String?, 70 | columns: Int, 71 | values: List 72 | ): String { 73 | val effectiveColumns = when { 74 | columns < 0 -> throw IllegalArgumentException("columns must be non-negative") 75 | columns == 0 -> Int.MAX_VALUE 76 | else -> columns 77 | } 78 | val sb = StringBuilder() 79 | appendUsage(sb, effectiveColumns, programName, values) 80 | sb.append("\n") 81 | 82 | if (!prologue.isNullOrEmpty()) { 83 | sb.append("\n\n") 84 | // we just checked that prologue is non-null 85 | sb.append(prologue!!.wrapText(effectiveColumns)) 86 | sb.append("\n\n") 87 | } 88 | 89 | val required = mutableListOf() 90 | val optional = mutableListOf() 91 | val positional = mutableListOf() 92 | 93 | for (value in values) { 94 | when { 95 | value.isPositional -> positional 96 | value.isRequired -> required 97 | else -> optional 98 | }.add(value) 99 | } 100 | val usageColumns = 2 * indentWidth - 1 + if (columns == 0) { 101 | values.map { usageText(it).length }.max() ?: 0 102 | } else { 103 | // Make left column as narrow as possible without wrapping any of the individual usages, though no wider 104 | // than half the screen. 105 | (values.map { 106 | usageText(it).split(" ").map { it.length }.max() ?: 0 107 | }.max() ?: 0).coerceAtMost(effectiveColumns / 2) 108 | } 109 | 110 | appendSection(sb, usageColumns, effectiveColumns, "required", required) 111 | appendSection(sb, usageColumns, effectiveColumns, "optional", optional) 112 | appendSection(sb, usageColumns, effectiveColumns, "positional", positional) 113 | 114 | if (!epilogue?.trim().isNullOrEmpty()) { 115 | sb.append("\n") 116 | // we just checked that epilogue is non-null 117 | sb.append(epilogue!!.trim().wrapText(effectiveColumns)) 118 | sb.append("\n") 119 | } 120 | 121 | return sb.toString() 122 | } 123 | 124 | private fun appendSection( 125 | sb: StringBuilder, 126 | usageColumns: Int, 127 | columns: Int, 128 | name: String, 129 | values: List 130 | ) { 131 | 132 | if (!values.isEmpty()) { 133 | sb.append("\n") 134 | sb.append("$name arguments:\n") 135 | for (value in values) { 136 | val left = usageText(value).wrapText(usageColumns - indentWidth).prependIndent(indent) 137 | val right = value.help.wrapText(columns - usageColumns - 2 * indentWidth).prependIndent(indent) 138 | sb.append(columnize(left, right, minWidths = intArrayOf(usageColumns))) 139 | sb.append("\n\n") 140 | } 141 | } 142 | } 143 | 144 | private fun usageText(value: HelpFormatter.Value) = 145 | value.usages.map { it.replace(' ', '\u00a0') }.joinToString(", ") 146 | 147 | private fun appendUsage(sb: StringBuilder, columns: Int, programName: String?, values: List) { 148 | var usageStart = USAGE_PREFIX + (if (programName != null) " $programName" else "") 149 | 150 | val valueSB = StringBuilder() 151 | for (value in values) value.run { 152 | if (!usages.isEmpty()) { 153 | val usage = usages[0].replace(' ', NBSP_CODEPOINT.toChar()) 154 | if (isRequired) { 155 | valueSB.append(" $usage") 156 | } else { 157 | valueSB.append(" [$usage]") 158 | } 159 | if (isRepeating) { 160 | valueSB.append("...") 161 | } 162 | } 163 | } 164 | 165 | if (usageStart.length > columns / 2) { 166 | sb.append(usageStart) 167 | sb.append("\n") 168 | val valueIndent = (USAGE_PREFIX + " " + indent).codePointWidth() 169 | val valueColumns = columns - valueIndent 170 | sb.append(valueSB.toString().wrapText(valueColumns).prependIndent(" ".repeat(valueIndent))) 171 | } else { 172 | usageStart += " " 173 | val valueColumns = columns - usageStart.length 174 | sb.append(columnize(usageStart, valueSB.toString().wrapText(valueColumns))) 175 | } 176 | } 177 | } 178 | 179 | private const val USAGE_PREFIX = "usage:" 180 | -------------------------------------------------------------------------------- /detekt.yml: -------------------------------------------------------------------------------- 1 | # `git diff detekt-default detekt.yml` to see what's changed from default 2 | # settings 3 | 4 | test-pattern: # Configure exclusions for test sources 5 | patterns: # Test file regexes 6 | - '.*/test/.*' 7 | exclude-rule-sets: 8 | - 'comments' 9 | exclude-rules: 10 | - 'NamingRules' 11 | - 'WildcardImport' 12 | - 'MagicNumber' 13 | - 'MaxLineLength' 14 | - 'LateinitUsage' 15 | - 'StringLiteralDuplication' 16 | - 'SpreadOperator' 17 | - 'TooManyFunctions' 18 | - 'ForEachOnRange' 19 | 20 | build: 21 | maxIssues: 10 # TODO: reduce this threshhold 22 | weights: 23 | # complexity: 2 24 | # LongParameterList: 1 25 | # style: 1 26 | # comments: 1 27 | 28 | processors: 29 | active: true 30 | exclude: 31 | # - 'FunctionCountProcessor' 32 | # - 'PropertyCountProcessor' 33 | # - 'ClassCountProcessor' 34 | # - 'PackageCountProcessor' 35 | # - 'KtFileCountProcessor' 36 | 37 | console-reports: 38 | active: true 39 | exclude: 40 | # - 'ProjectStatisticsReport' 41 | # - 'ComplexityReport' 42 | # - 'NotificationReport' 43 | # - 'FindingsReport' 44 | # - 'BuildFailureReport' 45 | 46 | output-reports: 47 | active: true 48 | exclude: 49 | # - 'HtmlOutputReport' 50 | # - 'PlainOutputReport' 51 | # - 'XmlOutputReport' 52 | 53 | comments: 54 | active: true 55 | CommentOverPrivateFunction: 56 | active: false 57 | CommentOverPrivateProperty: 58 | active: false 59 | EndOfSentenceFormat: 60 | active: false 61 | endOfSentenceFormat: ([.?!][ \t\n\r\f<])|([.?!]$) 62 | UndocumentedPublicClass: 63 | active: false 64 | searchInNestedClass: true 65 | searchInInnerClass: true 66 | searchInInnerObject: true 67 | searchInInnerInterface: true 68 | UndocumentedPublicFunction: 69 | active: false 70 | 71 | complexity: 72 | active: true 73 | ComplexCondition: 74 | active: true 75 | threshold: 4 76 | ComplexInterface: 77 | active: false 78 | threshold: 10 79 | includeStaticDeclarations: false 80 | ComplexMethod: 81 | active: true 82 | threshold: 10 83 | ignoreSingleWhenExpression: false 84 | LabeledExpression: 85 | active: false 86 | LargeClass: 87 | active: true 88 | threshold: 150 89 | LongMethod: 90 | active: true 91 | threshold: 20 92 | LongParameterList: 93 | active: true 94 | threshold: 6 95 | ignoreDefaultParameters: false 96 | MethodOverloading: 97 | active: false 98 | threshold: 6 99 | NestedBlockDepth: 100 | active: true 101 | threshold: 4 102 | StringLiteralDuplication: 103 | active: false 104 | threshold: 3 105 | ignoreAnnotation: true 106 | excludeStringsWithLessThan5Characters: true 107 | ignoreStringsRegex: '$^' 108 | TooManyFunctions: 109 | active: true 110 | thresholdInFiles: 11 111 | thresholdInClasses: 11 112 | thresholdInInterfaces: 11 113 | thresholdInObjects: 11 114 | thresholdInEnums: 11 115 | ignoreDeprecated: false 116 | 117 | empty-blocks: 118 | active: true 119 | EmptyCatchBlock: 120 | active: true 121 | allowedExceptionNameRegex: "^(_|(ignore|expected).*)" 122 | EmptyClassBlock: 123 | active: true 124 | EmptyDefaultConstructor: 125 | active: true 126 | EmptyDoWhileBlock: 127 | active: true 128 | EmptyElseBlock: 129 | active: true 130 | EmptyFinallyBlock: 131 | active: true 132 | EmptyForBlock: 133 | active: true 134 | EmptyFunctionBlock: 135 | active: true 136 | ignoreOverriddenFunctions: false 137 | EmptyIfBlock: 138 | active: true 139 | EmptyInitBlock: 140 | active: true 141 | EmptyKtFile: 142 | active: true 143 | EmptySecondaryConstructor: 144 | active: true 145 | EmptyWhenBlock: 146 | active: true 147 | EmptyWhileBlock: 148 | active: true 149 | 150 | exceptions: 151 | active: true 152 | ExceptionRaisedInUnexpectedLocation: 153 | active: false 154 | methodNames: 'toString,hashCode,equals,finalize' 155 | InstanceOfCheckForException: 156 | active: false 157 | NotImplementedDeclaration: 158 | active: false 159 | PrintStackTrace: 160 | active: false 161 | RethrowCaughtException: 162 | active: false 163 | ReturnFromFinally: 164 | active: false 165 | SwallowedException: 166 | active: false 167 | ThrowingExceptionFromFinally: 168 | active: false 169 | ThrowingExceptionInMain: 170 | active: false 171 | ThrowingExceptionsWithoutMessageOrCause: 172 | active: false 173 | exceptions: 'IllegalArgumentException,IllegalStateException,IOException' 174 | ThrowingNewInstanceOfSameException: 175 | active: false 176 | TooGenericExceptionCaught: 177 | active: true 178 | exceptionNames: 179 | - ArrayIndexOutOfBoundsException 180 | - Error 181 | - Exception 182 | - IllegalMonitorStateException 183 | - NullPointerException 184 | - IndexOutOfBoundsException 185 | - RuntimeException 186 | - Throwable 187 | TooGenericExceptionThrown: 188 | active: true 189 | exceptionNames: 190 | - Error 191 | - Exception 192 | - Throwable 193 | - RuntimeException 194 | 195 | formatting: 196 | active: true 197 | android: false 198 | autoCorrect: true 199 | ChainWrapping: 200 | active: true 201 | autoCorrect: true 202 | CommentSpacing: 203 | active: true 204 | autoCorrect: true 205 | Filename: 206 | active: true 207 | FinalNewline: 208 | active: true 209 | autoCorrect: true 210 | ImportOrdering: 211 | active: true 212 | autoCorrect: true 213 | Indentation: 214 | active: true 215 | autoCorrect: true 216 | indentSize: 4 217 | continuationIndentSize: 4 218 | MaximumLineLength: 219 | active: true 220 | maxLineLength: 120 221 | ModifierOrdering: 222 | active: true 223 | autoCorrect: true 224 | NoBlankLineBeforeRbrace: 225 | active: true 226 | autoCorrect: true 227 | NoConsecutiveBlankLines: 228 | active: true 229 | autoCorrect: true 230 | NoEmptyClassBody: 231 | active: true 232 | autoCorrect: true 233 | NoItParamInMultilineLambda: 234 | active: true 235 | NoLineBreakAfterElse: 236 | active: true 237 | autoCorrect: true 238 | NoLineBreakBeforeAssignment: 239 | active: true 240 | autoCorrect: true 241 | NoMultipleSpaces: 242 | active: true 243 | autoCorrect: true 244 | NoSemicolons: 245 | active: true 246 | autoCorrect: true 247 | NoTrailingSpaces: 248 | active: true 249 | autoCorrect: true 250 | NoUnitReturn: 251 | active: true 252 | autoCorrect: true 253 | NoUnusedImports: 254 | active: true 255 | autoCorrect: true 256 | NoWildcardImports: 257 | active: true 258 | autoCorrect: true 259 | ParameterListWrapping: 260 | active: true 261 | autoCorrect: true 262 | indentSize: 4 263 | SpacingAroundColon: 264 | active: true 265 | autoCorrect: true 266 | SpacingAroundComma: 267 | active: true 268 | autoCorrect: true 269 | SpacingAroundCurly: 270 | active: true 271 | autoCorrect: true 272 | SpacingAroundKeyword: 273 | active: true 274 | autoCorrect: true 275 | SpacingAroundOperators: 276 | active: true 277 | autoCorrect: true 278 | SpacingAroundRangeOperator: 279 | active: true 280 | autoCorrect: true 281 | StringTemplate: 282 | active: true 283 | autoCorrect: true 284 | 285 | naming: 286 | active: true 287 | ClassNaming: 288 | active: true 289 | classPattern: '[A-Z$][a-zA-Z0-9$]*' 290 | EnumNaming: 291 | active: true 292 | enumEntryPattern: '^[A-Z][_a-zA-Z0-9]*' 293 | ForbiddenClassName: 294 | active: false 295 | forbiddenName: '' 296 | FunctionMaxLength: 297 | active: false 298 | maximumFunctionNameLength: 30 299 | FunctionMinLength: 300 | active: false 301 | minimumFunctionNameLength: 3 302 | FunctionNaming: 303 | active: true 304 | functionPattern: '^([a-z$][a-zA-Z$0-9]*)|(`.*`)$' 305 | excludeClassPattern: '$^' 306 | MatchingDeclarationName: 307 | active: true 308 | MemberNameEqualsClassName: 309 | active: false 310 | ignoreOverriddenFunction: true 311 | ObjectPropertyNaming: 312 | active: true 313 | propertyPattern: '[A-Za-z][_A-Za-z0-9]*' 314 | constantPattern: '[A-Za-z][_A-Za-z0-9]*' 315 | PackageNaming: 316 | active: true 317 | packagePattern: '^[a-z]+(\.[a-z][a-z0-9]*)*$' 318 | TopLevelPropertyNaming: 319 | active: true 320 | constantPattern: '[A-Z][_A-Z0-9]*' 321 | propertyPattern: '[A-Za-z][_A-Za-z0-9]*' 322 | privatePropertyPattern: '(_)?[A-Za-z][_A-Za-z0-9]*' 323 | VariableMaxLength: 324 | active: false 325 | maximumVariableNameLength: 64 326 | VariableMinLength: 327 | active: false 328 | minimumVariableNameLength: 1 329 | VariableNaming: 330 | active: true 331 | variablePattern: '[a-z][A-Za-z0-9]*' 332 | privateVariablePattern: '(_)?[a-z][A-Za-z0-9]*' 333 | excludeClassPattern: '$^' 334 | 335 | performance: 336 | active: true 337 | ForEachOnRange: 338 | active: true 339 | SpreadOperator: 340 | active: false 341 | UnnecessaryTemporaryInstantiation: 342 | active: true 343 | 344 | potential-bugs: 345 | active: true 346 | DuplicateCaseInWhenExpression: 347 | active: true 348 | EqualsAlwaysReturnsTrueOrFalse: 349 | active: false 350 | EqualsWithHashCodeExist: 351 | active: true 352 | ExplicitGarbageCollectionCall: 353 | active: true 354 | InvalidRange: 355 | active: false 356 | IteratorHasNextCallsNextMethod: 357 | active: false 358 | IteratorNotThrowingNoSuchElementException: 359 | active: false 360 | LateinitUsage: 361 | active: false 362 | excludeAnnotatedProperties: "" 363 | ignoreOnClassesPattern: "" 364 | UnconditionalJumpStatementInLoop: 365 | active: false 366 | UnreachableCode: 367 | active: true 368 | UnsafeCallOnNullableType: 369 | active: false 370 | UnsafeCast: 371 | active: false 372 | UselessPostfixExpression: 373 | active: false 374 | WrongEqualsTypeParameter: 375 | active: false 376 | 377 | style: 378 | active: true 379 | CollapsibleIfStatements: 380 | active: false 381 | DataClassContainsFunctions: 382 | active: false 383 | conversionFunctionPrefix: 'to' 384 | EqualsNullCall: 385 | active: false 386 | ExpressionBodySyntax: 387 | active: false 388 | ForbiddenComment: 389 | active: true 390 | values: 'FIXME:,STOPSHIP:' 391 | ForbiddenImport: 392 | active: false 393 | imports: '' 394 | FunctionOnlyReturningConstant: 395 | active: false 396 | ignoreOverridableFunction: true 397 | excludedFunctions: 'describeContents' 398 | LoopWithTooManyJumpStatements: 399 | active: false 400 | maxJumpCount: 1 401 | MagicNumber: 402 | active: true 403 | ignoreNumbers: '-1,0,1,2' 404 | ignoreHashCodeFunction: false 405 | ignorePropertyDeclaration: false 406 | ignoreConstantDeclaration: true 407 | ignoreCompanionObjectPropertyDeclaration: true 408 | ignoreAnnotation: false 409 | ignoreNamedArgument: true 410 | ignoreEnums: false 411 | MaxLineLength: 412 | active: true 413 | maxLineLength: 120 414 | excludePackageStatements: false 415 | excludeImportStatements: false 416 | excludeCommentStatements: false 417 | MayBeConst: 418 | active: false 419 | ModifierOrder: 420 | active: true 421 | NestedClassesVisibility: 422 | active: false 423 | NewLineAtEndOfFile: 424 | active: true 425 | NoTabs: 426 | active: false 427 | OptionalAbstractKeyword: 428 | active: true 429 | OptionalUnit: 430 | active: false 431 | OptionalWhenBraces: 432 | active: false 433 | ProtectedMemberInFinalClass: 434 | active: false 435 | RedundantVisibilityModifierRule: 436 | active: false 437 | ReturnCount: 438 | active: true 439 | max: 2 440 | excludedFunctions: "equals" 441 | SafeCast: 442 | active: true 443 | SerialVersionUIDInSerializableClass: 444 | active: false 445 | SpacingBetweenPackageAndImports: 446 | active: false 447 | ThrowsCount: 448 | active: true 449 | max: 2 450 | TrailingWhitespace: 451 | active: false 452 | UnnecessaryAbstractClass: 453 | active: false 454 | UnnecessaryInheritance: 455 | active: false 456 | UnnecessaryParentheses: 457 | active: false 458 | UntilInsteadOfRangeTo: 459 | active: false 460 | UnusedImports: 461 | active: false 462 | UnusedPrivateMember: 463 | active: false 464 | allowedNames: "(_|ignored|expected)" 465 | UseDataClass: 466 | active: false 467 | excludeAnnotatedClasses: "" 468 | UtilityClassWithPublicConstructor: 469 | active: false 470 | WildcardImport: 471 | active: true 472 | excludeImports: 'java.util.*,kotlinx.android.synthetic.*' 473 | 474 | # vim: sw=2 475 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 50 | 52 | 53 | 55 | image/svg+xml 56 | 58 | 59 | 60 | 61 | 62 | 67 | 70 | 80 | 84 | 89 | 94 | 99 | 104 | 109 | 114 | 119 | 124 | 129 | 134 | 139 | 144 | 149 | 154 | 159 | 164 | 169 | 170 | 173 | 176 | 181 | 186 | 187 | 190 | 195 | 200 | 201 | 204 | 209 | 214 | 215 | 218 | 223 | 228 | 229 | 232 | 237 | 242 | 243 | 246 | 251 | 256 | 257 | 260 | 265 | 270 | 271 | 274 | 279 | 284 | 285 | 288 | 293 | 298 | 299 | 300 | 301 | 302 | 303 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kotlin --argparser 2 | 3 | 9 | 10 | [![Maven Central](https://img.shields.io/maven-central/v/com.xenomachina/kotlin-argparser.svg)](https://mvnrepository.com/artifact/com.xenomachina/kotlin-argparser) 11 | [![Build Status](https://travis-ci.org/xenomachina/kotlin-argparser.svg?branch=master)](https://travis-ci.org/xenomachina/kotlin-argparser) 12 | [![codebeat badge](https://codebeat.co/badges/902174e2-31be-4f9d-a4ba-40178b075d2a)](https://codebeat.co/projects/github-com-xenomachina-kotlin-argparser-master) 13 | [![Awesome Kotlin Badge](https://kotlin.link/awesome-kotlin.svg)](https://github.com/KotlinBy/awesome-kotlin) 14 | [![Javadocs](https://www.javadoc.io/badge/com.xenomachina/kotlin-argparser.svg)](https://www.javadoc.io/doc/com.xenomachina/kotlin-argparser) 15 | [![License: LGPL 2.1](https://img.shields.io/badge/license-LGPL--2.1-blue.svg)](https://www.gnu.org/licenses/old-licenses/lgpl-2.1.en.html) 16 | 17 | 18 | This is a library for parsing command-line arguments. It can parse both 19 | options and positional arguments. It aims to be easy to use and concise yet 20 | powerful and robust. 21 | 22 | 23 | ## Overview 24 | 25 | Defining options and positional arguments is as simple as: 26 | 27 | ```kotlin 28 | import com.xenomachina.argparser.ArgParser 29 | 30 | class MyArgs(parser: ArgParser) { 31 | val v by parser.flagging("enable verbose mode") 32 | 33 | val name by parser.storing("name of the user") 34 | 35 | val count by parser.storing("number of the widgets") { toInt() } 36 | 37 | val source by parser.positional("source filename") 38 | 39 | val destination by parser.positional("destination filename") 40 | } 41 | ``` 42 | 43 | An instance of `MyArgs` will represent the set of parsed arguments. Each option 44 | and positional argument is declared as a property that delegates through a 45 | delegate factory method on an instance of `ArgParser`. 46 | 47 | The name of an option is inferred from the name of the property it is bound to. 48 | The options above are named `-v`, `--name` and `--count`, respectively. There 49 | are also two positional arguments. 50 | 51 | Direct control over an option's name is also possible, and for most types of 52 | options it is also possible to have multiple names, like a short and long name: 53 | 54 | 55 | ```kotlin 56 | class MyArgs(parser: ArgParser) { 57 | val verbose by parser.flagging( 58 | "-v", "--verbose", 59 | help = "enable verbose mode") 60 | 61 | val name by parser.storing( 62 | "-N", "--name", 63 | help = "name of the user") 64 | 65 | val count by parser.storing( 66 | "-c", "--count", 67 | help = "number of widgets") { toInt() } 68 | 69 | val source by parser.positional( 70 | "SOURCE", 71 | help = "source filename") 72 | 73 | val destination by parser.positional( 74 | "DEST", 75 | help = "destination filename") 76 | } 77 | ``` 78 | 79 | The unparsed command-line arguments are passed to the `ArgParser` instance at 80 | construction: 81 | 82 | ```kotlin 83 | fun main(args: Array) = mainBody { 84 | ArgParser(args).parseInto(::MyArgs).run { 85 | println("Hello, ${name}!") 86 | println("I'm going to move ${count} widgets from ${source} to ${destination}.") 87 | // TODO: move widgets 88 | } 89 | } 90 | ``` 91 | 92 | See [kotlin-argparser-example](https://github.com/xenomachina/kotlin-argparser-example) 93 | for a complete example project. 94 | 95 | 96 | ## Nomenclature 97 | 98 | Options, arguments, flags... what's the difference? 99 | 100 | An application's `main` function is passed an array of strings. These are 101 | the *unparsed command-line arguments*, or *unparsed arguments* for short. 102 | 103 | The unparsed arguments can then be parsed into *options*, which start 104 | with a hyphen ("`-`"), and *positional arguments*. For example, in the command 105 | `ls -l /tmp/`, the unparsed arguments would be `"-l", "/tmp"` where `-l` 106 | is an option, while `/tmp/` is a positional argument. 107 | 108 | Options can also have *option arguments*. In the command `ls --time-style=iso`, 109 | the option is `--time-style` and that options argument is `iso`. Note that in 110 | parsing a single unparsed argument can be split into an option and an option 111 | argument, or even into multiple options in some cases. 112 | 113 | A *flag* is a boolean option which has no arguments and which is false if not 114 | provided, but true if provided. The `-l` option of `ls` is a flag. 115 | 116 | ## Option Types 117 | 118 | ### Boolean Flags 119 | 120 | Boolean flags are created by asking the parser for a `flagging` delegate. One 121 | or more option names, may be provided: 122 | 123 | ```kotlin 124 | val verbose by parser.flagging("-v", "--verbose", 125 | help = "enable verbose mode") 126 | ``` 127 | 128 | Here the presence of either `-v` or `--verbose` options in the 129 | arguments will cause the `Boolean` property `verbose` to be `true`, otherwise 130 | it will be `false`. 131 | 132 | ### Storing a Single Argument 133 | 134 | Single argument options are created by asking the parser for a 135 | `storing` delegate. 136 | 137 | ```kotlin 138 | val name by parser.storing("-N", "--name", 139 | help = "name of the user") 140 | ``` 141 | 142 | Here either `-N` or `--name` with an argument will cause the `name` property to 143 | have that argument as its value. 144 | 145 | A function can also be supplied to transform the argument into the desired 146 | type. Here the `size` property will be an `Int` rather than a `String`: 147 | 148 | ```kotlin 149 | val size by parser.storing("-c", "--count", 150 | help = "number of widgets") { toInt() } 151 | ``` 152 | 153 | ### Adding to a Collection 154 | 155 | Options that add to a `Collection` each time they appear in the arguments are 156 | created with using the `adding` delegate. Just like `storing` delegates, a 157 | transform function may optionally be supplied: 158 | 159 | ```kotlin 160 | val includeDirs by parser.adding( 161 | "-I", help = "directory to search for header files") { File(this) } 162 | ``` 163 | 164 | Now each time the `-I` option appears, its transformed argument is appended to 165 | `includeDirs`. 166 | 167 | ### Mapping from an option to a fixed value 168 | 169 | For choosing between a fixed set of values (typically, but not necessarily, 170 | from an enum), a `mapping` delegate can be used: 171 | 172 | ```kotlin 173 | val mode by parser.mapping( 174 | "--fast" to Mode.FAST, 175 | "--small" to Mode.SMALL, 176 | "--quiet" to Mode.QUIET, 177 | help = "mode of operation") 178 | ``` 179 | 180 | Here the `mode` property will be set to the corresponding `ArgParser.Mode` value depending 181 | on which of `--fast`, `--small`, and `--quiet` appears (last) in the arguments. 182 | 183 | `mapping` is one of the few cases where it is not possible to infer the option 184 | name from the property name. 185 | 186 | ### More advanced options 187 | 188 | For all other types of options, the `option` method should be used. The 189 | methods mentioned above are, in fact, convenience methods built on top of the 190 | `option` method. 191 | 192 | For example, it is possible to create an option that has multiple arguments: 193 | 194 | ```kotlin 195 | fun ArgParser.putting(vararg names: String, help: String) = 196 | option>(*names, 197 | argNames = listOf("KEY", "VALUE"), 198 | help = help) { 199 | value.orElse { mutableMapOf() }.apply { 200 | put(arguments.first(), arguments.last()) } 201 | } 202 | ``` 203 | 204 | Note that the `option` method does not have an auto-naming overload. If you 205 | need this capability, create a `DelegateProvider` that creates your `Delegate`: 206 | 207 | ```kotlin 208 | fun ArgParser.putting(help: String) = 209 | ArgParser.DelegateProvider { identifier -> 210 | putting(identifierToOptionName(identifier), help = help) } 211 | ``` 212 | 213 | 214 | ## Positional Arguments 215 | 216 | Positional arguments are collected by using the `positional` and 217 | `positionalList` methods. 218 | 219 | For a single positional argument: 220 | 221 | ```kotlin 222 | val destination by parser.positional("destination filename") 223 | ``` 224 | 225 | An explicit name may also be specified: 226 | 227 | ```kotlin 228 | val destination by parser.positional("DEST", 229 | help = "destination filename") 230 | ``` 231 | 232 | The name ("DEST", here) is used in error handling and help text. 233 | 234 | For a list of positional arguments: 235 | 236 | ```kotlin 237 | val sources by parser.positionalList("SOURCE", 1..Int.MAX_VALUE, 238 | help = "source filename") 239 | ``` 240 | 241 | The range indicates how many arguments should be collected, and defaults to the 242 | value shown in this example. As the name suggests, the resulting property will 243 | be a `List`. 244 | 245 | Both of these methods accept an optional transform function for converting 246 | arguments from `String` to whatever type is actually desired: 247 | 248 | ```kotlin 249 | val destination by parser.positional("DEST", 250 | help = "...") { File(this) } 251 | 252 | val sources by parser.positionalList("SOURCE", 1..Int.MAX_VALUE, 253 | help = "...") { File(this) } 254 | ``` 255 | 256 | 257 | ## Modifying Delegates 258 | 259 | The delegates returned by any of these methods also have a few methods for setting 260 | optional attributes: 261 | 262 | ### Adding a Default Value 263 | 264 | Certain types of delegates (notably `storing`, `mapping`, and `positional`) 265 | have no default value, and hence will be required options unless a default 266 | value is provided. This is done with the `default` method: 267 | 268 | ```kotlin 269 | val name by parser.storing("-N", "--name", help = "...").default("John Doe") 270 | ``` 271 | 272 | Note that it *is* possible to use `null` for the default, though this may 273 | require specifying the type parameter for `default` explicitly: 274 | 275 | ```kotlin 276 | val name by parser.storing("-N", "--name", help = "...").default(null) 277 | ``` 278 | 279 | The type of the resulting property be nullable (a `String?` in this case). 280 | 281 | 282 | ### Adding a Validator 283 | 284 | Sometimes it's easier to validate an option at the end of parsing, in which 285 | case the `addValidator` method can be used. 286 | 287 | ```kotlin 288 | val percentages by parser.adding("--percentages", help = "...") { toInt() } 289 | .addValidator { 290 | if (value.sum() != 100) 291 | throw InvalidArgumentException( 292 | "Percentages must add up to 100%") 293 | } 294 | ``` 295 | 296 | ## Error Handling 297 | 298 | If the parser determines that execution should not continue it will throw a 299 | `SystemExitException` which has a status code appropriate for passing to 300 | `exitProcess` as well as a message for the user. 301 | 302 | These exceptions can be caused by user error, or even if the user requests help 303 | (eg: via the `--help` option). 304 | 305 | It is recommended that transform functions (given to `storing`, 306 | `positionalList`, etc.) and post-parsing validation, including that performed 307 | via, `addValidator` also throw a `SystemExitException` on failure. 308 | 309 | As a convenience, these exceptions can be handled by using the `mainBody` 310 | function: 311 | 312 | ```kotlin 313 | class ParsedArgs(parser: ArgParser) { 314 | val name by positional("The user's name").default("world") 315 | } 316 | 317 | fun main(args: Array) = mainBody { 318 | ArgParser(args).parseInto(::ParsedArgs).run { 319 | println("Hello, {name}!") 320 | } 321 | } 322 | ``` 323 | 324 | ## Parsing 325 | 326 | Parsing of command-line arguments is performed sequentially. So long as 327 | option-processing is enabled, each not-yet-processed command-line argument that 328 | starts with a hyphen (`-`) is treated as an option. 329 | 330 | ### Short Options 331 | 332 | Short options start with a single hyphen. If the option takes an argument, the 333 | argument can either be appended: 334 | 335 | ```bash 336 | # "-o" with argument "ARGUMENT" 337 | my_program -oARGUMENT 338 | ``` 339 | 340 | or can be the following command-line argument: 341 | 342 | ```bash 343 | # "-o" with argument "ARGUMENT" 344 | my_program -o ARGUMENT 345 | ``` 346 | 347 | Zero argument short options can also be appended to each other without 348 | intermediate hyphens: 349 | 350 | ```bash 351 | # "-x", "-y" and "-z" options 352 | my_program -xyz 353 | ``` 354 | 355 | An option that accepts arguments is also allowed at the end of such a chain: 356 | 357 | ```bash 358 | # "-x", "-y" and "-z" options, with argument for "-z" 359 | my_program -xyzARGUMENT 360 | ``` 361 | 362 | ### Long Options 363 | 364 | Long options start with a double hyphen (`--`). An argument to a long option 365 | can 366 | either be delimited with an equal sign (`=`): 367 | 368 | ```bash 369 | # "--foo" with argument "ARGUMENT" 370 | my_program --foo=ARGUMENT 371 | ``` 372 | 373 | or can be the following command-line argument: 374 | 375 | ```bash 376 | # "--foo" with argument "ARGUMENT" 377 | my_program --foo ARGUMENT 378 | ``` 379 | 380 | ### Multi-argument Options 381 | 382 | Multi-argument options are supported, though currently not by any of the 383 | convenience methods. Option-arguments after the first must be separate 384 | command-line arguments, for both an long and short forms of an option. 385 | 386 | ### Positional Arguments 387 | 388 | In GNU mode (the default), options can be interspersed with positional 389 | arguments, but in POSIX mode the first positional argument that is encountered 390 | disables option processing for the remaining arguments. In either mode, if the 391 | argument "--" is encountered while option processing is enabled, then option 392 | processing is disabled for the rest of the command-line. Once the options and 393 | option-arguments have been eliminated, what remains are considered to be 394 | positional arguments. 395 | 396 | Each positional argument delegate can specify a minimum and maximum number of 397 | arguments it is willing to collect. 398 | 399 | The positional arguments are distributed to the delegates by allocating each 400 | positional delegate at least as many arguments as it requires. If more than the 401 | minimum number of positional arguments have been supplied then additional 402 | arguments will be allocated to the first delegate up to its maximum, then the 403 | second, and so on, until all arguments have been allocated to a delegate. 404 | 405 | This makes it easy to create a program that behaves like `grep`: 406 | 407 | ```kotlin 408 | class Args(parser: ArgParser) { 409 | // accept 1 regex followed by n filenames 410 | val regex by parser.positional("REGEX", 411 | help = "regular expression to search for") 412 | val files by parser.positionalList("FILE", 413 | help = "file to search in") 414 | } 415 | ``` 416 | 417 | And equally easy to create a program that behaves like `cp`: 418 | 419 | ```kotlin 420 | class Args(parser: ArgParser) { 421 | // accept n source files followed by 1 destination 422 | val sources by parser.positionalList("SOURCE", 423 | help = "source file") 424 | val destination by parser.positional("DEST", 425 | help = "destination file") 426 | } 427 | ``` 428 | 429 | 430 | ## Forcing Parsing 431 | 432 | Parsing normally does not begin until a delegate's value is accessed. Sometimes 433 | this is not desirable, so it is possible to enforce the parsing of arguments 434 | into a class of values. This ensures that all arguments that are required are 435 | provided, and all arguments provided are consumed. 436 | 437 | Forcing can be done in a separate step using the `force` method: 438 | 439 | ```kotlin 440 | val parser = ArgParser(args) 441 | val parsedArgs = ParsedArgs(parser) 442 | parser.force() 443 | // now you can use parsedArgs 444 | ``` 445 | 446 | Alternatively, forcing can be done inline via the `parseInto` method: 447 | 448 | ```kotlin 449 | val parsedArgs = ArgParser(args).parseInto(::ParsedArgs) 450 | // now you can use parsedArgs 451 | ``` 452 | 453 | In both cases exceptions will be thrown where parsing or validation errors are found. 454 | 455 | ## Help Formatting 456 | 457 | By default, `ArgParser` will add a `--help` option (short name `-h`) for 458 | displaying usage information. If this option is present a `ShowHelpException` will be thrown. 459 | If the default exception handling is being used (see [Error Handling](#error-handling)) the 460 | program will halt and print a help message like the one below, based on the `ArgParser` 461 | configuration: 462 | 463 | usage: program_name [-h] [-n] [-I INCLUDE]... -o OUTPUT 464 | [-v]... SOURCE... DEST 465 | 466 | 467 | This is the prologue. Lorem ipsum dolor sit amet, consectetur 468 | adipiscing elit. Aliquam malesuada maximus eros. Fusce 469 | luctus risus eget quam consectetur, eu auctor est 470 | ullamcorper. Maecenas eget suscipit dui, sed sodales erat. 471 | Phasellus. 472 | 473 | 474 | required arguments: 475 | -o OUTPUT, directory in which all output should 476 | --output OUTPUT be generated 477 | 478 | 479 | optional arguments: 480 | -h, --help show this help message and exit 481 | 482 | -n, --dry-run don't do anything 483 | 484 | -I INCLUDE, search in this directory for header 485 | --include INCLUDE files 486 | 487 | -v, --verbose increase verbosity 488 | 489 | 490 | positional arguments: 491 | SOURCE source file 492 | 493 | DEST destination file 494 | 495 | 496 | This is the epilogue. Lorem ipsum dolor sit amet, 497 | consectetur adipiscing elit. Donec vel tortor nunc. Sed eu 498 | massa sed turpis auctor faucibus. Donec vel pellentesque 499 | tortor. Ut ultrices tempus lectus fermentum vestibulum. 500 | Phasellus. 501 | 502 | The creation of the `--help` option can be disabled by passing `null` as the 503 | `helpFormatter` when constructing the `ArgParser`, or configured by manually 504 | constructing a `HelpFormatter` instance. In the above example a 505 | `DefaultHelpFormatter` was created with the prologue and epilogue. 506 | 507 | 508 | ## Caveats 509 | 510 | - This library should be considered to be *very beta*. While there are no plans 511 | to make any breaking changes to the API, it's possible that there may be some 512 | until it is mature. 513 | 514 | - Upon reading the value any of the delegated properties created by an 515 | `ArgParser`, the arguments used to construct that `ArgParser` will be 516 | parsed. This means it's important that you don't attempt to create delegates 517 | on an `ArgParser` after any of its existing delegated properties have been 518 | read. Attempting to do so will cause an `IllegalStateException`. It would be 519 | nice if Kotlin had facilities for doing some of the work of `ArgParser` at 520 | compile time rather than run time, but so far the run time errors seem to be 521 | reasonably easy to avoid. 522 | 523 | 524 | ## Configuring Your Build 525 | 526 | 527 | 528 | Kotlin-argparser binaries are hosted on Maven Central and also Bintray's 529 | JCenter. 530 | 531 | In Gradle, add something like this in your `build.gradle`: 532 | 533 | ```groovy 534 | // you probably already have this part 535 | buildscript { 536 | repositories { 537 | mavenCentral() // or jcenter() 538 | } 539 | } 540 | 541 | dependencies { 542 | compile "com.xenomachina:kotlin-argparser:$kotlin_argparser_version" 543 | } 544 | ``` 545 | 546 | In Maven add something like this to your `pom.xml`: 547 | 548 | ```xml 549 | 550 | com.xenomachina 551 | kotlin-argparser 552 | VERSION 553 | 554 | ``` 555 | 556 | Information on setting up other build systems, as well as the current version 557 | number, can be found on 558 | [MVN Repository's page for Kotlin-argparser](https://mvnrepository.com/artifact/com.xenomachina/kotlin-argparser/latest). 559 | 560 | ## Thanks 561 | 562 | Thanks to the creators of Python's 563 | [`argparse`](https://docs.python.org/3/library/argparse.html) module, which 564 | provided the initial inspiration for this library. 565 | 566 | Thanks also to the team behind [Kotlin](https://kotlinlang.org/). 567 | 568 | Finally, thanks to all of the people who have contributed 569 | [code](https://github.com/xenomachina/kotlin-argparser/graphs/contributors) 570 | and/or 571 | [issues](https://github.com/xenomachina/kotlin-argparser/issues). 572 | -------------------------------------------------------------------------------- /src/main/kotlin/com/xenomachina/argparser/ArgParser.kt: -------------------------------------------------------------------------------- 1 | // Copyright © 2016 Laurence Gonsalves 2 | // 3 | // This file is part of kotlin-argparser, a library which can be found at 4 | // http://github.com/xenomachina/kotlin-argparser 5 | // 6 | // This library is free software; you can redistribute it and/or modify it 7 | // under the terms of the GNU Lesser General Public License as published by the 8 | // Free Software Foundation; either version 2.1 of the License, or (at your 9 | // option) any later version. 10 | // 11 | // This library is distributed in the hope that it will be useful, but WITHOUT 12 | // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | // FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License 14 | // for more details. 15 | // 16 | // You should have received a copy of the GNU Lesser General Public License 17 | // along with this library; if not, see http://www.gnu.org/licenses/ 18 | 19 | package com.xenomachina.argparser 20 | 21 | import com.xenomachina.argparser.PosixNaming.identifierToArgName 22 | import com.xenomachina.argparser.PosixNaming.identifierToOptionName 23 | import com.xenomachina.argparser.PosixNaming.optionNameToArgName 24 | import com.xenomachina.argparser.PosixNaming.selectRepresentativeOptionName 25 | import com.xenomachina.common.Holder 26 | import com.xenomachina.common.orElse 27 | import java.util.LinkedHashSet 28 | import kotlin.reflect.KProperty 29 | 30 | /** 31 | * A command-line option/argument parser. 32 | * 33 | * @param args the command line arguments to parse 34 | * @param mode parsing mode, defaults to GNU-style parsing 35 | * @param helpFormatter if non-null, creates `--help` and `-h` options that trigger a [ShowHelpException] which will use 36 | * the supplied [HelpFormatter] to generate a help message. 37 | */ 38 | class ArgParser( 39 | args: Array, 40 | mode: Mode = Mode.GNU, 41 | helpFormatter: HelpFormatter? = DefaultHelpFormatter() 42 | ) { 43 | 44 | enum class Mode { 45 | /** For GNU-style option parsing, where options may appear after positional arguments. */ 46 | GNU, 47 | 48 | /** For POSIX-style option parsing, where options must appear before positional arguments. */ 49 | POSIX 50 | } 51 | 52 | /** 53 | * Creates a Delegate for a zero-argument option that returns true if and only the option is present in args. 54 | */ 55 | fun flagging(vararg names: String, help: String): Delegate = 56 | option( 57 | *names, 58 | help = help) { true }.default(false) 59 | 60 | /** 61 | * Creates a DelegateProvider for a zero-argument option that returns true if and only the option is present in 62 | * args. 63 | */ 64 | fun flagging(help: String) = 65 | DelegateProvider { identifier -> flagging(identifierToOptionName(identifier), help = help) } 66 | 67 | /** 68 | * Creates a Delegate for a zero-argument option that returns the count of how many times the option appears in 69 | * args. 70 | */ 71 | fun counting(vararg names: String, help: String): Delegate = 72 | option( 73 | *names, 74 | isRepeating = true, 75 | help = help) { value.orElse { 0 } + 1 }.default(0) 76 | 77 | /** 78 | * Creates a DelegateProvider for a zero-argument option that returns the count of how many times the option appears 79 | * in args. 80 | */ 81 | fun counting(help: String) = DelegateProvider { 82 | identifier -> counting(identifierToOptionName(identifier), help = help) 83 | } 84 | 85 | /** 86 | * Creates a Delegate for a single-argument option that stores and returns the option's (transformed) argument. 87 | */ 88 | fun storing( 89 | vararg names: String, 90 | help: String, 91 | argName: String? = null, 92 | transform: String.() -> T 93 | ): Delegate { 94 | val nonNullArgName = argName ?: optionNameToArgName(selectRepresentativeOptionName(names)) 95 | return option( 96 | *names, 97 | errorName = nonNullArgName, 98 | argNames = listOf(nonNullArgName), 99 | help = help) { transform(arguments.first()) } 100 | } 101 | 102 | /** 103 | * Creates a DelegateProvider for a single-argument option that stores and returns the option's (transformed) 104 | * argument. 105 | */ 106 | fun storing( 107 | help: String, 108 | argName: String? = null, 109 | transform: String.() -> T 110 | ) = DelegateProvider { identifier -> 111 | storing(identifierToOptionName(identifier), help = help, argName = argName, transform = transform) 112 | } 113 | 114 | /** 115 | * Creates a Delegate for a single-argument option that stores and returns the option's argument. 116 | */ 117 | fun storing(vararg names: String, help: String, argName: String? = null): Delegate = 118 | storing(*names, help = help, argName = argName) { this } 119 | 120 | /** 121 | * Creates a DelegateProvider for a single-argument option that stores and returns the option's argument. 122 | */ 123 | fun storing(help: String, argName: String? = null) = 124 | DelegateProvider { identifier -> 125 | storing(identifierToOptionName(identifier), help = help, argName = argName) } 126 | 127 | /** 128 | * Creates a Delegate for a single-argument option that adds the option's (transformed) argument to a 129 | * MutableCollection each time the option appears in args, and returns said MutableCollection. 130 | */ 131 | fun > adding( 132 | vararg names: String, 133 | help: String, 134 | argName: String? = null, 135 | initialValue: T, 136 | transform: String.() -> E 137 | ): Delegate { 138 | val nonNullArgName = argName ?: optionNameToArgName(selectRepresentativeOptionName(names)) 139 | return option( 140 | *names, 141 | help = help, 142 | argNames = listOf(nonNullArgName), 143 | isRepeating = true) { 144 | val result = value.orElse { initialValue } 145 | result.add(transform(arguments.first())) 146 | result 147 | }.default(initialValue) 148 | } 149 | 150 | /** 151 | * Creates a DelegateProvider for a single-argument option that adds the option's (transformed) argument to a 152 | * MutableCollection each time the option appears in args, and returns said MutableCollection. 153 | */ 154 | fun > adding( 155 | help: String, 156 | argName: String? = null, 157 | initialValue: T, 158 | transform: String.() -> E 159 | ) = DelegateProvider { identifier -> 160 | adding( 161 | identifierToOptionName(identifier), 162 | help = help, 163 | argName = argName, 164 | initialValue = initialValue, 165 | transform = transform) 166 | } 167 | 168 | /** 169 | * Creates a Delegate for a single-argument option that adds the option's (transformed) argument to a 170 | * MutableList each time the option appears in args, and returns said MutableCollection. 171 | */ 172 | fun adding( 173 | vararg names: String, 174 | help: String, 175 | argName: String? = null, 176 | transform: String.() -> T 177 | ) = adding(*names, help = help, argName = argName, initialValue = mutableListOf(), transform = transform) 178 | 179 | /** 180 | * Creates a DelegateProvider for a single-argument option that adds the option's (transformed) argument to a 181 | * MutableList each time the option appears in args, and returns said MutableCollection. 182 | */ 183 | fun adding( 184 | help: String, 185 | argName: String? = null, 186 | transform: String.() -> T 187 | ) = DelegateProvider { identifier -> 188 | adding(identifierToOptionName(identifier), help = help, argName = argName, transform = transform) } 189 | 190 | /** 191 | * Creates a Delegate for a single-argument option that adds the option's argument to a MutableList each time the 192 | * option appears in args, and returns said MutableCollection. 193 | */ 194 | fun adding(vararg names: String, help: String, argName: String? = null): Delegate> = 195 | adding(*names, help = help, argName = argName) { this } 196 | 197 | /** 198 | * Creates a DelegateProvider for a single-argument option that adds the option's argument to a MutableList each 199 | * time the option appears in args, and returns said MutableCollection. 200 | */ 201 | fun adding(help: String) = DelegateProvider { identifier -> 202 | adding(identifierToOptionName(identifier), help = help) } 203 | 204 | /** 205 | * Creates a Delegate for a zero-argument option that maps from the option's name as it appears in args to one of a 206 | * fixed set of values. 207 | */ 208 | fun mapping(vararg pairs: Pair, help: String): Delegate = 209 | mapping(mapOf(*pairs), help = help) 210 | 211 | /** 212 | * Creates a Delegate for a zero-argument option that maps from the option's name as it appears in args to one of a 213 | * fixed set of values. 214 | */ 215 | fun mapping(map: Map, help: String): Delegate { 216 | val names = map.keys.toTypedArray() 217 | return option(*names, 218 | errorName = map.keys.joinToString("|"), 219 | help = help) { 220 | // This cannot be null, because the optionName was added to the map 221 | // at the same time it was registered with the ArgParser. 222 | map[optionName]!! 223 | } 224 | } 225 | 226 | /** 227 | * Creates a Delegate for an option with the specified names. 228 | * @param names names of options, with leading "-" or "--" 229 | * @param errorName name to use when talking about this option in error messages, or null to base it upon the 230 | * option names 231 | * @param help the help text for this option 232 | * @param argNames names of this option's arguments 233 | * @param isRepeating whether or not it makes sense to repeat this option -- usually used for options where 234 | * specifying the option more than once yields a value than cannot be expressed by specifying the option only once 235 | * @param handler a function that computes the value of this option from an [OptionInvocation] 236 | */ 237 | fun option( 238 | // TODO: add optionalArg: Boolean 239 | vararg names: String, 240 | help: String, 241 | errorName: String? = null, 242 | argNames: List = emptyList(), 243 | isRepeating: Boolean = false, 244 | handler: OptionInvocation.() -> T 245 | ): Delegate { 246 | val delegate = OptionDelegate( 247 | parser = this, 248 | errorName = errorName ?: optionNameToArgName(selectRepresentativeOptionName(names)), 249 | help = help, 250 | optionNames = listOf(*names), 251 | argNames = argNames.toList(), 252 | isRepeating = isRepeating, 253 | handler = handler) 254 | return delegate 255 | } 256 | 257 | /** 258 | * Creates a Delegate for a single positional argument which returns the argument's value. 259 | */ 260 | fun positional(name: String, help: String) = positional(name, help = help) { this } 261 | 262 | /** 263 | * Creates a DelegateProvider for a single positional argument which returns the argument's value. 264 | */ 265 | fun positional(help: String) = 266 | DelegateProvider { identifier -> positional(identifierToArgName(identifier), help = help) } 267 | 268 | /** 269 | * Creates a Delegate for a single positional argument which returns the argument's transformed value. 270 | */ 271 | fun positional( 272 | name: String, 273 | help: String, 274 | transform: String.() -> T 275 | ): Delegate { 276 | return WrappingDelegate( 277 | positionalList(name, help = help, sizeRange = 1..1, transform = transform) 278 | ) { it[0] } 279 | } 280 | 281 | /** 282 | * Creates a DelegateProvider for a single positional argument which returns the argument's transformed value. 283 | */ 284 | fun positional( 285 | help: String, 286 | transform: String.() -> T 287 | ) = DelegateProvider { identifier -> 288 | positional(identifierToArgName(identifier), help = help, transform = transform) 289 | } 290 | 291 | /** 292 | * Creates a Delegate for a sequence of positional arguments which returns a List containing the arguments. 293 | */ 294 | fun positionalList( 295 | name: String, 296 | help: String, 297 | sizeRange: IntRange = 1..Int.MAX_VALUE 298 | ) = positionalList(name, help = help, sizeRange = sizeRange) { this } 299 | 300 | /** 301 | * Creates a DelegateProvider for a sequence of positional arguments which returns a List containing the arguments. 302 | */ 303 | fun positionalList( 304 | help: String, 305 | sizeRange: IntRange = 1..Int.MAX_VALUE 306 | ) = DelegateProvider { identifier -> 307 | positionalList(identifierToArgName(identifier), help = help, sizeRange = sizeRange) 308 | } 309 | 310 | /** 311 | * Creates a Delegate for a sequence of positional arguments which returns a List containing the transformed 312 | * arguments. 313 | */ 314 | fun positionalList( 315 | name: String, 316 | help: String, 317 | sizeRange: IntRange = 1..Int.MAX_VALUE, 318 | transform: String.() -> T 319 | ): Delegate> { 320 | sizeRange.run { 321 | require(step == 1) { "step must be 1, not $step" } 322 | require(first <= last) { "backwards ranges are not allowed: $first > $last" } 323 | require(first >= 0) { "sizeRange cannot start at $first, must be non-negative" } 324 | 325 | // Technically, last == 0 is ok but not especially useful, so we 326 | // disallow it as it's probably unintentional. 327 | require(last > 0) { "sizeRange only allows $last arguments, must allow at least 1" } 328 | } 329 | 330 | return PositionalDelegate(this, name, sizeRange, help = help, transform = transform) 331 | } 332 | 333 | /** 334 | * Creates a DelegateProvider for a sequence of positional arguments which returns a List containing the transformed 335 | * arguments. 336 | */ 337 | fun positionalList( 338 | help: String, 339 | sizeRange: IntRange = 1..Int.MAX_VALUE, 340 | transform: String.() -> T 341 | ) = DelegateProvider { identifier -> positionalList(identifierToArgName(identifier), help, sizeRange, transform) } 342 | 343 | abstract class Delegate internal constructor() { 344 | /** The value associated with this delegate */ 345 | abstract val value: T 346 | 347 | /** The name used to refer to this delegate's value in error messages */ 348 | abstract val errorName: String 349 | 350 | /** The user-visible help text for this delegate */ 351 | abstract val help: String 352 | 353 | /** Add validation logic. Validator should throw a [SystemExitException] on failure. */ 354 | abstract fun addValidator(validator: Delegate.() -> Unit): Delegate 355 | 356 | /** Allows this object to act as a property delegate */ 357 | operator fun getValue(thisRef: Any?, property: KProperty<*>): T = value 358 | 359 | /** 360 | * Allows this object to act as a property delegate provider. 361 | * 362 | * It provides itself, and also registers itself with the [ArgParser] at that time. 363 | */ 364 | operator fun provideDelegate(thisRef: Any?, prop: KProperty<*>): ArgParser.Delegate { 365 | registerRoot() 366 | return this 367 | } 368 | 369 | internal abstract val parser: ArgParser 370 | 371 | /** 372 | * Indicates whether or not a value has been set for this delegate 373 | */ 374 | internal abstract val hasValue: Boolean 375 | 376 | internal fun checkHasValue() { 377 | if (!hasValue) throw MissingValueException(errorName) 378 | } 379 | 380 | internal abstract fun validate() 381 | 382 | internal abstract fun toHelpFormatterValue(): HelpFormatter.Value 383 | 384 | internal fun registerRoot() { 385 | parser.checkNotParsed() 386 | parser.delegates.add(this) 387 | registerLeaf(this) 388 | } 389 | 390 | internal abstract fun registerLeaf(root: Delegate<*>) 391 | 392 | internal abstract val hasValidators: Boolean 393 | } 394 | 395 | /** 396 | * Provides a [Delegate] when given a name. This makes it possible to infer 397 | * a name for the `Delegate` based on the name it is bound to, rather than 398 | * specifying a name explicitly. 399 | */ 400 | class DelegateProvider( 401 | private val default: (() -> T)? = null, 402 | internal val ctor: (identifier: String) -> Delegate 403 | ) { 404 | operator fun provideDelegate(thisRef: Any?, prop: KProperty<*>): Delegate { 405 | val delegate = ctor(prop.name) 406 | return (if (default == null) delegate 407 | else delegate.default(default)).provideDelegate(thisRef, prop) 408 | } 409 | } 410 | 411 | /** 412 | * @property value a Holder containing the current value associated with this option, or null if unset 413 | * @property optionName the name used for this option in this invocation 414 | * @property arguments the arguments supplied for this option 415 | */ 416 | data class OptionInvocation internal constructor( 417 | // Internal constructor so future versions can add properties 418 | // without breaking compatibility. 419 | val value: Holder?, 420 | val optionName: String, 421 | val arguments: List 422 | ) 423 | 424 | private val shortOptionDelegates = mutableMapOf>() 425 | private val longOptionDelegates = mutableMapOf>() 426 | private val positionalDelegates = mutableListOf, Boolean>>() 427 | private val delegates = LinkedHashSet>() 428 | 429 | internal fun registerOption(name: String, delegate: OptionDelegate<*>) { 430 | if (name.startsWith("--")) { 431 | require(name.length > 2) { "long option '$name' must have at least one character after hyphen" } 432 | require(name !in longOptionDelegates) { "long option '$name' already in use" } 433 | longOptionDelegates.put(name, delegate) 434 | } else if (name.startsWith("-")) { 435 | require(name.length == 2) { "short option '$name' can only have one character after hyphen" } 436 | val key = name.get(1) 437 | require(key !in shortOptionDelegates) { "short option '$name' already in use" } 438 | shortOptionDelegates.put(key, delegate) 439 | } else { 440 | throw IllegalArgumentException("illegal option name '$name' -- must start with '-' or '--'") 441 | } 442 | } 443 | 444 | internal fun registerPositional(delegate: PositionalDelegate<*>, hasDefault: Boolean) { 445 | positionalDelegates.add(delegate to hasDefault) 446 | } 447 | 448 | private var inValidation = false 449 | private var finished = false 450 | 451 | /** 452 | * Ensures that arguments have been parsed and validated. 453 | * 454 | * @throws SystemExitException if parsing or validation failed. 455 | */ 456 | fun force() { 457 | if (!inParse) { 458 | if (!finished) { 459 | parseOptions 460 | if (!inValidation) { 461 | inValidation = true 462 | try { 463 | for (delegate in delegates) delegate.checkHasValue() 464 | for (delegate in delegates) delegate.validate() 465 | } finally { 466 | inValidation = false 467 | } 468 | } 469 | } 470 | } 471 | } 472 | 473 | /** 474 | * Provides an instance of T, where all arguments have already been parsed and validated 475 | */ 476 | fun parseInto(constructor: (ArgParser) -> T): T { 477 | if (builtinDelegateCount != delegates.size) { 478 | throw IllegalStateException("You can only use the parseInto function with a clean ArgParser instance") 479 | } 480 | val provided = constructor(this) 481 | force() 482 | return provided 483 | } 484 | 485 | private var inParse = false 486 | 487 | internal fun checkNotParsed() { 488 | if (inParse || finished) throw IllegalStateException("arguments have already been parsed") 489 | } 490 | 491 | private val parseOptions by lazy { 492 | val positionalArguments = mutableListOf() 493 | inParse = true 494 | try { 495 | var i = 0 496 | optionLoop@ while (i < args.size) { 497 | val arg = args[i] 498 | i += when { 499 | arg == "--" -> { 500 | i++ 501 | break@optionLoop 502 | } 503 | arg.startsWith("--") -> 504 | parseLongOpt(i, args) 505 | arg.startsWith("-") -> 506 | parseShortOpts(i, args) 507 | else -> { 508 | positionalArguments.add(arg) 509 | when (mode) { 510 | Mode.GNU -> 1 511 | Mode.POSIX -> { 512 | i++ 513 | break@optionLoop 514 | } 515 | } 516 | } 517 | } 518 | } 519 | 520 | // Collect remaining arguments as positional-only arguments 521 | positionalArguments.addAll(args.slice(i..args.size - 1)) 522 | 523 | parsePositionalArguments(positionalArguments) 524 | finished = true 525 | } finally { 526 | inParse = false 527 | } 528 | } 529 | 530 | private fun parsePositionalArguments(args: List) { 531 | var lastValueName: String? = null 532 | var index = 0 533 | var remaining = args.size 534 | var extra = (remaining - positionalDelegates.map { 535 | if (it.second) 0 else it.first.sizeRange.first 536 | }.sum()).coerceAtLeast(0) 537 | for ((delegate, hasDefault) in positionalDelegates) { 538 | val minSize = if (hasDefault) 0 else delegate.sizeRange.first 539 | val sizeRange = delegate.sizeRange 540 | val chunkSize = (minSize + extra).coerceAtMost(sizeRange.endInclusive) 541 | if (chunkSize > remaining) { 542 | throw MissingRequiredPositionalArgumentException(delegate.errorName) 543 | } 544 | if (chunkSize != 0 || !hasDefault) { 545 | delegate.parseArguments(args.subList(index, index + chunkSize)) 546 | } 547 | lastValueName = delegate.errorName 548 | index += chunkSize 549 | remaining -= chunkSize 550 | extra -= chunkSize - minSize 551 | } 552 | if (remaining > 0) { 553 | throw UnexpectedPositionalArgumentException(lastValueName) 554 | } 555 | } 556 | 557 | /** 558 | * @param index index into args, starting at a long option, eg: "--verbose" 559 | * @param args array of command-line arguments 560 | * @return number of arguments that have been processed 561 | */ 562 | private fun parseLongOpt(index: Int, args: Array): Int { 563 | val name: String 564 | val firstArg: String? 565 | val m = NAME_EQUALS_VALUE_REGEX.matchEntire(args[index]) 566 | if (m == null) { 567 | name = args[index] 568 | firstArg = null 569 | } else { 570 | // if NAME_EQUALS_VALUE_REGEX then there must be groups 1 and 2 571 | name = m.groups[1]!!.value 572 | firstArg = m.groups[2]!!.value 573 | } 574 | val delegate = longOptionDelegates.get(name) 575 | if (delegate == null) { 576 | throw UnrecognizedOptionException(name) 577 | } else { 578 | var consumedArgs = delegate.parseOption(name, firstArg, index + 1, args) 579 | if (firstArg != null) { 580 | if (consumedArgs < 1) throw UnexpectedOptionArgumentException(name) 581 | consumedArgs -= 1 582 | } 583 | return 1 + consumedArgs 584 | } 585 | } 586 | 587 | /** 588 | * @param index index into args, starting at a set of short options, eg: "-abXv" 589 | * @param args array of command-line arguments 590 | * @return number of arguments that have been processed 591 | */ 592 | private fun parseShortOpts(index: Int, args: Array): Int { 593 | val opts = args[index] 594 | var optIndex = 1 595 | while (optIndex < opts.length) { 596 | val optKey = opts[optIndex] 597 | val optName = "-$optKey" 598 | optIndex++ // optIndex now points just after optKey 599 | 600 | val delegate = shortOptionDelegates.get(optKey) 601 | if (delegate == null) { 602 | throw UnrecognizedOptionException(optName) 603 | } else { 604 | val firstArg = if (optIndex >= opts.length) null else opts.substring(optIndex) 605 | val consumed = delegate.parseOption(optName, firstArg, index + 1, args) 606 | if (consumed > 0) { 607 | return consumed + (if (firstArg == null) 1 else 0) 608 | } 609 | } 610 | } 611 | return 1 612 | } 613 | 614 | init { 615 | if (helpFormatter != null) { 616 | option("-h", "--help", 617 | errorName = "HELP", // This should never be used, but we need to say something 618 | help = "show this help message and exit") { 619 | throw ShowHelpException(helpFormatter, delegates.toList()) 620 | }.default(Unit).registerRoot() 621 | } 622 | } 623 | 624 | private val builtinDelegateCount = delegates.size 625 | } 626 | 627 | private val NAME_EQUALS_VALUE_REGEX = Regex("^([^=]+)=(.*)$") 628 | internal val LEADING_HYPHENS_REGEX = Regex("^-{1,2}") 629 | 630 | private const val OPTION_CHAR_CLASS = "[a-zA-Z0-9]" 631 | internal val OPTION_NAME_RE = Regex("^(-$OPTION_CHAR_CLASS)|(--$OPTION_CHAR_CLASS+([-_\\.]$OPTION_CHAR_CLASS+)*)$") 632 | 633 | private const val ARG_INITIAL_CHAR_CLASS = "[A-Z]" 634 | private const val ARG_CHAR_CLASS = "[A-Z0-9]" 635 | internal val ARG_NAME_RE = Regex("^$ARG_INITIAL_CHAR_CLASS+([-_\\.]$ARG_CHAR_CLASS+)*$") 636 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 2.1, February 1999 3 | 4 | Copyright (C) 1991, 1999 Free Software Foundation, Inc. 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | [This is the first released version of the Lesser GPL. It also counts 10 | as the successor of the GNU Library Public License, version 2, hence 11 | the version number 2.1.] 12 | 13 | Preamble 14 | 15 | The licenses for most software are designed to take away your 16 | freedom to share and change it. By contrast, the GNU General Public 17 | Licenses are intended to guarantee your freedom to share and change 18 | free software--to make sure the software is free for all its users. 19 | 20 | This license, the Lesser General Public License, applies to some 21 | specially designated software packages--typically libraries--of the 22 | Free Software Foundation and other authors who decide to use it. You 23 | can use it too, but we suggest you first think carefully about whether 24 | this license or the ordinary General Public License is the better 25 | strategy to use in any particular case, based on the explanations below. 26 | 27 | When we speak of free software, we are referring to freedom of use, 28 | not price. Our General Public Licenses are designed to make sure that 29 | you have the freedom to distribute copies of free software (and charge 30 | for this service if you wish); that you receive source code or can get 31 | it if you want it; that you can change the software and use pieces of 32 | it in new free programs; and that you are informed that you can do 33 | these things. 34 | 35 | To protect your rights, we need to make restrictions that forbid 36 | distributors to deny you these rights or to ask you to surrender these 37 | rights. These restrictions translate to certain responsibilities for 38 | you if you distribute copies of the library or if you modify it. 39 | 40 | For example, if you distribute copies of the library, whether gratis 41 | or for a fee, you must give the recipients all the rights that we gave 42 | you. You must make sure that they, too, receive or can get the source 43 | code. If you link other code with the library, you must provide 44 | complete object files to the recipients, so that they can relink them 45 | with the library after making changes to the library and recompiling 46 | it. And you must show them these terms so they know their rights. 47 | 48 | We protect your rights with a two-step method: (1) we copyright the 49 | library, and (2) we offer you this license, which gives you legal 50 | permission to copy, distribute and/or modify the library. 51 | 52 | To protect each distributor, we want to make it very clear that 53 | there is no warranty for the free library. Also, if the library is 54 | modified by someone else and passed on, the recipients should know 55 | that what they have is not the original version, so that the original 56 | author's reputation will not be affected by problems that might be 57 | introduced by others. 58 | 59 | Finally, software patents pose a constant threat to the existence of 60 | any free program. We wish to make sure that a company cannot 61 | effectively restrict the users of a free program by obtaining a 62 | restrictive license from a patent holder. Therefore, we insist that 63 | any patent license obtained for a version of the library must be 64 | consistent with the full freedom of use specified in this license. 65 | 66 | Most GNU software, including some libraries, is covered by the 67 | ordinary GNU General Public License. This license, the GNU Lesser 68 | General Public License, applies to certain designated libraries, and 69 | is quite different from the ordinary General Public License. We use 70 | this license for certain libraries in order to permit linking those 71 | libraries into non-free programs. 72 | 73 | When a program is linked with a library, whether statically or using 74 | a shared library, the combination of the two is legally speaking a 75 | combined work, a derivative of the original library. The ordinary 76 | General Public License therefore permits such linking only if the 77 | entire combination fits its criteria of freedom. The Lesser General 78 | Public License permits more lax criteria for linking other code with 79 | the library. 80 | 81 | We call this license the "Lesser" General Public License because it 82 | does Less to protect the user's freedom than the ordinary General 83 | Public License. It also provides other free software developers Less 84 | of an advantage over competing non-free programs. These disadvantages 85 | are the reason we use the ordinary General Public License for many 86 | libraries. However, the Lesser license provides advantages in certain 87 | special circumstances. 88 | 89 | For example, on rare occasions, there may be a special need to 90 | encourage the widest possible use of a certain library, so that it becomes 91 | a de-facto standard. To achieve this, non-free programs must be 92 | allowed to use the library. A more frequent case is that a free 93 | library does the same job as widely used non-free libraries. In this 94 | case, there is little to gain by limiting the free library to free 95 | software only, so we use the Lesser General Public License. 96 | 97 | In other cases, permission to use a particular library in non-free 98 | programs enables a greater number of people to use a large body of 99 | free software. For example, permission to use the GNU C Library in 100 | non-free programs enables many more people to use the whole GNU 101 | operating system, as well as its variant, the GNU/Linux operating 102 | system. 103 | 104 | Although the Lesser General Public License is Less protective of the 105 | users' freedom, it does ensure that the user of a program that is 106 | linked with the Library has the freedom and the wherewithal to run 107 | that program using a modified version of the Library. 108 | 109 | The precise terms and conditions for copying, distribution and 110 | modification follow. Pay close attention to the difference between a 111 | "work based on the library" and a "work that uses the library". The 112 | former contains code derived from the library, whereas the latter must 113 | be combined with the library in order to run. 114 | 115 | GNU LESSER GENERAL PUBLIC LICENSE 116 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 117 | 118 | 0. This License Agreement applies to any software library or other 119 | program which contains a notice placed by the copyright holder or 120 | other authorized party saying it may be distributed under the terms of 121 | this Lesser General Public License (also called "this License"). 122 | Each licensee is addressed as "you". 123 | 124 | A "library" means a collection of software functions and/or data 125 | prepared so as to be conveniently linked with application programs 126 | (which use some of those functions and data) to form executables. 127 | 128 | The "Library", below, refers to any such software library or work 129 | which has been distributed under these terms. A "work based on the 130 | Library" means either the Library or any derivative work under 131 | copyright law: that is to say, a work containing the Library or a 132 | portion of it, either verbatim or with modifications and/or translated 133 | straightforwardly into another language. (Hereinafter, translation is 134 | included without limitation in the term "modification".) 135 | 136 | "Source code" for a work means the preferred form of the work for 137 | making modifications to it. For a library, complete source code means 138 | all the source code for all modules it contains, plus any associated 139 | interface definition files, plus the scripts used to control compilation 140 | and installation of the library. 141 | 142 | Activities other than copying, distribution and modification are not 143 | covered by this License; they are outside its scope. The act of 144 | running a program using the Library is not restricted, and output from 145 | such a program is covered only if its contents constitute a work based 146 | on the Library (independent of the use of the Library in a tool for 147 | writing it). Whether that is true depends on what the Library does 148 | and what the program that uses the Library does. 149 | 150 | 1. You may copy and distribute verbatim copies of the Library's 151 | complete source code as you receive it, in any medium, provided that 152 | you conspicuously and appropriately publish on each copy an 153 | appropriate copyright notice and disclaimer of warranty; keep intact 154 | all the notices that refer to this License and to the absence of any 155 | warranty; and distribute a copy of this License along with the 156 | Library. 157 | 158 | You may charge a fee for the physical act of transferring a copy, 159 | and you may at your option offer warranty protection in exchange for a 160 | fee. 161 | 162 | 2. You may modify your copy or copies of the Library or any portion 163 | of it, thus forming a work based on the Library, and copy and 164 | distribute such modifications or work under the terms of Section 1 165 | above, provided that you also meet all of these conditions: 166 | 167 | a) The modified work must itself be a software library. 168 | 169 | b) You must cause the files modified to carry prominent notices 170 | stating that you changed the files and the date of any change. 171 | 172 | c) You must cause the whole of the work to be licensed at no 173 | charge to all third parties under the terms of this License. 174 | 175 | d) If a facility in the modified Library refers to a function or a 176 | table of data to be supplied by an application program that uses 177 | the facility, other than as an argument passed when the facility 178 | is invoked, then you must make a good faith effort to ensure that, 179 | in the event an application does not supply such function or 180 | table, the facility still operates, and performs whatever part of 181 | its purpose remains meaningful. 182 | 183 | (For example, a function in a library to compute square roots has 184 | a purpose that is entirely well-defined independent of the 185 | application. Therefore, Subsection 2d requires that any 186 | application-supplied function or table used by this function must 187 | be optional: if the application does not supply it, the square 188 | root function must still compute square roots.) 189 | 190 | These requirements apply to the modified work as a whole. If 191 | identifiable sections of that work are not derived from the Library, 192 | and can be reasonably considered independent and separate works in 193 | themselves, then this License, and its terms, do not apply to those 194 | sections when you distribute them as separate works. But when you 195 | distribute the same sections as part of a whole which is a work based 196 | on the Library, the distribution of the whole must be on the terms of 197 | this License, whose permissions for other licensees extend to the 198 | entire whole, and thus to each and every part regardless of who wrote 199 | it. 200 | 201 | Thus, it is not the intent of this section to claim rights or contest 202 | your rights to work written entirely by you; rather, the intent is to 203 | exercise the right to control the distribution of derivative or 204 | collective works based on the Library. 205 | 206 | In addition, mere aggregation of another work not based on the Library 207 | with the Library (or with a work based on the Library) on a volume of 208 | a storage or distribution medium does not bring the other work under 209 | the scope of this License. 210 | 211 | 3. You may opt to apply the terms of the ordinary GNU General Public 212 | License instead of this License to a given copy of the Library. To do 213 | this, you must alter all the notices that refer to this License, so 214 | that they refer to the ordinary GNU General Public License, version 2, 215 | instead of to this License. (If a newer version than version 2 of the 216 | ordinary GNU General Public License has appeared, then you can specify 217 | that version instead if you wish.) Do not make any other change in 218 | these notices. 219 | 220 | Once this change is made in a given copy, it is irreversible for 221 | that copy, so the ordinary GNU General Public License applies to all 222 | subsequent copies and derivative works made from that copy. 223 | 224 | This option is useful when you wish to copy part of the code of 225 | the Library into a program that is not a library. 226 | 227 | 4. You may copy and distribute the Library (or a portion or 228 | derivative of it, under Section 2) in object code or executable form 229 | under the terms of Sections 1 and 2 above provided that you accompany 230 | it with the complete corresponding machine-readable source code, which 231 | must be distributed under the terms of Sections 1 and 2 above on a 232 | medium customarily used for software interchange. 233 | 234 | If distribution of object code is made by offering access to copy 235 | from a designated place, then offering equivalent access to copy the 236 | source code from the same place satisfies the requirement to 237 | distribute the source code, even though third parties are not 238 | compelled to copy the source along with the object code. 239 | 240 | 5. A program that contains no derivative of any portion of the 241 | Library, but is designed to work with the Library by being compiled or 242 | linked with it, is called a "work that uses the Library". Such a 243 | work, in isolation, is not a derivative work of the Library, and 244 | therefore falls outside the scope of this License. 245 | 246 | However, linking a "work that uses the Library" with the Library 247 | creates an executable that is a derivative of the Library (because it 248 | contains portions of the Library), rather than a "work that uses the 249 | library". The executable is therefore covered by this License. 250 | Section 6 states terms for distribution of such executables. 251 | 252 | When a "work that uses the Library" uses material from a header file 253 | that is part of the Library, the object code for the work may be a 254 | derivative work of the Library even though the source code is not. 255 | Whether this is true is especially significant if the work can be 256 | linked without the Library, or if the work is itself a library. The 257 | threshold for this to be true is not precisely defined by law. 258 | 259 | If such an object file uses only numerical parameters, data 260 | structure layouts and accessors, and small macros and small inline 261 | functions (ten lines or less in length), then the use of the object 262 | file is unrestricted, regardless of whether it is legally a derivative 263 | work. (Executables containing this object code plus portions of the 264 | Library will still fall under Section 6.) 265 | 266 | Otherwise, if the work is a derivative of the Library, you may 267 | distribute the object code for the work under the terms of Section 6. 268 | Any executables containing that work also fall under Section 6, 269 | whether or not they are linked directly with the Library itself. 270 | 271 | 6. As an exception to the Sections above, you may also combine or 272 | link a "work that uses the Library" with the Library to produce a 273 | work containing portions of the Library, and distribute that work 274 | under terms of your choice, provided that the terms permit 275 | modification of the work for the customer's own use and reverse 276 | engineering for debugging such modifications. 277 | 278 | You must give prominent notice with each copy of the work that the 279 | Library is used in it and that the Library and its use are covered by 280 | this License. You must supply a copy of this License. If the work 281 | during execution displays copyright notices, you must include the 282 | copyright notice for the Library among them, as well as a reference 283 | directing the user to the copy of this License. Also, you must do one 284 | of these things: 285 | 286 | a) Accompany the work with the complete corresponding 287 | machine-readable source code for the Library including whatever 288 | changes were used in the work (which must be distributed under 289 | Sections 1 and 2 above); and, if the work is an executable linked 290 | with the Library, with the complete machine-readable "work that 291 | uses the Library", as object code and/or source code, so that the 292 | user can modify the Library and then relink to produce a modified 293 | executable containing the modified Library. (It is understood 294 | that the user who changes the contents of definitions files in the 295 | Library will not necessarily be able to recompile the application 296 | to use the modified definitions.) 297 | 298 | b) Use a suitable shared library mechanism for linking with the 299 | Library. A suitable mechanism is one that (1) uses at run time a 300 | copy of the library already present on the user's computer system, 301 | rather than copying library functions into the executable, and (2) 302 | will operate properly with a modified version of the library, if 303 | the user installs one, as long as the modified version is 304 | interface-compatible with the version that the work was made with. 305 | 306 | c) Accompany the work with a written offer, valid for at 307 | least three years, to give the same user the materials 308 | specified in Subsection 6a, above, for a charge no more 309 | than the cost of performing this distribution. 310 | 311 | d) If distribution of the work is made by offering access to copy 312 | from a designated place, offer equivalent access to copy the above 313 | specified materials from the same place. 314 | 315 | e) Verify that the user has already received a copy of these 316 | materials or that you have already sent this user a copy. 317 | 318 | For an executable, the required form of the "work that uses the 319 | Library" must include any data and utility programs needed for 320 | reproducing the executable from it. However, as a special exception, 321 | the materials to be distributed need not include anything that is 322 | normally distributed (in either source or binary form) with the major 323 | components (compiler, kernel, and so on) of the operating system on 324 | which the executable runs, unless that component itself accompanies 325 | the executable. 326 | 327 | It may happen that this requirement contradicts the license 328 | restrictions of other proprietary libraries that do not normally 329 | accompany the operating system. Such a contradiction means you cannot 330 | use both them and the Library together in an executable that you 331 | distribute. 332 | 333 | 7. You may place library facilities that are a work based on the 334 | Library side-by-side in a single library together with other library 335 | facilities not covered by this License, and distribute such a combined 336 | library, provided that the separate distribution of the work based on 337 | the Library and of the other library facilities is otherwise 338 | permitted, and provided that you do these two things: 339 | 340 | a) Accompany the combined library with a copy of the same work 341 | based on the Library, uncombined with any other library 342 | facilities. This must be distributed under the terms of the 343 | Sections above. 344 | 345 | b) Give prominent notice with the combined library of the fact 346 | that part of it is a work based on the Library, and explaining 347 | where to find the accompanying uncombined form of the same work. 348 | 349 | 8. You may not copy, modify, sublicense, link with, or distribute 350 | the Library except as expressly provided under this License. Any 351 | attempt otherwise to copy, modify, sublicense, link with, or 352 | distribute the Library is void, and will automatically terminate your 353 | rights under this License. However, parties who have received copies, 354 | or rights, from you under this License will not have their licenses 355 | terminated so long as such parties remain in full compliance. 356 | 357 | 9. You are not required to accept this License, since you have not 358 | signed it. However, nothing else grants you permission to modify or 359 | distribute the Library or its derivative works. These actions are 360 | prohibited by law if you do not accept this License. Therefore, by 361 | modifying or distributing the Library (or any work based on the 362 | Library), you indicate your acceptance of this License to do so, and 363 | all its terms and conditions for copying, distributing or modifying 364 | the Library or works based on it. 365 | 366 | 10. Each time you redistribute the Library (or any work based on the 367 | Library), the recipient automatically receives a license from the 368 | original licensor to copy, distribute, link with or modify the Library 369 | subject to these terms and conditions. You may not impose any further 370 | restrictions on the recipients' exercise of the rights granted herein. 371 | You are not responsible for enforcing compliance by third parties with 372 | this License. 373 | 374 | 11. If, as a consequence of a court judgment or allegation of patent 375 | infringement or for any other reason (not limited to patent issues), 376 | conditions are imposed on you (whether by court order, agreement or 377 | otherwise) that contradict the conditions of this License, they do not 378 | excuse you from the conditions of this License. If you cannot 379 | distribute so as to satisfy simultaneously your obligations under this 380 | License and any other pertinent obligations, then as a consequence you 381 | may not distribute the Library at all. For example, if a patent 382 | license would not permit royalty-free redistribution of the Library by 383 | all those who receive copies directly or indirectly through you, then 384 | the only way you could satisfy both it and this License would be to 385 | refrain entirely from distribution of the Library. 386 | 387 | If any portion of this section is held invalid or unenforceable under any 388 | particular circumstance, the balance of the section is intended to apply, 389 | and the section as a whole is intended to apply in other circumstances. 390 | 391 | It is not the purpose of this section to induce you to infringe any 392 | patents or other property right claims or to contest validity of any 393 | such claims; this section has the sole purpose of protecting the 394 | integrity of the free software distribution system which is 395 | implemented by public license practices. Many people have made 396 | generous contributions to the wide range of software distributed 397 | through that system in reliance on consistent application of that 398 | system; it is up to the author/donor to decide if he or she is willing 399 | to distribute software through any other system and a licensee cannot 400 | impose that choice. 401 | 402 | This section is intended to make thoroughly clear what is believed to 403 | be a consequence of the rest of this License. 404 | 405 | 12. If the distribution and/or use of the Library is restricted in 406 | certain countries either by patents or by copyrighted interfaces, the 407 | original copyright holder who places the Library under this License may add 408 | an explicit geographical distribution limitation excluding those countries, 409 | so that distribution is permitted only in or among countries not thus 410 | excluded. In such case, this License incorporates the limitation as if 411 | written in the body of this License. 412 | 413 | 13. The Free Software Foundation may publish revised and/or new 414 | versions of the Lesser General Public License from time to time. 415 | Such new versions will be similar in spirit to the present version, 416 | but may differ in detail to address new problems or concerns. 417 | 418 | Each version is given a distinguishing version number. If the Library 419 | specifies a version number of this License which applies to it and 420 | "any later version", you have the option of following the terms and 421 | conditions either of that version or of any later version published by 422 | the Free Software Foundation. If the Library does not specify a 423 | license version number, you may choose any version ever published by 424 | the Free Software Foundation. 425 | 426 | 14. If you wish to incorporate parts of the Library into other free 427 | programs whose distribution conditions are incompatible with these, 428 | write to the author to ask for permission. For software which is 429 | copyrighted by the Free Software Foundation, write to the Free 430 | Software Foundation; we sometimes make exceptions for this. Our 431 | decision will be guided by the two goals of preserving the free status 432 | of all derivatives of our free software and of promoting the sharing 433 | and reuse of software generally. 434 | 435 | NO WARRANTY 436 | 437 | 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO 438 | WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW. 439 | EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR 440 | OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY 441 | KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE 442 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 443 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE 444 | LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME 445 | THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 446 | 447 | 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN 448 | WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY 449 | AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU 450 | FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR 451 | CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE 452 | LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING 453 | RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A 454 | FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF 455 | SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 456 | DAMAGES. 457 | 458 | END OF TERMS AND CONDITIONS 459 | 460 | How to Apply These Terms to Your New Libraries 461 | 462 | If you develop a new library, and you want it to be of the greatest 463 | possible use to the public, we recommend making it free software that 464 | everyone can redistribute and change. You can do so by permitting 465 | redistribution under these terms (or, alternatively, under the terms of the 466 | ordinary General Public License). 467 | 468 | To apply these terms, attach the following notices to the library. It is 469 | safest to attach them to the start of each source file to most effectively 470 | convey the exclusion of warranty; and each file should have at least the 471 | "copyright" line and a pointer to where the full notice is found. 472 | 473 | 474 | Copyright (C) 475 | 476 | This library is free software; you can redistribute it and/or 477 | modify it under the terms of the GNU Lesser General Public 478 | License as published by the Free Software Foundation; either 479 | version 2.1 of the License, or (at your option) any later version. 480 | 481 | This library is distributed in the hope that it will be useful, 482 | but WITHOUT ANY WARRANTY; without even the implied warranty of 483 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 484 | Lesser General Public License for more details. 485 | 486 | You should have received a copy of the GNU Lesser General Public 487 | License along with this library; if not, write to the Free Software 488 | Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 489 | 490 | Also add information on how to contact you by electronic and paper mail. 491 | 492 | You should also get your employer (if you work as a programmer) or your 493 | school, if any, to sign a "copyright disclaimer" for the library, if 494 | necessary. Here is a sample; alter the names: 495 | 496 | Yoyodyne, Inc., hereby disclaims all copyright interest in the 497 | library `Frob' (a library for tweaking knobs) written by James Random Hacker. 498 | 499 | , 1 April 1990 500 | Ty Coon, President of Vice 501 | 502 | That's all there is to it! 503 | -------------------------------------------------------------------------------- /codecov.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e +o pipefail 4 | 5 | VERSION="8fda091" 6 | 7 | url="https://codecov.io" 8 | env="$CODECOV_ENV" 9 | service="" 10 | token="" 11 | search_in="" 12 | flags="" 13 | exit_with=0 14 | curlargs="" 15 | curlawsargs="" 16 | dump="0" 17 | clean="0" 18 | curl_s="-s" 19 | name="$CODECOV_NAME" 20 | include_cov="" 21 | exclude_cov="" 22 | ddp="$(echo ~)/Library/Developer/Xcode/DerivedData" 23 | xp="" 24 | files="" 25 | cacert="$CODECOV_CA_BUNDLE" 26 | gcov_ignore="" 27 | gcov_include="" 28 | 29 | ft_gcov="1" 30 | ft_coveragepy="1" 31 | ft_fix="1" 32 | ft_search="1" 33 | ft_s3="1" 34 | ft_xcode="1" 35 | ft_network="1" 36 | ft_xcodeplist="0" 37 | 38 | _git_root=$(git rev-parse --show-toplevel 2>/dev/null || hg root 2>/dev/null || echo $PWD) 39 | git_root="$_git_root" 40 | remote_addr="" 41 | if [ "$git_root" = "$PWD" ]; 42 | then 43 | git_root="." 44 | fi 45 | 46 | url_o="" 47 | pr_o="" 48 | build_o="" 49 | commit_o="" 50 | search_in_o="" 51 | tag_o="" 52 | branch_o="" 53 | slug_o="" 54 | 55 | commit="$VCS_COMMIT_ID" 56 | branch="$VCS_BRANCH_NAME" 57 | pr="$VCS_PULL_REQUEST" 58 | slug="$VCS_SLUG" 59 | tag="$VCS_TAG" 60 | build_url="$CI_BUILD_URL" 61 | build="$CI_BUILD_ID" 62 | job="$CI_JOB_ID" 63 | 64 | beta_xcode_partials="" 65 | 66 | proj_root="$git_root" 67 | gcov_exe="gcov" 68 | gcov_arg="" 69 | 70 | b="\033[0;36m" 71 | g="\033[0;32m" 72 | r="\033[0;31m" 73 | e="\033[0;90m" 74 | x="\033[0m" 75 | 76 | show_help() { 77 | cat << EOF 78 | 79 | Codecov Bash $VERSION 80 | 81 | Global report uploading tool for Codecov 82 | Documentation at https://docs.codecov.io/docs 83 | Contribute at https://github.com/codecov/codecov-bash 84 | 85 | 86 | -h Display this help and exit 87 | -f FILE Target file(s) to upload 88 | 89 | -f "path/to/file" only upload this file 90 | skips searching unless provided patterns below 91 | 92 | -f '!*.bar' ignore all files at pattern *.bar 93 | -f '*.foo' include all files at pattern *.foo 94 | Must use single quotes. 95 | This is non-exclusive, use -s "*.foo" to match specific paths. 96 | 97 | -s DIR Directory to search for coverage reports. 98 | Already searches project root and artifact folders. 99 | -t TOKEN Set the private repository token 100 | (option) set environment variable CODECOV_TOKEN=:uuid 101 | 102 | -t @/path/to/token_file 103 | -t uuid 104 | 105 | -n NAME Custom defined name of the upload. Visible in Codecov UI 106 | 107 | -e ENV Specify environment variables to be included with this build 108 | Also accepting environment variables: CODECOV_ENV=VAR,VAR2 109 | 110 | -e VAR,VAR2 111 | 112 | -X feature Toggle functionalities 113 | 114 | -X gcov Disable gcov 115 | -X coveragepy Disable python coverage 116 | -X fix Disable report fixing 117 | -X search Disable searching for reports 118 | -X xcode Disable xcode processing 119 | -X network Disable uploading the file network 120 | 121 | -R root dir Used when not in git/hg project to identify project root directory 122 | -F flag Flag the upload to group coverage metrics 123 | 124 | -F unittests This upload is only unittests 125 | -F integration This upload is only integration tests 126 | -F ui,chrome This upload is Chrome - UI tests 127 | 128 | -c Move discovered coverage reports to the trash 129 | -Z Exit with 1 if not successful. Default will Exit with 0 130 | 131 | -- xcode -- 132 | -D Custom Derived Data Path for Coverage.profdata and gcov processing 133 | Default '~/Library/Developer/Xcode/DerivedData' 134 | -J Specify packages to build coverage. 135 | This can significantly reduces time to build coverage reports. 136 | 137 | -J 'MyAppName' Will match "MyAppName" and "MyAppNameTests" 138 | -J '^ExampleApp$' Will match only "ExampleApp" not "ExampleAppTests" 139 | 140 | -- gcov -- 141 | -g GLOB Paths to ignore during gcov gathering 142 | -G GLOB Paths to include during gcov gathering 143 | -p dir Project root directory 144 | Also used when preparing gcov 145 | -x gcovexe gcov executable to run. Defaults to 'gcov' 146 | -a gcovargs extra arguments to pass to gcov 147 | 148 | -- Override CI Environment Variables -- 149 | These variables are automatically detected by popular CI providers 150 | 151 | -B branch Specify the branch name 152 | -C sha Specify the commit sha 153 | -P pr Specify the pull request number 154 | -b build Specify the build number 155 | -T tag Specify the git tag 156 | 157 | -- Enterprise -- 158 | -u URL Set the target url for Enterprise customers 159 | Not required when retrieving the bash uploader from your CCE 160 | (option) Set environment variable CODECOV_URL=https://my-hosted-codecov.com 161 | -r SLUG owner/repo slug used instead of the private repo token in Enterprise 162 | (option) set environment variable CODECOV_SLUG=:owner/:repo 163 | (option) set in your codecov.yml "codecov.slug" 164 | -S PATH File path to your cacert.pem file used to verify ssl with Codecov Enterprise (optional) 165 | (option) Set environment variable: CODECOV_CA_BUNDLE="/path/to/ca.pem" 166 | -U curlargs Extra curl arguments to communicate with Codecov. e.g., -U "--proxy http://http-proxy" 167 | -A curlargs Extra curl arguments to communicate with AWS. 168 | 169 | -- Debugging -- 170 | -d Dont upload and dump to stdout 171 | -K Remove color from the output 172 | -v Verbose mode 173 | 174 | EOF 175 | } 176 | 177 | 178 | say() { 179 | echo -e "$1" 180 | } 181 | 182 | 183 | urlencode() { 184 | echo "$1" | curl -Gso /dev/null -w %{url_effective} --data-urlencode @- "" | cut -c 3- | sed -e 's/%0A//' 185 | } 186 | 187 | 188 | swiftcov() { 189 | _dir=$(dirname "$1") 190 | for _type in app framework xctest 191 | do 192 | find "$_dir" -name "*.$_type" | while read f 193 | do 194 | _proj=${f##*/} 195 | _proj=${_proj%."$_type"} 196 | if [ "$2" = "" ] || [ "$(echo "$_proj" | grep -i "$2")" != "" ]; 197 | then 198 | say " $g+$x Building reports for $_proj $_type" 199 | dest=$([ -f "$f/$_proj" ] && echo "$f/$_proj" || echo "$f/Contents/MacOS/$_proj") 200 | _proj_name=$(echo "$_proj" | sed -e 's/[[:space:]]//g') 201 | xcrun llvm-cov show $beta_xcode_partials -instr-profile "$1" "$dest" > "$_proj_name.$_type.coverage.txt" \ 202 | || say " ${r}x>${x} llvm-cov failed to produce results for $dest" 203 | fi 204 | done 205 | done 206 | } 207 | 208 | 209 | # Credits to: https://gist.github.com/pkuczynski/8665367 210 | parse_yaml() { 211 | local prefix=$2 212 | local s='[[:space:]]*' w='[a-zA-Z0-9_]*' fs=$(echo @|tr @ '\034') 213 | sed -ne "s|^\($s\)\($w\)$s:$s\"\(.*\)\"$s\$|\1$fs\2$fs\3|p" \ 214 | -e "s|^\($s\)\($w\)$s:$s\(.*\)$s\$|\1$fs\2$fs\3|p" $1 | 215 | awk -F$fs '{ 216 | indent = length($1)/2; 217 | vname[indent] = $2; 218 | for (i in vname) {if (i > indent) {delete vname[i]}} 219 | if (length($3) > 0) { 220 | vn=""; if (indent > 0) {vn=(vn)(vname[0])("_")} 221 | printf("%s%s%s=\"%s\"\n", "'$prefix'",vn, $2, $3); 222 | } 223 | }' 224 | } 225 | 226 | 227 | if [ $# != 0 ]; 228 | then 229 | while getopts "a:A:b:B:cC:dD:e:f:F:g:G:hJ:Kn:p:P:r:R:s:S:t:T:u:U:vx:X:Z" o 230 | do 231 | case "$o" in 232 | "a") 233 | gcov_arg=$OPTARG 234 | ;; 235 | "A") 236 | curlawsargs="$OPTARG" 237 | ;; 238 | "b") 239 | build_o="$OPTARG" 240 | ;; 241 | "B") 242 | branch_o="$OPTARG" 243 | ;; 244 | "c") 245 | clean="1" 246 | ;; 247 | "C") 248 | commit_o="$OPTARG" 249 | ;; 250 | "d") 251 | dump="1" 252 | ;; 253 | "D") 254 | ddp="$OPTARG" 255 | ;; 256 | "e") 257 | env="$env,$OPTARG" 258 | ;; 259 | "f") 260 | if [ "${OPTARG::1}" = "!" ]; 261 | then 262 | exclude_cov="$exclude_cov -not -path '${OPTARG:1}'" 263 | 264 | elif [[ "$OPTARG" = *"*"* ]]; 265 | then 266 | include_cov="$include_cov -or -name '$OPTARG'" 267 | 268 | else 269 | ft_search=0 270 | if [ "$files" = "" ]; 271 | then 272 | files="$OPTARG" 273 | else 274 | files="$files 275 | $OPTARG" 276 | fi 277 | fi 278 | ;; 279 | "F") 280 | if [ "$flags" = "" ]; 281 | then 282 | flags="$OPTARG" 283 | else 284 | flags="$flags,$OPTARG" 285 | fi 286 | ;; 287 | "g") 288 | gcov_ignore="$gcov_ignore -not -path '$OPTARG'" 289 | ;; 290 | "G") 291 | gcov_include="$gcov_include -path '$OPTARG'" 292 | ;; 293 | "h") 294 | show_help 295 | exit 0; 296 | ;; 297 | "J") 298 | if [ "$xp" = "" ]; 299 | then 300 | xp="$OPTARG" 301 | else 302 | xp="$xp\|$OPTARG" 303 | fi 304 | ;; 305 | "K") 306 | b="" 307 | g="" 308 | r="" 309 | e="" 310 | x="" 311 | ;; 312 | "n") 313 | name="$OPTARG" 314 | ;; 315 | "p") 316 | proj_root="$OPTARG" 317 | ;; 318 | "P") 319 | pr_o="$OPTARG" 320 | ;; 321 | "r") 322 | slug_o="$OPTARG" 323 | ;; 324 | "R") 325 | git_root="$OPTARG" 326 | ;; 327 | "s") 328 | if [ "$search_in_o" = "" ]; 329 | then 330 | search_in_o="$OPTARG" 331 | else 332 | search_in_o="$search_in_o $OPTARG" 333 | fi 334 | ;; 335 | "S") 336 | cacert="--cacert \"$OPTARG\"" 337 | ;; 338 | "t") 339 | if [ "${OPTARG::1}" = "@" ]; 340 | then 341 | token=$(cat "${OPTARG:1}" | tr -d ' \n') 342 | else 343 | token="$OPTARG" 344 | fi 345 | ;; 346 | "T") 347 | tag_o="$OPTARG" 348 | ;; 349 | "u") 350 | url_o=$(echo "$OPTARG" | sed -e 's/\/$//') 351 | ;; 352 | "U") 353 | curlargs="$OPTARG" 354 | ;; 355 | "v") 356 | set -x 357 | curl_s="" 358 | ;; 359 | "x") 360 | gcov_exe=$OPTARG 361 | ;; 362 | "X") 363 | if [ "$OPTARG" = "gcov" ]; 364 | then 365 | ft_gcov="0" 366 | elif [ "$OPTARG" = "coveragepy" ] || [ "$OPTARG" = "py" ]; 367 | then 368 | ft_coveragepy="0" 369 | elif [ "$OPTARG" = "xcodeplist" ]; 370 | then 371 | ft_xcodeplist="1" 372 | ft_xcode="0" 373 | elif [ "$OPTARG" = "fix" ] || [ "$OPTARG" = "fixes" ]; 374 | then 375 | ft_fix="0" 376 | elif [ "$OPTARG" = "xcode" ]; 377 | then 378 | ft_xcode="0" 379 | elif [ "$OPTARG" = "search" ]; 380 | then 381 | ft_search="0" 382 | elif [ "$OPTARG" = "xcodepartials" ]; 383 | then 384 | beta_xcode_partials="-use-color" 385 | elif [ "$OPTARG" = "network" ]; 386 | then 387 | ft_network="0" 388 | fi 389 | ;; 390 | "Z") 391 | exit_with=1 392 | ;; 393 | esac 394 | done 395 | fi 396 | 397 | say " 398 | _____ _ 399 | / ____| | | 400 | | | ___ __| | ___ ___ _____ __ 401 | | | / _ \\ / _\` |/ _ \\/ __/ _ \\ \\ / / 402 | | |___| (_) | (_| | __/ (_| (_) \\ V / 403 | \\_____\\___/ \\__,_|\\___|\\___\\___/ \\_/ 404 | Bash-$VERSION 405 | 406 | " 407 | 408 | search_in="$proj_root" 409 | 410 | if [ "$JENKINS_URL" != "" ]; 411 | then 412 | say "$e==>$x Jenkins CI detected." 413 | # https://wiki.jenkins-ci.org/display/JENKINS/Building+a+software+project 414 | # https://wiki.jenkins-ci.org/display/JENKINS/GitHub+pull+request+builder+plugin#GitHubpullrequestbuilderplugin-EnvironmentVariables 415 | service="jenkins" 416 | branch=$([ ! -z "$ghprbSourceBranch" ] && echo "$ghprbSourceBranch" || \ 417 | [ ! -z "$GIT_BRANCH" ] && echo "$GIT_BRANCH" || \ 418 | [ ! -z "$BRANCH_NAME" ] && echo "$BRANCH_NAME") 419 | commit=$([ ! -z "$ghprbActualCommit" ] && echo "$ghprbActualCommit" || \ 420 | echo "$GIT_COMMIT") 421 | build="$BUILD_NUMBER" 422 | pr="${ghprbPullId:=$CHANGE_ID}" 423 | build_url=$(urlencode "$BUILD_URL") 424 | 425 | elif [ "$CI" = "true" ] && [ "$TRAVIS" = "true" ] && [ "$SHIPPABLE" != "true" ]; 426 | then 427 | say "$e==>$x Travis CI detected." 428 | # http://docs.travis-ci.com/user/ci-environment/#Environment-variables 429 | service="travis" 430 | branch="$TRAVIS_BRANCH" 431 | commit="$TRAVIS_COMMIT" 432 | build="$TRAVIS_JOB_NUMBER" 433 | pr="$TRAVIS_PULL_REQUEST" 434 | job="$TRAVIS_JOB_ID" 435 | slug="$TRAVIS_REPO_SLUG" 436 | tag="$TRAVIS_TAG" 437 | env="$env,TRAVIS_OS_NAME" 438 | 439 | language=$(printenv | grep "TRAVIS_.*_VERSION" | head -1) 440 | if [ "$language" != "" ]; 441 | then 442 | env="$env,${language%=*}" 443 | fi 444 | 445 | elif [ "$CI" = "true" ] && [ "$CI_NAME" = "codeship" ]; 446 | then 447 | say "$e==>$x Codeship CI detected." 448 | # https://www.codeship.io/documentation/continuous-integration/set-environment-variables/ 449 | service="codeship" 450 | branch="$CI_BRANCH" 451 | build="$CI_BUILD_NUMBER" 452 | build_url=$(urlencode "$CI_BUILD_URL") 453 | commit="$CI_COMMIT_ID" 454 | 455 | elif [ "$TEAMCITY_VERSION" != "" ]; 456 | then 457 | say "$e==>$x TeamCity CI detected." 458 | # https://confluence.jetbrains.com/display/TCD8/Predefined+Build+Parameters 459 | # https://confluence.jetbrains.com/plugins/servlet/mobile#content/view/74847298 460 | if [ "$TEAMCITY_BUILD_BRANCH" = '' ]; 461 | then 462 | echo " Teamcity does not automatically make build parameters available as environment variables." 463 | echo " Add the following environment parameters to the build configuration" 464 | echo " env.TEAMCITY_BUILD_BRANCH = %teamcity.build.branch%" 465 | echo " env.TEAMCITY_BUILD_ID = %teamcity.build.id%" 466 | echo " env.TEAMCITY_BUILD_URL = %teamcity.serverUrl%/viewLog.html?buildId=%teamcity.build.id%" 467 | echo " env.TEAMCITY_BUILD_COMMIT = %system.build.vcs.number%" 468 | echo " env.TEAMCITY_BUILD_REPOSITORY = %vcsroot..url%" 469 | fi 470 | service="teamcity" 471 | branch="$TEAMCITY_BUILD_BRANCH" 472 | build="$TEAMCITY_BUILD_ID" 473 | build_url=$(urlencode "$TEAMCITY_BUILD_URL") 474 | if [ "$TEAMCITY_BUILD_COMMIT" != "" ]; 475 | then 476 | commit="$TEAMCITY_BUILD_COMMIT" 477 | else 478 | commit="$BUILD_VCS_NUMBER" 479 | fi 480 | remote_addr="$TEAMCITY_BUILD_REPOSITORY" 481 | 482 | elif [ "$CI" = "true" ] && [ "$CIRCLECI" = "true" ]; 483 | then 484 | say "$e==>$x Circle CI detected." 485 | # https://circleci.com/docs/environment-variables 486 | service="circleci" 487 | branch="$CIRCLE_BRANCH" 488 | build="$CIRCLE_BUILD_NUM" 489 | job="$CIRCLE_NODE_INDEX" 490 | if [ "$CIRCLE_PROJECT_REPONAME" != "" ]; 491 | then 492 | slug="$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME" 493 | else 494 | # git@github.com:owner/repo.git 495 | slug="${CIRCLE_REPOSITORY_URL##*:}" 496 | # owner/repo.git 497 | slug="${slug%%.git}" 498 | fi 499 | pr="$CIRCLE_PR_NUMBER" 500 | commit="$CIRCLE_SHA1" 501 | search_in="$search_in $CIRCLE_ARTIFACTS $CIRCLE_TEST_REPORTS" 502 | 503 | elif [ "$BUDDYBUILD_BRANCH" != "" ]; 504 | then 505 | say "$e==>$x buddybuild detected" 506 | # http://docs.buddybuild.com/v6/docs/custom-prebuild-and-postbuild-steps 507 | service="buddybuild" 508 | branch="$BUDDYBUILD_BRANCH" 509 | build="$BUDDYBUILD_BUILD_NUMBER" 510 | build_url="https://dashboard.buddybuild.com/public/apps/$BUDDYBUILD_APP_ID/build/$BUDDYBUILD_BUILD_ID" 511 | # BUDDYBUILD_TRIGGERED_BY 512 | if [ "$ddp" = "$(echo ~)/Library/Developer/Xcode/DerivedData" ]; 513 | then 514 | ddp="/private/tmp/sandbox/${BUDDYBUILD_APP_ID}/bbtest" 515 | fi 516 | 517 | elif [ "${bamboo_planRepository_revision}" != "" ]; 518 | then 519 | say "$e==>$x Bamboo detected" 520 | # https://confluence.atlassian.com/bamboo/bamboo-variables-289277087.html#Bamboovariables-Build-specificvariables 521 | service="bamboo" 522 | commit="${bamboo_planRepository_revision}" 523 | branch="${bamboo_planRepository_branch}" 524 | build="${bamboo_buildNumber}" 525 | build_url="${bamboo_buildResultsUrl}" 526 | remote_addr="${bamboo_planRepository_repositoryUrl}" 527 | 528 | elif [ "$CI" = "true" ] && [ "$BITRISE_IO" = "true" ]; 529 | then 530 | # http://devcenter.bitrise.io/faq/available-environment-variables/ 531 | say "$e==>$x Bitrise CI detected." 532 | service="bitrise" 533 | branch="$BITRISE_GIT_BRANCH" 534 | build="$BITRISE_BUILD_NUMBER" 535 | build_url=$(urlencode "$BITRISE_BUILD_URL") 536 | pr="$BITRISE_PULL_REQUEST" 537 | if [ "$GIT_CLONE_COMMIT_HASH" != "" ]; 538 | then 539 | commit="$GIT_CLONE_COMMIT_HASH" 540 | fi 541 | 542 | elif [ "$CI" = "true" ] && [ "$SEMAPHORE" = "true" ]; 543 | then 544 | say "$e==>$x Semaphore CI detected." 545 | # https://semaphoreapp.com/docs/available-environment-variables.html 546 | service="semaphore" 547 | branch="$BRANCH_NAME" 548 | build="$SEMAPHORE_BUILD_NUMBER" 549 | job="$SEMAPHORE_CURRENT_THREAD" 550 | pr="$PULL_REQUEST_NUMBER" 551 | slug="$SEMAPHORE_REPO_SLUG" 552 | commit="$REVISION" 553 | env="$env,SEMAPHORE_TRIGGER_SOURCE" 554 | 555 | elif [ "$CI" = "true" ] && [ "$BUILDKITE" = "true" ]; 556 | then 557 | say "$e==>$x Buildkite CI detected." 558 | # https://buildkite.com/docs/guides/environment-variables 559 | service="buildkite" 560 | branch="$BUILDKITE_BRANCH" 561 | build="$BUILDKITE_BUILD_NUMBER" 562 | job="$BUILDKITE_JOB_ID" 563 | build_url=$(urlencode "$BUILDKITE_BUILD_URL") 564 | slug="$BUILDKITE_PROJECT_SLUG" 565 | commit="$BUILDKITE_COMMIT" 566 | 567 | elif [ "$CI" = "true" ] && [ "$DRONE" = "true" ]; 568 | then 569 | say "$e==>$x Drone CI detected." 570 | # http://docs.drone.io/env.html 571 | # drone commits are not full shas 572 | service="drone.io" 573 | branch="$DRONE_BRANCH" 574 | build="$DRONE_BUILD_NUMBER" 575 | build_url=$(urlencode "${DRONE_BUILD_URL:-$CI_BUILD_URL}") 576 | pr="$DRONE_PULL_REQUEST" 577 | job="$DRONE_JOB_NUMBER" 578 | tag="$DRONE_TAG" 579 | 580 | elif [ "$HEROKU_TEST_RUN_BRANCH" != "" ]; 581 | then 582 | say "$e==>$x Heroku CI detected." 583 | # https://devcenter.heroku.com/articles/heroku-ci#environment-variables 584 | service="heroku" 585 | branch="$HEROKU_TEST_RUN_BRANCH" 586 | build="$HEROKU_TEST_RUN_ID" 587 | 588 | elif [ "$CI" = "True" ] && [ "$APPVEYOR" = "True" ]; 589 | then 590 | say "$e==>$x Appveyor CI detected." 591 | # http://www.appveyor.com/docs/environment-variables 592 | service="appveyor" 593 | branch="$APPVEYOR_REPO_BRANCH" 594 | build=$(urlencode "$APPVEYOR_JOB_ID") 595 | pr="$APPVEYOR_PULL_REQUEST_NUMBER" 596 | job="$APPVEYOR_ACCOUNT_NAME%2F$APPVEYOR_PROJECT_SLUG%2F$APPVEYOR_BUILD_VERSION" 597 | slug="$APPVEYOR_REPO_NAME" 598 | commit="$APPVEYOR_REPO_COMMIT" 599 | 600 | elif [ "$CI" = "true" ] && [ "$WERCKER_GIT_BRANCH" != "" ]; 601 | then 602 | say "$e==>$x Wercker CI detected." 603 | # http://devcenter.wercker.com/articles/steps/variables.html 604 | service="wercker" 605 | branch="$WERCKER_GIT_BRANCH" 606 | build="$WERCKER_MAIN_PIPELINE_STARTED" 607 | slug="$WERCKER_GIT_OWNER/$WERCKER_GIT_REPOSITORY" 608 | commit="$WERCKER_GIT_COMMIT" 609 | 610 | elif [ "$CI" = "true" ] && [ "$MAGNUM" = "true" ]; 611 | then 612 | say "$e==>$x Magnum CI detected." 613 | # https://magnum-ci.com/docs/environment 614 | service="magnum" 615 | branch="$CI_BRANCH" 616 | build="$CI_BUILD_NUMBER" 617 | commit="$CI_COMMIT" 618 | 619 | elif [ "$CI" = "true" ] && [ "$SNAP_CI" = "true" ]; 620 | then 621 | say "$e==>$x Snap CI detected." 622 | # https://docs.snap-ci.com/environment-variables/ 623 | service="snap" 624 | branch=$([ "$SNAP_BRANCH" != "" ] && echo "$SNAP_BRANCH" || echo "$SNAP_UPSTREAM_BRANCH") 625 | build="$SNAP_PIPELINE_COUNTER" 626 | job="$SNAP_STAGE_NAME" 627 | pr="$SNAP_PULL_REQUEST_NUMBER" 628 | commit=$([ "$SNAP_COMMIT" != "" ] && echo "$SNAP_COMMIT" || echo "$SNAP_UPSTREAM_COMMIT") 629 | env="$env,DISPLAY" 630 | 631 | elif [ "$SHIPPABLE" = "true" ]; 632 | then 633 | say "$e==>$x Shippable CI detected." 634 | # http://docs.shippable.com/ci_configure/ 635 | service="shippable" 636 | branch=$([ "$HEAD_BRANCH" != "" ] && echo "$HEAD_BRANCH" || echo "$BRANCH") 637 | build="$BUILD_NUMBER" 638 | build_url=$(urlencode "$BUILD_URL") 639 | pr="$PULL_REQUEST" 640 | slug="$REPO_FULL_NAME" 641 | commit="$COMMIT" 642 | 643 | elif [ "$TDDIUM" = "true" ]; 644 | then 645 | say "Solano CI detected." 646 | # http://docs.solanolabs.com/Setup/tddium-set-environment-variables/ 647 | service="solano" 648 | commit="$TDDIUM_CURRENT_COMMIT" 649 | branch="$TDDIUM_CURRENT_BRANCH" 650 | build="$TDDIUM_TID" 651 | pr="$TDDIUM_PR_ID" 652 | 653 | elif [ "$GREENHOUSE" = "true" ]; 654 | then 655 | say "$e==>$x Greenhouse CI detected." 656 | # http://docs.greenhouseci.com/docs/environment-variables-files 657 | service="greenhouse" 658 | branch="$GREENHOUSE_BRANCH" 659 | build="$GREENHOUSE_BUILD_NUMBER" 660 | build_url=$(urlencode "$GREENHOUSE_BUILD_URL") 661 | pr="$GREENHOUSE_PULL_REQUEST" 662 | commit="$GREENHOUSE_COMMIT" 663 | search_in="$search_in $GREENHOUSE_EXPORT_DIR" 664 | 665 | elif [ "$GITLAB_CI" != "" ]; 666 | then 667 | say "$e==>$x GitLab CI detected." 668 | # http://doc.gitlab.com/ce/ci/variables/README.html 669 | service="gitlab" 670 | branch="$CI_BUILD_REF_NAME" 671 | build="$CI_BUILD_ID" 672 | remote_addr="${CI_BUILD_REPO:-$CI_REPOSITORY_URL}" 673 | commit="$CI_BUILD_REF" 674 | 675 | else 676 | say "${r}x>${x} No CI provider detected." 677 | say " Testing inside Docker? ${b}http://docs.codecov.io/docs/testing-with-docker${x}" 678 | say " Testing with Tox? ${b}https://docs.codecov.io/docs/python#section-testing-with-tox${x}" 679 | 680 | fi 681 | 682 | say " ${e}project root:${x} $git_root" 683 | 684 | # find branch, commit, repo from git command 685 | if [ "$GIT_BRANCH" != "" ]; 686 | then 687 | branch="$GIT_BRANCH" 688 | 689 | elif [ "$branch" = "" ]; 690 | then 691 | branch=$(git rev-parse --abbrev-ref HEAD 2>/dev/null || hg branch 2>/dev/null || echo "") 692 | if [ "$branch" = "HEAD" ]; 693 | then 694 | branch="" 695 | fi 696 | fi 697 | 698 | if [ "$commit_o" = "" ]; 699 | then 700 | # merge commit -> actual commit 701 | mc=$(git log -1 --pretty=%B 2>/dev/null | tr -d '[[:space:]]' || true) 702 | if [[ "$mc" =~ ^Merge[[:space:]][a-z0-9]{40}[[:space:]]into[[:space:]][a-z0-9]{40}$ ]]; 703 | then 704 | # Merge xxx into yyy 705 | say " Fixing merge commit sha" 706 | commit=$(echo "$mc" | cut -d' ' -f2) 707 | elif [ "$GIT_COMMIT" != "" ]; 708 | then 709 | commit="$GIT_COMMIT" 710 | elif [ "$commit" = "" ]; 711 | then 712 | commit=$(git log -1 --format="%H" 2>/dev/null || hg id -i --debug 2>/dev/null | tr -d '+' || echo "") 713 | fi 714 | else 715 | commit="$commit_o" 716 | fi 717 | 718 | if [ "$CODECOV_TOKEN" != "" ] && [ "$token" = "" ]; 719 | then 720 | say "${e}-->${x} token set from env" 721 | token="$CODECOV_TOKEN" 722 | fi 723 | 724 | if [ "$CODECOV_URL" != "" ] && [ "$url_o" = "" ]; 725 | then 726 | say "${e}-->${x} url set from env" 727 | url_o=$(echo "$CODECOV_URL" | sed -e 's/\/$//') 728 | fi 729 | 730 | if [ "$CODECOV_SLUG" != "" ]; 731 | then 732 | say "${e}-->${x} slug set from env" 733 | slug_o="$CODECOV_SLUG" 734 | 735 | elif [ "$slug" = "" ]; 736 | then 737 | if [ "$remote_addr" = "" ]; 738 | then 739 | remote_addr=$(git config --get remote.origin.url || hg paths default || echo '') 740 | fi 741 | if [ "$remote_addr" != "" ]; 742 | then 743 | if echo "$remote_addr" | grep -q "//"; then 744 | # https 745 | slug=$(echo "$remote_addr" | cut -d / -f 4,5 | sed -e 's/\.git$//') 746 | else 747 | # ssh 748 | slug=$(echo "$remote_addr" | cut -d : -f 2 | sed -e 's/\.git$//') 749 | fi 750 | fi 751 | if [ "$slug" = "/" ]; 752 | then 753 | slug="" 754 | fi 755 | fi 756 | 757 | yaml=$(cd "$git_root" && \ 758 | git ls-files "*codecov.yml" "*codecov.yaml" 2>/dev/null \ 759 | || hg locate "*codecov.yml" "*codecov.yaml" 2>/dev/null \ 760 | || echo '') 761 | yaml=$(echo "$yaml" | head -1) 762 | 763 | if [ "$yaml" != "" ]; 764 | then 765 | say " ${e}Yaml found at:${x} $yaml" 766 | config=$(parse_yaml "$git_root/$yaml" || echo '') 767 | 768 | # TODO validate the yaml here 769 | 770 | if [ "$(echo "$config" | grep 'codecov_token="')" != "" ] && [ "$token" = "" ]; 771 | then 772 | say "${e}-->${x} token set from yaml" 773 | token="$(echo "$config" | grep 'codecov_token="' | sed -e 's/codecov_token="//' | sed -e 's/"\.*//')" 774 | fi 775 | 776 | if [ "$(echo "$config" | grep 'codecov_url="')" != "" ] && [ "$url_o" = "" ]; 777 | then 778 | say "${e}-->${x} url set from yaml" 779 | url_o="$(echo "$config" | grep 'codecov_url="' | sed -e 's/codecov_url="//' | sed -e 's/"\.*//')" 780 | fi 781 | 782 | if [ "$(echo "$config" | grep 'codecov_slug="')" != "" ] && [ "$slug_o" = "" ]; 783 | then 784 | say "${e}-->${x} slug set from yaml" 785 | slug_o="$(echo "$config" | grep 'codecov_slug="' | sed -e 's/codecov_slug="//' | sed -e 's/"\.*//')" 786 | fi 787 | else 788 | say " ${g}Yaml not found, that's ok! Learn more at${x} ${b}http://docs.codecov.io/docs/codecov-yaml${x}" 789 | 790 | fi 791 | 792 | if [ "$branch_o" != "" ]; 793 | then 794 | branch=$(urlencode "$branch_o") 795 | else 796 | branch=$(urlencode "$branch") 797 | fi 798 | 799 | query="branch=$branch\ 800 | &commit=$commit\ 801 | &build=$([ "$build_o" = "" ] && echo "$build" || echo "$build_o")\ 802 | &build_url=$build_url\ 803 | &name=$(urlencode "$name")\ 804 | &tag=$([ "$tag_o" = "" ] && echo "$tag" || echo "$tag_o")\ 805 | &slug=$([ "$slug_o" = "" ] && urlencode "$slug" || urlencode "$slug_o")\ 806 | &yaml=$(urlencode "$yaml")\ 807 | &service=$service\ 808 | &flags=$flags\ 809 | &pr=$([ "$pr_o" = "" ] && echo "${pr##\#}" || echo "${pr_o##\#}")\ 810 | &job=$job" 811 | 812 | if [ "$ft_search" = "1" ]; 813 | then 814 | # detect bower comoponents location 815 | bower_components="bower_components" 816 | bower_rc=$(cd "$git_root" && cat .bowerrc 2>/dev/null || echo "") 817 | if [ "$bower_rc" != "" ]; 818 | then 819 | bower_components=$(echo "$bower_rc" | tr -d '\n' | grep '"directory"' | cut -d'"' -f4 | sed -e 's/\/$//') 820 | if [ "$bower_components" = "" ]; 821 | then 822 | bower_components="bower_components" 823 | fi 824 | fi 825 | 826 | # Swift Coverage 827 | if [ "$ft_xcode" = "1" ] && [ -d "$ddp" ]; 828 | then 829 | say "${e}==>${x} Processing Xcode reports" 830 | say " DerivedData folder: $ddp" 831 | profdata_files=$(find "$ddp" -name '*.profdata' 2>/dev/null || echo '') 832 | if [ "$profdata_files" != "" ]; 833 | then 834 | # xcode via profdata 835 | if [ "$xp" = "" ]; 836 | then 837 | # xp=$(xcodebuild -showBuildSettings 2>/dev/null | grep -i "^\s*PRODUCT_NAME" | sed -e 's/.*= \(.*\)/\1/') 838 | # say " ${e}->${x} Speed up Xcode processing by adding ${e}-J '$xp'${x}" 839 | say " ${g}hint${x} Speed up Swift processing by using use ${g}-J 'AppName'${x} (regexp accepted)" 840 | say " ${g}hint${x} This will remove Pods/ from your report. Also ${b}https://docs.codecov.io/docs/ignoring-paths${x}" 841 | fi 842 | while read -r profdata; 843 | do 844 | if [ "$profdata" != "" ]; 845 | then 846 | swiftcov "$profdata" "$xp" 847 | fi 848 | done <<< "$profdata_files" 849 | else 850 | say " ${e}->${x} No Swift coverage found" 851 | fi 852 | 853 | # Obj-C Gcov Coverage 854 | if [ "$ft_gcov" = "1" ]; 855 | then 856 | say " ${e}->${x} Running $gcov_exe for Obj-C" 857 | bash -c "find $ddp -type f -name '*.gcda' $gcov_include $gcov_ignore -exec $gcov_exe -pbcu $gcov_arg {} +" || true 858 | fi 859 | fi 860 | 861 | if [ "$ft_xcodeplist" = "1" ] && [ -d "$ddp" ]; 862 | then 863 | say "${e}==>${x} Processing Xcode plists" 864 | plists_files=$(find "$ddp" -name '*.xccoverage' 2>/dev/null || echo '') 865 | if [ "$plists_files" != "" ]; 866 | then 867 | while read -r plist; 868 | do 869 | if [ "$plist" != "" ]; 870 | then 871 | say " ${g}Found${x} plist file at $plist" 872 | plutil -convert xml1 -o "$(basename "$plist").plist" -- $plist 873 | fi 874 | done <<< "$plists_files" 875 | fi 876 | fi 877 | 878 | # Gcov Coverage 879 | if [ "$ft_gcov" = "1" ]; 880 | then 881 | say "${e}==>${x} Running gcov in $proj_root ${e}(disable via -X gcov)${x}" 882 | bash -c "find $proj_root -type f -name '*.gcno' $gcov_include $gcov_ignore -exec $gcov_exe -pb $gcov_arg {} +" || true 883 | else 884 | say "${e}==>${x} gcov disabled" 885 | fi 886 | 887 | # Python Coverage 888 | if [ "$ft_coveragepy" = "1" ]; 889 | then 890 | if [ ! -f coverage.xml ]; 891 | then 892 | if which coverage >/dev/null 2>&1; 893 | then 894 | say "${e}==>${x} Python coveragepy exists ${e}disable via -X coveragepy${x}" 895 | 896 | dotcoverage=$(find "$git_root" -name '.coverage' -or -name '.coverage.*' | head -1 || echo '') 897 | if [ "$dotcoverage" != "" ]; 898 | then 899 | cd "$(dirname "$dotcoverage")" 900 | if [ ! -f .coverage ]; 901 | then 902 | say " ${e}->${x} Running coverage combine" 903 | coverage combine 904 | fi 905 | say " ${e}->${x} Running coverage xml" 906 | if [ "$(coverage xml -i)" != "No data to report." ]; 907 | then 908 | files="$files 909 | $PWD/coverage.xml" 910 | else 911 | say " ${r}No data to report.${x}" 912 | fi 913 | cd "$proj_root" 914 | else 915 | say " ${r}No .coverage file found.${x}" 916 | fi 917 | else 918 | say "${e}==>${x} Python coveragepy not found" 919 | fi 920 | fi 921 | else 922 | say "${e}==>${x} Python coveragepy disabled" 923 | fi 924 | 925 | if [ "$search_in_o" != "" ]; 926 | then 927 | # location override 928 | search_in="$search_in_o" 929 | fi 930 | 931 | say "$e==>$x Searching for coverage reports in:" 932 | for _path in $search_in 933 | do 934 | say " ${g}+${x} $_path" 935 | done 936 | 937 | patterns="find $search_in -type f \( -name '*coverage*.*' \ 938 | -or -name 'nosetests.xml' \ 939 | -or -name 'jacoco*.xml' \ 940 | -or -name 'clover.xml' \ 941 | -or -name 'report.xml' \ 942 | -or -name '*.codecov.*' \ 943 | -or -name 'codecov.*' \ 944 | -or -name 'cobertura.xml' \ 945 | -or -name 'excoveralls.json' \ 946 | -or -name 'luacov.report.out' \ 947 | -or -name 'coverage-final.json' \ 948 | -or -name 'naxsi.info' \ 949 | -or -name 'lcov.info' \ 950 | -or -name 'lcov.dat' \ 951 | -or -name '*.lcov' \ 952 | -or -name '*.clover' \ 953 | -or -name 'cover.out' \ 954 | -or -name 'gcov.info' \ 955 | -or -name '*.gcov' \ 956 | -or -name '*.lst' \ 957 | $include_cov \) \ 958 | $exclude_cov \ 959 | -not -name '*.profdata' \ 960 | -not -name 'coverage-summary.json' \ 961 | -not -name 'phpunit-code-coverage.xml' \ 962 | -not -name 'remapInstanbul.coverage*.json' \ 963 | -not -name 'phpunit-coverage.xml' \ 964 | -not -name '*codecov.yml' \ 965 | -not -name '*.serialized' \ 966 | -not -name '.coverage*' \ 967 | -not -name '.*coveragerc' \ 968 | -not -name '*.sh' \ 969 | -not -name '*.bat' \ 970 | -not -name '*.ps1' \ 971 | -not -name '*.cmake' \ 972 | -not -name '*.dox' \ 973 | -not -name '*.ec' \ 974 | -not -name '*.rst' \ 975 | -not -name '*.h' \ 976 | -not -name '*.scss' \ 977 | -not -name '*.o' \ 978 | -not -name '*.proto' \ 979 | -not -name '*.sbt' \ 980 | -not -name '*.xcoverage.*' \ 981 | -not -name '*.gz' \ 982 | -not -name '*.conf' \ 983 | -not -name '*.p12' \ 984 | -not -name '*.csv' \ 985 | -not -name '*.rsp' \ 986 | -not -name '*.m4' \ 987 | -not -name '*.pem' \ 988 | -not -name '*~' \ 989 | -not -name '*.exe' \ 990 | -not -name '*.am' \ 991 | -not -name '*.template' \ 992 | -not -name '*.cp' \ 993 | -not -name '*.bw' \ 994 | -not -name '*.crt' \ 995 | -not -name '*.log' \ 996 | -not -name '*.cmake' \ 997 | -not -name '*.pth' \ 998 | -not -name '*.in' \ 999 | -not -name '*.jar*' \ 1000 | -not -name '*.pom*' \ 1001 | -not -name '*.png' \ 1002 | -not -name '*.jpg' \ 1003 | -not -name '*.sql' \ 1004 | -not -name '*.jpeg' \ 1005 | -not -name '*.svg' \ 1006 | -not -name '*.gif' \ 1007 | -not -name '*.csv' \ 1008 | -not -name '*.snapshot' \ 1009 | -not -name '*.mak*' \ 1010 | -not -name '*.bash' \ 1011 | -not -name '*.data' \ 1012 | -not -name '*.py' \ 1013 | -not -name '*.class' \ 1014 | -not -name '*.xcconfig' \ 1015 | -not -name '*.ec' \ 1016 | -not -name '*.coverage' \ 1017 | -not -name '*.pyc' \ 1018 | -not -name '*.cfg' \ 1019 | -not -name '*.egg' \ 1020 | -not -name '*.ru' \ 1021 | -not -name '*.css' \ 1022 | -not -name '*.less' \ 1023 | -not -name '*.pyo' \ 1024 | -not -name '*.whl' \ 1025 | -not -name '*.html' \ 1026 | -not -name '*.ftl' \ 1027 | -not -name '*.erb' \ 1028 | -not -name '*.rb' \ 1029 | -not -name '*.js' \ 1030 | -not -name '*.jade' \ 1031 | -not -name '*.db' \ 1032 | -not -name '*.md' \ 1033 | -not -name '*.cpp' \ 1034 | -not -name '*.gradle' \ 1035 | -not -name '*.tar.tz' \ 1036 | -not -name '*.scss' \ 1037 | -not -name 'include.lst' \ 1038 | -not -name 'fullLocaleNames.lst' \ 1039 | -not -name 'inputFiles.lst' \ 1040 | -not -name 'createdFiles.lst' \ 1041 | -not -name 'scoverage.measurements.*' \ 1042 | -not -name 'test_*_coverage.txt' \ 1043 | -not -name 'testrunner-coverage*' \ 1044 | -not -path '*/vendor/*' \ 1045 | -not -path '*/htmlcov/*' \ 1046 | -not -path '*/virtualenv/*' \ 1047 | -not -path '*/js/generated/coverage/*' \ 1048 | -not -path '*/.virtualenv/*' \ 1049 | -not -path '*/virtualenvs/*' \ 1050 | -not -path '*/.virtualenvs/*' \ 1051 | -not -path '*/.env/*' \ 1052 | -not -path '*/.envs/*' \ 1053 | -not -path '*/env/*' \ 1054 | -not -path '*/envs/*' \ 1055 | -not -path '*/.venv/*' \ 1056 | -not -path '*/.venvs/*' \ 1057 | -not -path '*/venv/*' \ 1058 | -not -path '*/venvs/*' \ 1059 | -not -path '*/.git/*' \ 1060 | -not -path '*/.hg/*' \ 1061 | -not -path '*/.tox/*' \ 1062 | -not -path '*/__pycache__/*' \ 1063 | -not -path '*/.egg-info*' \ 1064 | -not -path '*/$bower_components/*' \ 1065 | -not -path '*/node_modules/*' \ 1066 | -not -path '*/conftest_*.c.gcov' 2>/dev/null" 1067 | files=$(eval "$patterns" || echo '') 1068 | 1069 | elif [ "$include_cov" != "" ]; 1070 | then 1071 | files=$(eval "find $search_in -type f \( ${include_cov:5} \)$exclude_cov 2>/dev/null" || echo '') 1072 | fi 1073 | 1074 | num_of_files=$(echo "$files" | wc -l | tr -d ' ') 1075 | if [ "$num_of_files" != '' ] && [ "$files" != '' ]; 1076 | then 1077 | say " ${e}->${x} Found $num_of_files reports" 1078 | fi 1079 | 1080 | # no files found 1081 | if [ "$files" = "" ]; 1082 | then 1083 | say "${r}-->${x} No coverage report found." 1084 | say " Please visit ${b}http://docs.codecov.io/docs/supported-languages${x}" 1085 | exit ${exit_with}; 1086 | fi 1087 | 1088 | if [ "$ft_network" == "1" ]; 1089 | then 1090 | say "${e}==>${x} Detecting git/mercurial file structure" 1091 | network=$(cd "$git_root" && git ls-files 2>/dev/null || hg locate 2>/dev/null || echo "") 1092 | if [ "$network" = "" ]; 1093 | then 1094 | network=$(find "$git_root" -type f \ 1095 | -not -path '*/virtualenv/*' \ 1096 | -not -path '*/.virtualenv/*' \ 1097 | -not -path '*/virtualenvs/*' \ 1098 | -not -path '*/.virtualenvs/*' \ 1099 | -not -path '*.png' \ 1100 | -not -path '*.gif' \ 1101 | -not -path '*.jpg' \ 1102 | -not -path '*.jpeg' \ 1103 | -not -path '*.md' \ 1104 | -not -path '*/.env/*' \ 1105 | -not -path '*/.envs/*' \ 1106 | -not -path '*/env/*' \ 1107 | -not -path '*/envs/*' \ 1108 | -not -path '*/.venv/*' \ 1109 | -not -path '*/.venvs/*' \ 1110 | -not -path '*/venv/*' \ 1111 | -not -path '*/venvs/*' \ 1112 | -not -path '*/build/lib/*' \ 1113 | -not -path '*/.git/*' \ 1114 | -not -path '*/.egg-info/*' \ 1115 | -not -path '*/shunit2-2.1.6/*' \ 1116 | -not -path '*/vendor/*' \ 1117 | -not -path '*/js/generated/coverage/*' \ 1118 | -not -path '*/__pycache__/*' \ 1119 | -not -path '*/node_modules/*' \ 1120 | -not -path "*/$bower_components/*" 2>/dev/null || echo '') 1121 | fi 1122 | fi 1123 | 1124 | upload_file=`mktemp /tmp/codecov.XXXXXX` 1125 | adjustments_file=`mktemp /tmp/codecov.adjustments.XXXXXX` 1126 | 1127 | cleanup() { 1128 | rm -f $upload_file $adjustments_file 1129 | } 1130 | 1131 | trap cleanup INT ABRT TERM 1132 | 1133 | if [ "$env" != "" ]; 1134 | then 1135 | inc_env="" 1136 | say "${e}==>${x} Appending build variables" 1137 | for varname in $(echo "$env" | tr ',' ' ') 1138 | do 1139 | if [ "$varname" != "" ]; 1140 | then 1141 | say " ${g}+${x} $varname" 1142 | inc_env="${inc_env}${varname}=$(eval echo "\$${varname}") 1143 | " 1144 | fi 1145 | done 1146 | 1147 | echo "$inc_env<<<<<< ENV" >> $upload_file 1148 | fi 1149 | 1150 | if [ "$ft_network" == "1" ]; 1151 | then 1152 | i="woff|eot|otf" # fonts 1153 | i="$i|gif|png|jpg|jpeg|psd" # images 1154 | i="$i|ptt|pptx|numbers|pages|md|txt|xlsx|docx|doc|pdf" # docs 1155 | i="$i|yml|yaml|.gitignore" # supporting docs 1156 | echo "$network" | grep -vwE "($i)$" >> $upload_file 1157 | echo "<<<<<< network" >> $upload_file 1158 | fi 1159 | 1160 | fr=0 1161 | say "${e}==>${x} Reading reports" 1162 | while IFS='' read -r file; 1163 | do 1164 | # read the coverage file 1165 | if [ "$(echo "$file" | tr -d ' ')" != '' ]; 1166 | then 1167 | if [ -f "$file" ]; 1168 | then 1169 | report_len=$(wc -c < "$file") 1170 | if [ "$report_len" -ne 0 ]; 1171 | then 1172 | say " ${g}+${x} $file ${e}bytes=$(echo "$report_len" | tr -d ' ')${x}" 1173 | # append to to upload 1174 | echo "# path=$(echo "$file" | sed "s|^$git_root/||")" >> $upload_file 1175 | cat "$file" >> $upload_file 1176 | echo "<<<<<< EOF" >> $upload_file 1177 | fr=1 1178 | if [ "$clean" = "1" ]; 1179 | then 1180 | rm "$file" 1181 | fi 1182 | else 1183 | say " ${r}-${x} Skipping empty file $file" 1184 | fi 1185 | else 1186 | say " ${r}-${x} file not found at $file" 1187 | fi 1188 | fi 1189 | done <<< "$(echo -e "$files")" 1190 | 1191 | if [ "$fr" = "0" ]; 1192 | then 1193 | say "${r}-->${x} No coverage data found." 1194 | say " Please visit ${b}http://docs.codecov.io/docs/supported-languages${x}" 1195 | say " search for your projects language to learn how to collect reports." 1196 | exit ${exit_with}; 1197 | fi 1198 | 1199 | if [ "$ft_fix" = "1" ]; 1200 | then 1201 | say "${e}==>${x} Appending adjustments" 1202 | say " ${b}http://docs.codecov.io/docs/fixing-reports${x}" 1203 | 1204 | empty_line='^[[:space:]]*$' 1205 | # // 1206 | syntax_comment='^[[:space:]]*//.*' 1207 | # /* or */ 1208 | syntax_comment_block='^[[:space:]]*(\/\*|\*\/)[[:space:]]*$' 1209 | # { or } 1210 | syntax_bracket='^[[:space:]]*[\{\}][[:space:]]*(//.*)?$' 1211 | # [ or ] 1212 | syntax_list='^[[:space:]]*[][][[:space:]]*(//.*)?$' 1213 | 1214 | skip_dirs="-not -path '*/$bower_components/*' \ 1215 | -not -path '*/node_modules/*'" 1216 | 1217 | cut_and_join() { 1218 | awk 'BEGIN { FS=":" } 1219 | $3 ~ /\/\*/ || $3 ~ /\*\// { print $0 ; next } 1220 | $1!=key { if (key!="") print out ; key=$1 ; out=$1":"$2 ; next } 1221 | { out=out","$2 } 1222 | END { print out }' 2>/dev/null 1223 | } 1224 | 1225 | if echo "$network" | grep -m1 '.kt$' 1>/dev/null; 1226 | then 1227 | # skip brackets and comments 1228 | find "$git_root" -type f \ 1229 | -name '*.kt' \ 1230 | -exec \ 1231 | grep -nIHE -e $syntax_bracket \ 1232 | -e $syntax_comment_block {} \; \ 1233 | | cut_and_join \ 1234 | >> $adjustments_file \ 1235 | || echo '' 1236 | 1237 | # last line in file 1238 | find "$git_root" -type f \ 1239 | -name '*.kt' -exec \ 1240 | wc -l {} \; \ 1241 | | while read l; do echo "EOF: $l"; done \ 1242 | 2>/dev/null \ 1243 | >> $adjustments_file \ 1244 | || echo '' 1245 | 1246 | fi 1247 | 1248 | if echo "$network" | grep -m1 '.go$' 1>/dev/null; 1249 | then 1250 | # skip empty lines, comments, and brackets 1251 | find "$git_root" -type f \ 1252 | -not -path '*/vendor/*' \ 1253 | -name '*.go' \ 1254 | -exec \ 1255 | grep -nIHE \ 1256 | -e $empty_line \ 1257 | -e $syntax_comment \ 1258 | -e $syntax_comment_block \ 1259 | -e $syntax_bracket \ 1260 | {} \; \ 1261 | | cut_and_join \ 1262 | >> $adjustments_file \ 1263 | || echo '' 1264 | fi 1265 | 1266 | if echo "$network" | grep -m1 '.jsx$' 1>/dev/null; 1267 | then 1268 | # skip empty lines, comments, and brackets 1269 | find "$git_root" -type f \ 1270 | -name '*.jsx' \ 1271 | $skip_dirs \ 1272 | -exec \ 1273 | grep -nIHE \ 1274 | -e $empty_line \ 1275 | -e $syntax_comment \ 1276 | -e $syntax_bracket \ 1277 | {} \; \ 1278 | | cut_and_join \ 1279 | >> $adjustments_file \ 1280 | || echo '' 1281 | fi 1282 | 1283 | if echo "$network" | grep -m1 '.php$' 1>/dev/null; 1284 | then 1285 | # skip empty lines, comments, and brackets 1286 | find "$git_root" -type f \ 1287 | -not -path '*/vendor/*' \ 1288 | -name '*.php' \ 1289 | -exec \ 1290 | grep -nIHE \ 1291 | -e $syntax_list \ 1292 | -e $syntax_bracket \ 1293 | -e '^[[:space:]]*\);[[:space:]]*(//.*)?$' \ 1294 | {} \; \ 1295 | | cut_and_join \ 1296 | >> $adjustments_file \ 1297 | || echo '' 1298 | fi 1299 | 1300 | if echo "$network" | grep -m1 '\(.cpp\|.h\|.cxx\|.c\|.hpp\|.m\)$' 1>/dev/null; 1301 | then 1302 | # skip brackets 1303 | find "$git_root" -type f \ 1304 | $skip_dirs \ 1305 | \( \ 1306 | -name '*.h' \ 1307 | -or -name '*.cpp' \ 1308 | -or -name '*.cxx' \ 1309 | -or -name '*.m' \ 1310 | -or -name '*.c' \ 1311 | -or -name '*.hpp' \ 1312 | \) -exec \ 1313 | grep -nIHE \ 1314 | -e $empty_line \ 1315 | -e $syntax_bracket \ 1316 | -e '// LCOV_EXCL' \ 1317 | {} \; \ 1318 | | cut_and_join \ 1319 | >> $adjustments_file \ 1320 | || echo '' 1321 | 1322 | # skip brackets 1323 | find "$git_root" -type f \ 1324 | $skip_dirs \ 1325 | \( \ 1326 | -name '*.h' \ 1327 | -or -name '*.cpp' \ 1328 | -or -name '*.cxx' \ 1329 | -or -name '*.m' \ 1330 | -or -name '*.c' \ 1331 | -or -name '*.hpp' \ 1332 | \) -exec \ 1333 | grep -nIH '// LCOV_EXCL' \ 1334 | {} \; \ 1335 | >> $adjustments_file \ 1336 | || echo '' 1337 | 1338 | fi 1339 | 1340 | found=$(cat $adjustments_file | tr -d ' ') 1341 | 1342 | if [ "$found" != "" ]; 1343 | then 1344 | say " ${g}+${x} Found adjustments" 1345 | echo "# path=fixes" >> $upload_file 1346 | cat $adjustments_file >> $upload_file 1347 | echo "<<<<<< EOF" >> $upload_file 1348 | rm -rf $adjustments_file 1349 | else 1350 | say " ${e}->${x} No adjustments found" 1351 | fi 1352 | fi 1353 | 1354 | if [ "$url_o" != "" ]; 1355 | then 1356 | url="$url_o" 1357 | fi 1358 | 1359 | if [ "$dump" != "0" ]; 1360 | then 1361 | # trim whitespace from query 1362 | echo "$url/upload/v4?$(echo "package=bash-$VERSION&token=$token&$query" | tr -d ' ')" 1363 | cat $upload_file 1364 | else 1365 | 1366 | query=$(echo "${query}" | tr -d ' ') 1367 | say "${e}==>${x} Uploading reports" 1368 | say " ${e}url:${x} $url" 1369 | say " ${e}query:${x} $query" 1370 | 1371 | # now add token to query 1372 | query=$(echo "package=bash-$VERSION&token=$token&$query" | tr -d ' ') 1373 | 1374 | if [ "$ft_s3" = "1" ]; 1375 | then 1376 | i="0" 1377 | while [ $i -lt 4 ] 1378 | do 1379 | i=$[$i+1] 1380 | say " ${e}->${x} Pinging Codecov" 1381 | res=$(curl $curl_s -X POST $curlargs $cacert "$url/upload/v4?$query" -H 'Accept: text/plain' || true) 1382 | # a good replay is "https://codecov.io" + "\n" + "https://codecov.s3.amazonaws.com/..." 1383 | status=$(echo "$res" | head -1 | grep 'HTTP ' | cut -d' ' -f2) 1384 | if [ "$status" = "" ]; 1385 | then 1386 | s3target=$(echo "$res" | sed -n 2p) 1387 | say " ${e}->${x} Uploading to S3 $(echo "$s3target" | cut -c1-32)" 1388 | s3=$(curl $curl_s -fiX PUT $curlawsargs \ 1389 | --data-binary @$upload_file \ 1390 | -H 'Content-Type: text/plain' \ 1391 | -H 'x-amz-acl: public-read' \ 1392 | -H 'x-amz-storage-class: REDUCED_REDUNDANCY' \ 1393 | "$s3target" || true) 1394 | if [ "$s3" != "" ]; 1395 | then 1396 | say " ${g}->${x} View reports at ${b}$(echo "$res" | sed -n 1p)${x}" 1397 | exit 0 1398 | else 1399 | say " ${r}X>${x} Failed to upload to S3" 1400 | fi 1401 | elif [ "$status" = "400" ]; 1402 | then 1403 | # 400 Error 1404 | say "${g}${res}${x}" 1405 | exit ${exit_with} 1406 | fi 1407 | say " ${e}->${x} Sleeping for 30s and trying again..." 1408 | sleep 30 1409 | done 1410 | fi 1411 | 1412 | say " ${e}->${x} Uploading to Codecov" 1413 | i="0" 1414 | while [ $i -lt 4 ] 1415 | do 1416 | i=$[$i+1] 1417 | 1418 | res=$(curl $curl_s -X POST $curlargs $cacert --data-binary @$upload_file "$url/upload/v2?$query" -H 'Accept: text/plain' || echo 'HTTP 500') 1419 | # HTTP 200 1420 | # http://.... 1421 | status=$(echo "$res" | head -1 | cut -d' ' -f2) 1422 | if [ "$status" = "" ]; 1423 | then 1424 | say " View reports at ${b}$(echo "$res" | head -2 | tail -1)${x}" 1425 | exit 0 1426 | 1427 | elif [ "${status:0:1}" = "5" ]; 1428 | then 1429 | say " ${e}->${x} Sleeping for 30s and trying again..." 1430 | sleep 30 1431 | 1432 | else 1433 | say " ${g}${res}${x}" 1434 | exit 0 1435 | exit ${exit_with} 1436 | fi 1437 | 1438 | done 1439 | 1440 | fi 1441 | 1442 | say " ${r}X> Failed to upload coverage reports${x}" 1443 | exit ${exit_with} 1444 | 1445 | # EOF 1446 | --------------------------------------------------------------------------------