├── .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 |
--------------------------------------------------------------------------------