ajalt / clikt

Multiplatform command line interface parsing for Kotlin
https://ajalt.github.io/clikt/
Apache License 2.0
2.53k stars 120 forks source link

Argument Files (@argfiles) Fail On Redirects (e.g., @<(echo foo)) #527

Closed adamsmd closed 3 months ago

adamsmd commented 3 months ago

Summary

Using a Bash redirect with an argument file (@argfile) produces an error Error: /dev/fd/63 not found.

Environment

Clikt: 4.4.0 Kotlin: 1.9.22 Platform: Tested on JVM with Linux. Unsure about others.

Code

The following code demonstraits this error:

src/main/kotlin/Main.kt:

import com.github.ajalt.clikt.core.CliktCommand
import com.github.ajalt.clikt.core.context
import java.io.File

fun main(args: Array<String>) {
    println(args.toList())
    Main().main(args)
}

class Main : CliktCommand() {
    // init { context { argumentFileReader = { File(it).readText() } } }
    override fun run() {}
}

build.gradle.kts:

repositories {
  mavenCentral()
}

plugins {
  kotlin("jvm") version "1.9.22"
  application
}

dependencies {
  implementation("com.github.ajalt.clikt:clikt:4.4.0")
}

application {
  mainClass = "MainKt"
}

Actual Output

$ ./gradlew build installDist
> Task :checkKotlinGradlePluginConfigurationErrors
> Task :compileKotlin UP-TO-DATE
> Task :compileJava NO-SOURCE
> Task :processResources NO-SOURCE
> Task :classes UP-TO-DATE
> Task :jar UP-TO-DATE
> Task :startScripts UP-TO-DATE
> Task :distTar UP-TO-DATE
> Task :distZip UP-TO-DATE
> Task :assemble UP-TO-DATE
> Task :compileTestKotlin NO-SOURCE
> Task :compileTestJava NO-SOURCE
> Task :processTestResources NO-SOURCE
> Task :testClasses UP-TO-DATE
> Task :test NO-SOURCE
> Task :check UP-TO-DATE
> Task :build UP-TO-DATE
> Task :installDist UP-TO-DATE

BUILD SUCCESSFUL in 665ms
7 actionable tasks: 1 executed, 6 up-to-date

$ ./build/install/clikt-bug/bin/clikt-bug @<(echo foo)
[@/dev/fd/63]
Usage: main [<options>]

Error: /dev/fd/63 not found

Expected Output

$ ./build/install/clikt-bug/bin/clikt-bug @<(echo foo)
[@/dev/fd/63]
Usage: main [<options>]

Error: got unexpected extra argument (foo)

Workaround

Uncommenting the init line in Main produces the expected output.

Cause

The default argumentFileReader is at https://github.com/ajalt/clikt/blob/2f8827814224e426a3557cbe52680985a6838b42/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/core/Context.kt#L238-L239

This calls readArgumentFile in MordantContext.kt at https://github.com/ajalt/clikt/blob/2f8827814224e426a3557cbe52680985a6838b42/clikt-mordant/src/commonMain/kotlin/com/github/ajalt/clikt/core/MordantContext.kt#L55

That calls MultiplatformSystem.readFileAsUtf8() at https://github.com/ajalt/mordant/blob/d24d2fa2794d57ac92e8a59445fe408ca0a598f1/mordant/src/commonMain/kotlin/com/github/ajalt/mordant/platform/MultiplatformSystem.kt#L34-L41

That calls readFileIfExists at https://github.com/ajalt/mordant/blob/d24d2fa2794d57ac92e8a59445fe408ca0a598f1/mordant/src/jvmMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.jvm.kt#L151-L155

Which finally calls java.io.File.isFile() which returns false on pipes and devices.

Recommended Fix

In https://github.com/ajalt/mordant/blob/d24d2fa2794d57ac92e8a59445fe408ca0a598f1/mordant/src/jvmMain/kotlin/com/github/ajalt/mordant/internal/MppInternal.jvm.kt#L151-L155, instead of calling isFile(), catch the FileNotFoundException when opening the file.

Note

The following documentation should be updated to reflect whatever fix is made here.

https://github.com/ajalt/clikt/blob/2f8827814224e426a3557cbe52680985a6838b42/clikt/src/commonMain/kotlin/com/github/ajalt/clikt/core/CoreCliktCommand.kt#L26-L30

ajalt commented 3 months ago

Thanks for the detailed write-up! I fixed this in https://github.com/ajalt/mordant/pull/188, and I'll be making a bugfix release of mordant soon, so you can use it without waiting for a clikt release.

I also updated the clikt docs with your recommendation.