├── .github └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE.txt ├── README.md ├── build.gradle.kts ├── clikt-mordant-markdown ├── README.md ├── api │ └── clikt-mordant-markdown.api ├── build.gradle.kts ├── gradle.properties └── src │ └── commonMain │ └── kotlin │ └── com │ └── github │ └── ajalt │ └── clikt │ ├── core │ └── MordantMarkdownContext.kt │ └── output │ └── MordantMarkdownHelpFormatter.kt ├── clikt-mordant ├── README.md ├── api │ └── clikt-mordant.api ├── build.gradle.kts ├── gradle.properties └── src │ └── commonMain │ └── kotlin │ └── com │ └── github │ └── ajalt │ └── clikt │ ├── command │ ├── ChainedCliktCommand.kt │ ├── SuspendingCliktCommand.kt │ └── SuspendingNoOpCliktCommand.kt │ ├── core │ ├── CliktCommand.kt │ ├── MordantContext.kt │ └── NoOpCliktCommand.kt │ ├── output │ └── MordantHelpFormatter.kt │ ├── parameters │ ├── options │ │ └── PromptOptions.kt │ └── transform │ │ └── MordantTransformContext.kt │ └── testing │ └── CliktTesting.kt ├── clikt ├── README.md ├── api │ └── clikt.api ├── build.gradle.kts ├── gradle.properties └── src │ ├── commonMain │ └── kotlin │ │ └── com │ │ └── github │ │ └── ajalt │ │ └── clikt │ │ ├── command │ │ ├── CoreChainedCliktCommand.kt │ │ ├── CoreSuspendingCliktCommand.kt │ │ └── CoreSuspendingNoOpCliktCommand.kt │ │ ├── completion │ │ ├── BashCompletionGenerator.kt │ │ ├── CompletionBuiltins.kt │ │ ├── CompletionCandidates.kt │ │ ├── CompletionGenerator.kt │ │ └── FishCompletionGenerator.kt │ │ ├── core │ │ ├── BaseCliktCommand.kt │ │ ├── Context.kt │ │ ├── CoreCliktCommand.kt │ │ ├── CoreNoOpCliktCommand.kt │ │ ├── JaroWinkerSimilarity.kt │ │ ├── ParameterHolder.kt │ │ └── exceptions.kt │ │ ├── internal │ │ ├── Finalization.kt │ │ └── Util.kt │ │ ├── output │ │ ├── AbstractHelpFormatter.kt │ │ ├── HelpFormatter.kt │ │ ├── Localization.kt │ │ ├── PlaintextHelpFormatter.kt │ │ └── text.kt │ │ ├── parameters │ │ ├── arguments │ │ │ └── Argument.kt │ │ ├── groups │ │ │ ├── ChoiceGroup.kt │ │ │ ├── CoOccurringOptionGroup.kt │ │ │ ├── MutuallyExclusiveOption.kt │ │ │ └── ParameterGroup.kt │ │ ├── internal │ │ │ └── NullableLateinit.kt │ │ ├── options │ │ │ ├── Convert.kt │ │ │ ├── EagerOption.kt │ │ │ ├── FlagOption.kt │ │ │ ├── Option.kt │ │ │ ├── OptionWithValues.kt │ │ │ ├── TransformAll.kt │ │ │ ├── TransformEach.kt │ │ │ ├── Validate.kt │ │ │ └── ValueWithDefault.kt │ │ ├── transform │ │ │ └── TransformContext.kt │ │ └── types │ │ │ ├── boolean.kt │ │ │ ├── choice.kt │ │ │ ├── double.kt │ │ │ ├── enum.kt │ │ │ ├── float.kt │ │ │ ├── int.kt │ │ │ ├── long.kt │ │ │ ├── range.kt │ │ │ ├── uint.kt │ │ │ └── ulong.kt │ │ ├── parsers │ │ ├── CommandLineParser.kt │ │ ├── Invocation.kt │ │ ├── ParserInternals.kt │ │ └── atfile.kt │ │ └── sources │ │ ├── ChainedValueSource.kt │ │ ├── MapValueSource.kt │ │ └── ValueSource.kt │ └── jvmMain │ └── kotlin │ └── com │ └── github │ └── ajalt │ └── clikt │ ├── core │ └── ContextJvm.kt │ ├── parameters │ └── types │ │ ├── file.kt │ │ ├── inputStream.kt │ │ ├── outputStream.kt │ │ └── path.kt │ └── sources │ └── PropertiesValueSource.kt ├── docs ├── advanced.md ├── arguments.md ├── autocomplete.md ├── commands.md ├── css │ └── extra.css ├── documenting.md ├── exceptions.md ├── img │ ├── animation.png │ ├── favicon.ico │ ├── readme_screenshot1.png │ ├── readme_screenshot2.png │ ├── readme_screenshot3.png │ ├── wordmark.svg │ ├── wordmark_small.svg │ └── wordmark_small_dark.svg ├── migration.md ├── options.md ├── parameters.md ├── quickstart.md ├── testing.md └── whyclikt.md ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── mkdocs.yml ├── prepare_docs.sh ├── runsample ├── runsample.bat ├── samples ├── README.md ├── aliases │ ├── README.md │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── kotlin │ │ └── com │ │ └── github │ │ └── ajalt │ │ └── clikt │ │ └── samples │ │ └── aliases │ │ ├── aliases.cfg │ │ └── main.kt ├── build.gradle.kts ├── copy │ ├── README.md │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── kotlin │ │ └── com │ │ └── github │ │ └── ajalt │ │ └── clikt │ │ └── samples │ │ └── copy │ │ └── main.kt ├── helpformat │ ├── README.md │ ├── build.gradle.kts │ ├── screenshot.png │ └── src │ │ └── main │ │ └── kotlin │ │ └── com │ │ └── github │ │ └── ajalt │ │ └── clikt │ │ └── samples │ │ └── helpformat │ │ └── main.kt ├── json │ ├── README.md │ ├── build.gradle.kts │ ├── config.json │ └── src │ │ └── main │ │ └── kotlin │ │ └── com │ │ └── github │ │ └── ajalt │ │ └── clikt │ │ └── samples │ │ └── json │ │ ├── JsonValueSource.kt │ │ └── main.kt ├── plugins │ ├── README.md │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── kotlin │ │ └── com │ │ └── github │ │ └── ajalt │ │ └── clikt │ │ └── samples │ │ └── plugins │ │ ├── clone.kt │ │ ├── commit.kt │ │ ├── delete.kt │ │ ├── main.kt │ │ └── setuser.kt ├── repo │ ├── README.md │ ├── build.gradle.kts │ └── src │ │ └── main │ │ └── kotlin │ │ └── com │ │ └── github │ │ └── ajalt │ │ └── clikt │ │ └── samples │ │ └── repo │ │ └── main.kt └── validation │ ├── README.md │ ├── build.gradle.kts │ └── src │ └── main │ └── kotlin │ └── com │ └── github │ └── ajalt │ └── clikt │ └── samples │ └── validation │ └── main.kt ├── settings.gradle.kts └── test ├── README.md ├── api └── test.api ├── build.gradle.kts └── src ├── commonTest └── kotlin │ └── com │ └── github │ └── ajalt │ └── clikt │ ├── command │ ├── ChainedCliktCommandTest.kt │ └── SuspendingCliktCommandTest.kt │ ├── completion │ ├── BashCompletionTest.kt │ ├── CompletionTestBase.kt │ ├── EnvvarCompletionTest.kt │ └── FishCompletionTest.kt │ ├── core │ ├── CliktCommandTest.kt │ ├── ContextTest.kt │ └── ExceptionsTest.kt │ ├── output │ ├── MordantHelpFormatterTest.kt │ ├── MordantMarkdownHelpFormatterTest.kt │ └── PlaintextHelpFormatterTest.kt │ ├── parameters │ ├── ArgumentTest.kt │ ├── EagerOptionsTest.kt │ ├── EnvvarInferTest.kt │ ├── EnvvarOptionsTest.kt │ ├── MultiUsageErrorTest.kt │ ├── OptionSwitchTest.kt │ ├── OptionTest.kt │ ├── PromptOptionsTest.kt │ ├── SubcommandTest.kt │ ├── VarargOptionsTest.kt │ ├── groups │ │ └── OptionGroupsTest.kt │ └── types │ │ ├── BooleanTest.kt │ │ ├── ChoiceTest.kt │ │ ├── DoubleTest.kt │ │ ├── EnumTest.kt │ │ ├── FloatTest.kt │ │ ├── IntTest.kt │ │ ├── LongTest.kt │ │ ├── RangeTest.kt │ │ ├── UIntTest.kt │ │ └── ULongTest.kt │ ├── parsers │ └── AtFileTest.kt │ ├── sources │ ├── ChainedValueSourceTest.kt │ └── MapValueSourceTest.kt │ └── testing │ ├── TestCommand.kt │ ├── TestSource.kt │ ├── TestingUtilsTest.kt │ └── utils.kt └── jvmTest └── kotlin └── com └── github └── ajalt └── clikt ├── core └── ContextJvmTest.kt ├── parameters └── types │ ├── FileTest.kt │ ├── InputStreamTest.kt │ ├── OutputStreamTest.kt │ └── PathTest.kt └── sources └── PropertiesValueSourceTest.kt /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | paths-ignore: 6 | - 'docs/**' 7 | - 'samples/**' 8 | - '*.md' 9 | push: 10 | branches: 11 | - 'master' 12 | paths-ignore: 13 | - 'docs/**' 14 | - 'samples/**' 15 | - '*.md' 16 | 17 | jobs: 18 | test: 19 | strategy: 20 | matrix: 21 | os: [ macos-latest, windows-latest, ubuntu-latest ] 22 | include: 23 | - os: macos-latest 24 | TEST_TASK: macosArm64Test 25 | - os: windows-latest 26 | TEST_TASK: mingwX64Test 27 | - os: ubuntu-latest 28 | TEST_TASK: apiCheck check 29 | runs-on: ${{matrix.os}} 30 | steps: 31 | - uses: actions/checkout@v4 32 | - uses: actions/setup-java@v4 33 | with: 34 | distribution: 'zulu' 35 | java-version: 17 36 | - uses: gradle/actions/setup-gradle@v4 37 | - run: ./gradlew ${{matrix.TEST_TASK}} --stacktrace 38 | - name: Upload the build report 39 | if: failure() 40 | uses: actions/upload-artifact@master 41 | with: 42 | name: build-report-${{ matrix.os }} 43 | path: '**/build/reports' 44 | publish: 45 | needs: test 46 | runs-on: macos-latest 47 | if: ${{ github.ref == 'refs/heads/master' && github.repository == 'ajalt/clikt' }} 48 | steps: 49 | - uses: actions/checkout@v4 50 | - uses: actions/setup-java@v4 51 | with: 52 | distribution: 'zulu' 53 | java-version: 17 54 | - name: Deploy to sonatype 55 | uses: gradle/actions/setup-gradle@v4 56 | # disable configuration cache due to https://github.com/gradle/gradle/issues/22779 57 | - run: ./gradlew publishToMavenCentral -PsnapshotVersion=true --no-configuration-cache 58 | env: 59 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ORG_GRADLE_PROJECT_mavenCentralPassword }} 60 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ORG_GRADLE_PROJECT_signingInMemoryKey }} 61 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ORG_GRADLE_PROJECT_mavenCentralUsername }} 62 | 63 | env: 64 | GRADLE_OPTS: -Dorg.gradle.configureondemand=true -Dorg.gradle.parallel=true -Dkotlin.incremental=false -Dorg.gradle.project.kotlin.incremental.multiplatform=false -Dorg.gradle.project.kotlin.native.disableCompilerDaemon=true -Dorg.gradle.jvmargs="-Xmx12g -Dfile.encoding=UTF-8" 65 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | push: 5 | tags: 6 | - '**' 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | if: ${{ github.repository == 'ajalt/clikt' }} 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-java@v4 15 | with: 16 | distribution: 'zulu' 17 | java-version: 17 18 | - uses: gradle/actions/setup-gradle@v4 19 | # disable configuration cache due to https://github.com/gradle/gradle/issues/22779 20 | - run: ./gradlew publishToMavenCentral --no-configuration-cache 21 | env: 22 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.ORG_GRADLE_PROJECT_mavenCentralPassword }} 23 | ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.ORG_GRADLE_PROJECT_signingInMemoryKey }} 24 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.ORG_GRADLE_PROJECT_mavenCentralUsername }} 25 | 26 | env: 27 | # configureondemand=false to work around KT-51763 28 | GRADLE_OPTS: -Dorg.gradle.configureondemand=false -Dorg.gradle.parallel=true -Dkotlin.incremental=false -Dorg.gradle.project.kotlin.incremental.multiplatform=false -Dorg.gradle.project.kotlin.native.disableCompilerDaemon=true -Dorg.gradle.jvmargs="-Xmx12g -Dfile.encoding=UTF-8" 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/ 5 | .DS_Store 6 | build/ 7 | captures/ 8 | .externalNativeBuild 9 | out/ 10 | docs/api/ 11 | docs/changelog.md 12 | docs/index.md 13 | kotlin-js-store/ 14 | .kotlin/ 15 | site/ 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 |

5 | 6 | Clikt *(pronounced "clicked")* is a multiplatform Kotlin library that makes writing command line 7 | interfaces simple and intuitive. It's the "Command Line Interface for Kotlin". 8 | 9 | It is designed to make the process of writing command line tools effortless 10 | while supporting a wide variety of use cases and allowing advanced 11 | customization when needed. 12 | 13 | Clikt has: 14 | 15 | * arbitrary nesting of commands 16 | * composable, type safe parameter values 17 | * generation of help output and shell autocomplete scripts 18 | * multiplatform packages for JVM, Node.js, and native Linux, Windows and macOS 19 | 20 | What does it look like? Here's a complete example of a simple Clikt program: 21 | 22 | ```kotlin 23 | class Hello : CliktCommand() { 24 | val count: Int by option().int().default(1).help("Number of greetings") 25 | val name: String by option().prompt("Your name").help("The person to greet") 26 | 27 | override fun run() { 28 | repeat(count) { 29 | echo("Hello $name!") 30 | } 31 | } 32 | } 33 | 34 | fun main(args: Array) = Hello().main(args) 35 | ``` 36 | 37 | And here's what it looks like when run: 38 | 39 |

40 | 41 | The help page is generated for you: 42 | 43 |

44 | 45 | Errors are also taken care of: 46 | 47 |

48 | 49 | 50 | ## Documentation 51 | 52 | The full documentation can be found on [the website](https://ajalt.github.io/clikt). 53 | 54 | There are also a number of [sample applications](samples). You can run 55 | them with the included [`runsample` script](runsample). 56 | 57 | ## Installation 58 | 59 | Clikt is distributed through [Maven Central](https://search.maven.org/artifact/com.github.ajalt.clikt/clikt). 60 | 61 | ```kotlin 62 | dependencies { 63 | implementation("com.github.ajalt.clikt:clikt:5.0.3") 64 | 65 | // optional support for rendering markdown in help messages 66 | implementation("com.github.ajalt.clikt:clikt-markdown:5.0.3") 67 | } 68 | ``` 69 | 70 | There is also a smaller core module available. [See the docs for details](https://ajalt.github.io/clikt/advanced/#core-module). 71 | 72 | 73 | ###### If you're using Maven instead of Gradle, use `clikt-jvm` 74 | 75 | #### Multiplatform 76 | 77 | Clikt supports most multiplatform targets. 78 | [See the docs](https://ajalt.github.io/clikt/advanced/#multiplatform-support) 79 | for more information about functionality supported on each target. You'll need to use Gradle 6 or 80 | newer. 81 | 82 | #### Snapshots 83 | 84 |
85 | Snapshot builds are also available 86 | 87 | 88 | 89 |

90 | You'll need to add the Sonatype snapshots repository: 91 | 92 | ```kotlin 93 | repositories { 94 | maven { 95 | url = uri("https://oss.sonatype.org/content/repositories/snapshots/") 96 | } 97 | } 98 | ``` 99 |

100 |
101 | 102 | ## License 103 | 104 | Copyright 2018 AJ Alt 105 | 106 | Licensed under the Apache License, Version 2.0 (the "License"); 107 | you may not use this file except in compliance with the License. 108 | You may obtain a copy of the License at 109 | 110 | http://www.apache.org/licenses/LICENSE-2.0 111 | 112 | Unless required by applicable law or agreed to in writing, software 113 | distributed under the License is distributed on an "AS IS" BASIS, 114 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 115 | See the License for the specific language governing permissions and 116 | limitations under the License. 117 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.dokka.gradle.DokkaMultiModuleTask 2 | import org.jetbrains.dokka.gradle.DokkaTask 3 | import org.jetbrains.dokka.gradle.DokkaTaskPartial 4 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 5 | import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile 6 | 7 | 8 | plugins { 9 | kotlin("multiplatform").version(libs.versions.kotlin).apply(false) 10 | alias(libs.plugins.publish).apply(false) 11 | alias(libs.plugins.dokka) 12 | alias(libs.plugins.kotlinBinaryCompatibilityValidator) 13 | } 14 | 15 | apiValidation { 16 | // https://github.com/Kotlin/binary-compatibility-validator/issues/3 17 | project("samples").subprojects.mapTo(ignoredProjects) { it.name } 18 | } 19 | 20 | fun getPublishVersion(): String { 21 | val version = project.property("VERSION_NAME").toString() 22 | // Call gradle with -PsnapshotVersion to set the version as a snapshot. 23 | if (!project.hasProperty("snapshotVersion")) return version 24 | val buildNumber = System.getenv("GITHUB_RUN_NUMBER") ?: "0" 25 | return "$version.$buildNumber-SNAPSHOT" 26 | } 27 | 28 | 29 | private val dokkaConfig = mapOf( 30 | "org.jetbrains.dokka.base.DokkaBase" to """{ 31 | "footerMessage": "Copyright © 2018 AJ Alt" 32 | }""" 33 | ) 34 | 35 | subprojects { 36 | project.setProperty("VERSION_NAME", getPublishVersion()) 37 | 38 | tasks.withType().configureEach { 39 | compilerOptions { 40 | jvmTarget.set(JvmTarget.JVM_1_8) 41 | } 42 | } 43 | tasks.withType().configureEach { 44 | options.release.set(8) 45 | } 46 | pluginManager.withPlugin("com.vanniktech.maven.publish") { 47 | apply(plugin = "org.jetbrains.dokka") 48 | } 49 | tasks.withType().configureEach { 50 | dokkaSourceSets.configureEach { 51 | reportUndocumented.set(false) 52 | skipDeprecated.set(false) 53 | } 54 | } 55 | tasks.withType().configureEach { 56 | pluginsMapConfiguration.set(dokkaConfig) 57 | } 58 | } 59 | 60 | tasks.named("dokkaHtmlMultiModule") { 61 | outputDirectory.set(rootProject.rootDir.resolve("docs/api")) 62 | pluginsMapConfiguration.set(dokkaConfig) 63 | } 64 | -------------------------------------------------------------------------------- /clikt-mordant-markdown/README.md: -------------------------------------------------------------------------------- 1 | # Module clikt-mordant-markdown 2 | 3 | This module provides the `MordantMarkdownHelpFormatter` that renders text as markdown. 4 | 5 | ## Installation 6 | 7 | ```kotlin 8 | dependencies { 9 | implementation("com.github.ajalt.clikt:clikt-markdown:$cliktVersion") 10 | } 11 | ``` 12 | -------------------------------------------------------------------------------- /clikt-mordant-markdown/api/clikt-mordant-markdown.api: -------------------------------------------------------------------------------- 1 | public final class com/github/ajalt/clikt/core/MordantMarkdownContextKt { 2 | public static final fun installMordantMarkdown (Lcom/github/ajalt/clikt/core/BaseCliktCommand;)V 3 | } 4 | 5 | public class com/github/ajalt/clikt/output/MordantMarkdownHelpFormatter : com/github/ajalt/clikt/output/MordantHelpFormatter { 6 | public fun (Lcom/github/ajalt/clikt/core/Context;Ljava/lang/String;ZZ)V 7 | public synthetic fun (Lcom/github/ajalt/clikt/core/Context;Ljava/lang/String;ZZILkotlin/jvm/internal/DefaultConstructorMarker;)V 8 | public fun renderWrappedText (Ljava/lang/String;)Lcom/github/ajalt/mordant/rendering/Widget; 9 | } 10 | 11 | -------------------------------------------------------------------------------- /clikt-mordant-markdown/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.dokka.gradle.DokkaTaskPartial 2 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 3 | 4 | plugins { 5 | kotlin("multiplatform") 6 | alias(libs.plugins.publish) 7 | } 8 | 9 | kotlin { 10 | jvm() 11 | 12 | js { nodejs() } 13 | @OptIn(ExperimentalWasmDsl::class) 14 | wasmJs { nodejs() } 15 | 16 | linuxX64() 17 | linuxArm64() 18 | mingwX64() 19 | macosX64() 20 | macosArm64() 21 | 22 | iosArm64() 23 | iosX64() 24 | 25 | sourceSets { 26 | val commonMain by getting { 27 | dependencies { 28 | api(project(":clikt")) 29 | api(project(":clikt-mordant")) 30 | api(libs.mordant) 31 | api(libs.mordant.markdown) 32 | } 33 | } 34 | 35 | val commonTest by getting { 36 | dependencies { 37 | api(kotlin("test")) 38 | api(libs.kotest) 39 | api(libs.coroutines.core) 40 | api(libs.coroutines.test) 41 | } 42 | } 43 | 44 | val jvmTest by getting { 45 | dependencies { 46 | api(libs.systemrules) 47 | api(libs.jimfs) 48 | } 49 | } 50 | } 51 | } 52 | 53 | tasks.withType { 54 | dokkaSourceSets.configureEach { 55 | includes.from("README.md") 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /clikt-mordant-markdown/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=clikt-markdown 2 | POM_NAME=Clikt Markdown Support 3 | -------------------------------------------------------------------------------- /clikt-mordant-markdown/src/commonMain/kotlin/com/github/ajalt/clikt/core/MordantMarkdownContext.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.core 2 | 3 | import com.github.ajalt.clikt.output.MordantMarkdownHelpFormatter 4 | 5 | /** 6 | * Set up this command's context to use Mordant for rendering output as Markdown. 7 | */ 8 | fun BaseCliktCommand<*>.installMordantMarkdown() { 9 | installMordant(force = true) 10 | configureContext { 11 | helpFormatter = { MordantMarkdownHelpFormatter(it) } 12 | } 13 | } 14 | 15 | -------------------------------------------------------------------------------- /clikt-mordant-markdown/src/commonMain/kotlin/com/github/ajalt/clikt/output/MordantMarkdownHelpFormatter.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.output 2 | 3 | import com.github.ajalt.clikt.core.Context 4 | import com.github.ajalt.mordant.markdown.Markdown 5 | import com.github.ajalt.mordant.rendering.Widget 6 | 7 | /** 8 | * A [HelpFormatter] that uses Mordant to render its output as GitHub Flavored Markdown. 9 | * 10 | * To customize help text, you can create a subclass and set it as the `helpFormatter` on your 11 | * command's context. 12 | */ 13 | open class MordantMarkdownHelpFormatter( 14 | /** 15 | * The current command's context. 16 | */ 17 | context: Context, 18 | /** 19 | * The string to show before the names of required options, or null to not show a mark. 20 | */ 21 | requiredOptionMarker: String? = null, 22 | /** 23 | * If true, the default values will be shown in the help text for parameters that have them. 24 | */ 25 | showDefaultValues: Boolean = false, 26 | /** 27 | * If true, a tag indicating the parameter is required will be shown after the description of 28 | * required parameters. 29 | */ 30 | showRequiredTag: Boolean = false, 31 | ) : MordantHelpFormatter( 32 | context, 33 | requiredOptionMarker, 34 | showDefaultValues, 35 | showRequiredTag 36 | ) { 37 | override fun renderWrappedText(text: String): Widget = Markdown(text, showHtml = true) 38 | } 39 | -------------------------------------------------------------------------------- /clikt-mordant/README.md: -------------------------------------------------------------------------------- 1 | # Module clikt-mordant 2 | 3 | This is the full Clikt module that uses Mordant for rendering help messages and interacting with the 4 | system. 5 | 6 | ## Installation 7 | 8 | ```kotlin 9 | dependencies { 10 | implementation("com.github.ajalt.clikt:clikt:$cliktVersion") 11 | } 12 | ``` 13 | -------------------------------------------------------------------------------- /clikt-mordant/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.dokka.gradle.DokkaTaskPartial 2 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 3 | 4 | plugins { 5 | kotlin("multiplatform") 6 | alias(libs.plugins.publish) 7 | } 8 | 9 | kotlin { 10 | jvm() 11 | 12 | js { nodejs() } 13 | @OptIn(ExperimentalWasmDsl::class) 14 | wasmJs { nodejs() } 15 | 16 | linuxX64() 17 | linuxArm64() 18 | mingwX64() 19 | macosX64() 20 | macosArm64() 21 | 22 | iosArm64() 23 | iosX64() 24 | iosSimulatorArm64() 25 | watchosX64() 26 | watchosArm32() 27 | watchosArm64() 28 | watchosSimulatorArm64() 29 | tvosX64() 30 | tvosArm64() 31 | tvosSimulatorArm64() 32 | 33 | sourceSets { 34 | val commonMain by getting { 35 | dependencies { 36 | api(project(":clikt")) 37 | api(libs.mordant) 38 | } 39 | } 40 | } 41 | } 42 | 43 | tasks.withType { 44 | dokkaSourceSets.configureEach { 45 | includes.from("README.md") 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /clikt-mordant/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=clikt 2 | POM_NAME=Clikt 3 | -------------------------------------------------------------------------------- /clikt-mordant/src/commonMain/kotlin/com/github/ajalt/clikt/command/SuspendingNoOpCliktCommand.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.command 2 | 3 | /** 4 | * A [SuspendingCliktCommand] that has a default implementation of 5 | * [run][SuspendingCliktCommand.run] that is a no-op. 6 | */ 7 | abstract class SuspendingNoOpCliktCommand( 8 | /** 9 | * The name of the program to use in the help output. If not given, it is inferred from the 10 | * class name. 11 | */ 12 | name: String? = null, 13 | ) : SuspendingCliktCommand(name) { 14 | override suspend fun run() = Unit 15 | } 16 | -------------------------------------------------------------------------------- /clikt-mordant/src/commonMain/kotlin/com/github/ajalt/clikt/core/CliktCommand.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.core 2 | 3 | import com.github.ajalt.clikt.parameters.arguments.argument 4 | import com.github.ajalt.clikt.parameters.options.option 5 | 6 | /** 7 | * The [CliktCommand] is the core of command line interfaces in Clikt. 8 | * 9 | * Command line interfaces created by creating a subclass of [CliktCommand] with properties defined with 10 | * [option] and [argument]. You can then parse `argv` by calling [main], which will take care of printing 11 | * errors and help to the user. If you want to handle output yourself, you can use [parse] instead. 12 | * 13 | * Once the command line has been parsed and all the parameters are populated, [run] is called. 14 | */ 15 | abstract class CliktCommand( 16 | /** 17 | * The name of the program to use in the help output. If not given, it is inferred from the 18 | * class name. 19 | */ 20 | name: String? = null, 21 | ) : CoreCliktCommand(name) { 22 | init { 23 | installMordant() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /clikt-mordant/src/commonMain/kotlin/com/github/ajalt/clikt/core/MordantContext.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.core 2 | 3 | import com.github.ajalt.clikt.core.Context.Companion.TERMINAL_KEY 4 | import com.github.ajalt.clikt.output.MordantHelpFormatter 5 | import com.github.ajalt.mordant.platform.MultiplatformSystem 6 | import com.github.ajalt.mordant.rendering.Theme 7 | import com.github.ajalt.mordant.terminal.Terminal 8 | 9 | /** 10 | * The terminal to used to read and write messages. 11 | */ 12 | val Context.terminal: Terminal 13 | get() = findObject(TERMINAL_KEY) ?: Terminal().also { 14 | // Set the terminal on the root so that we don't create multiple 15 | selfAndAncestors().last().data[TERMINAL_KEY] = it 16 | } 17 | 18 | /** The current terminal's theme */ 19 | val Context.theme: Theme get() = terminal.theme 20 | 21 | /** 22 | * The terminal that will handle reading and writing text. 23 | */ 24 | var Context.Builder.terminal: Terminal 25 | get() = data[TERMINAL_KEY] as? Terminal 26 | ?: parent?.terminal 27 | ?: Terminal().also { data[TERMINAL_KEY] = it } 28 | set(value) { 29 | data[TERMINAL_KEY] = value 30 | } 31 | 32 | 33 | internal fun Context.selfAndAncestors() = generateSequence(this) { it.parent } 34 | 35 | 36 | /** 37 | * A shortcut for accessing the terminal from the [currentContext][CliktCommand.currentContext] 38 | */ 39 | val BaseCliktCommand<*>.terminal: Terminal 40 | get() = currentContext.terminal 41 | 42 | /** 43 | * Set up this command's context to use Mordant for rendering. 44 | * 45 | * This is done automatically for [CliktCommand]s, but you can call this if you are making a custom 46 | * command class. 47 | * 48 | * @param force If true, install mordant even if the parent command has already installed a help 49 | * formatter. 50 | */ 51 | fun BaseCliktCommand<*>.installMordant(force: Boolean = false) { 52 | configureContext { 53 | // Only install mordant if we're the parent command so that we don't override inherited 54 | // settings. 55 | if (!force && parent != null) return@configureContext 56 | helpFormatter = { MordantHelpFormatter(it) } 57 | readEnvvar = { MultiplatformSystem.readEnvironmentVariable(it) } 58 | readArgumentFile = { MultiplatformSystem.readFileAsUtf8(it) ?: throw FileNotFound(it) } 59 | exitProcess = { MultiplatformSystem.exitProcess(it) } 60 | echoMessage = { context: Context, message: Any?, trailingNewline: Boolean, err: Boolean -> 61 | if (trailingNewline) { 62 | context.terminal.println(message, stderr = err) 63 | } else { 64 | context.terminal.print(message, stderr = err) 65 | } 66 | } 67 | } 68 | } 69 | 70 | -------------------------------------------------------------------------------- /clikt-mordant/src/commonMain/kotlin/com/github/ajalt/clikt/core/NoOpCliktCommand.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.core 2 | 3 | /** 4 | * A [CliktCommand] that has a default implementation of [CliktCommand.run] that is a no-op. 5 | */ 6 | open class NoOpCliktCommand( 7 | /** 8 | * The name of the program to use in the help output. If not given, it is inferred from the 9 | * class name. 10 | */ 11 | name: String? = null, 12 | ) : CliktCommand(name) { 13 | final override fun run() {} 14 | } 15 | -------------------------------------------------------------------------------- /clikt-mordant/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/transform/MordantTransformContext.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters.transform 2 | 3 | import com.github.ajalt.clikt.core.terminal 4 | import com.github.ajalt.clikt.parameters.options.OptionTransformContext 5 | import com.github.ajalt.mordant.rendering.Theme 6 | import com.github.ajalt.mordant.terminal.Terminal 7 | 8 | /** The terminal for the current context */ 9 | val TransformContext.terminal: Terminal get() = context.terminal 10 | 11 | /** The theme for the current context */ 12 | val TransformContext.theme: Theme get() = terminal.theme 13 | 14 | /** The terminal from the current context */ 15 | val OptionTransformContext.terminal: Terminal get() = context.terminal 16 | -------------------------------------------------------------------------------- /clikt/README.md: -------------------------------------------------------------------------------- 1 | # Module clikt-core 2 | 3 | This module provides the core functionality of Clikt. It doesn't have any dependencies. 4 | 5 | This is a transitive dependency of the main `clikt` module, so you won't need to include it directly 6 | unless you aren't using the main module. 7 | 8 | ## Installation 9 | 10 | ```kotlin 11 | dependencies { 12 | implementation("com.github.ajalt.clikt:clikt-core:$cliktVersion") 13 | } 14 | ``` 15 | -------------------------------------------------------------------------------- /clikt/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.dokka.gradle.DokkaTaskPartial 2 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 3 | import org.jetbrains.kotlin.gradle.targets.js.nodejs.NodeJsRootExtension 4 | import org.jetbrains.kotlin.gradle.targets.js.npm.tasks.KotlinNpmInstallTask 5 | 6 | plugins { 7 | kotlin("multiplatform") 8 | alias(libs.plugins.publish) 9 | } 10 | 11 | kotlin { 12 | jvm() 13 | 14 | js { nodejs() } 15 | @OptIn(ExperimentalWasmDsl::class) 16 | wasmJs { nodejs() } 17 | @OptIn(ExperimentalWasmDsl::class) 18 | wasmWasi { nodejs() } 19 | 20 | linuxX64() 21 | linuxArm64() 22 | mingwX64() 23 | macosX64() 24 | macosArm64() 25 | 26 | iosArm64() 27 | iosX64() 28 | iosSimulatorArm64() 29 | watchosX64() 30 | watchosArm32() 31 | watchosArm64() 32 | watchosSimulatorArm64() 33 | tvosX64() 34 | tvosArm64() 35 | tvosSimulatorArm64() 36 | } 37 | 38 | // https://youtrack.jetbrains.com/issue/KT-63014 39 | // https://github.com/Kotlin/kotlin-wasm-examples/blob/1b007347bf9f8a1ec3d420d30de1815768d5df02/nodejs-example/build.gradle.kts#L22 40 | rootProject.the().apply { 41 | version = "22.0.0-nightly202404032241e8c5b3" 42 | downloadBaseUrl = "https://nodejs.org/download/nightly" 43 | } 44 | 45 | rootProject.tasks.withType().configureEach { 46 | args.add("--ignore-engines") 47 | } 48 | 49 | tasks.withType { 50 | dokkaSourceSets.configureEach { 51 | moduleName.set("clikt-core") 52 | includes.from("README.md") 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /clikt/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=clikt-core 2 | POM_NAME=Clikt Core 3 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/command/CoreChainedCliktCommand.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.command 2 | 3 | import com.github.ajalt.clikt.core.* 4 | import com.github.ajalt.clikt.parsers.CommandLineParser 5 | 6 | /** 7 | * A version of [CoreCliktCommand] that returns a value from the [run] function, which 8 | * is then passed to subcommands. 9 | * 10 | * This command works best if you set [allowMultipleSubcommands] to `true`. 11 | */ 12 | abstract class CoreChainedCliktCommand( 13 | /** 14 | * The name of the program to use in the help output. If not given, it is inferred from the 15 | * class name. 16 | */ 17 | name: String? = null, 18 | ) : BaseCliktCommand>(name) { 19 | /** 20 | * Perform actions after parsing is complete and this command is invoked. 21 | * 22 | * This takes the value returned by the previously invoked command and returns a new value. 23 | * 24 | * This is called after command line parsing is complete. If this command is a subcommand, this 25 | * will only be called if the subcommand is invoked. 26 | * 27 | * If one of this command's subcommands is invoked, this is called before the subcommand's 28 | * arguments are parsed. 29 | */ 30 | abstract fun run(value: T): T 31 | } 32 | 33 | 34 | /** 35 | * Parse the command line and print helpful output if any errors occur. 36 | * 37 | * This function calls [parse] and catches any [CliktError]s that are thrown, exiting the process 38 | * with the specified [status code][CliktError.statusCode]. Other errors are allowed to pass 39 | * through. 40 | * 41 | * If you don't want Clikt to exit your process, call [parse] instead. 42 | */ 43 | fun CoreChainedCliktCommand.main(argv: List, initial: T): T { 44 | return CommandLineParser.mainReturningValue(this) { parse(argv, initial) } 45 | } 46 | 47 | /** 48 | * Parse the command line and print helpful output if any errors occur. 49 | * 50 | * This function calls [parse] and catches any [CliktError]s that are thrown, exiting the process 51 | * with the specified [status code][CliktError.statusCode]. Other errors are allowed to pass 52 | * through. 53 | * 54 | * If you don't want Clikt to exit your process, call [parse] instead. 55 | */ 56 | fun CoreChainedCliktCommand.main(argv: Array, initial: T): T { 57 | return main(argv.asList(), initial) 58 | } 59 | 60 | /** 61 | * Parse the command line and throw an exception if parsing fails. 62 | * 63 | * You should use [main] instead unless you want to handle output yourself. 64 | */ 65 | fun CoreChainedCliktCommand.parse(argv: Array, initial: T): T { 66 | return parse(argv.asList(), initial) 67 | } 68 | 69 | /** 70 | * Parse the command line and throw an exception if parsing fails. 71 | * 72 | * You should use [main] instead unless you want to handle output yourself. 73 | */ 74 | fun CoreChainedCliktCommand.parse(argv: List, initial: T): T { 75 | var value = initial 76 | CommandLineParser.parseAndRun(this, argv) { value = it.run(value) } 77 | return value 78 | } 79 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/command/CoreSuspendingCliktCommand.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.command 2 | 3 | import com.github.ajalt.clikt.core.* 4 | import com.github.ajalt.clikt.parsers.CommandLineParser 5 | 6 | /** 7 | * A version of [CoreCliktCommand] that supports a suspending [run] function. 8 | */ 9 | abstract class CoreSuspendingCliktCommand( 10 | /** 11 | * The name of the program to use in the help output. If not given, it is inferred from the 12 | * class name. 13 | */ 14 | name: String? = null, 15 | ) : BaseCliktCommand(name) { 16 | /** 17 | * Perform actions after parsing is complete and this command is invoked. 18 | * 19 | * This is called after command line parsing is complete. If this command is a subcommand, this 20 | * will only be called if the subcommand is invoked. 21 | * 22 | * If one of this command's subcommands is invoked, this is called before the subcommand's 23 | * arguments are parsed. 24 | */ 25 | abstract suspend fun run() 26 | } 27 | 28 | 29 | /** 30 | * Parse the command line and print helpful output if any errors occur. 31 | * 32 | * This function calls [parse] and catches any [CliktError]s that are thrown, exiting the process 33 | * with the specified [status code][CliktError.statusCode]. Other errors are allowed to pass 34 | * through. 35 | * 36 | * If you don't want Clikt to exit your process, call [parse] instead. 37 | */ 38 | suspend fun CoreSuspendingCliktCommand.main(argv: List) { 39 | CommandLineParser.main(this) { parse(argv) } 40 | } 41 | 42 | /** 43 | * Parse the command line and print helpful output if any errors occur. 44 | * 45 | * This function calls [parse] and catches any [CliktError]s that are thrown, exiting the process 46 | * with the specified [status code][CliktError.statusCode]. Other errors are allowed to pass 47 | * through. 48 | * 49 | * If you don't want Clikt to exit your process, call [parse] instead. 50 | */ 51 | suspend fun CoreSuspendingCliktCommand.main(argv: Array) = main(argv.asList()) 52 | 53 | /** 54 | * Parse the command line and throw an exception if parsing fails. 55 | * 56 | * You should use [main] instead unless you want to handle output yourself. 57 | */ 58 | suspend fun CoreSuspendingCliktCommand.parse(argv: Array) { 59 | parse(argv.asList()) 60 | } 61 | 62 | /** 63 | * Parse the command line and throw an exception if parsing fails. 64 | * 65 | * You should use [main] instead unless you want to handle output yourself. 66 | */ 67 | suspend fun CoreSuspendingCliktCommand.parse(argv: List) { 68 | CommandLineParser.parseAndRun(this, argv) { it.run() } 69 | } 70 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/command/CoreSuspendingNoOpCliktCommand.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.command 2 | 3 | /** 4 | * A [CoreSuspendingCliktCommand] that has a default implementation of 5 | * [run][CoreSuspendingCliktCommand.run] that is a no-op. 6 | */ 7 | abstract class CoreSuspendingNoOpCliktCommand( 8 | /** 9 | * The name of the program to use in the help output. If not given, it is inferred from the 10 | * class name. 11 | */ 12 | name: String? = null, 13 | ) : CoreSuspendingCliktCommand(name) { 14 | override suspend fun run() = Unit 15 | } 16 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/completion/CompletionBuiltins.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.completion 2 | 3 | import com.github.ajalt.clikt.command.CoreChainedCliktCommand 4 | import com.github.ajalt.clikt.command.CoreSuspendingCliktCommand 5 | import com.github.ajalt.clikt.completion.CompletionGenerator.generateCompletionForCommand 6 | import com.github.ajalt.clikt.core.BaseCliktCommand 7 | import com.github.ajalt.clikt.core.Context 8 | import com.github.ajalt.clikt.core.CoreCliktCommand 9 | import com.github.ajalt.clikt.core.PrintCompletionMessage 10 | import com.github.ajalt.clikt.parameters.arguments.argument 11 | import com.github.ajalt.clikt.parameters.options.option 12 | import com.github.ajalt.clikt.parameters.options.validate 13 | import com.github.ajalt.clikt.parameters.types.choice 14 | 15 | private val choices = arrayOf("bash", "zsh", "fish") 16 | 17 | /** 18 | * Add an option to a command that will print a completion script for the given shell when invoked. 19 | */ 20 | fun > T.completionOption( 21 | vararg names: String = arrayOf("--generate-completion"), 22 | help: String = "", 23 | hidden: Boolean = false, 24 | ): T = apply { 25 | registerOption( 26 | option( 27 | *names, help = help, hidden = hidden, eager = true, 28 | metavar = choices.joinToString("|", prefix = "(", postfix = ")") 29 | ).validate { 30 | throw PrintCompletionMessage(generateCompletionForCommand(context.command, it)) 31 | }) 32 | } 33 | 34 | /** 35 | * A subcommand that will print a completion script for the given shell when invoked. 36 | */ 37 | class CompletionCommand( 38 | private val help: String = "Generate a tab-complete script for the given shell", 39 | private val epilog: String = "", 40 | name: String = "generate-completion", 41 | ) : CoreCliktCommand(name) { 42 | override fun help(context: Context): String = help 43 | override fun helpEpilog(context: Context): String = epilog 44 | private val shell by argument("shell").choice(*choices) 45 | override fun run() { 46 | val cmd = currentContext.parent?.command ?: this 47 | throw PrintCompletionMessage(generateCompletionForCommand(cmd, shell)) 48 | } 49 | } 50 | 51 | /** 52 | * A [CoreSuspendingCliktCommand] subcommand that will print a completion script for the given shell 53 | * when invoked. 54 | */ 55 | class SuspendingCompletionCommand( 56 | private val help: String = "Generate a tab-complete script for the given shell", 57 | private val epilog: String = "", 58 | name: String = "generate-completion", 59 | ) : CoreSuspendingCliktCommand(name) { 60 | override fun help(context: Context): String = help 61 | override fun helpEpilog(context: Context): String = epilog 62 | private val shell by argument("shell").choice(*choices) 63 | override suspend fun run() { 64 | val cmd = currentContext.parent?.command ?: this 65 | throw PrintCompletionMessage(generateCompletionForCommand(cmd, shell)) 66 | } 67 | } 68 | 69 | /** 70 | * A [CoreChainedCliktCommand] subcommand that will print a completion script for the given shell 71 | * when invoked. 72 | */ 73 | class ChainedCompletionCommand( 74 | private val help: String = "Generate a tab-complete script for the given shell", 75 | private val epilog: String = "", 76 | name: String = "generate-completion", 77 | ) : CoreChainedCliktCommand(name) { 78 | override fun help(context: Context): String = help 79 | override fun helpEpilog(context: Context): String = epilog 80 | private val shell by argument("shell").choice(*choices) 81 | override fun run(value: T): T { 82 | val cmd = currentContext.parent?.command ?: this 83 | throw PrintCompletionMessage(generateCompletionForCommand(cmd, shell)) 84 | } 85 | } 86 | 87 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/completion/CompletionCandidates.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.completion 2 | 3 | /** 4 | * Configurations for generating shell autocomplete suggestions 5 | */ 6 | sealed class CompletionCandidates { 7 | /** Do not autocomplete this parameter */ 8 | data object None : CompletionCandidates() 9 | 10 | /** Complete with filesystem paths */ 11 | data object Path : CompletionCandidates() 12 | 13 | /** Complete with entries in the system's hostfile */ 14 | data object Hostname : CompletionCandidates() 15 | 16 | /** Complete with usernames from the current system */ 17 | data object Username : CompletionCandidates() 18 | 19 | /** Complete the parameter with a fixed set of strings */ 20 | data class Fixed(val candidates: Set) : CompletionCandidates() { 21 | constructor(vararg candidates: String) : this(candidates.toSet()) 22 | } 23 | 24 | /** 25 | * Complete the parameter with words emitted from a custom script. 26 | * 27 | * The [generator] takes the type of shell to generate a script for and returns code to add to 28 | * the generated completion script. If you just want to call another script or binary that 29 | * prints all possible completion words to stdout, you can use [fromStdout]. 30 | * 31 | * ## Bash/ZSH 32 | * 33 | * Both Bash and ZSH scripts use Bash's Programmable Completion system (ZSH via a comparability 34 | * layer). The string returned from [generator] should be the body of a function that will be 35 | * passed to `compgen -F`. 36 | * 37 | * Specifically, you should set the variable `COMPREPLY` to the completion(s) for the current 38 | * word being typed. The word being typed can be retrieved from the `COMP_WORDS` array at index 39 | * `COMP_CWORD`. 40 | * 41 | * ## Fish 42 | * 43 | * The string returned from [generator] should be a Fish string with each completion suggestion 44 | * separated by a newline. Each completion can optionally end with a tab, followed by a 45 | * description of the suggestion. 46 | * 47 | * ``` 48 | * """' 49 | * start 50 | * stop 51 | * help\\t"show the help for this command" 52 | * test\\t"run test suite" 53 | * '""" 54 | * ``` 55 | * 56 | * You can also construct the string from a fish command or function call, e.g. 57 | * 58 | * ``` 59 | * "\"(__fish_print_hostnames)\"" 60 | * ``` 61 | * 62 | * or 63 | * 64 | * ``` 65 | * "\"(ls -1)\"" 66 | * ``` 67 | */ 68 | data class Custom(val generator: (ShellType) -> String?) : CompletionCandidates() { 69 | enum class ShellType { BASH, FISH } 70 | companion object { 71 | fun fromStdout(command: String) = Custom { 72 | when (it) { 73 | ShellType.FISH -> "\"($command)\"" 74 | else -> "COMPREPLY=(\$(compgen -W \"\$($command)\" -- \"\${COMP_WORDS[\$COMP_CWORD]}\"))" 75 | } 76 | } 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/completion/CompletionGenerator.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.completion 2 | 3 | import com.github.ajalt.clikt.core.BaseCliktCommand 4 | 5 | object CompletionGenerator { 6 | /** 7 | * Generate a completion script for the given shell and command. 8 | * 9 | * @param command The command to generate a completion script for. 10 | * @param shell The shell to generate a completion script for. One of "bash", "zsh", or "fish". 11 | * If any other value is provided, the script will be generated for bash. 12 | */ 13 | fun generateCompletionForCommand(command: BaseCliktCommand<*>, shell: String): String { 14 | return when (shell.trim().lowercase()) { 15 | "fish" -> FishCompletionGenerator.generateFishCompletion(command = command) 16 | "zsh" -> BashCompletionGenerator.generateBashOrZshCompletion( 17 | command = command, 18 | zsh = true 19 | ) 20 | 21 | else -> BashCompletionGenerator.generateBashOrZshCompletion( 22 | command = command, 23 | zsh = false 24 | ) 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/core/CoreCliktCommand.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.core 2 | 3 | import com.github.ajalt.clikt.output.PlaintextHelpFormatter 4 | import com.github.ajalt.clikt.parsers.CommandLineParser 5 | 6 | /** 7 | * A command with a [run] function that's called when the command is invoked. 8 | * 9 | * It uses the [PlaintextHelpFormatter] by default. You usually want to inherit from `CliktCommand` 10 | * instead, which uses the `MordantHelpFormatter`. 11 | * 12 | * By default, this class doesn't support and text formatting or wrapping, and it doesn't support 13 | * environment variables, arg files, exit status codes, or printing to stderr. Although you can use 14 | * this class without those features, if you need then you can set a custom [Context.helpFormatter], 15 | * [Context.readArgumentFile], [Context.Builder.readEnvvar], [Context.exitProcess], or 16 | * [Context.echoMessage]. 17 | * 18 | * ### Example 19 | * 20 | * On JVM, if you want to use this class and define the above features: 21 | * 22 | * ```kotlin 23 | * abstract class MyCoreCommand : CoreCliktCommand() { 24 | * init { 25 | * context { 26 | * readArgumentFile = { 27 | * try { 28 | * Path(it).readText() 29 | * } catch (e: IOException) { 30 | * throw FileNotFound(it) 31 | * } 32 | * } 33 | * readEnvvar = { System.getenv(it) } 34 | * exitProcess = { System.exit(it) } 35 | * echoMessage = { context, message, newline, err -> 36 | * val writer = if (err) System.err else System.out 37 | * if (newline) { 38 | * writer.println(message) 39 | * } else { 40 | * writer.print(message) 41 | * } 42 | * } 43 | * } 44 | * } 45 | * } 46 | * ``` 47 | * 48 | */ 49 | abstract class CoreCliktCommand( 50 | /** 51 | * The name of the program to use in the help output. If not given, it is inferred from the 52 | * class name. 53 | */ 54 | name: String? = null, 55 | ) : BaseCliktCommand(name) { 56 | /** 57 | * Perform actions after parsing is complete and this command is invoked. 58 | * 59 | * This is called after command line parsing is complete. If this command is a subcommand, this 60 | * will only be called if the subcommand is invoked. 61 | * 62 | * If one of this command's subcommands is invoked, this is called before the subcommand's 63 | * arguments are parsed. 64 | */ 65 | abstract fun run() 66 | } 67 | 68 | /** 69 | * Parse the command line and print helpful output if any errors occur. 70 | * 71 | * This function calls [parse] and catches any [CliktError]s that are thrown, exiting the process 72 | * with the specified [status code][CliktError.statusCode]. Other errors are allowed to pass 73 | * through. 74 | * 75 | * If you don't want Clikt to exit your process, call [parse] instead. 76 | */ 77 | fun CoreCliktCommand.main(argv: List) { 78 | CommandLineParser.main(this) { parse(argv) } 79 | } 80 | 81 | /** 82 | * Parse the command line and print helpful output if any errors occur. 83 | * 84 | * This function calls [parse] and catches any [CliktError]s that are thrown, exiting the process 85 | * with the specified [status code][CliktError.statusCode]. Other errors are allowed to pass 86 | * through. 87 | * 88 | * If you don't want Clikt to exit your process, call [parse] instead. 89 | */ 90 | fun CoreCliktCommand.main(argv: Array) = main(argv.asList()) 91 | 92 | /** 93 | * Parse the command line and throw an exception if parsing fails. 94 | * 95 | * You should use [main] instead unless you want to handle output yourself. 96 | */ 97 | fun CoreCliktCommand.parse(argv: Array) { 98 | parse(argv.asList()) 99 | } 100 | 101 | /** 102 | * Parse the command line and throw an exception if parsing fails. 103 | * 104 | * You should use [main] instead unless you want to handle output yourself. 105 | */ 106 | fun CoreCliktCommand.parse(argv: List) { 107 | CommandLineParser.parseAndRun(this, argv) { it.run() } 108 | } 109 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/core/CoreNoOpCliktCommand.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.core 2 | 3 | /** 4 | * A [CoreCliktCommand] that has a default implementation of [CoreCliktCommand.run] that is a no-op. 5 | */ 6 | open class CoreNoOpCliktCommand( 7 | /** 8 | * The name of the program to use in the help output. If not given, it is inferred from the 9 | * class name. 10 | */ 11 | name: String? = null, 12 | ) : CoreCliktCommand(name) { 13 | final override fun run() {} 14 | } 15 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/core/JaroWinkerSimilarity.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.core 2 | 3 | import kotlin.math.max 4 | import kotlin.math.min 5 | 6 | 7 | private fun jaroSimilarity(s1: String, s2: String): Double { 8 | if (s1.isEmpty() && s2.isEmpty()) return 1.0 9 | else if (s1.isEmpty() || s2.isEmpty()) return 0.0 10 | else if (s1.length == 1 && s2.length == 1) return if (s1[0] == s2[0]) 1.0 else 0.0 11 | 12 | val searchRange: Int = max(s1.length, s2.length) / 2 - 1 13 | val s2Consumed = BooleanArray(s2.length) 14 | var matches = 0.0 15 | var transpositions = 0 16 | var s2MatchIndex = 0 17 | 18 | for ((i, c1) in s1.withIndex()) { 19 | val start = max(0, i - searchRange) 20 | val end = min(s2.lastIndex, i + searchRange) 21 | for (j in start..end) { 22 | val c2 = s2[j] 23 | if (c1 != c2 || s2Consumed[j]) continue 24 | s2Consumed[j] = true 25 | matches += 1 26 | if (j < s2MatchIndex) transpositions += 1 27 | s2MatchIndex = j 28 | break 29 | } 30 | } 31 | 32 | return when (matches) { 33 | 0.0 -> 0.0 34 | else -> (matches / s1.length + 35 | matches / s2.length + 36 | (matches - transpositions) / matches) / 3.0 37 | } 38 | } 39 | 40 | internal fun jaroWinklerSimilarity(s1: String, s2: String): Double { 41 | // Unlike classic Jaro-Winkler, we don't set a limit on the prefix length 42 | val prefixLength = s1.commonPrefixWith(s2).length 43 | val jaro = jaroSimilarity(s1, s2) 44 | val winkler = jaro + (0.1 * prefixLength * (1 - jaro)) 45 | return min(winkler, 1.0) 46 | } 47 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/core/ParameterHolder.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.core 2 | 3 | import com.github.ajalt.clikt.parameters.groups.ParameterGroup 4 | import com.github.ajalt.clikt.parameters.options.Option 5 | 6 | @DslMarker 7 | annotation class ParameterHolderDsl 8 | 9 | @ParameterHolderDsl 10 | interface ParameterHolder { 11 | /** 12 | * Register an option with this command or group. 13 | * 14 | * This is called automatically for the built-in options, but you need to call this if you want to add a 15 | * custom option. 16 | */ 17 | fun registerOption(option: GroupableOption) 18 | } 19 | 20 | interface StaticallyGroupedOption : Option { 21 | /** The name of the group, or null if this option should not be grouped in the help output. */ 22 | val groupName: String? 23 | } 24 | 25 | /** 26 | * An option that can be added to a [ParameterGroup] 27 | */ 28 | interface GroupableOption : StaticallyGroupedOption { 29 | /** The group that this option belongs to, or null. Set by the group. */ 30 | var parameterGroup: ParameterGroup? 31 | 32 | /** The name of the group, or null if this option should not be grouped in the help output. */ 33 | override var groupName: String? 34 | } 35 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/internal/Util.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.internal 2 | 3 | import com.github.ajalt.clikt.core.GroupableOption 4 | import com.github.ajalt.clikt.core.MultiUsageError 5 | import com.github.ajalt.clikt.core.UsageError 6 | import com.github.ajalt.clikt.parameters.groups.ParameterGroup 7 | import com.github.ajalt.clikt.parameters.options.Option 8 | 9 | internal val Option.group: ParameterGroup? get() = (this as? GroupableOption)?.parameterGroup 10 | 11 | internal fun List.throwErrors() { 12 | MultiUsageError.buildOrNull(this)?.let { throw it } 13 | } 14 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/output/PlaintextHelpFormatter.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.output 2 | 3 | import com.github.ajalt.clikt.core.Context 4 | import com.github.ajalt.clikt.core.UsageError 5 | 6 | /** 7 | * A simple help formatter that outputs plain text. 8 | * 9 | * It doesn't support text wrapping, markdown, or any other styles or formatting. 10 | */ 11 | class PlaintextHelpFormatter( 12 | /** 13 | * The current command's context. 14 | */ 15 | context: Context, 16 | /** 17 | * The string to show before the names of required options, or null to not show a mark. 18 | */ 19 | requiredOptionMarker: String? = null, 20 | /** 21 | * If true, the default values will be shown in the help text for parameters that have them. 22 | */ 23 | showDefaultValues: Boolean = false, 24 | /** 25 | * If true, a tag indicating the parameter is required will be shown after the description of 26 | * required parameters. 27 | */ 28 | showRequiredTag: Boolean = false, 29 | ) : AbstractHelpFormatter( 30 | context, 31 | requiredOptionMarker, 32 | showDefaultValues, 33 | showRequiredTag 34 | ) { 35 | override fun formatHelp( 36 | error: UsageError?, 37 | prolog: String, 38 | epilog: String, 39 | parameters: List, 40 | programName: String, 41 | ): String { 42 | val parts = collectHelpParts(error, prolog, epilog, parameters, programName) 43 | return parts.joinToString("\n\n") 44 | } 45 | 46 | override fun renderError( 47 | parameters: List, 48 | error: UsageError, 49 | ): String = renderErrorString(parameters, error) 50 | 51 | override fun renderUsage( 52 | parameters: List, 53 | programName: String, 54 | ): String { 55 | val params = renderUsageParametersString(parameters) 56 | val title = localization.usageTitle() 57 | return if (params.isEmpty()) "$title $programName" 58 | else "$title $programName $params" 59 | } 60 | 61 | override fun renderProlog(prolog: String): String { 62 | return prolog.lineSequence().joinToString("\n") { if (it.isEmpty()) "" else " $it" } 63 | } 64 | 65 | override fun renderEpilog(epilog: String): String = epilog 66 | 67 | override fun renderParameters(parameters: List): String { 68 | return collectParameterSections(parameters).joinToString("\n\n") { (title, content) -> 69 | "$title\n$content" 70 | } 71 | } 72 | 73 | override fun renderOptionGroup( 74 | help: String?, 75 | parameters: List, 76 | ): String = buildString { 77 | if (help != null) { 78 | appendLine() 79 | appendLine(help) 80 | appendLine() 81 | } 82 | val options = parameters.map { renderOptionDefinition(it) } 83 | append(buildParameterList(options)) 84 | } 85 | 86 | override fun renderDefinitionTerm(row: DefinitionRow): String { 87 | val termPrefix = when { 88 | row.marker.isNullOrEmpty() -> " " 89 | else -> row.marker + " ".drop(row.marker.length).ifEmpty { " " } 90 | } 91 | return termPrefix + row.term 92 | } 93 | 94 | override fun renderDefinitionDescription(row: DefinitionRow): String = row.description 95 | 96 | override fun buildParameterList(rows: List): String { 97 | val termLength = (rows.maxOfOrNull { it.term.length } ?: 0) + 4 98 | return rows.joinToString("\n") { 99 | val term = renderDefinitionTerm(it) 100 | val definition = renderDefinitionDescription(it).ifBlank { null } 101 | val separator = " ".repeat(termLength - term.length) 102 | listOfNotNull(term, definition).joinToString(separator) 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/output/text.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.output 2 | 3 | internal const val NEL = "\u0085" 4 | 5 | internal fun String.wrapText( 6 | sb: StringBuilder, 7 | width: Int = 78, 8 | initialIndent: String = "", 9 | subsequentIndent: String = "", 10 | ) { 11 | require(initialIndent.length < width) { "initialIndent >= width: ${initialIndent.length} >= $width" } 12 | require(subsequentIndent.length < width) { "subsequentIndent >= width: ${subsequentIndent.length} >= $width" } 13 | 14 | for ((i, paragraph) in splitParagraphs(this).withIndex()) { 15 | if (i > 0) sb.append("\n\n") 16 | sb.wrapParagraph( 17 | paragraph, 18 | width, 19 | if (i == 0) initialIndent else subsequentIndent, 20 | subsequentIndent 21 | ) 22 | } 23 | } 24 | 25 | private val TEXT_START_REGEX = Regex("\\S") 26 | private val PRE_P_END_REGEX = Regex("""```[ \t]*(?:\n\s*|[ \t]*$)""") 27 | private val PLAIN_P_END_REGEX = Regex("""[ \t]*\n(?:\s*```|[ \t]*\n\s*)|$NEL?\s*$""") 28 | private val LINE_BREAK_REGEX = Regex("\r?\n") 29 | private val WHITESPACE_OR_NEL_REGEX = Regex("""\s+|$NEL""") 30 | private val WORD_OR_NEL_REGEX = Regex("""[^\s$NEL]+|$NEL""") 31 | 32 | // there's no dotall flag on JS, so we have to use [\s\S] instead 33 | private val PRE_P_CONTENTS_REGEX = Regex("""```([\s\S]*?)```""") 34 | 35 | internal fun splitParagraphs(text: String): List { 36 | val paragraphs = mutableListOf() 37 | var i = TEXT_START_REGEX.find(text)?.range?.first ?: return emptyList() 38 | while (i < text.length) { 39 | val range = if (text.startsWith("```", startIndex = i)) { 40 | PRE_P_END_REGEX.find(text, i + 3)?.let { 41 | (it.range.first + 3)..it.range.last 42 | } 43 | } else { 44 | PLAIN_P_END_REGEX.find(text, i)?.let { 45 | when { 46 | it.value.startsWith(NEL) -> it.range.first + 1..it.range.first + 1 47 | it.value.endsWith("```") -> it.range.first..(it.range.last - 3) 48 | else -> it.range 49 | } 50 | } 51 | } ?: text.length..text.length 52 | paragraphs += text.substring(i, range.first) 53 | i = range.last + 1 54 | } 55 | return paragraphs 56 | } 57 | 58 | private fun StringBuilder.tryPreformat( 59 | text: String, 60 | initialIndent: String, 61 | subsequentIndent: String, 62 | ): Boolean { 63 | val value = PRE_P_CONTENTS_REGEX.matchEntire(text)?.groups?.get(1)?.value 64 | val pre = value?.replaceIndent(subsequentIndent)?.removePrefix(subsequentIndent) 65 | ?: return false 66 | 67 | for ((i, line) in pre.split(LINE_BREAK_REGEX).withIndex()) { 68 | if (i == 0) append(initialIndent) 69 | else append("\n") 70 | append(line.trimEnd()) 71 | } 72 | return true 73 | } 74 | 75 | private fun StringBuilder.wrapParagraph( 76 | text: String, 77 | width: Int, 78 | initialIndent: String, 79 | subsequentIndent: String, 80 | ) { 81 | if (tryPreformat(text, initialIndent, subsequentIndent)) return 82 | val breakLine = "\n$subsequentIndent" 83 | 84 | if (initialIndent.length + text.length <= width) { 85 | val newText = text.trim().replace(WHITESPACE_OR_NEL_REGEX) { 86 | if (it.value == NEL) breakLine else " " 87 | } 88 | append(initialIndent).append(newText) 89 | return 90 | } 91 | 92 | val words = WORD_OR_NEL_REGEX.findAll(text).map { it.value }.toList() 93 | append(initialIndent) 94 | var currentWidth = initialIndent.length 95 | var prevWasNel = false 96 | for ((i, word) in words.withIndex()) { 97 | if (word == NEL) { 98 | append(breakLine) 99 | currentWidth = subsequentIndent.length 100 | prevWasNel = true 101 | continue 102 | } 103 | 104 | if (i > 0 && !prevWasNel) { 105 | if (currentWidth + word.length + 1 > width) { 106 | append(breakLine) 107 | currentWidth = subsequentIndent.length 108 | } else { 109 | append(" ") 110 | currentWidth += 1 111 | } 112 | } 113 | 114 | prevWasNel = false 115 | append(word) 116 | currentWidth += word.length 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/groups/CoOccurringOptionGroup.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters.groups 2 | 3 | import com.github.ajalt.clikt.core.BaseCliktCommand 4 | import com.github.ajalt.clikt.core.Context 5 | import com.github.ajalt.clikt.output.HelpFormatter 6 | import com.github.ajalt.clikt.parameters.internal.NullableLateinit 7 | import com.github.ajalt.clikt.parameters.options.Option 8 | import com.github.ajalt.clikt.parameters.options.hasEnvvarOrSourcedValue 9 | import com.github.ajalt.clikt.parameters.options.required 10 | import com.github.ajalt.clikt.parsers.OptionInvocation 11 | import kotlin.properties.ReadOnlyProperty 12 | import kotlin.reflect.KProperty 13 | 14 | typealias CoOccurringOptionGroupTransform = (occurred: Boolean?, group: GroupT, context: Context) -> OutT 15 | 16 | class CoOccurringOptionGroup internal constructor( 17 | internal val group: GroupT, 18 | private val transform: CoOccurringOptionGroupTransform, 19 | ) : ParameterGroupDelegate { 20 | init { 21 | require(group.options.any { HelpFormatter.Tags.REQUIRED in it.helpTags }) { 22 | "At least one option in a co-occurring group must use `required()`" 23 | } 24 | require(group.options.none { it.eager }) { 25 | "eager options are not allowed in choice and switch option groups" 26 | } 27 | } 28 | 29 | override val groupName: String? get() = group.groupName 30 | override val groupHelp: String? get() = group.groupHelp 31 | private var value: OutT by NullableLateinit("Cannot read from option delegate before parsing command line") 32 | private var occurred = false 33 | 34 | override fun provideDelegate( 35 | thisRef: BaseCliktCommand<*>, 36 | property: KProperty<*>, 37 | ): ReadOnlyProperty, OutT> { 38 | thisRef.registerOptionGroup(this) 39 | for (option in group.options) { 40 | 41 | option.parameterGroup = this 42 | option.groupName = groupName 43 | thisRef.registerOption(option) 44 | } 45 | return this 46 | } 47 | 48 | override fun getValue(thisRef: BaseCliktCommand<*>, property: KProperty<*>): OutT = value 49 | 50 | override fun finalize( 51 | context: Context, 52 | invocationsByOption: Map>, 53 | ) { 54 | occurred = invocationsByOption.isNotEmpty() || group.options.any { 55 | it.hasEnvvarOrSourcedValue(context, invocationsByOption[it] ?: emptyList()) 56 | } 57 | if (occurred) group.finalize(context, invocationsByOption) 58 | value = transform(occurred, group, context) 59 | } 60 | 61 | override fun postValidate(context: Context) { 62 | if (occurred) group.postValidate(context) 63 | } 64 | 65 | fun copy(transform: CoOccurringOptionGroupTransform): CoOccurringOptionGroup { 66 | return CoOccurringOptionGroup(group, transform) 67 | } 68 | } 69 | 70 | /** 71 | * Make this group a co-occurring group. 72 | * 73 | * The group becomes nullable. At least one option in the group must be [required]. If none of the 74 | * options in the group are given on the command line, the group is null and none of the `required` 75 | * constraints are enforced. If any option in the group is given, all `required` options in the 76 | * group must be given as well. 77 | */ 78 | fun T.cooccurring(): CoOccurringOptionGroup { 79 | return CoOccurringOptionGroup(this) { occurred, g, _ -> 80 | if (occurred == true) g 81 | else null 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/groups/ParameterGroup.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters.groups 2 | 3 | import com.github.ajalt.clikt.core.BaseCliktCommand 4 | import com.github.ajalt.clikt.core.Context 5 | import com.github.ajalt.clikt.core.GroupableOption 6 | import com.github.ajalt.clikt.core.ParameterHolder 7 | import com.github.ajalt.clikt.internal.finalizeOptions 8 | import com.github.ajalt.clikt.output.HelpFormatter 9 | import com.github.ajalt.clikt.parameters.options.Option 10 | import com.github.ajalt.clikt.parsers.OptionInvocation 11 | import kotlin.properties.PropertyDelegateProvider 12 | import kotlin.properties.ReadOnlyProperty 13 | import kotlin.reflect.KProperty 14 | 15 | interface ParameterGroup { 16 | /** 17 | * The name of the group, or null if parameters in the group should not be separated from other 18 | * parameters in the help output. 19 | */ 20 | val groupName: String? 21 | 22 | /** 23 | * A help message to display for this group. 24 | * 25 | * If [groupName] is null, the help formatter will ignore this value. 26 | */ 27 | val groupHelp: String? 28 | 29 | fun parameterHelp(context: Context): HelpFormatter.ParameterHelp.Group? { 30 | val n = groupName 31 | val h = groupHelp 32 | return if (n == null || h == null) null else HelpFormatter.ParameterHelp.Group(n, h) 33 | } 34 | 35 | /** 36 | * Called after this command's argv is parsed and all options are validated to validate the group constraints. 37 | * 38 | * @param context The context for this parse 39 | * @param invocationsByOption The invocations of options in this group. 40 | */ 41 | fun finalize(context: Context, invocationsByOption: Map>) 42 | 43 | /** 44 | * Called after all of a command's parameters have been [finalize]d to perform validation of the final values. 45 | */ 46 | fun postValidate(context: Context) 47 | } 48 | 49 | /** A [ParameterGroup] that can be used as a property delegate */ 50 | interface ParameterGroupDelegate : 51 | ParameterGroup, 52 | ReadOnlyProperty, T>, 53 | PropertyDelegateProvider, ReadOnlyProperty, T>> { 54 | /** Implementations must call [BaseCliktCommand<*>.registerOptionGroup] */ 55 | override operator fun provideDelegate( 56 | thisRef: BaseCliktCommand<*>, 57 | property: KProperty<*>, 58 | ): ReadOnlyProperty, T> 59 | } 60 | 61 | /** 62 | * A group of options that can be shown together in help output, or restricted to be [cooccurring]. 63 | * 64 | * Declare a subclass with option delegate properties, then use an instance of your subclass is a 65 | * delegate property in your command with [provideDelegate]. 66 | * 67 | * ### Example: 68 | * 69 | * ``` 70 | * class UserOptions : OptionGroup(name = "User Options", help = "Options controlling the user") { 71 | * val name by option() 72 | * val age by option().int() 73 | * } 74 | * 75 | * class Tool : CliktCommand() { 76 | * val userOptions by UserOptions() 77 | * } 78 | * ``` 79 | */ 80 | open class OptionGroup( 81 | name: String? = null, 82 | help: String? = null, 83 | ) : ParameterGroup, ParameterHolder { 84 | internal val options: MutableList = mutableListOf() 85 | override val groupName: String? = name 86 | override val groupHelp: String? = help 87 | 88 | override fun registerOption(option: GroupableOption) { 89 | option.parameterGroup = this 90 | options += option 91 | } 92 | 93 | override fun finalize( 94 | context: Context, 95 | invocationsByOption: Map>, 96 | ) { 97 | finalizeOptions(context, options, invocationsByOption) 98 | } 99 | 100 | override fun postValidate(context: Context) = options.forEach { it.postValidate(context) } 101 | } 102 | 103 | operator fun T.provideDelegate( 104 | thisRef: BaseCliktCommand<*>, 105 | prop: KProperty<*>, 106 | ): ReadOnlyProperty, T> { 107 | thisRef.registerOptionGroup(this) 108 | options.forEach { thisRef.registerOption(it) } 109 | return ReadOnlyProperty { _, _ -> this@provideDelegate } 110 | } 111 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/internal/NullableLateinit.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters.internal 2 | 3 | import kotlin.properties.ReadWriteProperty 4 | import kotlin.reflect.KProperty 5 | 6 | /** 7 | * A container for a value that is initialized after the container is created. 8 | * 9 | * Similar to a lateinit variable, but allows nullable types. If the value is not set before 10 | * being read, it will return null if T is nullable, or throw an IllegalStateException otherwise. 11 | */ 12 | internal class NullableLateinit(private val errorMessage: String) : ReadWriteProperty { 13 | private object UNINITIALIZED 14 | 15 | private var value: Any? = UNINITIALIZED 16 | 17 | override fun getValue(thisRef: Any, property: KProperty<*>): T { 18 | if (value === UNINITIALIZED) throw LateinitException(errorMessage) 19 | 20 | try { 21 | @Suppress("UNCHECKED_CAST") 22 | return value as T 23 | } catch (e: ClassCastException) { 24 | throw LateinitException(errorMessage) 25 | } 26 | } 27 | 28 | override fun setValue(thisRef: Any, property: KProperty<*>, value: T) { 29 | this.value = value 30 | } 31 | } 32 | 33 | internal class LateinitException(message: String) : IllegalStateException(message) 34 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/options/EagerOption.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters.options 2 | 3 | import com.github.ajalt.clikt.core.Abort 4 | import com.github.ajalt.clikt.core.BaseCliktCommand 5 | import com.github.ajalt.clikt.core.PrintMessage 6 | import com.github.ajalt.clikt.core.ProgramResult 7 | 8 | 9 | /** 10 | * Add an eager option to this command that, when invoked, runs [action]. 11 | * 12 | * @param names The names that can be used to invoke this option. They must start with a punctuation character. 13 | * @param help The description of this option, usually a single line. 14 | * @param hidden Hide this option from help outputs. 15 | * @param helpTags Extra information about this option to pass to the help formatter 16 | * @param groupName All options that share a group name will be grouped together in help output. 17 | * @param action This callback is called when the option is encountered on the command line. If 18 | * you want to print a message and halt execution normally, you should throw a [PrintMessage] 19 | * exception. If you want to exit normally without printing a message, you should throw 20 | * [`Abort(error=false)`][Abort]. 21 | */ 22 | fun > T.eagerOption( 23 | vararg names: String, 24 | help: String = "", 25 | hidden: Boolean = false, 26 | helpTags: Map = emptyMap(), 27 | groupName: String? = null, 28 | action: OptionTransformContext.() -> Unit, 29 | ): T = eagerOption(names.asList(), help, hidden, helpTags, groupName, action) 30 | 31 | /** 32 | * Add an eager option to this command that, when invoked, runs [action]. 33 | * 34 | * @param names The names that can be used to invoke this option. They must start with a punctuation character. 35 | * @param help The description of this option, usually a single line. 36 | * @param hidden Hide this option from help outputs. 37 | * @param helpTags Extra information about this option to pass to the help formatter 38 | * @param groupName All options that share a group name will be grouped together in help output. 39 | * @param action This callback is called when the option is encountered on the command line. If 40 | * you want to print a message and halt execution normally, you should throw a [PrintMessage] 41 | * exception. If you want to exit normally without printing a message, you should throw 42 | * [`ProgramResult(0)`][ProgramResult]. 43 | */ 44 | fun > T.eagerOption( 45 | names: Collection, 46 | help: String = "", 47 | hidden: Boolean = false, 48 | helpTags: Map = emptyMap(), 49 | groupName: String? = null, 50 | action: OptionTransformContext.() -> Unit, 51 | ): T { 52 | val o = option( 53 | *names.toTypedArray(), 54 | help = help, 55 | hidden = hidden, 56 | helpTags = helpTags, 57 | eager = true, 58 | ).flag().validate { if (it) action() } 59 | o.groupName = groupName 60 | registerOption(o) 61 | return this 62 | } 63 | 64 | /** Add an eager option to this command that, when invoked, prints a version message and exits. */ 65 | inline fun > T.versionOption( 66 | /** The version to print */ 67 | version: String, 68 | /** The help message for the option */ 69 | help: String = "Show the version and exit", 70 | /** The names of this option. Defaults to --version */ 71 | names: Set = setOf("--version"), 72 | /** A block that returns the message to print. The [version] is passed as a parameter */ 73 | crossinline message: (String) -> String = { "$commandName version $it" }, 74 | ): T = eagerOption(names, help) { throw PrintMessage(message(version)) } 75 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/options/Validate.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters.options 2 | 3 | import com.github.ajalt.clikt.parameters.transform.message 4 | 5 | /** 6 | * Check the final option value and raise an error if it's not valid. 7 | * 8 | * The [validator] is called with the final option type (the output of [transformAll]), and should 9 | * call [fail][OptionTransformContext.fail] if the value is not valid. The [validator] is not called 10 | * if the delegate value is null. 11 | * 12 | * Your [validator] can also call [require][OptionTransformContext.require] to fail automatically if 13 | * an expression is false, or [message][OptionTransformContext.message] to show the user a warning 14 | * message without aborting. 15 | * 16 | * ### Example: 17 | * 18 | * ``` 19 | * val opt by option().int().validate { require(it % 2 == 0) { "value must be even" } } 20 | * ``` 21 | */ 22 | inline fun OptionWithValues.validate( 23 | crossinline validator: OptionValidator, 24 | ): OptionDelegate { 25 | return copy(transformValue, transformEach, transformAll, { if (it != null) validator(it) }) 26 | } 27 | 28 | /** 29 | * Check the final option value and raise an error if it's not valid. 30 | * 31 | * The [validator] is called with the final option type (the output of [transformAll]), and should 32 | * return `false` if the value is not valid. You can specify a [message] to include in the error 33 | * output. The [validator] is not called if the delegate value is null. 34 | * 35 | * You can use [validate] for more complex checks. 36 | * 37 | * ### Example: 38 | * 39 | * ``` 40 | * val opt by option().int().check("value must be even") { it % 2 == 0 } 41 | * ``` 42 | */ 43 | inline fun OptionWithValues.check( 44 | message: String, 45 | crossinline validator: (AllT & Any) -> Boolean, 46 | ): OptionDelegate { 47 | return check({ message }, validator) 48 | } 49 | 50 | /** 51 | * Check the final argument value and raise an error if it's not valid. 52 | * 53 | * The [validator] is called with the final option type (the output of [transformAll]), and should 54 | * return `false` if the value is not valid. You can specify a [lazyMessage] that returns a message 55 | * to include in the error output. The [validator] is not called if the delegate value is null. 56 | * 57 | * You can use [validate] for more complex checks. 58 | * 59 | * ### Example: 60 | * 61 | * ``` 62 | * val opt by option().int().check(lazyMessage={"$it is not even"}) { it % 2 == 0 } 63 | * ``` 64 | */ 65 | inline fun OptionWithValues.check( 66 | crossinline lazyMessage: (AllT & Any) -> String = { it.toString() }, 67 | crossinline validator: (AllT & Any) -> Boolean, 68 | ): OptionDelegate { 69 | return validate { require(validator(it)) { lazyMessage(it) } } 70 | } 71 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/options/ValueWithDefault.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters.options 2 | 3 | /** A container for a value that can have a default value and can be manually set */ 4 | data class ValueWithDefault(val explicit: T?, val default: T) { 5 | val value: T get() = explicit ?: default 6 | } 7 | 8 | /** Create a copy with a new [default] value */ 9 | fun ValueWithDefault.withDefault(default: T) = copy(default = default) 10 | 11 | /** Create a copy with a new [explicit] value */ 12 | fun ValueWithDefault.withExplicit(explicit: T) = copy(explicit = explicit) 13 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/transform/TransformContext.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters.transform 2 | 3 | import com.github.ajalt.clikt.core.Context 4 | import com.github.ajalt.clikt.core.UsageError 5 | 6 | interface TransformContext { 7 | /** The current context object */ 8 | val context: Context 9 | 10 | /** Throw an exception indicating that usage was incorrect. */ 11 | fun fail(message: String): Nothing 12 | } 13 | 14 | /** Issue a message that can be shown to the user */ 15 | fun TransformContext.message(message: String) { 16 | context.command.issueMessage(message) 17 | } 18 | 19 | data class HelpTransformContext(override val context: Context) : TransformContext { 20 | override fun fail(message: String): Nothing { 21 | throw UsageError(message) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/types/boolean.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters.types 2 | 3 | import com.github.ajalt.clikt.parameters.arguments.ProcessedArgument 4 | import com.github.ajalt.clikt.parameters.arguments.RawArgument 5 | import com.github.ajalt.clikt.parameters.arguments.convert 6 | import com.github.ajalt.clikt.parameters.options.NullableOption 7 | import com.github.ajalt.clikt.parameters.options.RawOption 8 | import com.github.ajalt.clikt.parameters.options.convert 9 | import com.github.ajalt.clikt.parameters.options.flag 10 | import com.github.ajalt.clikt.parameters.transform.TransformContext 11 | 12 | private fun TransformContext.valueToBool(value: String): Boolean { 13 | return when (value.lowercase()) { 14 | "true", "t", "1", "yes", "y", "on" -> true 15 | "false", "f", "0", "no", "n", "off" -> false 16 | else -> fail(context.localization.boolConversionError(value)) 17 | } 18 | } 19 | 20 | /** 21 | * Convert the argument values to `Boolean`. 22 | * 23 | * ## Conversion 24 | * 25 | * Conversion is case-insensitive. 26 | * 27 | * - `true`: "true", "t", "1", "yes", "y", "on" 28 | * - `false`: "false", "f", "0", "no", "n", "off" 29 | * 30 | * All other values are an error. 31 | */ 32 | fun RawArgument.boolean(): ProcessedArgument = convert { valueToBool(it) } 33 | 34 | /** 35 | * Convert the option values to `Boolean` 36 | * 37 | * In most cases, you should use [flag] instead of this function, but this allows you to have 38 | * tri-state delegates of type `Boolean?`. 39 | * 40 | * ## Conversion 41 | * 42 | * Conversion is case-insensitive. 43 | * 44 | * - `true`: "true", "t", "1", "yes", "y", "on" 45 | * - `false`: "false", "f", "0", "no", "n", "off" 46 | * 47 | * All other values are an error. 48 | */ 49 | fun RawOption.boolean(): NullableOption = 50 | convert("[true|false]") { valueToBool(it) } 51 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/types/double.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters.types 2 | 3 | import com.github.ajalt.clikt.parameters.arguments.ProcessedArgument 4 | import com.github.ajalt.clikt.parameters.arguments.RawArgument 5 | import com.github.ajalt.clikt.parameters.arguments.convert 6 | import com.github.ajalt.clikt.parameters.options.OptionWithValues 7 | import com.github.ajalt.clikt.parameters.options.RawOption 8 | import com.github.ajalt.clikt.parameters.options.convert 9 | import com.github.ajalt.clikt.parameters.transform.TransformContext 10 | 11 | private val conversion: TransformContext.(String) -> Double = 12 | { it.toDoubleOrNull() ?: fail(context.localization.floatConversionError(it)) } 13 | 14 | /** Convert the argument values to a `Double` */ 15 | fun RawArgument.double(): ProcessedArgument = convert(conversion = conversion) 16 | 17 | /** Convert the option values to a `Double` */ 18 | fun RawOption.double(): OptionWithValues { 19 | return convert({ localization.floatMetavar() }, conversion = conversion) 20 | } 21 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/types/enum.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters.types 2 | 3 | import com.github.ajalt.clikt.parameters.arguments.ProcessedArgument 4 | import com.github.ajalt.clikt.parameters.arguments.RawArgument 5 | import com.github.ajalt.clikt.parameters.options.NullableOption 6 | import com.github.ajalt.clikt.parameters.options.RawOption 7 | 8 | /** 9 | * Convert the argument to the values of an enum. 10 | * 11 | * If [ignoreCase] is `false`, the argument will only accept values that match the case of the enum values. 12 | * 13 | * ### Example: 14 | * 15 | * ``` 16 | * enum class Size { SMALL, LARGE } 17 | * argument().enum() 18 | * ``` 19 | * 20 | * @param key A block that returns the command line value to use for an enum value. The default is 21 | * the enum name. 22 | */ 23 | inline fun > RawArgument.enum( 24 | ignoreCase: Boolean = true, 25 | key: (T) -> String = { it.name }, 26 | ): ProcessedArgument { 27 | return choice(enumValues().associateBy { key(it) }, ignoreCase) 28 | } 29 | 30 | /** 31 | * Convert the option to the values of an enum. 32 | * 33 | * If [ignoreCase] is `false`, the option will only accept values that match the case of the enum values. 34 | * 35 | * ### Example: 36 | * 37 | * ``` 38 | * enum class Size { SMALL, LARGE } 39 | * option().enum() 40 | * ``` 41 | * 42 | * @param key A block that returns the command line value to use for an enum value. The default is 43 | * the enum name. 44 | */ 45 | inline fun > RawOption.enum( 46 | ignoreCase: Boolean = true, 47 | key: (T) -> String = { it.name }, 48 | ): NullableOption { 49 | return choice(enumValues().associateBy { key(it) }, ignoreCase = ignoreCase) 50 | } 51 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/types/float.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters.types 2 | 3 | import com.github.ajalt.clikt.parameters.arguments.ProcessedArgument 4 | import com.github.ajalt.clikt.parameters.arguments.RawArgument 5 | import com.github.ajalt.clikt.parameters.arguments.convert 6 | import com.github.ajalt.clikt.parameters.options.OptionWithValues 7 | import com.github.ajalt.clikt.parameters.options.RawOption 8 | import com.github.ajalt.clikt.parameters.options.convert 9 | import com.github.ajalt.clikt.parameters.transform.TransformContext 10 | 11 | private val conversion: TransformContext.(String) -> Float = 12 | { it.toFloatOrNull() ?: fail(context.localization.floatConversionError(it)) } 13 | 14 | /** Convert the argument values to a `Float` */ 15 | fun RawArgument.float(): ProcessedArgument = convert(conversion = conversion) 16 | 17 | /** Convert the option values to a `Float` */ 18 | fun RawOption.float(): OptionWithValues = 19 | convert({ localization.floatMetavar() }, conversion = conversion) 20 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/types/int.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters.types 2 | 3 | import com.github.ajalt.clikt.parameters.arguments.ProcessedArgument 4 | import com.github.ajalt.clikt.parameters.arguments.RawArgument 5 | import com.github.ajalt.clikt.parameters.arguments.convert 6 | import com.github.ajalt.clikt.parameters.options.NullableOption 7 | import com.github.ajalt.clikt.parameters.options.RawOption 8 | import com.github.ajalt.clikt.parameters.options.convert 9 | import com.github.ajalt.clikt.parameters.transform.TransformContext 10 | 11 | private val conversion: TransformContext.(String) -> Int = 12 | { it.toIntOrNull() ?: fail(context.localization.intConversionError(it)) } 13 | 14 | 15 | /** Convert the argument values to an `Int` */ 16 | fun RawArgument.int(): ProcessedArgument = convert(conversion = conversion) 17 | 18 | /** 19 | * Convert the option values to an `Int` 20 | * 21 | * @param acceptsValueWithoutName If `true`, this option can be specified like `-2` or `-3` in 22 | * addition to `--opt=2` or `-o3` 23 | */ 24 | fun RawOption.int(acceptsValueWithoutName: Boolean = false): NullableOption { 25 | return convert({ localization.intMetavar() }, conversion = conversion) 26 | .copy(acceptsNumberValueWithoutName = acceptsValueWithoutName) 27 | } 28 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/types/long.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters.types 2 | 3 | import com.github.ajalt.clikt.parameters.arguments.ProcessedArgument 4 | import com.github.ajalt.clikt.parameters.arguments.RawArgument 5 | import com.github.ajalt.clikt.parameters.arguments.convert 6 | import com.github.ajalt.clikt.parameters.options.NullableOption 7 | import com.github.ajalt.clikt.parameters.options.RawOption 8 | import com.github.ajalt.clikt.parameters.options.convert 9 | import com.github.ajalt.clikt.parameters.transform.TransformContext 10 | 11 | private val conversion: TransformContext.(String) -> Long = 12 | { it.toLongOrNull() ?: fail(context.localization.intConversionError(it)) } 13 | 14 | /** Convert the argument values to a `Long` */ 15 | fun RawArgument.long(): ProcessedArgument { 16 | return convert(conversion = conversion) 17 | } 18 | 19 | /** 20 | * Convert the option values to an `Long` 21 | * 22 | * @param acceptsValueWithoutName If `true`, this option can be specified like `-2` or `-3` in 23 | * addition to `--opt=2` or `-o3` 24 | */ 25 | fun RawOption.long(acceptsValueWithoutName: Boolean = false): NullableOption { 26 | return convert({ localization.intMetavar() }, conversion = conversion) 27 | .copy(acceptsNumberValueWithoutName = acceptsValueWithoutName) 28 | } 29 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/types/range.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters.types 2 | 3 | import com.github.ajalt.clikt.parameters.arguments.ProcessedArgument 4 | import com.github.ajalt.clikt.parameters.arguments.convert 5 | import com.github.ajalt.clikt.parameters.options.OptionWithValues 6 | import com.github.ajalt.clikt.parameters.options.convert 7 | import com.github.ajalt.clikt.parameters.transform.TransformContext 8 | 9 | private fun > TransformContext.checkRange( 10 | it: T, min: T?, max: T?, clamp: Boolean, 11 | ): T { 12 | require(min == null || max == null || min < max) { "min must be less than max" } 13 | if (clamp) { 14 | if (min != null && it < min) return min 15 | if (max != null && it > max) return max 16 | } else if (min != null && it < min || max != null && it > max) { 17 | val message = when { 18 | min == null -> context.localization.rangeExceededMax(it.toString(), max.toString()) 19 | max == null -> context.localization.rangeExceededMin(it.toString(), min.toString()) 20 | else -> context.localization.rangeExceededBoth( 21 | it.toString(), min.toString(), max.toString() 22 | ) 23 | } 24 | fail(message) 25 | } 26 | return it 27 | } 28 | 29 | 30 | // Arguments 31 | /** 32 | * Restrict the argument values to fit into a range. 33 | * 34 | * By default, conversion fails if the value is outside the range, but if [clamp] is true, the value will be 35 | * silently clamped to fit in the range. 36 | * 37 | * This must be called before transforms like `pair`, `default`, or `multiple`, since it checks each 38 | * individual value. 39 | * 40 | * ### Example: 41 | * 42 | * ``` 43 | * argument().int().restrictTo(max=10, clamp=true).default(10) 44 | * ``` 45 | */ 46 | fun > ProcessedArgument.restrictTo( 47 | min: T? = null, 48 | max: T? = null, 49 | clamp: Boolean = false, 50 | ): ProcessedArgument = convert { checkRange(it, min, max, clamp) } 51 | 52 | /** 53 | * Restrict the argument values to fit into a range. 54 | * 55 | * By default, conversion fails if the value is outside the range, but if [clamp] is true, the value will be 56 | * silently clamped to fit in the range. 57 | * 58 | * This must be called before transforms like `pair`, `default`, or `multiple`, since it checks each 59 | * individual value. 60 | * 61 | * ### Example: 62 | * 63 | * ``` 64 | * argument().int().restrictTo(1..10, clamp=true).default(10) 65 | * ``` 66 | */ 67 | fun > ProcessedArgument.restrictTo( 68 | range: ClosedRange, 69 | clamp: Boolean = false, 70 | ): ProcessedArgument = restrictTo(range.start, range.endInclusive, clamp) 71 | 72 | // Options 73 | 74 | /** 75 | * Restrict the option values to fit into a range. 76 | * 77 | * By default, conversion fails if the value is outside the range, but if [clamp] is true, the value will be 78 | * silently clamped to fit in the range. 79 | * 80 | * This must be called before transforms like `pair`, `default`, or `multiple`, since it checks each 81 | * individual value. 82 | * 83 | * ### Example: 84 | * 85 | * ``` 86 | * option().int().restrictTo(max=10, clamp=true).default(10) 87 | * ``` 88 | */ 89 | fun > OptionWithValues.restrictTo( 90 | min: T? = null, 91 | max: T? = null, 92 | clamp: Boolean = false, 93 | ): OptionWithValues = convert { checkRange(it, min, max, clamp) } 94 | 95 | 96 | /** 97 | * Restrict the option values to fit into a range. 98 | * 99 | * By default, conversion fails if the value is outside the range, but if [clamp] is true, the value will be 100 | * silently clamped to fit in the range. 101 | * 102 | * This must be called before transforms like `pair`, `default`, or `multiple`, since it checks each 103 | * individual value. 104 | * 105 | * ### Example: 106 | * 107 | * ``` 108 | * option().int().restrictTo(1..10, clamp=true).default(10) 109 | * ``` 110 | */ 111 | fun > OptionWithValues.restrictTo( 112 | range: ClosedRange, 113 | clamp: Boolean = false, 114 | ): OptionWithValues = restrictTo(range.start, range.endInclusive, clamp) 115 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/types/uint.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters.types 2 | 3 | import com.github.ajalt.clikt.parameters.arguments.ProcessedArgument 4 | import com.github.ajalt.clikt.parameters.arguments.RawArgument 5 | import com.github.ajalt.clikt.parameters.arguments.convert 6 | import com.github.ajalt.clikt.parameters.options.NullableOption 7 | import com.github.ajalt.clikt.parameters.options.RawOption 8 | import com.github.ajalt.clikt.parameters.options.convert 9 | import com.github.ajalt.clikt.parameters.transform.TransformContext 10 | 11 | 12 | private val conversion: TransformContext.(String) -> UInt = 13 | { it.toUIntOrNull() ?: fail(context.localization.intConversionError(it)) } 14 | 15 | /** Convert the argument values to an `UInt` */ 16 | fun RawArgument.uint(): ProcessedArgument = convert(conversion = conversion) 17 | 18 | /** 19 | * Convert the option values to a `UInt` 20 | * 21 | * @param acceptsValueWithoutName If `true`, this option can be specified like `-2` or `-3` in 22 | * addition to `--opt=2` or `-o3` 23 | */ 24 | fun RawOption.uint(acceptsValueWithoutName: Boolean = false): NullableOption { 25 | return convert({ localization.intMetavar() }, conversion = conversion) 26 | .copy(acceptsNumberValueWithoutName = acceptsValueWithoutName) 27 | } 28 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parameters/types/ulong.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters.types 2 | 3 | import com.github.ajalt.clikt.parameters.arguments.ProcessedArgument 4 | import com.github.ajalt.clikt.parameters.arguments.RawArgument 5 | import com.github.ajalt.clikt.parameters.arguments.convert 6 | import com.github.ajalt.clikt.parameters.options.NullableOption 7 | import com.github.ajalt.clikt.parameters.options.RawOption 8 | import com.github.ajalt.clikt.parameters.options.convert 9 | import com.github.ajalt.clikt.parameters.transform.TransformContext 10 | 11 | 12 | private val conversion: TransformContext.(String) -> ULong = 13 | { it.toULongOrNull() ?: fail(context.localization.intConversionError(it)) } 14 | 15 | /** Convert the argument values to a `ULong` */ 16 | fun RawArgument.ulong(): ProcessedArgument = convert(conversion = conversion) 17 | 18 | /** 19 | * Convert the option values to a `ULong` 20 | * 21 | * @param acceptsValueWithoutName If `true`, this option can be specified like `-2` or `-3` in 22 | * addition to `--opt=2` or `-o3` 23 | */ 24 | fun RawOption.ulong(acceptsValueWithoutName: Boolean = false): NullableOption { 25 | return convert({ localization.intMetavar() }, conversion = conversion) 26 | .copy(acceptsNumberValueWithoutName = acceptsValueWithoutName) 27 | } 28 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/parsers/atfile.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parsers 2 | 3 | import com.github.ajalt.clikt.core.InvalidFileFormat 4 | import com.github.ajalt.clikt.output.Localization 5 | import com.github.ajalt.clikt.output.defaultLocalization 6 | 7 | internal fun shlex( 8 | filename: String, 9 | text: String, 10 | localization: Localization = defaultLocalization, 11 | ): List { 12 | val toks = mutableListOf() 13 | var inQuote: Char? = null 14 | val sb = StringBuilder() 15 | var i = 0 16 | fun err(msg: String): Nothing { 17 | throw InvalidFileFormat(filename, msg, text.take(i).count { it == '\n' }) 18 | } 19 | loop@ while (i < text.length) { 20 | val c = text[i] 21 | when { 22 | c in "\r\n" && inQuote != null -> { 23 | sb.append(c) 24 | i += 1 25 | } 26 | 27 | c == '\\' -> { 28 | if (i >= text.lastIndex) err(localization.fileEndsWithSlash()) 29 | if (text[i + 1] in "\r\n") { 30 | do { 31 | i += 1 32 | } while (i <= text.lastIndex && text[i].isWhitespace()) 33 | } else { 34 | sb.append(text[i + 1]) 35 | i += 2 36 | } 37 | } 38 | 39 | c == inQuote -> { 40 | toks += sb.toString() 41 | sb.clear() 42 | inQuote = null 43 | i += 1 44 | } 45 | 46 | c == '#' && inQuote == null -> { 47 | i = text.indexOf('\n', i) 48 | if (i < 0) break@loop 49 | } 50 | 51 | c in "\"'" && inQuote == null -> { 52 | inQuote = c 53 | i += 1 54 | } 55 | 56 | c.isWhitespace() && inQuote == null -> { 57 | if (sb.isNotEmpty()) { 58 | toks += sb.toString() 59 | sb.clear() 60 | } 61 | i += 1 62 | } 63 | 64 | else -> { 65 | sb.append(c) 66 | i += 1 67 | } 68 | } 69 | } 70 | 71 | if (inQuote != null) { 72 | err(localization.unclosedQuote()) 73 | } 74 | 75 | if (sb.isNotEmpty()) { 76 | toks += sb.toString() 77 | } 78 | 79 | return toks 80 | } 81 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/sources/ChainedValueSource.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.sources 2 | 3 | import com.github.ajalt.clikt.core.Context 4 | import com.github.ajalt.clikt.parameters.options.Option 5 | 6 | /** 7 | * A [ValueSource] that looks for values in multiple other sources. 8 | */ 9 | class ChainedValueSource(val sources: List) : ValueSource { 10 | init { 11 | require(sources.isNotEmpty()) { "Must provide configuration sources" } 12 | } 13 | 14 | override fun getValues(context: Context, option: Option): List { 15 | return sources.asSequence() 16 | .map { it.getValues(context, option) } 17 | .firstOrNull { it.isNotEmpty() } 18 | .orEmpty() 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/sources/MapValueSource.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.sources 2 | 3 | import com.github.ajalt.clikt.core.Context 4 | import com.github.ajalt.clikt.parameters.options.Option 5 | import com.github.ajalt.clikt.parameters.options.pair 6 | import com.github.ajalt.clikt.parameters.options.triple 7 | 8 | /** 9 | * A [ValueSource] that reads values from a map. 10 | * 11 | * This implementation will only return a single value for each option. If you use conversions like 12 | * [pair] or [triple], you'll need to implement a [ValueSource] yourself. 13 | * 14 | * @param values The map of key to value for each option 15 | * @param getKey A function that return the key in [values] for a given option. By default, it joins the 16 | */ 17 | class MapValueSource( 18 | private val values: Map, 19 | private val getKey: (Context, Option) -> String = ValueSource.getKey(joinSubcommands = "."), 20 | ) : ValueSource { 21 | override fun getValues(context: Context, option: Option): List { 22 | return values[option.valueSourceKey ?: getKey(context, option)] 23 | ?.let { ValueSource.Invocation.just(it) }.orEmpty() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /clikt/src/commonMain/kotlin/com/github/ajalt/clikt/sources/ValueSource.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.sources 2 | 3 | import com.github.ajalt.clikt.core.Context 4 | import com.github.ajalt.clikt.parameters.options.* 5 | 6 | interface ValueSource { 7 | data class Invocation(val values: List) { 8 | companion object { 9 | /** Create a list of a single Invocation with a single value */ 10 | fun just(value: Any?): List = listOf(value(value)) 11 | 12 | /** Create an Invocation with a single value */ 13 | fun value(value: Any?): Invocation = Invocation(listOf(value.toString())) 14 | } 15 | } 16 | 17 | fun getValues(context: Context, option: Option): List 18 | 19 | companion object { 20 | /** 21 | * Get a name for an option that can be useful as a key for a value source. 22 | * 23 | * The returned value is the longest option name with its prefix removed 24 | * 25 | * ## Examples 26 | * 27 | * ``` 28 | * name(option("-h", "--help")) == "help" 29 | * name(option("/INPUT")) == "INPUT" 30 | * name(option("--new-name", "--name")) == "new-name 31 | * ``` 32 | */ 33 | fun name(option: Option): String { 34 | val longestName = option.longestName() 35 | requireNotNull(longestName) { "Option must have a name" } 36 | return splitOptionPrefix(longestName).second 37 | } 38 | 39 | /** 40 | * Create a function that will return string keys for options. 41 | * 42 | * By default, keys will be equal to the value returned by [name]. 43 | * 44 | * @param prefix A static string prepended to all keys 45 | * @param joinSubcommands If null, keys will not include names of subcommands. If given, 46 | * this string be used will join subcommand names to the beginning of keys. For options 47 | * that are in a root command, this has no effect. For option in subcommands, the 48 | * subcommand name will be joined. The root command name is never included. 49 | * @param uppercase If true, returned keys will be entirely uppercase. 50 | * @param replaceDashes `-` characters in option names will be replaced with this character. 51 | */ 52 | fun getKey( 53 | prefix: String = "", 54 | joinSubcommands: String? = null, 55 | uppercase: Boolean = false, 56 | replaceDashes: String = "-", 57 | ): (Context, Option) -> String = { context, option -> 58 | var k = name(option).replace("-", replaceDashes) 59 | if (joinSubcommands != null) { 60 | k = (context.commandNameWithParents().drop(1) + k).joinToString(joinSubcommands) 61 | } 62 | k = k.replace("-", replaceDashes) 63 | if (uppercase) k = k.uppercase() 64 | prefix + k 65 | } 66 | 67 | /** 68 | * Create a function that will return string keys that match the key used for environment variables. 69 | */ 70 | fun envvarKey(): (Context, Option) -> String = { context, option -> 71 | val env = when (option) { 72 | is OptionWithValues<*, *, *> -> option.envvar 73 | else -> null 74 | } 75 | inferEnvvar(option.names, env, context.autoEnvvarPrefix) ?: "" 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /clikt/src/jvmMain/kotlin/com/github/ajalt/clikt/core/ContextJvm.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.core 2 | 3 | /** 4 | * Register an [AutoCloseable] to be closed when this command and all its subcommands have 5 | * finished running. 6 | * 7 | * This is useful for resources that need to be shared across multiple commands. For resources 8 | * that aren't shared, it's often simpler to use [use] directly. 9 | * 10 | * Registered closeables will be closed in the reverse order that they were registered. 11 | * 12 | * ### Example 13 | * 14 | * ``` 15 | * currentContext.obj = currentContext.registerCloseable(File("foo").bufferedReader()) 16 | * ``` 17 | * 18 | * @return the closeable that was registered 19 | * @see Context.callOnClose 20 | * @see Context.registerCloseable 21 | */ 22 | fun Context.registerJvmCloseable(closeable: T): T { 23 | callOnClose { closeable.close() } 24 | return closeable 25 | } 26 | -------------------------------------------------------------------------------- /clikt/src/jvmMain/kotlin/com/github/ajalt/clikt/parameters/types/file.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters.types 2 | 3 | import com.github.ajalt.clikt.completion.CompletionCandidates 4 | import com.github.ajalt.clikt.parameters.arguments.ProcessedArgument 5 | import com.github.ajalt.clikt.parameters.arguments.RawArgument 6 | import com.github.ajalt.clikt.parameters.arguments.convert 7 | import com.github.ajalt.clikt.parameters.options.NullableOption 8 | import com.github.ajalt.clikt.parameters.options.RawOption 9 | import com.github.ajalt.clikt.parameters.options.convert 10 | import java.io.File 11 | import java.nio.file.FileSystems 12 | 13 | /** 14 | * Convert the argument to a [File]. 15 | * 16 | * @param mustExist If true, fail if the given path does not exist 17 | * @param canBeFile If false, fail if the given path is a file 18 | * @param canBeDir If false, fail if the given path is a directory 19 | * @param mustBeWritable If true, fail if the given path is not writable 20 | * @param mustBeReadable If true, fail if the given path is not readable 21 | * @param canBeSymlink If false, fail if the given path is a symlink 22 | */ 23 | fun RawArgument.file( 24 | mustExist: Boolean = false, 25 | canBeFile: Boolean = true, 26 | canBeDir: Boolean = true, 27 | mustBeWritable: Boolean = false, 28 | mustBeReadable: Boolean = false, 29 | canBeSymlink: Boolean = true, 30 | ): ProcessedArgument { 31 | return convert(CompletionCandidates.Path) { str -> 32 | convertToPath( 33 | path = str, 34 | mustExist = mustExist, 35 | canBeFile = canBeFile, 36 | canBeFolder = canBeDir, 37 | mustBeWritable = mustBeWritable, 38 | mustBeReadable = mustBeReadable, 39 | canBeSymlink = canBeSymlink, 40 | fileSystem = FileSystems.getDefault(), 41 | context = context 42 | ) { fail(it) }.toFile() 43 | } 44 | } 45 | 46 | /** 47 | * Convert the option to a [File]. 48 | * 49 | * @param mustExist If true, fail if the given path does not exist 50 | * @param canBeFile If false, fail if the given path is a file 51 | * @param canBeDir If false, fail if the given path is a directory 52 | * @param mustBeWritable If true, fail if the given path is not writable 53 | * @param mustBeReadable If true, fail if the given path is not readable 54 | * @param canBeSymlink If false, fail if the given path is a symlink 55 | */ 56 | fun RawOption.file( 57 | mustExist: Boolean = false, 58 | canBeFile: Boolean = true, 59 | canBeDir: Boolean = true, 60 | mustBeWritable: Boolean = false, 61 | mustBeReadable: Boolean = false, 62 | canBeSymlink: Boolean = true, 63 | ): NullableOption { 64 | return convert({ localization.pathMetavar() }, CompletionCandidates.Path) { str -> 65 | convertToPath( 66 | path = str, 67 | mustExist = mustExist, 68 | canBeFile = canBeFile, 69 | canBeFolder = canBeDir, 70 | mustBeWritable = mustBeWritable, 71 | mustBeReadable = mustBeReadable, 72 | canBeSymlink = canBeSymlink, 73 | fileSystem = FileSystems.getDefault(), 74 | context = context 75 | ) { fail(it) }.toFile() 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /clikt/src/jvmMain/kotlin/com/github/ajalt/clikt/parameters/types/inputStream.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters.types 2 | 3 | import com.github.ajalt.clikt.completion.CompletionCandidates 4 | import com.github.ajalt.clikt.core.Context 5 | import com.github.ajalt.clikt.parameters.arguments.* 6 | import com.github.ajalt.clikt.parameters.options.* 7 | import java.io.IOException 8 | import java.io.InputStream 9 | import java.nio.file.FileSystem 10 | import java.nio.file.FileSystems 11 | import java.nio.file.Files 12 | 13 | // region ========== Options ========== 14 | 15 | /** 16 | * Convert the option to an [InputStream]. 17 | * 18 | * The value given on the command line must be either a path to a readable file, or `-`. If `-` is 19 | * given, stdin will be used. 20 | * 21 | * If stdin is used, the resulting [InputStream] will be a proxy for [System. in] that will not close 22 | * the underlying stream. So you can always [close][InputStream.close] the resulting stream without 23 | * worrying about accidentally closing [System. in]. 24 | */ 25 | fun RawOption.inputStream( 26 | fileSystem: FileSystem = FileSystems.getDefault(), 27 | ): NullableOption { 28 | return convert({ localization.fileMetavar() }, CompletionCandidates.Path) { s -> 29 | convertToInputStream(s, fileSystem, context) { fail(it) } 30 | } 31 | } 32 | 33 | /** 34 | * Use `-` as the default value for an [inputStream] option. 35 | */ 36 | fun NullableOption.defaultStdin(): OptionWithValues { 37 | return default(UncloseableInputStream(System.`in`), "-") 38 | } 39 | 40 | // endregion 41 | // region ========== Arguments ========== 42 | 43 | /** 44 | * Convert the argument to an [InputStream]. 45 | * 46 | * The value given on the command line must be either a path to a readable file, or `-`. If `-` is 47 | * given, stdin will be used. 48 | * 49 | * If stdin is used, the resulting [InputStream] will be a proxy for [System. in] that will not close 50 | * the underlying stream. So you can always [close][InputStream.close] the resulting stream without 51 | * worrying about accidentally closing [System. in]. 52 | */ 53 | fun RawArgument.inputStream( 54 | fileSystem: FileSystem = FileSystems.getDefault(), 55 | ): ProcessedArgument { 56 | return convert(completionCandidates = CompletionCandidates.Path) { s -> 57 | convertToInputStream(s, fileSystem, context) { fail(it) } 58 | } 59 | } 60 | 61 | /** 62 | * Use `-` as the default value for an [inputStream] argument. 63 | */ 64 | fun ProcessedArgument.defaultStdin(): ArgumentDelegate { 65 | return default(UncloseableInputStream(System.`in`)) 66 | } 67 | 68 | // endregion 69 | 70 | /** 71 | * Checks whether this stream was returned from an [inputStream] parameter, and that it is 72 | * reading from [System. in] (because `-` was given, or no value was given and the parameter uses 73 | * [defaultStdin]). 74 | */ 75 | val InputStream.isCliktParameterDefaultStdin: Boolean 76 | get() = this is UncloseableInputStream 77 | 78 | private fun convertToInputStream( 79 | s: String, 80 | fileSystem: FileSystem, 81 | context: Context, 82 | fail: (String) -> Unit, 83 | ): InputStream { 84 | return if (s == "-") { 85 | UncloseableInputStream(System.`in`) 86 | } else { 87 | val path = convertToPath( 88 | path = s, 89 | mustExist = true, 90 | canBeFile = true, 91 | canBeFolder = false, 92 | mustBeWritable = false, 93 | mustBeReadable = true, 94 | canBeSymlink = true, 95 | fileSystem = fileSystem, 96 | context = context, 97 | fail = fail 98 | ) 99 | Files.newInputStream(path) 100 | } 101 | } 102 | 103 | private class UncloseableInputStream(private var delegate: InputStream?) : InputStream() { 104 | private val stream get() = delegate ?: throw IOException("Stream closed") 105 | override fun available(): Int = stream.available() 106 | override fun read(): Int = stream.read() 107 | override fun read(b: ByteArray, off: Int, len: Int): Int = stream.read(b, off, len) 108 | override fun skip(n: Long): Long = stream.skip(n) 109 | override fun reset() = stream.reset() 110 | override fun markSupported(): Boolean = stream.markSupported() 111 | override fun mark(readlimit: Int) { 112 | stream.mark(readlimit) 113 | } 114 | 115 | override fun close() { 116 | delegate = null 117 | } 118 | } 119 | 120 | // endregion 121 | -------------------------------------------------------------------------------- /clikt/src/jvmMain/kotlin/com/github/ajalt/clikt/sources/PropertiesValueSource.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.sources 2 | 3 | import com.github.ajalt.clikt.core.Context 4 | import com.github.ajalt.clikt.core.InvalidFileFormat 5 | import com.github.ajalt.clikt.parameters.options.Option 6 | import java.io.File 7 | import java.nio.file.Files 8 | import java.nio.file.Path 9 | import java.util.* 10 | 11 | /** 12 | * A [ValueSource] that reads values from a [Properties] object. 13 | */ 14 | object PropertiesValueSource { 15 | /** 16 | * Parse a properties [file] into a value source. 17 | * 18 | * If the [file] does not exist, an empty value source will be returned. 19 | * 20 | * @param file The file to read from. 21 | * @param requireValid If true, a [InvalidFileFormat] will be thrown if the file doesn't parse correctly. 22 | * @param getKey A function that will return the property key for a given option. You can use 23 | * [ValueSource.getKey] for most use cases. 24 | */ 25 | fun from( 26 | file: Path, 27 | requireValid: Boolean = false, 28 | getKey: (Context, Option) -> String = ValueSource.getKey(joinSubcommands = "."), 29 | ): ValueSource { 30 | val properties = Properties() 31 | if (Files.isRegularFile(file)) { 32 | try { 33 | Files.newInputStream(file).buffered().use { properties.load(it) } 34 | } catch (e: Throwable) { 35 | if (requireValid) throw InvalidFileFormat( 36 | file.toString(), 37 | e.message ?: "could not read file" 38 | ) 39 | } 40 | } 41 | 42 | return from(properties, getKey) 43 | } 44 | 45 | /** 46 | * Parse a properties [file] into a value source. 47 | * 48 | * If the [file] does not exist, an empty value source will be returned. 49 | * 50 | * @param file The file to read from. 51 | * @param requireValid If true, a [InvalidFileFormat] will be thrown if the file doesn't parse correctly. 52 | * @param getKey A function that will return the property key for a given option. You can use 53 | * [ValueSource.getKey] for most use cases. 54 | */ 55 | fun from( 56 | file: File, 57 | requireValid: Boolean = false, 58 | getKey: (Context, Option) -> String = ValueSource.getKey(joinSubcommands = "."), 59 | ): ValueSource { 60 | return from(file.toPath(), requireValid, getKey) 61 | } 62 | 63 | /** 64 | * Parse a properties [file] into a value source. 65 | * 66 | * If the [file] does not exist, an empty value source will be returned. 67 | * 68 | * @param file The file to read from. 69 | * @param requireValid If true, a [InvalidFileFormat] will be thrown if the file doesn't parse correctly. 70 | * @param getKey A function that will return the property key for a given option. You can use 71 | * [ValueSource.getKey] for most use cases. 72 | */ 73 | fun from( 74 | file: String, 75 | requireValid: Boolean = false, 76 | getKey: (Context, Option) -> String = ValueSource.getKey(joinSubcommands = "."), 77 | ): ValueSource = from(File(file), requireValid, getKey) 78 | 79 | /** 80 | * Return a [ValueSource] that reads values from a [properties] object. 81 | * 82 | * The [properties] object is copied when this function is called; changes to the object will 83 | * not be reflected in the value source. 84 | * 85 | * @param properties The properties to read from. 86 | * @param getKey A function that will return the property key for a given option. 87 | */ 88 | fun from( 89 | properties: Properties, 90 | getKey: (Context, Option) -> String = ValueSource.getKey(joinSubcommands = "."), 91 | ): ValueSource { 92 | val values = properties.entries.associate { it.key.toString() to it.value.toString() } 93 | return MapValueSource(values, getKey) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /docs/arguments.md: -------------------------------------------------------------------------------- 1 | # Arguments 2 | 3 | Arguments are declared and customized similarly to [options][options], 4 | but are provided on the command line positionally instead of by name. 5 | Arguments are declared with [`argument()`][argument], 6 | and the order that they are declared defines the order that they 7 | must be provided on the command line. 8 | 9 | ## Basic Arguments 10 | 11 | By default, [`argument`][argument] takes a single `String` value which is required to be 12 | provided on the command line. 13 | 14 | === "Example" 15 | ```kotlin 16 | 17 | class Hello : CliktCommand() { 18 | val name by argument() 19 | override fun run() { 20 | echo("Hello $name!") 21 | } 22 | } 23 | ``` 24 | 25 | === "Usage" 26 | ```text 27 | $ ./hello Foo 28 | Hello Foo! 29 | ``` 30 | 31 | Arguments appear in the usage string, but listed in the help page unless you set their `help` value. 32 | It's usually clearer to document arguments in the command help. 33 | 34 | === "Example" 35 | ```kotlin 36 | class Cp : CliktCommand( 37 | help = "Copy to , or multiple (s) to directory ." 38 | ) { 39 | private val source by argument().file(mustExist = true).multiple() 40 | private val dest by argument().file() 41 | override fun run() { 42 | // ... 43 | } 44 | } 45 | ``` 46 | 47 | === "Help Output" 48 | ```text 49 | Usage: cp [] []... 50 | 51 | Copy to , or multiple (s) to directory . 52 | 53 | Options: 54 | -h, --help Show this message and exit 55 | ``` 56 | 57 | ## Variadic Arguments 58 | 59 | Like [options][options], arguments can take any fixed number of values, which you can change with 60 | functions like [`pair`][pair] and [`triple`][triple]. Unlike options, arguments can also take a 61 | variable (or unlimited) number of values. This is common with file path arguments, since 62 | they are frequently expanded with a glob pattern on the command line. 63 | 64 | Variadic arguments are declared with [`multiple`][multiple]. You can declare any number of arguments 65 | with fixed numbers of values, but only one variadic argument in a command. 66 | 67 | === "Example" 68 | ```kotlin 69 | class Copy : CliktCommand() { 70 | val source: List by argument().path(mustExist = true).multiple() 71 | val dest: Path by argument().path(canBeFile = false) 72 | override fun run() { 73 | echo("Copying files $source to $dest") 74 | } 75 | } 76 | ``` 77 | 78 | === "Usage" 79 | ```text 80 | $ ./copy file.* out/ 81 | Copying files [file.txt, file.md] to out/ 82 | ``` 83 | 84 | You can also use [`unique`][unique] to discard duplicates: 85 | 86 | ```kotlin 87 | val source: Set by argument().path(mustExist = true).multiple().unique() 88 | ``` 89 | 90 | ## Option-Like Arguments (Using `--`) 91 | 92 | Clikt normally parses any value that starts with punctuation as an 93 | option, which allows users to intermix options and arguments. However, 94 | sometimes you need to pass a value that starts with punctuation to an 95 | argument. For example, you might have a file named `-file.txt` that you 96 | want to use as an argument. 97 | 98 | Clikt supports the POSIX convention of using `--` to force all following 99 | values to be treated as arguments. Any values before the `--` will be 100 | parsed normally. 101 | 102 | === "Example" 103 | ```kotlin 104 | class Touch : CliktCommand() { 105 | val verbose by option().flag() 106 | val files by argument().multiple() 107 | override fun run() { 108 | if (verbose) echo(files.joinToString("\n")) 109 | } 110 | } 111 | ``` 112 | 113 | === "Usage 1" 114 | ```text 115 | $ ./touch --foo.txt 116 | Usage: touch [] []... 117 | 118 | Error: no such option: "--foo.txt". 119 | ``` 120 | 121 | === "Usage 2" 122 | ```text 123 | $ ./touch --verbose -- --foo.txt bar.txt 124 | --foo.txt 125 | bar.txt 126 | ``` 127 | 128 | 129 | [argument]: api/clikt/com.github.ajalt.clikt.parameters.arguments/argument.html 130 | [multiple]: api/clikt/com.github.ajalt.clikt.parameters.arguments/multiple.html 131 | [options]: options.md 132 | [pair]: api/clikt/com.github.ajalt.clikt.parameters.arguments/pair.html 133 | [triple]: api/clikt/com.github.ajalt.clikt.parameters.arguments/triple.html 134 | [unique]: api/clikt/com.github.ajalt.clikt.parameters.arguments/unique.html 135 | -------------------------------------------------------------------------------- /docs/css/extra.css: -------------------------------------------------------------------------------- 1 | /* Custom CSS for mkdocs */ 2 | 3 | .md-header__topic { 4 | color: transparent; 5 | } 6 | 7 | .md-header .md-logo img { 8 | width: 83px; 9 | } 10 | -------------------------------------------------------------------------------- /docs/img/animation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajalt/clikt/cdb04411b301bca4c69824d994ba64d160f6bf59/docs/img/animation.png -------------------------------------------------------------------------------- /docs/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajalt/clikt/cdb04411b301bca4c69824d994ba64d160f6bf59/docs/img/favicon.ico -------------------------------------------------------------------------------- /docs/img/readme_screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajalt/clikt/cdb04411b301bca4c69824d994ba64d160f6bf59/docs/img/readme_screenshot1.png -------------------------------------------------------------------------------- /docs/img/readme_screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajalt/clikt/cdb04411b301bca4c69824d994ba64d160f6bf59/docs/img/readme_screenshot2.png -------------------------------------------------------------------------------- /docs/img/readme_screenshot3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajalt/clikt/cdb04411b301bca4c69824d994ba64d160f6bf59/docs/img/readme_screenshot3.png -------------------------------------------------------------------------------- /docs/img/wordmark.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /docs/img/wordmark_small.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/img/wordmark_small_dark.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | VERSION_NAME=5.0.3 2 | # Silence the compile warning that MPP is experimental 3 | kotlin.mpp.stability.nowarn=true 4 | # Enable experimental cross compilation 5 | kotlin.native.enableKlibsCrossCompilation=true 6 | # gradle-maven-publish configuration 7 | SONATYPE_HOST=DEFAULT 8 | RELEASE_SIGNING_ENABLED=true 9 | GROUP=com.github.ajalt.clikt 10 | POM_DESCRIPTION=Multiplatform command line interface parsing for Kotlin 11 | POM_INCEPTION_YEAR=2018 12 | POM_URL=https://github.com/ajalt/clikt/ 13 | POM_LICENSE_NAME=Apache-2.0 14 | POM_LICENSE_URL=https://www.apache.org/licenses/LICENSE-2.0 15 | POM_LICENSE_DIST=repo 16 | POM_SCM_URL=https://github.com/ajalt/clikt/ 17 | POM_SCM_CONNECTION=scm:git:git://github.com/ajalt/clikt.git 18 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/ajalt/clikt.git 19 | POM_DEVELOPER_ID=ajalt 20 | POM_DEVELOPER_NAME=AJ Alt 21 | POM_DEVELOPER_URL=https://github.com/ajalt/ 22 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | kotlin = "2.1.0" 3 | coroutines = "1.9.0" 4 | mordant = "3.0.1" 5 | 6 | [libraries] 7 | mordant = {module = "com.github.ajalt.mordant:mordant", version.ref = "mordant"} 8 | mordant-markdown = {module = "com.github.ajalt.mordant:mordant-markdown", version.ref = "mordant"} 9 | 10 | # used in tests 11 | coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } 12 | coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } 13 | kotest = "io.kotest:kotest-assertions-core:5.9.1" 14 | systemrules = "com.github.stefanbirkner:system-rules:1.19.0" 15 | jimfs = "com.google.jimfs:jimfs:1.3.0" 16 | 17 | # used in samples 18 | kodein = "org.kodein.di:kodein-di-generic-jvm:6.5.5" 19 | kotlinx-serialization = "org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2" 20 | 21 | [plugins] 22 | dokka = "org.jetbrains.dokka:1.9.20" 23 | publish = "com.vanniktech.maven.publish:0.30.0" 24 | kotlinBinaryCompatibilityValidator = "org.jetbrains.kotlinx.binary-compatibility-validator:0.16.3" 25 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajalt/clikt/cdb04411b301bca4c69824d994ba64d160f6bf59/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 1>&2 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 48 | echo. 1>&2 49 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 50 | echo location of your Java installation. 1>&2 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 1>&2 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 62 | echo. 1>&2 63 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 64 | echo location of your Java installation. 1>&2 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 0 goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /prepare_docs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # The website is built using MkDocs with the Material theme. 4 | # https://squidfunk.github.io/mkdocs-material/ 5 | # It requires Python to run. 6 | # Install the packages with the following command: 7 | # Install the packages: `pip install mkdocs-material` 8 | # Build the api docs: `./gradlew dokkaHtmlMultiModule` 9 | # Then run this script to prepare the docs for the website. 10 | # Finally, run `mkdocs serve` to preview the site locally or `mkdocs build` to build the site. 11 | 12 | set -ex 13 | 14 | # Copy the changelog into the site, omitting the unreleased section 15 | cat CHANGELOG.md \ 16 | | grep -v '^## Unreleased' \ 17 | | sed '/^## /,$!d' \ 18 | > docs/changelog.md 19 | 20 | 21 | # Add the jinja frontmatter to the index 22 | cat > docs/index.md <<- EOM 23 | --- 24 | hide: 25 | - toc # Hide table of contents 26 | --- 27 | 28 | EOM 29 | 30 | # Copy the README into the index, omitting the license, docs links, and fixing hrefs 31 | cat README.md \ 32 | | sed 's:docs/img:img:g' \ 33 | | sed -e '/## Documentation/,/(runsample)\./d' \ 34 | | sed '/## License/Q' \ 35 | >> docs/index.md 36 | 37 | # Add some extra links to the index page 38 | cat >> docs/index.md <<- EOM 39 | 40 | # API Reference 41 | 42 | * [Commands and Exceptions](api/clikt/com.github.ajalt.clikt.core/) 43 | * [Options](api/clikt/com.github.ajalt.clikt.parameters.options/) 44 | * [Arguments](api/clikt/com.github.ajalt.clikt.parameters.arguments/) 45 | * [Parameter Type Conversions](api/clikt/com.github.ajalt.clikt.parameters.types/) 46 | * [Output Formatting](api/clikt/com.github.ajalt.clikt.output/) 47 | EOM 48 | -------------------------------------------------------------------------------- /runsample: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Run one of the samples. 3 | # The first argument must be the name of the sample task (e.g. echo). 4 | # Any remaining arguments are forwarded to the sample's argv. 5 | 6 | task=$1 7 | shift 1 8 | 9 | if [ -z "${task}" ] || [ ! -d "samples/${task}" ] 10 | then 11 | echo "Unknown sample: '${task}'" 12 | exit 1 13 | fi 14 | 15 | ./gradlew --quiet ":samples:${task}:installDist" && "./samples/${task}/build/install/${task}/bin/${task}" "$@" 16 | -------------------------------------------------------------------------------- /runsample.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%"=="" @echo off 2 | :: Run one of the samples. 3 | :: The first argument must be the name of the sample task (e.g. echo). 4 | :: Any remaining arguments are forwarded to the sample's argv. 5 | 6 | if "%OS%"=="Windows_NT" setlocal EnableDelayedExpansion 7 | 8 | set TASK=%~1 9 | 10 | set SAMPLE=false 11 | if defined TASK if not "!TASK: =!"=="" if exist "samples\%TASK%\*" set SAMPLE=true 12 | 13 | if "%SAMPLE%"=="false" ( 14 | echo Unknown sample: '%TASK%' 15 | exit /b 1 16 | ) 17 | 18 | set ARGS=%* 19 | set ARGS=!ARGS:*%1=! 20 | if "!ARGS:~0,1!"==" " set ARGS=!ARGS:~1! 21 | 22 | call gradlew --quiet ":samples:%TASK%:installDist" && call "samples\%TASK%\build\install\%TASK%\bin\%TASK%" %ARGS% 23 | 24 | if "%OS%"=="Windows_NT" endlocal 25 | -------------------------------------------------------------------------------- /samples/README.md: -------------------------------------------------------------------------------- 1 | # Clikt examples 2 | 3 | This directory contains samples showing various features of Clikt. You 4 | can run each sample using the `runsample` script in the root of this 5 | project. 6 | 7 | ``` 8 | ./runsample repo commit -m 'commit message' 9 | ``` 10 | 11 | -------------------------------------------------------------------------------- /samples/aliases/README.md: -------------------------------------------------------------------------------- 1 | # Command aliases 2 | 3 | This example shows how you can load command aliases dynamically from a 4 | file. Aliases are read from the `aliases.cfg` file. 5 | 6 | With the example alias, the following two calls will produce the same 7 | result: 8 | 9 | ``` 10 | ./runsample aliases commit -m "commit message" 11 | ./runsample aliases cm "commit message" 12 | ``` 13 | -------------------------------------------------------------------------------- /samples/aliases/build.gradle.kts: -------------------------------------------------------------------------------- 1 | application { 2 | mainClass.set("com.github.ajalt.clikt.samples.aliases.MainKt") 3 | } 4 | -------------------------------------------------------------------------------- /samples/aliases/src/main/kotlin/com/github/ajalt/clikt/samples/aliases/aliases.cfg: -------------------------------------------------------------------------------- 1 | cm = commit -m 2 | -------------------------------------------------------------------------------- /samples/aliases/src/main/kotlin/com/github/ajalt/clikt/samples/aliases/main.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.samples.aliases 2 | 3 | import com.github.ajalt.clikt.core.* 4 | import com.github.ajalt.clikt.parameters.options.multiple 5 | import com.github.ajalt.clikt.parameters.options.option 6 | import java.io.File 7 | 8 | 9 | /** 10 | * @param configFile A config file containing aliases, one per line, in the form `token = alias`. Aliases can 11 | * have multiple tokens (e.g. `cm = commit -m`). 12 | */ 13 | class AliasedCli(private val configFile: File) : CoreNoOpCliktCommand() { 14 | override fun help(context: Context) = "An example that supports aliased subcommands" 15 | override fun aliases(): Map> { 16 | return configFile.readLines().map { it.split("=", limit = 2) } 17 | .associate { it[0].trim() to it[1].trim().split(Regex("\\s+")) } 18 | } 19 | } 20 | 21 | 22 | class Push : CliktCommand() { 23 | override fun help(context: Context) = "push changes" 24 | override fun run() = echo("push") 25 | } 26 | 27 | class Pull : CliktCommand() { 28 | override fun help(context: Context) = "pull changes" 29 | override fun run() = echo("pull") 30 | } 31 | 32 | class Clone : CliktCommand() { 33 | override fun help(context: Context) = "clone a repository" 34 | override fun run() = echo("clone") 35 | } 36 | 37 | class Commit : CliktCommand() { 38 | override fun help(context: Context) = "clone a repository" 39 | val message by option("-m", "--message").multiple() 40 | override fun run() = echo("commit message=${message.joinToString("\n")}") 41 | } 42 | 43 | fun main(args: Array) { 44 | // The file path is relative to the project root for use with `runsample` 45 | AliasedCli(File("samples/aliases/src/main/kotlin/com/github/ajalt/clikt/samples/aliases/aliases.cfg")) 46 | .subcommands(Push(), Pull(), Clone(), Commit()) 47 | .main(args) 48 | } 49 | -------------------------------------------------------------------------------- /samples/build.gradle.kts: -------------------------------------------------------------------------------- 1 | subprojects { 2 | apply(plugin = "kotlin") 3 | apply(plugin = "application") 4 | 5 | dependencies { 6 | "implementation"(project(":clikt")) 7 | "implementation"(project(":clikt-mordant")) 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /samples/copy/README.md: -------------------------------------------------------------------------------- 1 | # File copy sample 2 | 3 | This example works like the `cp` command, copying files or directories. 4 | It shows how to use `prompt` to get an input from user. 5 | 6 | ``` 7 | $ touch src.txt dest.txt 8 | $ ./runsample copy -i src.txt dest.txt 9 | ``` 10 | -------------------------------------------------------------------------------- /samples/copy/build.gradle.kts: -------------------------------------------------------------------------------- 1 | application { 2 | mainClass.set("com.github.ajalt.clikt.samples.copy.MainKt") 3 | } 4 | -------------------------------------------------------------------------------- /samples/copy/src/main/kotlin/com/github/ajalt/clikt/samples/copy/main.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.samples.copy 2 | 3 | import com.github.ajalt.clikt.core.CliktCommand 4 | import com.github.ajalt.clikt.core.Context 5 | import com.github.ajalt.clikt.core.main 6 | import com.github.ajalt.clikt.core.terminal 7 | import com.github.ajalt.clikt.parameters.arguments.argument 8 | import com.github.ajalt.clikt.parameters.arguments.multiple 9 | import com.github.ajalt.clikt.parameters.options.flag 10 | import com.github.ajalt.clikt.parameters.options.option 11 | import com.github.ajalt.clikt.parameters.types.file 12 | import com.github.ajalt.mordant.terminal.YesNoPrompt 13 | 14 | class Copy : CliktCommand() { 15 | override fun help(context: Context): String { 16 | return "Copy SOURCE to DEST, or multiple SOURCE(s) to directory DEST." 17 | } 18 | 19 | val interactive by option("-i", "--interactive", help = "prompt before overwrite").flag() 20 | val recursive by option("-r", "--recursive", help = "copy directories recursively").flag() 21 | val source by argument().file(mustExist = true).multiple() 22 | val dest by argument().file(canBeFile = false) 23 | 24 | override fun run() { 25 | for (file in source) { 26 | try { 27 | if (recursive) file.copyRecursively(dest) 28 | else file.copyTo(dest) 29 | } catch (e: FileAlreadyExistsException) { 30 | if (interactive) { 31 | val response = YesNoPrompt("overwrite '$dest'?", terminal, default = true).ask() 32 | if (response == false) continue 33 | } 34 | if (recursive) file.copyRecursively(dest, overwrite = true) 35 | else file.copyTo(dest, overwrite = true) 36 | } 37 | } 38 | } 39 | } 40 | 41 | fun main(args: Array) = Copy().main(args) 42 | -------------------------------------------------------------------------------- /samples/helpformat/README.md: -------------------------------------------------------------------------------- 1 | # Custom help formatter 2 | 3 | This example shows how to use a custom help formatter in a command. This formatter changes the 4 | parameter sections to be drawn in panels, changes metavars to be uppercase, and changes the colors 5 | used. 6 | 7 |

8 | -------------------------------------------------------------------------------- /samples/helpformat/build.gradle.kts: -------------------------------------------------------------------------------- 1 | application { 2 | mainClass.set("com.github.ajalt.clikt.samples.helpformat.MainKt") 3 | } 4 | -------------------------------------------------------------------------------- /samples/helpformat/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajalt/clikt/cdb04411b301bca4c69824d994ba64d160f6bf59/samples/helpformat/screenshot.png -------------------------------------------------------------------------------- /samples/helpformat/src/main/kotlin/com/github/ajalt/clikt/samples/helpformat/main.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.samples.helpformat 2 | 3 | import com.github.ajalt.clikt.core.* 4 | import com.github.ajalt.clikt.output.HelpFormatter 5 | import com.github.ajalt.clikt.output.MordantHelpFormatter 6 | import com.github.ajalt.clikt.parameters.arguments.argument 7 | import com.github.ajalt.clikt.parameters.arguments.multiple 8 | import com.github.ajalt.clikt.parameters.options.flag 9 | import com.github.ajalt.clikt.parameters.options.option 10 | import com.github.ajalt.mordant.rendering.TextAlign 11 | import com.github.ajalt.mordant.rendering.TextColors 12 | import com.github.ajalt.mordant.rendering.Theme 13 | import com.github.ajalt.mordant.rendering.Widget 14 | import com.github.ajalt.mordant.table.ColumnWidth 15 | import com.github.ajalt.mordant.table.verticalLayout 16 | import com.github.ajalt.mordant.terminal.Terminal 17 | import com.github.ajalt.mordant.widgets.Panel 18 | import com.github.ajalt.mordant.widgets.Text 19 | 20 | 21 | class PanelHelpFormatter(context: Context) : MordantHelpFormatter(context) { 22 | // You can override which styles are used for each part of the output. 23 | // If you want to change the color of the styles themselves, you can set them in the terminal's 24 | // theme (see the main function below). 25 | override fun styleSectionTitle(title: String): String = theme.style("muted")(title) 26 | 27 | // Print section titles like "Options" instead of "Options:" 28 | override fun renderSectionTitle(title: String): String = title 29 | 30 | // Print metavars like INT instead of 31 | override fun normalizeParameter(name: String): String = name.uppercase() 32 | 33 | // Print option values like `--option VALUE instead of `--option=VALUE` 34 | override fun renderAttachedOptionValue(metavar: String): String = " $metavar" 35 | 36 | // Put each parameter section in its own panel 37 | override fun renderParameters( 38 | parameters: List, 39 | ): Widget = verticalLayout { 40 | width = ColumnWidth.Expand() 41 | for (section in collectParameterSections(parameters)) { 42 | cell( 43 | Panel( 44 | section.content, 45 | Text(section.title), 46 | expand = true, 47 | titleAlign = TextAlign.LEFT, 48 | borderStyle = theme.style("muted") 49 | ) 50 | ) 51 | } 52 | } 53 | } 54 | 55 | class Echo(t: Terminal) : CliktCommand() { 56 | override fun help(context: Context): String { 57 | return "Echo the STRING(s) to standard output" 58 | } 59 | 60 | init { 61 | context { 62 | terminal = t 63 | helpFormatter = { PanelHelpFormatter(it) } 64 | } 65 | } 66 | 67 | val suppressNewline by option("-n", help = "do not output the trailing newline").flag() 68 | val strings by argument(help = "the strings to echo").multiple() 69 | 70 | override fun run() { 71 | val message = if (strings.isEmpty()) String(System.`in`.readBytes()) 72 | else strings.joinToString(" ", postfix = if (suppressNewline) "" else "\n") 73 | echo(message, trailingNewline = false) 74 | } 75 | } 76 | 77 | fun main(args: Array) { 78 | val theme = Theme { 79 | // Use ANSI-16 codes for help colors 80 | styles["info"] = TextColors.green 81 | styles["warning"] = TextColors.blue 82 | styles["danger"] = TextColors.magenta 83 | styles["muted"] = TextColors.gray 84 | 85 | // Remove the border around code blocks 86 | flags["markdown.code.block.border"] = false 87 | } 88 | Echo(Terminal(theme = theme)).main(args) 89 | } 90 | -------------------------------------------------------------------------------- /samples/json/README.md: -------------------------------------------------------------------------------- 1 | # JSON Configuration files 2 | 3 | This example shows how to read option values from JSON files using the `kotlinx.serialization` 4 | library. It reads config files from the `config.json` file located in this directory. 5 | 6 | ``` 7 | ./runsample json subcommand 8 | ``` 9 | -------------------------------------------------------------------------------- /samples/json/build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | kotlin("plugin.serialization") version libs.versions.kotlin 3 | } 4 | 5 | application { 6 | mainClass.set("com.github.ajalt.clikt.samples.json.MainKt") 7 | } 8 | 9 | dependencies { 10 | implementation(libs.kotlinx.serialization) 11 | } 12 | -------------------------------------------------------------------------------- /samples/json/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "option": ["option value 1", "value 2"], 3 | "flag": true, 4 | "subcommand": { "number": 123 } 5 | } 6 | -------------------------------------------------------------------------------- /samples/json/src/main/kotlin/com/github/ajalt/clikt/samples/json/JsonValueSource.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.samples.json 2 | 3 | import com.github.ajalt.clikt.core.Context 4 | import com.github.ajalt.clikt.core.InvalidFileFormat 5 | import com.github.ajalt.clikt.parameters.options.Option 6 | import com.github.ajalt.clikt.sources.ValueSource 7 | import kotlinx.serialization.SerializationException 8 | import kotlinx.serialization.json.* 9 | import java.io.File 10 | 11 | /** 12 | * A [ValueSource] that uses Kotlin serialization to parse JSON files 13 | */ 14 | class JsonValueSource( 15 | private val root: JsonObject, 16 | ) : ValueSource { 17 | override fun getValues(context: Context, option: Option): List { 18 | var cursor: JsonElement? = root 19 | val parts = option.valueSourceKey?.split(".") 20 | ?: (context.commandNameWithParents().drop(1) + ValueSource.name(option)) 21 | for (part in parts) { 22 | if (cursor !is JsonObject) return emptyList() 23 | cursor = cursor[part] 24 | } 25 | if (cursor == null) return emptyList() 26 | 27 | try { 28 | // This implementation interprets a list as multiple invocations, but you could also 29 | // implement it as a single invocation with multiple values. 30 | if (cursor is JsonArray) return cursor.map { 31 | ValueSource.Invocation.value(it.jsonPrimitive.content) 32 | } 33 | return ValueSource.Invocation.just(cursor.jsonPrimitive.content) 34 | } catch (e: IllegalArgumentException) { 35 | // This implementation skips invalid values, but you could handle them differently. 36 | return emptyList() 37 | } 38 | } 39 | 40 | companion object { 41 | fun from(file: File, requireValid: Boolean = false): JsonValueSource { 42 | if (!file.isFile) return JsonValueSource(JsonObject(emptyMap())) 43 | 44 | val json = try { 45 | Json.parseToJsonElement(file.readText()) as? JsonObject 46 | ?: throw InvalidFileFormat(file.path, "object expected", 1) 47 | } catch (e: SerializationException) { 48 | if (requireValid) { 49 | throw InvalidFileFormat(file.name, e.message ?: "could not read file") 50 | } 51 | JsonObject(emptyMap()) 52 | } 53 | return JsonValueSource(json) 54 | } 55 | 56 | fun from(file: String, requireValid: Boolean = false): JsonValueSource { 57 | return from(File(file), requireValid) 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /samples/json/src/main/kotlin/com/github/ajalt/clikt/samples/json/main.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.samples.json 2 | 3 | import com.github.ajalt.clikt.core.* 4 | import com.github.ajalt.clikt.parameters.options.flag 5 | import com.github.ajalt.clikt.parameters.options.multiple 6 | import com.github.ajalt.clikt.parameters.options.option 7 | import com.github.ajalt.clikt.parameters.types.float 8 | 9 | class Subcommand : CliktCommand() { 10 | private val number by option(help = "an integer").float() 11 | 12 | override fun run() { 13 | echo("subcommand --number=$number") 14 | } 15 | } 16 | 17 | class Cli : CliktCommand() { 18 | override fun help(context: Context) = 19 | "An example using json files for configuration values" 20 | 21 | init { 22 | context { 23 | valueSources( 24 | JsonValueSource.from(System.getProperty("user.dir") + "/config.json"), 25 | JsonValueSource.from(System.getProperty("user.dir") + "/samples/json/config.json") 26 | ) 27 | } 28 | } 29 | 30 | private val option by option( 31 | "-o", 32 | "--option", 33 | help = "this option takes multiple values" 34 | ).multiple() 35 | private val flag by option("-f", "--flag", help = "this option is a flag").flag() 36 | 37 | override fun run() { 38 | echo("--option=$option") 39 | echo("--flag=$flag") 40 | } 41 | } 42 | 43 | fun main(args: Array) = Cli().subcommands(Subcommand()).main(args) 44 | -------------------------------------------------------------------------------- /samples/plugins/README.md: -------------------------------------------------------------------------------- 1 | # Dynamic subcommands example 2 | 3 | This example shows how to create a complex application that registers 4 | subcommands dynamically through dependency injection. 5 | 6 | ``` 7 | ./runsample plugins --help 8 | ``` 9 | -------------------------------------------------------------------------------- /samples/plugins/build.gradle.kts: -------------------------------------------------------------------------------- 1 | application { 2 | mainClass.set("com.github.ajalt.clikt.samples.plugins.MainKt") 3 | } 4 | 5 | dependencies { 6 | api(libs.kodein) 7 | } 8 | -------------------------------------------------------------------------------- /samples/plugins/src/main/kotlin/com/github/ajalt/clikt/samples/plugins/clone.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.samples.plugins 2 | 3 | import com.github.ajalt.clikt.core.CliktCommand 4 | import com.github.ajalt.clikt.core.Context 5 | import com.github.ajalt.clikt.core.requireObject 6 | import com.github.ajalt.clikt.parameters.arguments.argument 7 | import com.github.ajalt.clikt.parameters.arguments.optional 8 | import com.github.ajalt.clikt.parameters.options.default 9 | import com.github.ajalt.clikt.parameters.options.flag 10 | import com.github.ajalt.clikt.parameters.options.option 11 | import org.kodein.di.Kodein 12 | import org.kodein.di.generic.bind 13 | import org.kodein.di.generic.inSet 14 | import org.kodein.di.generic.provider 15 | import java.io.File 16 | 17 | class Clone : CliktCommand() { 18 | override fun help(context: Context) = """ 19 | Clones a repository. 20 | 21 | This will clone the repository at SRC into the folder DEST. If DEST 22 | is not provided this will automatically use the last path component 23 | of SRC and create that folder. 24 | """.trimIndent() 25 | 26 | val repo: Repo by requireObject() 27 | val src: String by argument() 28 | val dest: String? by argument().optional() 29 | val shallow: Boolean by option(help = "Makes a checkout shallow or deep. Deep by default.") 30 | .flag("--deep") 31 | 32 | val rev: String by option("--rev", "-r", help = "Clone a specific revision instead of HEAD.") 33 | .default("HEAD") 34 | 35 | override fun run() { 36 | val destName = dest ?: File(src).name 37 | echo("Cloning repo $src to ${File(destName).absolutePath}") 38 | repo.home = destName 39 | if (shallow) { 40 | echo("Making shallow checkout") 41 | } 42 | echo("Checking out revision $rev") 43 | } 44 | } 45 | 46 | val cloneModule = Kodein.Module("clone") { 47 | bind().inSet() with provider { Clone() } 48 | } 49 | -------------------------------------------------------------------------------- /samples/plugins/src/main/kotlin/com/github/ajalt/clikt/samples/plugins/commit.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.samples.plugins 2 | 3 | import com.github.ajalt.clikt.core.CliktCommand 4 | import com.github.ajalt.clikt.core.Context 5 | import com.github.ajalt.clikt.core.requireObject 6 | import com.github.ajalt.clikt.parameters.arguments.argument 7 | import com.github.ajalt.clikt.parameters.arguments.multiple 8 | import com.github.ajalt.clikt.parameters.options.help 9 | import com.github.ajalt.clikt.parameters.options.multiple 10 | import com.github.ajalt.clikt.parameters.options.option 11 | import com.github.ajalt.clikt.parameters.types.file 12 | import org.kodein.di.Kodein 13 | import org.kodein.di.generic.bind 14 | import org.kodein.di.generic.inSet 15 | import org.kodein.di.generic.provider 16 | import java.io.File 17 | 18 | class Commit : CliktCommand() { 19 | override fun help(context: Context) = """ 20 | Commits outstanding changes. 21 | 22 | Commit changes to the given files into the repository. You will need to 23 | "repo push" to push up your changes to other repositories. 24 | 25 | If a list of files is omitted, all changes reported by "repo status" 26 | will be committed. 27 | """.trimIndent() 28 | 29 | val repo: Repo by requireObject() 30 | val message: List by option("--message", "-m").multiple() 31 | .help( 32 | "The commit message. If provided multiple times " + 33 | "each argument gets converted into a new line." 34 | ) 35 | val files: List by argument() 36 | .file() 37 | .multiple() 38 | 39 | override fun run() { 40 | echo("Files to be committed: $files") 41 | echo("Commit message:") 42 | echo(message.joinToString("\n")) 43 | } 44 | } 45 | 46 | val commitModule = Kodein.Module("commit") { 47 | bind().inSet() with provider { Commit() } 48 | } 49 | -------------------------------------------------------------------------------- /samples/plugins/src/main/kotlin/com/github/ajalt/clikt/samples/plugins/delete.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.samples.plugins 2 | 3 | import com.github.ajalt.clikt.core.CliktCommand 4 | import com.github.ajalt.clikt.core.Context 5 | import com.github.ajalt.clikt.core.requireObject 6 | import org.kodein.di.Kodein 7 | import org.kodein.di.generic.bind 8 | import org.kodein.di.generic.inSet 9 | import org.kodein.di.generic.provider 10 | 11 | class Delete : CliktCommand() { 12 | override fun help(context: Context) = """ 13 | Deletes a repository. 14 | 15 | This will throw away the current repository. 16 | """.trimIndent() 17 | 18 | val repo: Repo by requireObject() 19 | 20 | override fun run() { 21 | echo("Destroying repo ${repo.home}") 22 | echo("Deleted!") 23 | } 24 | } 25 | 26 | val deleteModule = Kodein.Module("delete") { 27 | bind().inSet() with provider { Delete() } 28 | } 29 | -------------------------------------------------------------------------------- /samples/plugins/src/main/kotlin/com/github/ajalt/clikt/samples/plugins/main.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.samples.plugins 2 | 3 | import com.github.ajalt.clikt.core.* 4 | import com.github.ajalt.clikt.parameters.options.* 5 | import org.kodein.di.Kodein 6 | import org.kodein.di.generic.bind 7 | import org.kodein.di.generic.instance 8 | import org.kodein.di.generic.setBinding 9 | 10 | data class Repo(var home: String, val config: MutableMap, var verbose: Boolean) 11 | 12 | class Cli : CliktCommand() { 13 | override fun help(context: Context) = """ 14 | Repo is a command line tool that showcases how to build complex 15 | command line interfaces with Clikt. 16 | 17 | This tool is supposed to look like a distributed version control 18 | system to show how something like this can be structured. 19 | """.trimIndent() 20 | 21 | init { 22 | versionOption("1.0") 23 | } 24 | 25 | val repoHome: String by option(help = "Changes the repository folder location.") 26 | .default(".repo") 27 | val config: List> by option(help = "Overrides a config key/value pair.") 28 | .pair() 29 | .multiple() 30 | val verbose: Boolean by option("-v", "--verbose", help = "Enables verbose mode.") 31 | .flag() 32 | 33 | override fun run() { 34 | val repo = Repo(repoHome, HashMap(), verbose) 35 | for ((k, v) in config) { 36 | repo.config[k] = v 37 | } 38 | currentContext.obj = repo 39 | } 40 | } 41 | 42 | fun main(args: Array) { 43 | val kodein = Kodein { 44 | bind() from setBinding() 45 | import(cloneModule) 46 | import(deleteModule) 47 | import(setuserModule) 48 | import(commitModule) 49 | } 50 | 51 | val commands: Set by kodein.instance() 52 | 53 | Cli().subcommands(commands).main(args) 54 | } 55 | -------------------------------------------------------------------------------- /samples/plugins/src/main/kotlin/com/github/ajalt/clikt/samples/plugins/setuser.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.samples.plugins 2 | 3 | import com.github.ajalt.clikt.core.CliktCommand 4 | import com.github.ajalt.clikt.core.Context 5 | import com.github.ajalt.clikt.core.requireObject 6 | import com.github.ajalt.clikt.parameters.options.option 7 | import com.github.ajalt.clikt.parameters.options.prompt 8 | import org.kodein.di.Kodein 9 | import org.kodein.di.generic.bind 10 | import org.kodein.di.generic.inSet 11 | import org.kodein.di.generic.provider 12 | 13 | class SetUser : CliktCommand(name = "setuser") { 14 | override fun help(context: Context) = """ 15 | Sets the user credentials. 16 | 17 | This will override the current user config. 18 | """.trimIndent() 19 | 20 | val repo: Repo by requireObject() 21 | val username: String by option(help = "The developer's shown username.") 22 | .prompt() 23 | val email: String by option(help = "The developer's email address.") 24 | .prompt(text = "E-Mail") 25 | val password: String by option(help = "The login password.") 26 | .prompt(hideInput = true) 27 | 28 | override fun run() { 29 | repo.config["username"] = username 30 | repo.config["email"] = email 31 | repo.config["password"] = "*".repeat(password.length) 32 | echo("Changed credentials.") 33 | } 34 | } 35 | 36 | val setuserModule = Kodein.Module("setuser") { 37 | bind().inSet() with provider { SetUser() } 38 | } 39 | -------------------------------------------------------------------------------- /samples/repo/README.md: -------------------------------------------------------------------------------- 1 | # Repo example 2 | 3 | This sample demonstrates building a complex commline app like git or hg. 4 | It has multiple subcommands, and uses the `Context.obj` to send data 5 | from the root command to subcommands. 6 | 7 | ``` 8 | ./runsample repo --help 9 | ./runsample repo clone --help 10 | ./runsample setuser 11 | ``` 12 | -------------------------------------------------------------------------------- /samples/repo/build.gradle.kts: -------------------------------------------------------------------------------- 1 | application { 2 | mainClass.set("com.github.ajalt.clikt.samples.repo.MainKt") 3 | } 4 | -------------------------------------------------------------------------------- /samples/validation/README.md: -------------------------------------------------------------------------------- 1 | # Validation example 2 | 3 | This sample shows how to perform custom conversion and validation of 4 | parameters. 5 | 6 | * The `--count` option has a custom validator 7 | * The `--bigger-count` option validates its value based on the value of `--count` 8 | * The `--quad` option takes four integer values 9 | * The `--sum` option can be provided multiple times, and all values will be added. 10 | * The `URL` argument is converted to a java URL object, and must be in the form `http://www.example.com` 11 | 12 | 13 | ``` 14 | ./runsample validation --count 2 --bigger-count 3 --quad 1 2 3 4 --sum 3 --sum 4 http://www.example.com 15 | ``` 16 | -------------------------------------------------------------------------------- /samples/validation/build.gradle.kts: -------------------------------------------------------------------------------- 1 | application { 2 | mainClass.set("com.github.ajalt.clikt.samples.validation.MainKt") 3 | } 4 | -------------------------------------------------------------------------------- /samples/validation/src/main/kotlin/com/github/ajalt/clikt/samples/validation/main.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.samples.validation 2 | 3 | import com.github.ajalt.clikt.core.CliktCommand 4 | import com.github.ajalt.clikt.core.Context 5 | import com.github.ajalt.clikt.core.main 6 | import com.github.ajalt.clikt.parameters.arguments.argument 7 | import com.github.ajalt.clikt.parameters.arguments.convert 8 | import com.github.ajalt.clikt.parameters.options.* 9 | import com.github.ajalt.clikt.parameters.types.int 10 | import java.net.URL 11 | 12 | data class Quad(val a: Int, val b: Int, val c: Int, val d: Int) 13 | 14 | class Cli : CliktCommand() { 15 | override fun help(context: Context) = "Validation examples" 16 | 17 | val count by option(help = "A positive even number").int() 18 | .check("Should be a positive, even integer") { it > 0 && it % 2 == 0 } 19 | 20 | val biggerCount by option(help = "A number larger than --count").int() 21 | .validate { 22 | require(count == null || count!! < it) { 23 | "--bigger-count must be larger than --count" 24 | } 25 | } 26 | 27 | val quad by option(help = "A four-valued option") 28 | .int() 29 | .transformValues(4) { Quad(it[0], it[1], it[2], it[3]) } 30 | 31 | val sum by option(help = "All values will be added") 32 | .int() 33 | .transformAll { it.sum() } 34 | 35 | val url by argument(help = "A URL") 36 | .convert { URL(it) } 37 | 38 | override fun run() { 39 | echo("count: $count") 40 | echo("biggerCount: $biggerCount") 41 | echo("quad: $quad") 42 | echo("sum: $sum") 43 | echo("url: $url") 44 | } 45 | } 46 | 47 | fun main(args: Array) = Cli().main(args) 48 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | include("clikt") 2 | include("clikt-mordant") 3 | include("clikt-mordant-markdown") 4 | include("test") 5 | include("samples:copy") 6 | include("samples:repo") 7 | include("samples:validation") 8 | include("samples:aliases") 9 | include("samples:helpformat") 10 | include("samples:plugins") 11 | include("samples:json") 12 | 13 | 14 | @Suppress("UnstableApiUsage") 15 | dependencyResolutionManagement { 16 | repositories { 17 | mavenCentral() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Module test 2 | 3 | This module contains all the tests for the project. It depends on all other modules. 4 | -------------------------------------------------------------------------------- /test/api/test.api: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ajalt/clikt/cdb04411b301bca4c69824d994ba64d160f6bf59/test/api/test.api -------------------------------------------------------------------------------- /test/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.ExperimentalWasmDsl 2 | 3 | plugins { 4 | kotlin("multiplatform") 5 | } 6 | 7 | kotlin { 8 | jvm() 9 | 10 | js { nodejs() } 11 | @OptIn(ExperimentalWasmDsl::class) 12 | wasmJs { nodejs() } 13 | 14 | linuxX64() 15 | linuxArm64() 16 | mingwX64() 17 | macosX64() 18 | macosArm64() 19 | 20 | iosArm64() 21 | iosX64() 22 | 23 | sourceSets { 24 | val commonMain by getting { 25 | dependencies { 26 | api(project(":clikt")) 27 | api(project(":clikt-mordant")) 28 | api(project(":clikt-mordant-markdown")) 29 | api(libs.mordant) 30 | api(libs.mordant.markdown) 31 | } 32 | } 33 | 34 | val commonTest by getting { 35 | dependencies { 36 | api(kotlin("test")) 37 | api(libs.kotest) 38 | api(libs.coroutines.core) 39 | api(libs.coroutines.test) 40 | } 41 | } 42 | 43 | val jvmTest by getting { 44 | dependencies { 45 | api(libs.systemrules) 46 | api(libs.jimfs) 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/clikt/command/ChainedCliktCommandTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.command 2 | 3 | import com.github.ajalt.clikt.core.context 4 | import com.github.ajalt.clikt.core.obj 5 | import com.github.ajalt.clikt.core.requireObject 6 | import com.github.ajalt.clikt.core.subcommands 7 | import com.github.ajalt.clikt.parameters.arguments.argument 8 | import com.github.ajalt.clikt.parameters.types.int 9 | import io.kotest.matchers.shouldBe 10 | import kotlinx.coroutines.test.runTest 11 | import kotlin.js.JsName 12 | import kotlin.test.Test 13 | 14 | 15 | class ChainedCliktCommandTest { 16 | @[Test JsName("chained_run")] 17 | fun `chained run`() = runTest { 18 | class C : ChainedCliktCommand>() { 19 | override val allowMultipleSubcommands: Boolean = true 20 | override fun run(value: List): List { 21 | return value + 1 22 | } 23 | } 24 | 25 | class Sub : ChainedCliktCommand>() { 26 | val arg by argument().int() 27 | override fun run(value: List): List { 28 | return value + arg 29 | } 30 | } 31 | 32 | val sub = Sub() 33 | val c: C = C().subcommands(sub) 34 | val result = c.parse(listOf("sub", "2", "sub", "3"), emptyList()) 35 | result shouldBe listOf(1, 2, 3) 36 | } 37 | 38 | 39 | @[Test JsName("chained_command_context")] 40 | fun `chained command context`() { 41 | class C : ChainedCliktCommand() { 42 | val arg by argument().int() 43 | val ctx by requireObject() 44 | override fun run(value: Int): Int { 45 | return value + arg + ctx 46 | } 47 | } 48 | 49 | C().context { obj = 10 }.parse(listOf("1"), 100) shouldBe 111 50 | } 51 | 52 | 53 | @[Test JsName("chained_command_test")] 54 | fun `chained command test`() { 55 | class C : ChainedCliktCommand() { 56 | val arg by argument().int() 57 | override fun run(value: Int): Int { 58 | echo(value + arg) 59 | return 0 60 | } 61 | } 62 | 63 | C().test("10", 1).output shouldBe "11\n" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/clikt/command/SuspendingCliktCommandTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.command 2 | 3 | import com.github.ajalt.clikt.core.subcommands 4 | import com.github.ajalt.clikt.parameters.arguments.argument 5 | import com.github.ajalt.clikt.parsers.CommandLineParser 6 | import io.kotest.matchers.shouldBe 7 | import kotlinx.coroutines.test.runTest 8 | import kotlinx.coroutines.yield 9 | import kotlin.js.JsName 10 | import kotlin.test.Test 11 | 12 | 13 | class SuspendingCliktCommandTest { 14 | @[Test JsName("suspending_run")] 15 | fun `suspending run`() = runTest { 16 | class C : SuspendingCliktCommand() { 17 | val arg by argument() 18 | var ran = false 19 | 20 | override suspend fun run() { 21 | yield() 22 | ran = true 23 | } 24 | } 25 | 26 | class Sub : SuspendingCliktCommand() { 27 | var ran = false 28 | 29 | override suspend fun run() { 30 | ran = true 31 | } 32 | } 33 | 34 | val sub = Sub() 35 | val c: C = C().subcommands(sub) 36 | c.parse(CommandLineParser.tokenize("foo sub")) 37 | c.arg shouldBe "foo" 38 | c.ran shouldBe true 39 | sub.ran shouldBe true 40 | } 41 | 42 | @[Test JsName("suspending_command_test")] 43 | fun `suspending command test`() = runTest { 44 | class C : SuspendingCliktCommand() { 45 | val arg by argument() 46 | override suspend fun run() { 47 | echo(arg) 48 | } 49 | } 50 | 51 | C().test("baz").output shouldBe "baz\n" 52 | } 53 | 54 | @[Test JsName("suspending_noop_command_test")] 55 | fun `suspending no-op command test`() = runTest { 56 | class C : SuspendingNoOpCliktCommand() 57 | class Sub : CoreSuspendingNoOpCliktCommand() 58 | 59 | C().subcommands(Sub()).test("sub").output shouldBe "" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/clikt/completion/EnvvarCompletionTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.completion 2 | 3 | import com.github.ajalt.clikt.core.PrintCompletionMessage 4 | import com.github.ajalt.clikt.core.context 5 | import com.github.ajalt.clikt.core.subcommands 6 | import com.github.ajalt.clikt.testing.TestCommand 7 | import com.github.ajalt.clikt.testing.parse 8 | import io.kotest.assertions.throwables.shouldThrow 9 | import io.kotest.data.blocking.forAll 10 | import io.kotest.data.row 11 | import io.kotest.matchers.string.shouldContain 12 | import kotlin.js.JsName 13 | import kotlin.test.Test 14 | 15 | 16 | class EnvvarCompletionTest { 17 | @[Test JsName("test_completion_from_envvar")] 18 | fun `test completion from envvar`() = forAll( 19 | row("bash"), 20 | row("zsh"), 21 | row("fish") 22 | ) { shell -> 23 | class C : TestCommand(autoCompleteEnvvar = "TEST_COMPLETE") { 24 | init { 25 | context { 26 | readEnvvar = mapOf("TEST_COMPLETE" to shell)::get 27 | } 28 | } 29 | } 30 | 31 | shouldThrow { 32 | C().subcommands(C()).parse("") 33 | }.message shouldContain shell 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/clikt/completion/FishCompletionTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.completion 2 | 3 | @Suppress("unused") 4 | class FishCompletionTest : CompletionTestBase("fish") { 5 | override fun `custom completions expected`(): String { 6 | return """ 7 | |# Command completion for c 8 | |# Generated by Clikt 9 | | 10 | |## Options for c 11 | |complete -c c -l o -r -fa "(echo foo bar)" 12 | |complete -c c -s h -l help -d 'Show this message and exit' 13 | | 14 | |## Arguments for c 15 | |complete -c c -fa "(echo zzz xxx)" 16 | | 17 | """ 18 | } 19 | 20 | override fun `subcommands with multi-word names expected`(): String { 21 | return """ 22 | |# Command completion for c 23 | |# Generated by Clikt 24 | | 25 | | 26 | |### Setup for c 27 | |set -l c_subcommands 'sub sub-command' 28 | | 29 | |## Options for c 30 | |complete -c c -n "not __fish_seen_subcommand_from ${'$'}c_subcommands" -s h -l help -d 'Show this message and exit' 31 | | 32 | | 33 | |### Setup for sub 34 | |complete -c c -f -n __fish_use_subcommand -a sub 35 | | 36 | |## Options for sub 37 | |complete -c c -n "__fish_seen_subcommand_from sub" -s h -l help -d 'Show this message and exit' 38 | | 39 | | 40 | |### Setup for sub-command 41 | |set -l c_sub_command_subcommands 'sub-sub long-sub-command' 42 | |complete -c c -f -n __fish_use_subcommand -a sub-command 43 | | 44 | |## Options for sub-command 45 | |complete -c c -n "__fish_seen_subcommand_from sub-command" -s h -l help -d 'Show this message and exit' 46 | | 47 | | 48 | |### Setup for sub-sub 49 | |complete -c c -f -n "__fish_seen_subcommand_from sub-command; and not __fish_seen_subcommand_from ${'$'}c_sub_command_subcommands" -a sub-sub 50 | | 51 | |## Options for sub-sub 52 | |complete -c c -n "__fish_seen_subcommand_from sub-sub" -s h -l help -d 'Show this message and exit' 53 | | 54 | | 55 | |### Setup for long-sub-command 56 | |complete -c c -f -n "__fish_seen_subcommand_from sub-command; and not __fish_seen_subcommand_from ${'$'}c_sub_command_subcommands" -a long-sub-command 57 | | 58 | |## Options for long-sub-command 59 | |complete -c c -n "__fish_seen_subcommand_from long-sub-command" -s h -l help -d 'Show this message and exit' 60 | | 61 | """ 62 | } 63 | 64 | override fun `option secondary names expected`(): String { 65 | return """ 66 | |# Command completion for c 67 | |# Generated by Clikt 68 | | 69 | |## Options for c 70 | |complete -c c -l flag -l no-flag 71 | |complete -c c -s h -l help -d 'Show this message and exit' 72 | | 73 | """ 74 | } 75 | 76 | override fun `explicit completion candidates expected`(): String { 77 | return """ 78 | |# Command completion for c 79 | |# Generated by Clikt 80 | | 81 | |## Options for c 82 | |complete -c c -l none -r 83 | |complete -c c -l path -r -F 84 | |complete -c c -l host -r -fa "(__fish_print_hostnames)" 85 | |complete -c c -l user -r -fa "(__fish_complete_users)" 86 | |complete -c c -l fixed -r -fa "foo bar" 87 | | 88 | |## Arguments for c 89 | |complete -c c -fa "(__fish_complete_users)" 90 | |complete -c c -fa "baz qux" 91 | | 92 | """ 93 | } 94 | 95 | override fun `arg names with spaces expected`(): String { 96 | return """ 97 | |# Command completion for c 98 | |# Generated by Clikt 99 | | 100 | |## Options for c 101 | |complete -c c -s h -l help -d 'Show this message and exit' 102 | | 103 | |## Arguments for c 104 | |complete -c c -d 'help' 105 | | 106 | """ 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/clikt/core/ExceptionsTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.core 2 | 3 | import com.github.ajalt.clikt.testing.TestCommand 4 | import com.github.ajalt.clikt.testing.test 5 | import io.kotest.data.blocking.forAll 6 | import io.kotest.data.row 7 | import io.kotest.matchers.shouldBe 8 | import kotlin.js.JsName 9 | import kotlin.test.Test 10 | 11 | class ExceptionsTest { 12 | @[Test JsName("exceptions_statusCode")] 13 | fun `exceptions statusCode`() = forAll( 14 | row(CliktError(), 1), 15 | row(CliktError(statusCode = 2), 2), 16 | row(UsageError(""), 1), 17 | row(UsageError("", statusCode = 2), 2), 18 | row(PrintHelpMessage(null), 0), 19 | row(PrintHelpMessage(null, statusCode = 2), 2), 20 | row(PrintMessage(""), 0), 21 | row(PrintMessage("", statusCode = 2), 2), 22 | row(PrintCompletionMessage(""), 0), 23 | row(Abort(), 1), 24 | row(ProgramResult(2), 2), 25 | row(NoSuchOption(""), 1), 26 | ) { err, code -> 27 | class C : TestCommand() { 28 | override fun run_() { 29 | throw err 30 | } 31 | } 32 | C().test("").statusCode shouldBe code 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/clikt/parameters/EnvvarInferTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters 2 | 3 | import com.github.ajalt.clikt.core.context 4 | import com.github.ajalt.clikt.parameters.options.option 5 | import com.github.ajalt.clikt.testing.TestCommand 6 | import com.github.ajalt.clikt.testing.parse 7 | import io.kotest.data.blocking.forAll 8 | import io.kotest.data.row 9 | import io.kotest.matchers.shouldBe 10 | import kotlin.test.Test 11 | 12 | class EnvvarInferTest { 13 | @Test 14 | fun inferEnvvar() = forAll( 15 | row(arrayOf("--foo"), null, null), 16 | row(arrayOf("--bar"), "FOO", "FOO_BAR"), 17 | row(arrayOf("/bar"), "FOO", "FOO_BAR"), 18 | row(arrayOf("-b"), "FOO", "FOO_B"), 19 | row(arrayOf("-b", "--bar"), "FOO", "FOO_BAR") 20 | ) { names, prefix, expected -> 21 | class C : TestCommand(autoCompleteEnvvar = null) { 22 | val o by option(*names) 23 | 24 | init { 25 | context { 26 | // Return the key as the value of the envvar 27 | readEnvvar = { it } 28 | autoEnvvarPrefix = prefix 29 | } 30 | } 31 | } 32 | C().parse("").o shouldBe expected 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/clikt/parameters/MultiUsageErrorTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters 2 | 3 | import com.github.ajalt.clikt.core.MultiUsageError 4 | import com.github.ajalt.clikt.core.UsageError 5 | import com.github.ajalt.clikt.parameters.arguments.argument 6 | import com.github.ajalt.clikt.parameters.arguments.check 7 | import com.github.ajalt.clikt.parameters.options.check 8 | import com.github.ajalt.clikt.parameters.options.option 9 | import com.github.ajalt.clikt.parameters.options.required 10 | import com.github.ajalt.clikt.parameters.types.int 11 | import com.github.ajalt.clikt.testing.TestCommand 12 | import com.github.ajalt.clikt.testing.formattedMessage 13 | import com.github.ajalt.clikt.testing.parse 14 | import io.kotest.assertions.throwables.shouldThrow 15 | import io.kotest.data.blocking.forAll 16 | import io.kotest.data.row 17 | import io.kotest.matchers.shouldBe 18 | import kotlin.test.Test 19 | 20 | @Suppress("unused") 21 | class MultiUsageErrorTest { 22 | @Test 23 | fun optionalValue() = forAll( 24 | row("", listOf("missing argument A", "missing option --x", "missing option --y")), 25 | row("--y=1", listOf("missing argument A", "missing option --x")), 26 | row( 27 | "--x=foo 1", 28 | listOf("invalid value for --x: foo is not a valid integer", "missing option --y") 29 | ), 30 | row("--x=0 --y=0 1", listOf("invalid value for A: 1")), 31 | row( 32 | // don't report unknown arg error after unknown opts 33 | "--y=0 --x=0 --n 1 2 3", 34 | listOf("no such option --n. (Possible options: --x, --y)") 35 | ), 36 | row( 37 | // do report missing arg after unknown opts 38 | "--y=0 --x=0 --n", 39 | listOf("no such option --n. (Possible options: --x, --y)", "missing argument A") 40 | ) 41 | ) { argv, ex -> 42 | class C : TestCommand(called = false) { 43 | val x by option().int().required().check { it == 0 } 44 | val y by option().int().required().check { it == 0 } 45 | val a by argument().int().check { it == 0 } 46 | } 47 | 48 | val e = shouldThrow { 49 | C().parse(argv) 50 | } 51 | ((e as? MultiUsageError)?.errors ?: listOf(e)) 52 | .map { it.formattedMessage } 53 | .shouldBe(ex) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/clikt/parameters/OptionSwitchTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters 2 | 3 | import com.github.ajalt.clikt.core.MissingOption 4 | import com.github.ajalt.clikt.parameters.options.* 5 | import com.github.ajalt.clikt.testing.TestCommand 6 | import com.github.ajalt.clikt.testing.parse 7 | import io.kotest.assertions.throwables.shouldThrow 8 | import io.kotest.data.blocking.forAll 9 | import io.kotest.data.row 10 | import io.kotest.matchers.shouldBe 11 | import kotlin.js.JsName 12 | import kotlin.test.Test 13 | 14 | class OptionSwitchTest { 15 | @[Test JsName("switch_option_map")] 16 | fun `switch option map`() = forAll( 17 | row("", null), 18 | row("-x", 1), 19 | row("-y", 2) 20 | ) { argv, ex -> 21 | class C : TestCommand() { 22 | val x by option().switch(mapOf("-x" to 1, "-y" to 2)) 23 | override fun run_() { 24 | x shouldBe ex 25 | } 26 | } 27 | C().parse(argv) 28 | } 29 | 30 | @[Test JsName("switch_option_vararg")] 31 | fun `switch option vararg`() = forAll( 32 | row("", null, -1, -2), 33 | row("-xyz", 1, 3, 5), 34 | row("--xx -yy -zz", 2, 4, 6), 35 | ) { argv, ex, ey, ez -> 36 | class C : TestCommand() { 37 | val x: Int? by option().switch("-x" to 1, "--xx" to 2) 38 | val y: Int by option().switch("-y" to 3, "-yy" to 4).default(-1) 39 | val z: Int by option().switch("-z" to 5, "-zz" to 6).defaultLazy { -2 } 40 | override fun run_() { 41 | x shouldBe ex 42 | y shouldBe ey 43 | z shouldBe ez 44 | } 45 | } 46 | 47 | C().parse(argv) 48 | } 49 | 50 | @[Test JsName("required_switch_options")] 51 | fun `required switch options`() { 52 | class C : TestCommand() { 53 | val x by option().switch("-x" to 1, "-xx" to 2).required() 54 | } 55 | 56 | C().parse("-x").x shouldBe 1 57 | C().parse("-xx").x shouldBe 2 58 | shouldThrow { C().parse("") } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/clikt/parameters/types/BooleanTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters.types 2 | 3 | import com.github.ajalt.clikt.core.BadParameterValue 4 | import com.github.ajalt.clikt.parameters.arguments.argument 5 | import com.github.ajalt.clikt.parameters.arguments.multiple 6 | import com.github.ajalt.clikt.parameters.options.default 7 | import com.github.ajalt.clikt.parameters.options.option 8 | import com.github.ajalt.clikt.testing.TestCommand 9 | import com.github.ajalt.clikt.testing.formattedMessage 10 | import com.github.ajalt.clikt.testing.parse 11 | import io.kotest.assertions.throwables.shouldThrow 12 | import io.kotest.data.blocking.forAll 13 | import io.kotest.data.row 14 | import io.kotest.matchers.shouldBe 15 | import kotlin.js.JsName 16 | import kotlin.test.Test 17 | 18 | class BooleanTest { 19 | @[Test JsName("boolean_option")] 20 | fun `boolean option`() = forAll( 21 | row("", null), 22 | row("--x=true", true), 23 | row("--x=t", true), 24 | row("--x=1", true), 25 | row("--x=yes", true), 26 | row("--x=y", true), 27 | row("--x=on", true), 28 | row("--x=True", true), 29 | row("--x=ON", true), 30 | row("--x=false", false), 31 | row("--x=f", false), 32 | row("--x=0", false), 33 | row("--x=no", false), 34 | row("--x=n", false), 35 | row("--x=off", false), 36 | row("--x=False", false), 37 | row("--x=OFF", false), 38 | ) { argv, ex -> 39 | class C : TestCommand() { 40 | val x: Boolean? by option().boolean() 41 | override fun run_() { 42 | x shouldBe ex 43 | } 44 | } 45 | 46 | C().parse(argv) 47 | } 48 | 49 | @[Test JsName("boolean_option_error")] 50 | fun `boolean option error`() { 51 | @Suppress("unused") 52 | class C : TestCommand(called = false) { 53 | val foo by option().boolean() 54 | } 55 | 56 | shouldThrow { C().parse("--foo bar") } 57 | .formattedMessage shouldBe "invalid value for --foo: bar is not a valid boolean" 58 | } 59 | 60 | @[Test JsName("boolean_option_with_default")] 61 | fun `boolean option with default`() = forAll( 62 | row("", true), 63 | row("--x=true", true), 64 | row("--x off", false), 65 | ) { argv, expected -> 66 | class C : TestCommand() { 67 | val x: Boolean by option().boolean().default(true) 68 | override fun run_() { 69 | x shouldBe expected 70 | } 71 | } 72 | C().parse(argv) 73 | } 74 | 75 | @[Test JsName("boolean_argument")] 76 | fun `boolean argument`() = forAll( 77 | row("", emptyList()), 78 | row("1 0 ON off", listOf(true, false, true, false)), 79 | ) { argv, ex -> 80 | class C : TestCommand() { 81 | val a: List by argument().boolean().multiple() 82 | override fun run_() { 83 | a shouldBe ex 84 | } 85 | } 86 | 87 | C().parse(argv) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/clikt/parameters/types/DoubleTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters.types 2 | 3 | import com.github.ajalt.clikt.core.BadParameterValue 4 | import com.github.ajalt.clikt.parameters.arguments.argument 5 | import com.github.ajalt.clikt.parameters.arguments.multiple 6 | import com.github.ajalt.clikt.parameters.arguments.optional 7 | import com.github.ajalt.clikt.parameters.options.default 8 | import com.github.ajalt.clikt.parameters.options.option 9 | import com.github.ajalt.clikt.testing.TestCommand 10 | import com.github.ajalt.clikt.testing.formattedMessage 11 | import com.github.ajalt.clikt.testing.parse 12 | import io.kotest.assertions.throwables.shouldThrow 13 | import io.kotest.data.blocking.forAll 14 | import io.kotest.data.row 15 | import io.kotest.matchers.shouldBe 16 | import kotlin.js.JsName 17 | import kotlin.test.Test 18 | 19 | @Suppress("unused") 20 | class DoubleTest { 21 | @[Test JsName("double_option")] 22 | fun `double option`() = forAll( 23 | row("", null), 24 | row("--xx 3", 3.0), 25 | row("--xx=4.0", 4.0), 26 | row("-x5.5", 5.5) 27 | ) { argv, expected -> 28 | class C : TestCommand() { 29 | val x: Double? by option("-x", "--xx").double() 30 | override fun run_() { 31 | x shouldBe expected 32 | } 33 | } 34 | 35 | C().parse(argv) 36 | } 37 | 38 | @[Test JsName("double_option_error")] 39 | fun `double option error`() { 40 | class C : TestCommand() { 41 | val foo by option().double() 42 | } 43 | 44 | shouldThrow { C().parse("--foo bar") } 45 | .formattedMessage shouldBe "invalid value for --foo: bar is not a valid floating point value" 46 | } 47 | 48 | @[Test JsName("double_option_with_default")] 49 | fun `double option with default`() = forAll( 50 | row("", -1.0), 51 | row("--xx=4.0", 4.0), 52 | row("-x5.5", 5.5) 53 | ) { argv, expected -> 54 | class C : TestCommand() { 55 | val x: Double by option("-x", "--xx").double().default(-1.0) 56 | override fun run_() { 57 | x shouldBe expected 58 | } 59 | } 60 | C().parse(argv) 61 | } 62 | 63 | @[Test JsName("double_argument")] 64 | fun `double argument`() = forAll( 65 | row("", null, emptyList()), 66 | row("1.1 2", 1.1, listOf(2.0)), 67 | row("1.1 2 3", 1.1, listOf(2.0, 3.0)) 68 | ) { argv, ex, ey -> 69 | class C : TestCommand() { 70 | val x: Double? by argument().double().optional() 71 | val y: List by argument().double().multiple() 72 | override fun run_() { 73 | x shouldBe ex 74 | y shouldBe ey 75 | } 76 | } 77 | 78 | C().parse(argv) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/clikt/parameters/types/FloatTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters.types 2 | 3 | import com.github.ajalt.clikt.core.BadParameterValue 4 | import com.github.ajalt.clikt.parameters.arguments.argument 5 | import com.github.ajalt.clikt.parameters.arguments.multiple 6 | import com.github.ajalt.clikt.parameters.arguments.optional 7 | import com.github.ajalt.clikt.parameters.options.default 8 | import com.github.ajalt.clikt.parameters.options.option 9 | import com.github.ajalt.clikt.testing.TestCommand 10 | import com.github.ajalt.clikt.testing.formattedMessage 11 | import com.github.ajalt.clikt.testing.parse 12 | import io.kotest.assertions.throwables.shouldThrow 13 | import io.kotest.data.blocking.forAll 14 | import io.kotest.data.row 15 | import io.kotest.matchers.shouldBe 16 | import kotlin.js.JsName 17 | import kotlin.test.Test 18 | 19 | class FloatTest { 20 | @[Test JsName("float_option")] 21 | fun `float option`() = forAll( 22 | row("", null), 23 | row("--xx=4.0", 4f), 24 | row("-x5.5", 5.5f) 25 | ) { argv, expected -> 26 | class C : TestCommand() { 27 | val x: Float? by option("-x", "--xx").float() 28 | override fun run_() { 29 | x shouldBe expected 30 | } 31 | } 32 | 33 | C().parse(argv) 34 | } 35 | 36 | @Test 37 | @Suppress("unused") 38 | @JsName("float_option_error") 39 | fun `float option error`() { 40 | class C : TestCommand(called = false) { 41 | val foo by option().float() 42 | } 43 | 44 | shouldThrow { C().parse("--foo bar") } 45 | .formattedMessage shouldBe "invalid value for --foo: bar is not a valid floating point value" 46 | } 47 | 48 | @[Test JsName("float_option_with_default")] 49 | fun `float option with default`() = forAll( 50 | row("", -1f), 51 | row("--xx=4.0", 4f), 52 | row("-x5.5", 5.5f) 53 | ) { argv, expected -> 54 | class C : TestCommand() { 55 | val x: Float by option("-x", "--xx").float().default(-1f) 56 | override fun run_() { 57 | x shouldBe expected 58 | } 59 | } 60 | C().parse(argv) 61 | } 62 | 63 | @[Test JsName("float_argument")] 64 | fun `float argument`() = forAll( 65 | row("", null, emptyList()), 66 | row("1.1 2", 1.1f, listOf(2f)), 67 | row("1.1 2 3", 1.1f, listOf(2f, 3f)) 68 | ) { argv, ex, ey -> 69 | class C : TestCommand() { 70 | val x: Float? by argument().float().optional() 71 | val y: List by argument().float().multiple() 72 | override fun run_() { 73 | x shouldBe ex 74 | y shouldBe ey 75 | } 76 | } 77 | 78 | C().parse(argv) 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/clikt/parameters/types/IntTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters.types 2 | 3 | import com.github.ajalt.clikt.core.BadParameterValue 4 | import com.github.ajalt.clikt.core.NoSuchOption 5 | import com.github.ajalt.clikt.parameters.arguments.argument 6 | import com.github.ajalt.clikt.parameters.arguments.multiple 7 | import com.github.ajalt.clikt.parameters.arguments.optional 8 | import com.github.ajalt.clikt.parameters.options.default 9 | import com.github.ajalt.clikt.parameters.options.option 10 | import com.github.ajalt.clikt.testing.TestCommand 11 | import com.github.ajalt.clikt.testing.formattedMessage 12 | import com.github.ajalt.clikt.testing.parse 13 | import io.kotest.assertions.throwables.shouldThrow 14 | import io.kotest.data.blocking.forAll 15 | import io.kotest.data.row 16 | import io.kotest.matchers.shouldBe 17 | import kotlin.js.JsName 18 | import kotlin.test.Test 19 | 20 | @Suppress("unused") 21 | class IntTypeTest { 22 | @[Test JsName("int_option")] 23 | fun `int option`() = forAll( 24 | row("", null), 25 | row("--xx=4", 4), 26 | row("-x5", 5), 27 | row("-5", 5), 28 | row("-0", 0), 29 | ) { argv, expected -> 30 | class C : TestCommand() { 31 | val x: Int? by option("-x", "--xx").int(acceptsValueWithoutName = true) 32 | override fun run_() { 33 | x shouldBe expected 34 | } 35 | } 36 | 37 | C().parse(argv) 38 | } 39 | 40 | @[Test JsName("multiple_number_options")] 41 | fun `multiple number options`() { 42 | class C : TestCommand(called = false) { 43 | val foo by option().int(acceptsValueWithoutName = true) 44 | val bar by option().int(acceptsValueWithoutName = true) 45 | } 46 | 47 | shouldThrow { C() } 48 | } 49 | 50 | @[Test JsName("int_option_error")] 51 | fun `int option error`() { 52 | class C : TestCommand(called = false) { 53 | val foo by option().int() 54 | } 55 | 56 | shouldThrow { C().parse("--foo bar") } 57 | .formattedMessage shouldBe "invalid value for --foo: bar is not a valid integer" 58 | 59 | shouldThrow { C().parse("-2") } 60 | } 61 | 62 | @[Test JsName("int_option_with_default")] 63 | fun `int option with default`() = forAll( 64 | row("", 111), 65 | row("--xx=4", 4), 66 | row("-x5", 5) 67 | ) { argv, expected -> 68 | class C : TestCommand() { 69 | val x: Int by option("-x", "--xx").int().default(111) 70 | override fun run_() { 71 | x shouldBe expected 72 | } 73 | } 74 | C().parse(argv) 75 | } 76 | 77 | @[Test JsName("int_argument")] 78 | fun `int argument`() = forAll( 79 | row("", null, emptyList()), 80 | row("1 2", 1, listOf(2)), 81 | row("-- -1 -2", -1, listOf(-2)), 82 | row("1 2 3", 1, listOf(2, 3)) 83 | ) { argv, ex, ey -> 84 | class C : TestCommand() { 85 | val x: Int? by argument().int().optional() 86 | val y: List by argument().int().multiple() 87 | override fun run_() { 88 | x shouldBe ex 89 | y shouldBe ey 90 | } 91 | } 92 | 93 | C().parse(argv) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/clikt/parameters/types/LongTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters.types 2 | 3 | import com.github.ajalt.clikt.core.BadParameterValue 4 | import com.github.ajalt.clikt.parameters.arguments.argument 5 | import com.github.ajalt.clikt.parameters.arguments.multiple 6 | import com.github.ajalt.clikt.parameters.arguments.optional 7 | import com.github.ajalt.clikt.parameters.options.default 8 | import com.github.ajalt.clikt.parameters.options.option 9 | import com.github.ajalt.clikt.testing.TestCommand 10 | import com.github.ajalt.clikt.testing.formattedMessage 11 | import com.github.ajalt.clikt.testing.parse 12 | import io.kotest.assertions.throwables.shouldThrow 13 | import io.kotest.data.blocking.forAll 14 | import io.kotest.data.row 15 | import io.kotest.matchers.shouldBe 16 | import kotlin.js.JsName 17 | import kotlin.test.Test 18 | 19 | class LongTypeTest { 20 | @[Test JsName("long_option")] 21 | fun `long option`() = forAll( 22 | row("", null), 23 | row("--xx=4", 4L), 24 | row("-x5", 5L), 25 | row("-5", 5L), 26 | row("-0", 0L), 27 | ) { argv, ex -> 28 | class C : TestCommand() { 29 | val x: Long? by option("-x", "--xx").long(acceptsValueWithoutName = true) 30 | override fun run_() { 31 | x shouldBe ex 32 | } 33 | } 34 | 35 | C().parse(argv) 36 | } 37 | 38 | @Test 39 | @Suppress("unused") 40 | @JsName("long_option_error") 41 | fun `long option error`() { 42 | class C : TestCommand(called = false) { 43 | val foo by option().long() 44 | } 45 | 46 | shouldThrow { C().parse("--foo bar") } 47 | .formattedMessage shouldBe "invalid value for --foo: bar is not a valid integer" 48 | } 49 | 50 | @[Test JsName("long_option_with_default")] 51 | fun `long option with default`() = forAll( 52 | row("", 111L), 53 | row("--xx=4", 4L), 54 | row("-x5", 5L) 55 | ) { argv, expected -> 56 | class C : TestCommand() { 57 | val x: Long by option("-x", "--xx").long().default(111L) 58 | override fun run_() { 59 | x shouldBe expected 60 | } 61 | } 62 | C().parse(argv) 63 | } 64 | 65 | @[Test JsName("long_argument")] 66 | fun `long argument`() = forAll( 67 | row("", null, emptyList()), 68 | row("1 2", 1L, listOf(2L)), 69 | row("1 2 3", 1L, listOf(2L, 3L)) 70 | ) { argv, ex, ey -> 71 | class C : TestCommand() { 72 | val x by argument().long().optional() 73 | val y by argument().long().multiple() 74 | override fun run_() { 75 | x shouldBe ex 76 | y shouldBe ey 77 | } 78 | } 79 | 80 | C().parse(argv) 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/clikt/parameters/types/UIntTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters.types 2 | 3 | import com.github.ajalt.clikt.core.BadParameterValue 4 | import com.github.ajalt.clikt.core.NoSuchOption 5 | import com.github.ajalt.clikt.parameters.arguments.argument 6 | import com.github.ajalt.clikt.parameters.options.default 7 | import com.github.ajalt.clikt.parameters.options.option 8 | import com.github.ajalt.clikt.testing.TestCommand 9 | import com.github.ajalt.clikt.testing.formattedMessage 10 | import com.github.ajalt.clikt.testing.parse 11 | import io.kotest.assertions.throwables.shouldThrow 12 | import io.kotest.data.blocking.forAll 13 | import io.kotest.data.row 14 | import io.kotest.matchers.shouldBe 15 | import kotlin.js.JsName 16 | import kotlin.test.Test 17 | 18 | class UIntTest { 19 | @[Test JsName("uint_option")] 20 | fun `uint option`() = forAll( 21 | row("", null), 22 | row("-x0", 0u), 23 | row("-${UInt.MAX_VALUE}", UInt.MAX_VALUE), 24 | ) { argv, expected -> 25 | class C : TestCommand() { 26 | val x: UInt? by option("-x").uint(acceptsValueWithoutName = true) 27 | override fun run_() { 28 | x shouldBe expected 29 | } 30 | } 31 | 32 | C().parse(argv) 33 | } 34 | 35 | @[Test JsName("uint_option_with_default")] 36 | fun `uint option with default`() = forAll( 37 | row("", 111u), 38 | row("--xx=4", 4u), 39 | row("-x5", 5u) 40 | ) { argv, expected -> 41 | class C : TestCommand() { 42 | val x: UInt by option("-x", "--xx").uint().default(111u) 43 | override fun run_() { 44 | x shouldBe expected 45 | } 46 | } 47 | C().parse(argv) 48 | } 49 | 50 | @[Test JsName("uint_option_error")] 51 | fun `uint option error`() { 52 | class C : TestCommand(called = false) { 53 | @Suppress("unused") 54 | val foo by option().uint() 55 | } 56 | 57 | shouldThrow { C().parse("--foo bar") } 58 | .formattedMessage shouldBe "invalid value for --foo: bar is not a valid integer" 59 | 60 | shouldThrow { C().parse("-2") } 61 | shouldThrow { C().parse("--foo=-1") } 62 | } 63 | 64 | @[Test JsName("uint_argument")] 65 | fun `uint argument`() { 66 | class C : TestCommand(treatUnknownOptionsAsArgs = true) { 67 | val a by argument().uint() 68 | } 69 | 70 | C().parse("2").a shouldBe 2u 71 | shouldThrow { C().parse("-1") } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/clikt/parameters/types/ULongTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters.types 2 | 3 | import com.github.ajalt.clikt.core.BadParameterValue 4 | import com.github.ajalt.clikt.core.NoSuchOption 5 | import com.github.ajalt.clikt.parameters.arguments.argument 6 | import com.github.ajalt.clikt.parameters.options.default 7 | import com.github.ajalt.clikt.parameters.options.option 8 | import com.github.ajalt.clikt.testing.TestCommand 9 | import com.github.ajalt.clikt.testing.formattedMessage 10 | import com.github.ajalt.clikt.testing.parse 11 | import io.kotest.assertions.throwables.shouldThrow 12 | import io.kotest.data.blocking.forAll 13 | import io.kotest.data.row 14 | import io.kotest.matchers.shouldBe 15 | import kotlin.js.JsName 16 | import kotlin.test.Test 17 | 18 | class ULongTest { 19 | @[Test JsName("ulong_option")] 20 | fun `ulong option`() = forAll( 21 | row("", null), 22 | row("-x0", 0uL), 23 | row("-${ULong.MAX_VALUE}", ULong.MAX_VALUE), 24 | ) { argv, expected -> 25 | class C : TestCommand() { 26 | val x: ULong? by option("-x").ulong(acceptsValueWithoutName = true) 27 | override fun run_() { 28 | x shouldBe expected 29 | } 30 | } 31 | 32 | C().parse(argv) 33 | } 34 | 35 | @[Test JsName("ulong_option_error")] 36 | fun `ulong option error`() { 37 | class C : TestCommand(called = false) { 38 | @Suppress("unused") 39 | val foo by option().ulong() 40 | } 41 | 42 | shouldThrow { C().parse("--foo bar") } 43 | .formattedMessage shouldBe "invalid value for --foo: bar is not a valid integer" 44 | 45 | shouldThrow { C().parse("-2") } 46 | shouldThrow { C().parse("--foo=-1") } 47 | } 48 | 49 | @[Test JsName("ulong_option_with_default")] 50 | fun `ulong option with default`() = forAll( 51 | row("", 111uL), 52 | row("--xx=4", 4uL), 53 | row("-x5", 5uL) 54 | ) { argv, expected -> 55 | class C : TestCommand() { 56 | val x: ULong by option("-x", "--xx").ulong().default(111uL) 57 | override fun run_() { 58 | x shouldBe expected 59 | } 60 | } 61 | C().parse(argv) 62 | } 63 | 64 | @[Test JsName("int_option_with_default")] 65 | fun `int option with default`() = forAll( 66 | row("", 111), 67 | row("--xx=4", 4), 68 | row("-x5", 5) 69 | ) { argv, expected -> 70 | class C : TestCommand() { 71 | val x: Int by option("-x", "--xx").int().default(111) 72 | override fun run_() { 73 | x shouldBe expected 74 | } 75 | } 76 | C().parse(argv) 77 | } 78 | 79 | @[Test JsName("ulong_argument")] 80 | fun `ulong argument`() { 81 | class C : TestCommand(treatUnknownOptionsAsArgs = true) { 82 | val a by argument().ulong() 83 | } 84 | 85 | C().parse("2").a shouldBe 2uL 86 | shouldThrow { C().parse("-1") } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/clikt/sources/ChainedValueSourceTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.sources 2 | 3 | import com.github.ajalt.clikt.core.context 4 | import com.github.ajalt.clikt.parameters.options.option 5 | import com.github.ajalt.clikt.testing.TestCommand 6 | import com.github.ajalt.clikt.testing.TestSource 7 | import com.github.ajalt.clikt.testing.parse 8 | import io.kotest.matchers.shouldBe 9 | import kotlin.js.JsName 10 | import kotlin.test.Test 11 | 12 | class ChainedValueSourceTest { 13 | @[Test JsName("reads_from_the_first_available_value")] 14 | fun `reads from the first available value`() { 15 | val sources = arrayOf( 16 | TestSource(), 17 | TestSource("foo" to "bar"), 18 | TestSource("foo" to "baz") 19 | ) 20 | 21 | class C : TestCommand() { 22 | init { 23 | context { 24 | valueSources(*sources) 25 | } 26 | } 27 | 28 | val foo by option() 29 | 30 | override fun run_() { 31 | foo shouldBe "bar" 32 | } 33 | } 34 | 35 | C().parse("") 36 | sources[0].assert(read = true) 37 | sources[1].assert(read = true) 38 | sources[2].assert(read = false) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/clikt/sources/MapValueSourceTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.sources 2 | 3 | import com.github.ajalt.clikt.core.BadParameterValue 4 | import com.github.ajalt.clikt.core.Context 5 | import com.github.ajalt.clikt.core.context 6 | import com.github.ajalt.clikt.core.subcommands 7 | import com.github.ajalt.clikt.parameters.options.Option 8 | import com.github.ajalt.clikt.parameters.options.flag 9 | import com.github.ajalt.clikt.parameters.options.option 10 | import com.github.ajalt.clikt.sources.ValueSource.Invocation 11 | import com.github.ajalt.clikt.testing.TestCommand 12 | import com.github.ajalt.clikt.testing.defaultLocalization 13 | import com.github.ajalt.clikt.testing.parse 14 | import io.kotest.assertions.throwables.shouldThrow 15 | import io.kotest.data.blocking.forAll 16 | import io.kotest.data.row 17 | import io.kotest.matchers.shouldBe 18 | import kotlin.test.Test 19 | 20 | 21 | class MapValueSourceTest { 22 | @Test 23 | fun getKey() = forAll( 24 | row("p_", null, false, "-", "p_foo-bar"), 25 | row("", ":", false, "-", "sub:foo-bar"), 26 | row("", ":", true, ":", "SUB:FOO:BAR"), 27 | row("", null, true, "-", "FOO-BAR"), 28 | row("", null, false, "_", "foo_bar") 29 | ) { p, j, c, r, k -> 30 | class Root : TestCommand() 31 | class Sub : TestCommand() { 32 | init { 33 | context { 34 | valueSource = MapValueSource( 35 | mapOf( 36 | "other" to "other", 37 | "FX" to "fixed", 38 | k to "foo" 39 | ), getKey = ValueSource.getKey(p, j, c, r) 40 | ) 41 | } 42 | } 43 | 44 | val fooBar by option() 45 | val fixed by option(valueSourceKey = "FX") 46 | override fun run_() { 47 | fooBar shouldBe "foo" 48 | fixed shouldBe "fixed" 49 | } 50 | } 51 | Root().subcommands(Sub()).parse("sub") 52 | } 53 | 54 | 55 | @Test 56 | fun envvarKey() { 57 | class C : TestCommand() { 58 | init { 59 | context { 60 | autoEnvvarPrefix = "A" 61 | valueSource = MapValueSource( 62 | mapOf( 63 | "FOO_E" to "foo", 64 | "A_BAR" to "bar", 65 | "B_V" to "baz" 66 | ), getKey = ValueSource.envvarKey() 67 | ) 68 | } 69 | } 70 | 71 | val foo by option(envvar = "FOO_E") 72 | val bar by option() 73 | val baz by option(envvar = "B_E", valueSourceKey = "B_V") 74 | 75 | override fun run_() { 76 | foo shouldBe "foo" 77 | bar shouldBe "bar" 78 | baz shouldBe "baz" 79 | } 80 | } 81 | 82 | C().parse("") 83 | } 84 | 85 | @Test 86 | fun flag() { 87 | class C : TestCommand() { 88 | val foo by option(valueSourceKey = "k").flag() 89 | } 90 | 91 | C().context { 92 | valueSource = MapValueSource(mapOf("k" to "true"), getKey = ValueSource.envvarKey()) 93 | }.parse("").foo shouldBe true 94 | 95 | class VS : ValueSource { 96 | override fun getValues(context: Context, option: Option): List { 97 | return listOf(Invocation(listOf("false", "true"))) 98 | } 99 | } 100 | shouldThrow { 101 | C().context { valueSource = VS() }.parse("") 102 | }.message shouldBe defaultLocalization.invalidFlagValueInFile("") 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/clikt/testing/TestCommand.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.testing 2 | 3 | import com.github.ajalt.clikt.core.* 4 | import com.github.ajalt.clikt.parsers.CommandLineParser 5 | import com.github.ajalt.mordant.rendering.AnsiLevel 6 | import com.github.ajalt.mordant.terminal.Terminal 7 | import kotlin.test.assertEquals 8 | 9 | class TestException(message: String) : Exception(message) 10 | 11 | open class TestCommand( 12 | called: Boolean = true, 13 | count: Int? = null, 14 | private val help: String = "", 15 | private val epilog: String = "", 16 | name: String? = null, 17 | override val invokeWithoutSubcommand: Boolean = false, 18 | override val printHelpOnEmptyArgs: Boolean = false, 19 | override val helpTags: Map = emptyMap(), 20 | override val autoCompleteEnvvar: String? = "", 21 | override val allowMultipleSubcommands: Boolean = false, 22 | override val treatUnknownOptionsAsArgs: Boolean = false, 23 | override val hiddenFromHelp: Boolean = false, 24 | noHelp: Boolean = false, 25 | ) : CliktCommand(name) { 26 | init { 27 | context { 28 | terminal = parent?.terminal ?: Terminal(AnsiLevel.NONE) 29 | if (noHelp) helpOptionNames = emptyList() 30 | } 31 | } 32 | 33 | override fun help(context: Context): String = help 34 | 35 | override fun helpEpilog(context: Context): String = epilog 36 | 37 | private val count = count ?: if (called) 1 else 0 38 | private var actualCount = 0 39 | 40 | final override fun run() { 41 | actualCount++ 42 | run_() 43 | } 44 | 45 | open fun run_() = Unit 46 | 47 | companion object { 48 | fun assertCalled(cmd: BaseCliktCommand<*>) { 49 | if (cmd is TestCommand) { 50 | assertEquals(cmd.count, cmd.actualCount, "${cmd.commandName} call count") 51 | } 52 | for (sub in cmd.registeredSubcommands()) { 53 | assertCalled(sub) 54 | } 55 | } 56 | } 57 | } 58 | 59 | fun T.parse(argv: String): T { 60 | parse(CommandLineParser.tokenize(argv)) 61 | TestCommand.assertCalled(this) 62 | return this 63 | } 64 | 65 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/clikt/testing/TestSource.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.testing 2 | 3 | import com.github.ajalt.clikt.core.Context 4 | import com.github.ajalt.clikt.parameters.options.Option 5 | import com.github.ajalt.clikt.sources.MapValueSource 6 | import com.github.ajalt.clikt.sources.ValueSource 7 | import io.kotest.assertions.withClue 8 | import io.kotest.matchers.shouldBe 9 | 10 | class TestSource(vararg values: Pair) : ValueSource { 11 | private var read: Boolean = false 12 | private val source = MapValueSource(values.toMap()) 13 | 14 | override fun getValues(context: Context, option: Option): List { 15 | read = true 16 | return source.getValues(context, option) 17 | } 18 | 19 | fun assert(read: Boolean) { 20 | withClue("ValueSource read") { 21 | this.read shouldBe read 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/clikt/testing/TestingUtilsTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.testing 2 | 3 | import com.github.ajalt.clikt.core.terminal 4 | import com.github.ajalt.clikt.parameters.options.option 5 | import com.github.ajalt.clikt.parameters.options.prompt 6 | import com.github.ajalt.mordant.rendering.AnsiLevel 7 | import io.kotest.matchers.shouldBe 8 | import kotlin.js.JsName 9 | import kotlin.test.Test 10 | 11 | class TestingUtilsTest { 12 | @[Test JsName("testing_envvar")] 13 | fun `testing envvar`() { 14 | class C : TestCommand(called = false) { 15 | val o1 by option() 16 | val o2 by option(envvar = "O") 17 | 18 | override fun run_() { 19 | o1 shouldBe "foo" 20 | o2 shouldBe "bar" 21 | echo("$o1") 22 | echo("$o2") 23 | } 24 | } 25 | 26 | val result = C().test("--o1=foo", envvars = mapOf("O" to "bar")) 27 | result.stdout shouldBe "foo\nbar\n" 28 | result.stderr shouldBe "" 29 | result.output shouldBe "foo\nbar\n" 30 | result.statusCode shouldBe 0 31 | } 32 | 33 | @[Test JsName("testing_error")] 34 | fun `testing error`() { 35 | @Suppress("unused") 36 | class C : TestCommand(called = false) { 37 | val o by option() 38 | } 39 | 40 | val ex = """ 41 | |Usage: c [] 42 | | 43 | |Error: no such option --foo. Did you mean --o? 44 | | 45 | """.trimMargin() 46 | val result = C().test("--foo bar", stdin = "unused") 47 | result.stdout shouldBe "" 48 | result.stderr shouldBe ex 49 | result.output shouldBe ex 50 | result.statusCode shouldBe 1 51 | } 52 | 53 | @[Test JsName("testing_with_prompt")] 54 | fun `testing with prompt`() { 55 | class C : TestCommand() { 56 | val o1 by option().prompt() 57 | val o2 by option().prompt() 58 | 59 | override fun run_() { 60 | o1 shouldBe "foo" 61 | o2 shouldBe "bar" 62 | echo("err", err = true) 63 | } 64 | } 65 | 66 | val result = C().test("", stdin = "foo\nbar") 67 | result.stdout shouldBe "O1: O2: " 68 | result.stderr shouldBe "err\n" 69 | result.output shouldBe "O1: O2: err\n" 70 | result.statusCode shouldBe 0 71 | } 72 | 73 | @[Test JsName("test_TerminalInfo_configuration")] 74 | fun `test TerminalInfo configuration`() { 75 | class C : TestCommand() { 76 | override fun run_() { 77 | with(currentContext.terminal) { 78 | terminalInfo.ansiLevel shouldBe AnsiLevel.NONE 79 | size.width shouldBe 11 80 | size.height shouldBe 22 81 | terminalInfo.ansiHyperLinks shouldBe true 82 | terminalInfo.outputInteractive shouldBe true 83 | terminalInfo.inputInteractive shouldBe true 84 | } 85 | } 86 | } 87 | C().test( 88 | "", 89 | ansiLevel = AnsiLevel.NONE, 90 | width = 11, 91 | height = 22, 92 | hyperlinks = true, 93 | outputInteractive = true, 94 | inputInteractive = true, 95 | ) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /test/src/commonTest/kotlin/com/github/ajalt/clikt/testing/utils.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.testing 2 | 3 | import com.github.ajalt.clikt.core.CliktCommand 4 | import com.github.ajalt.clikt.core.UsageError 5 | import com.github.ajalt.clikt.core.context 6 | import com.github.ajalt.clikt.output.Localization 7 | import com.github.ajalt.clikt.output.ParameterFormatter 8 | 9 | val Throwable.formattedMessage: String? 10 | get() = (this as? UsageError)?.formatMessage( 11 | context?.localization ?: defaultLocalization, 12 | ParameterFormatter.Plain 13 | ) ?: message 14 | 15 | internal val defaultLocalization = object : Localization {} 16 | 17 | internal fun T.withEnv(vararg entries: Pair): T { 18 | return context { readEnvvar = { entries.toMap()[it] } } 19 | } 20 | -------------------------------------------------------------------------------- /test/src/jvmTest/kotlin/com/github/ajalt/clikt/core/ContextJvmTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.core 2 | 3 | import com.github.ajalt.clikt.testing.TestCommand 4 | import com.github.ajalt.clikt.testing.parse 5 | import io.kotest.matchers.shouldBe 6 | import io.kotest.matchers.types.shouldBeSameInstanceAs 7 | import kotlin.test.Test 8 | 9 | class ContextJvmTest { 10 | @Test 11 | fun registerJvmCloseable() { 12 | var closed = 0 13 | 14 | class C : TestCommand() { 15 | override fun run_() { 16 | val c = AutoCloseable { closed += 1 } 17 | currentContext.registerJvmCloseable(c) shouldBeSameInstanceAs c 18 | } 19 | } 20 | C().parse("") 21 | closed shouldBe 1 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /test/src/jvmTest/kotlin/com/github/ajalt/clikt/parameters/types/FileTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters.types 2 | 3 | import com.github.ajalt.clikt.parameters.arguments.argument 4 | import com.github.ajalt.clikt.parameters.options.option 5 | import com.github.ajalt.clikt.testing.TestCommand 6 | import com.github.ajalt.clikt.testing.parse 7 | import io.kotest.matchers.types.shouldBeInstanceOf 8 | import java.io.File 9 | import kotlin.test.Test 10 | 11 | class FileTypeTest { 12 | @Test 13 | fun `file option with default args`() { 14 | class C : TestCommand() { 15 | val x by option("-x", "--xx").file() 16 | override fun run_() { 17 | x.shouldBeInstanceOf() 18 | } 19 | } 20 | 21 | C().parse("-x.") 22 | } 23 | 24 | @Test 25 | fun `file argument with default args`() { 26 | class C : TestCommand() { 27 | val x by argument().file() 28 | override fun run_() { 29 | x.shouldBeInstanceOf() 30 | } 31 | } 32 | 33 | C().parse(".") 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/src/jvmTest/kotlin/com/github/ajalt/clikt/parameters/types/InputStreamTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters.types 2 | 3 | import com.github.ajalt.clikt.parameters.arguments.argument 4 | import com.github.ajalt.clikt.parameters.options.option 5 | import com.github.ajalt.clikt.testing.TestCommand 6 | import com.github.ajalt.clikt.testing.parse 7 | import com.google.common.jimfs.Configuration 8 | import com.google.common.jimfs.Jimfs 9 | import io.kotest.matchers.booleans.shouldBeFalse 10 | import io.kotest.matchers.booleans.shouldBeTrue 11 | import io.kotest.matchers.shouldBe 12 | import org.junit.Rule 13 | import org.junit.contrib.java.lang.system.TextFromStandardInputStream 14 | import org.junit.contrib.java.lang.system.TextFromStandardInputStream.emptyStandardInputStream 15 | import java.nio.file.FileSystem 16 | import java.nio.file.Files 17 | import kotlin.test.Test 18 | 19 | 20 | @Suppress("unused") 21 | class InputStreamTest { 22 | @get:Rule 23 | val stdin: TextFromStandardInputStream = emptyStandardInputStream() 24 | val fs: FileSystem = Jimfs.newFileSystem(Configuration.unix()) 25 | 26 | @Test 27 | fun `options can be inputStreams`() { 28 | val file = Files.createFile(fs.getPath("foo")) 29 | Files.write(file, "text".encodeToByteArray()) 30 | 31 | class C : TestCommand() { 32 | val stream by option().inputStream(fs) 33 | 34 | override fun run_() { 35 | stream?.readBytes()?.decodeToString() shouldBe "text" 36 | } 37 | } 38 | 39 | C().parse("--stream=foo") 40 | } 41 | 42 | @Test 43 | fun `passing explicit -`() { 44 | stdin.provideLines("text") 45 | class C : TestCommand() { 46 | val stream by argument().inputStream(fs) 47 | 48 | override fun run_() { 49 | stream.readBytes().decodeToString().replace("\r", "") shouldBe "text\n" 50 | } 51 | } 52 | 53 | C().parse("-") 54 | } 55 | 56 | @Test 57 | fun `option and arg with defaultStdin`() { 58 | class C : TestCommand() { 59 | val option by option().inputStream(fs).defaultStdin() 60 | val stream by argument().inputStream(fs).defaultStdin() 61 | 62 | override fun run_() { 63 | stdin.provideLines("text1") 64 | option.readBytes().decodeToString().replace("\r", "") shouldBe "text1\n" 65 | 66 | stdin.provideLines("text2") 67 | stream.readBytes().decodeToString().replace("\r", "") shouldBe "text2\n" 68 | } 69 | } 70 | 71 | C().parse("") 72 | } 73 | 74 | @Test 75 | fun `option inputStream is defaultStdin`() { 76 | class C : TestCommand() { 77 | val option by option().inputStream(fs).defaultStdin() 78 | 79 | override fun run_() { 80 | option.isCliktParameterDefaultStdin.shouldBeTrue() 81 | } 82 | } 83 | 84 | C().parse("") 85 | } 86 | 87 | @Test 88 | fun `option inputStream is not defaultStdin`() { 89 | Files.createFile(fs.getPath("foo")) 90 | 91 | class C : TestCommand() { 92 | val option by option().inputStream(fs) 93 | 94 | override fun run_() { 95 | option?.isCliktParameterDefaultStdin?.shouldBeFalse() 96 | } 97 | } 98 | 99 | C().parse("--option=foo") 100 | } 101 | 102 | @Test 103 | fun `argument inputStream is defaultStdin`() { 104 | class C : TestCommand() { 105 | val stream by argument().inputStream(fs).defaultStdin() 106 | 107 | override fun run_() { 108 | stream.isCliktParameterDefaultStdin.shouldBeTrue() 109 | } 110 | } 111 | 112 | C().parse("") 113 | } 114 | 115 | @Test 116 | fun `argument inputStream is not defaultStdin`() { 117 | Files.createFile(fs.getPath("foo")) 118 | 119 | class C : TestCommand() { 120 | val stream by argument().inputStream(fs) 121 | 122 | override fun run_() { 123 | stream.isCliktParameterDefaultStdin.shouldBeFalse() 124 | } 125 | } 126 | 127 | C().parse("foo") 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /test/src/jvmTest/kotlin/com/github/ajalt/clikt/parameters/types/PathTest.kt: -------------------------------------------------------------------------------- 1 | package com.github.ajalt.clikt.parameters.types 2 | 3 | import com.github.ajalt.clikt.core.BadParameterValue 4 | import com.github.ajalt.clikt.parameters.arguments.argument 5 | import com.github.ajalt.clikt.parameters.arguments.multiple 6 | import com.github.ajalt.clikt.parameters.options.convert 7 | import com.github.ajalt.clikt.parameters.options.option 8 | import com.github.ajalt.clikt.parameters.options.required 9 | import com.github.ajalt.clikt.testing.TestCommand 10 | import com.github.ajalt.clikt.testing.formattedMessage 11 | import com.github.ajalt.clikt.testing.parse 12 | import com.google.common.jimfs.Configuration 13 | import com.google.common.jimfs.Jimfs 14 | import io.kotest.assertions.throwables.shouldThrow 15 | import io.kotest.matchers.shouldBe 16 | import org.junit.Rule 17 | import org.junit.contrib.java.lang.system.RestoreSystemProperties 18 | import java.nio.file.FileSystem 19 | import java.nio.file.Files 20 | import kotlin.test.Test 21 | 22 | 23 | @Suppress("unused") 24 | class PathTest { 25 | @Rule 26 | @JvmField 27 | val restoreSystemProperties = RestoreSystemProperties() 28 | val fs: FileSystem = Jimfs.newFileSystem(Configuration.unix()) 29 | 30 | @Test 31 | fun `paths are resolved using the provided filesystem, if any`() { 32 | class C : TestCommand() { 33 | val path by option("-p") 34 | .path(fileSystem = fs) 35 | .required() 36 | 37 | override fun run_() { 38 | path.fileSystem shouldBe fs 39 | } 40 | } 41 | 42 | C().parse("-p/var/log/foo") 43 | } 44 | 45 | @Test 46 | fun `options can be paths`() { 47 | class C : TestCommand() { 48 | val path by option("-p") 49 | .path(fileSystem = fs) 50 | .required() 51 | 52 | override fun run_() { 53 | path.toString() shouldBe "foo" 54 | } 55 | } 56 | 57 | C().parse("-pfoo") 58 | } 59 | 60 | @Test 61 | fun `arguments can be paths`() { 62 | class C : TestCommand() { 63 | val paths by argument() 64 | .path(fileSystem = fs) 65 | .multiple() 66 | 67 | override fun run_() { 68 | paths.map { it.toString() } shouldBe listOf("foo", "bar", "baz") 69 | } 70 | } 71 | 72 | C().parse("foo bar baz") 73 | } 74 | 75 | @Test 76 | fun `values can be converted before path is called`() { 77 | class C : TestCommand() { 78 | val path by option("-p") 79 | .convert { "/tmp/$it" } 80 | .path(fileSystem = fs) 81 | .required() 82 | 83 | override fun run_() { 84 | path.toString() shouldBe "/tmp/foo" 85 | } 86 | } 87 | 88 | C().parse("-pfoo") 89 | } 90 | 91 | @Test 92 | fun `canBeFile = false will reject files`() { 93 | class C : TestCommand() { 94 | val folderOnly by option("-f").path(canBeFile = false, fileSystem = fs) 95 | } 96 | 97 | Files.createDirectory(fs.getPath("/var")) 98 | Files.createFile(fs.getPath("/var/foo")) 99 | 100 | shouldThrow { 101 | C().parse("-f/var/foo") 102 | }.formattedMessage shouldBe """invalid value for -f: directory "/var/foo" is a file.""" 103 | } 104 | 105 | @Test 106 | fun `canBeDir = false will reject folders`() { 107 | class C : TestCommand() { 108 | val fileOnly by option("-f").path(canBeDir = false, fileSystem = fs) 109 | } 110 | 111 | Files.createDirectories(fs.getPath("/var/foo")) 112 | 113 | shouldThrow { 114 | C().parse("-f/var/foo") 115 | }.formattedMessage shouldBe """invalid value for -f: file "/var/foo" is a directory.""" 116 | } 117 | 118 | @Test 119 | fun `mustExist = true will reject paths that don't exist`() { 120 | class C : TestCommand() { 121 | val homeDir by option("-h").path(mustExist = true, fileSystem = fs) 122 | } 123 | 124 | shouldThrow { 125 | C().parse("-h /home/cli") 126 | }.formattedMessage shouldBe """invalid value for -h: path "/home/cli" does not exist.""" 127 | } 128 | } 129 | --------------------------------------------------------------------------------