Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Complete stream conversion implementation #159

Merged
merged 1 commit into from
Apr 8, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
## [Unreleased]
### Added
- Ability to use custom program exit status codes via `ProgramResult`.
- `inputStream` and `outputStream` conversions for options and arguments. ([#157](https://github.com/ajalt/clikt/issues/157) and [#158](https://github.com/ajalt/clikt/issues/158))

### Changed
- Update Kotlin to 1.3.71

## [2.6.0] - 2020-03-15
### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,8 +314,10 @@ fun <AllT, EachT : Any, ValueT> NullableOption<EachT, ValueT>.transformAll(
* val opt: Pair<Int, Int> by option().int().pair().default(1 to 2)
* ```
*/
fun <EachT : Any, ValueT> NullableOption<EachT, ValueT>.default(value: EachT, defaultForHelp: String = value.toString())
: OptionWithValues<EachT, EachT, ValueT> {
fun <EachT : Any, ValueT> NullableOption<EachT, ValueT>.default(
value: EachT,
defaultForHelp: String = value.toString()
): OptionWithValues<EachT, EachT, ValueT> {
return transformAll(defaultForHelp) { it.lastOrNull() ?: value }
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,104 @@
package com.github.ajalt.clikt.parameters.types

import com.github.ajalt.clikt.completion.CompletionCandidates
import com.github.ajalt.clikt.parameters.options.NullableOption
import com.github.ajalt.clikt.parameters.options.OptionWithValues
import com.github.ajalt.clikt.parameters.options.RawOption
import com.github.ajalt.clikt.parameters.options.convert
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.arguments.*
import com.github.ajalt.clikt.parameters.options.*
import java.io.IOException
import java.io.InputStream
import java.nio.file.FileSystem
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Paths

fun RawOption.inputStream(): NullableOption<InputStream, InputStream> {
return convert("FILE", completionCandidates = CompletionCandidates.Path) {
if (it == "-") {
System.`in`
} else {
Files.newInputStream(Paths.get(it))
}

private fun convertToInputStream(s: String, fileSystem: FileSystem, fail: (String) -> Unit): InputStream {
return if (s == "-") {
UnclosableInputStream(System.`in`)
} else {
val path = convertToPath(
path = s,
mustExist = true,
canBeFile = true,
canBeFolder = false,
mustBeWritable = false,
mustBeReadable = true,
canBeSymlink = true,
fileSystem = fileSystem,
fail = fail
)
Files.newInputStream(path)
}
}

//<editor-fold desc="options">

/**
* Convert the option to an [InputStream].
*
* The value given on the command line must be either a path to a readable file, or `-`. If `-` is
* given, stdin will be used.
*
* If stdin is used, the resulting [InputStream] will be a proxy for [System.in] that will not close
* the underlying stream. So you can always [close][InputStream.close] the resulting stream without
* worrying about accidentally closing [System.in].
*/
fun RawOption.inputStream(
fileSystem: FileSystem = FileSystems.getDefault()
): NullableOption<InputStream, InputStream> {
return convert("FILE", completionCandidates = CompletionCandidates.Path) { s ->
convertToInputStream(s, fileSystem) { fail(it) }
}
}

/**
* Use `-` as the default value for an [inputStream] option.
*/
fun NullableOption<InputStream, InputStream>.defaultStdin(): OptionWithValues<InputStream, InputStream, InputStream> {
return default(System.`in`, "-")
return default(UnclosableInputStream(System.`in`), "-")
}

//</editor-fold>
//<editor-fold desc="arguments">

/**
* Convert the argument to an [InputStream].
*
* The value given on the command line must be either a path to a readable file, or `-`. If `-` is
* given, stdin will be used.
*
* If stdin is used, the resulting [InputStream] will be a proxy for [System.in] that will not close
* the underlying stream. So you can always [close][InputStream.close] the resulting stream without
* worrying about accidentally closing [System.in].
*/
fun RawArgument.inputStream(
fileSystem: FileSystem = FileSystems.getDefault()
): ProcessedArgument<InputStream, InputStream> {
return convert(completionCandidates = CompletionCandidates.Path) { s ->
convertToInputStream(s, fileSystem) { fail(it) }
}
}

/**
* Use `-` as the default value for an [inputStream] argument.
*/
fun ProcessedArgument<InputStream, InputStream>.defaultStdin(): ArgumentDelegate<InputStream> {
return default(UnclosableInputStream(System.`in`))
}

//</editor-fold>


private class UnclosableInputStream(private var delegate: InputStream?) : InputStream() {
private val stream get() = delegate ?: throw IOException("Stream closed")
override fun available(): Int = stream.available()
override fun read(): Int = stream.read()
override fun read(b: ByteArray, off: Int, len: Int): Int = stream.read(b, off, len)
override fun skip(n: Long): Long = stream.skip(n)
override fun reset() = stream.reset()
override fun markSupported(): Boolean = stream.markSupported()
override fun mark(readlimit: Int) {
stream.mark(readlimit)
}

override fun close() {
delegate = null
}
}
Original file line number Diff line number Diff line change
@@ -1,25 +1,117 @@
package com.github.ajalt.clikt.parameters.types

import com.github.ajalt.clikt.completion.CompletionCandidates
import com.github.ajalt.clikt.parameters.options.NullableOption
import com.github.ajalt.clikt.parameters.options.OptionWithValues
import com.github.ajalt.clikt.parameters.options.RawOption
import com.github.ajalt.clikt.parameters.options.convert
import com.github.ajalt.clikt.parameters.options.default
import com.github.ajalt.clikt.parameters.arguments.*
import com.github.ajalt.clikt.parameters.options.*
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import java.nio.file.FileSystem
import java.nio.file.FileSystems
import java.nio.file.Files
import java.nio.file.Paths

fun RawOption.outputStream(): NullableOption<OutputStream, OutputStream> {
return convert("FILE", completionCandidates = CompletionCandidates.Path) {
if (it == "-") {
System.out
} else {
Files.newOutputStream(Paths.get(it))
}
import java.nio.file.StandardOpenOption.*

private fun convertToOutputStream(
s: String,
createIfNotExist: Boolean,
truncateExisting: Boolean,
fileSystem: FileSystem,
fail: (String) -> Unit
): OutputStream {
return if (s == "-") {
UnclosableOutputStream(System.out)
} else {
val path = convertToPath(
s,
mustExist = !createIfNotExist,
canBeFile = true,
canBeFolder = false,
mustBeWritable = !createIfNotExist,
mustBeReadable = false,
canBeSymlink = true,
fileSystem = fileSystem
) { fail(it) }
val openType = if (truncateExisting) TRUNCATE_EXISTING else APPEND
val options = arrayOf(WRITE, CREATE, openType)
Files.newOutputStream(path, *options)
}
}

//<editor-fold desc="options">

/**
* Convert the option to an [OutputStream].
*
* The value given on the command line must be either a path to a writable file, or `-`. If `-` is
* given, stdout will be used.
*
* If stdout is used, the resulting [OutputStream] will be a proxy for [System.out] that will not close
* the underlying stream. So you can always [close][OutputStream.close] the resulting stream without
* worrying about accidentally closing [System.out].
*
* @param createIfNotExist If false, an error will be reported if the given value doesn't exist. By default, the file will be created.
* @param truncateExisting If true, existing files will be truncated when opened. By default, the file will be appended to.
*/
fun RawOption.outputStream(
createIfNotExist: Boolean = true,
truncateExisting: Boolean = false,
fileSystem: FileSystem = FileSystems.getDefault()
): NullableOption<OutputStream, OutputStream> {
return convert("FILE", completionCandidates = CompletionCandidates.Path) { s ->
convertToOutputStream(s, createIfNotExist, truncateExisting, fileSystem) { fail(it) }
}
}

/**
* Use `-` as the default value for an [outputStream] option.
*/
fun NullableOption<OutputStream, OutputStream>.defaultStdout(): OptionWithValues<OutputStream, OutputStream, OutputStream> {
return default(System.out, "-")
return default(UnclosableOutputStream(System.out), "-")
}

//</editor-fold>
//<editor-fold desc="arguments">

/**
* Convert the argument to an [OutputStream].
*
* The value given on the command line must be either a path to a writable file, or `-`. If `-` is
* given, stdout will be used.
*
* If stdout is used, the resulting [OutputStream] will be a proxy for [System.out] that will not close
* the underlying stream. So you can always [close][OutputStream.close] the resulting stream without
* worrying about accidentally closing [System.out].
*
* @param createIfNotExist If false, an error will be reported if the given value doesn't exist. By default, the file will be created.
* @param truncateExisting If true, existing files will be truncated when opened. By default, the file will be appended to.
*/
fun RawArgument.outputStream(
createIfNotExist: Boolean = true,
truncateExisting: Boolean = false,
fileSystem: FileSystem = FileSystems.getDefault()
): ProcessedArgument<OutputStream, OutputStream> {
return convert(completionCandidates = CompletionCandidates.Path) { s ->
convertToOutputStream(s, createIfNotExist, truncateExisting, fileSystem) { fail(it) }
}
}

/**
* Use `-` as the default value for an [outputStream] argument.
*/
fun ProcessedArgument<OutputStream, OutputStream>.defaultStdout(): ArgumentDelegate<OutputStream> {
return default(UnclosableOutputStream(System.out))
}

//</editor-fold>

private class UnclosableOutputStream(private var delegate: OutputStream?) : OutputStream() {
private val stream get() = delegate ?: throw IOException("Stream closed")

override fun write(b: Int) = stream.write(b)
override fun write(b: ByteArray) = stream.write(b)
override fun write(b: ByteArray, off: Int, len: Int) = stream.write(b, off, len)
override fun flush() = stream.flush()
override fun close() {
delegate = null
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ private fun pathType(fileOkay: Boolean, folderOkay: Boolean): String = when {
else -> "Path"
}

private fun convertToPath(
internal fun convertToPath(
path: String,
mustExist: Boolean,
canBeFile: Boolean,
Expand All @@ -41,6 +41,8 @@ private fun convertToPath(
}
}

//<editor-fold desc="arguments">

// This overload exists so that calls to `file()` aren't marked as deprecated.
// Remove once the deprecated function is removed.
/**
Expand Down Expand Up @@ -98,6 +100,9 @@ fun RawArgument.path(
}
}

//</editor-fold>
//<editor-fold desc="options">

// This overload exists so that calls to `file()` aren't marked as deprecated.
// Remove once the deprecated function is removed.
/**
Expand Down Expand Up @@ -156,3 +161,4 @@ fun RawOption.path(
convertToPath(it, mustExist, canBeFile, canBeDir, mustBeWritable, mustBeReadable, canBeSymlink, fileSystem) { fail(it) }
}
}
//</editor-fold>
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.github.ajalt.clikt.parameters.types

import com.github.ajalt.clikt.parameters.arguments.argument
import com.github.ajalt.clikt.parameters.options.option
import com.github.ajalt.clikt.testing.TestCommand
import com.google.common.jimfs.Configuration
import com.google.common.jimfs.Jimfs
import io.kotest.matchers.shouldBe
import org.junit.Rule
import org.junit.contrib.java.lang.system.TextFromStandardInputStream
import org.junit.contrib.java.lang.system.TextFromStandardInputStream.emptyStandardInputStream
import java.nio.file.FileSystem
import java.nio.file.Files
import kotlin.test.Test


@Suppress("unused")
@OptIn(ExperimentalStdlibApi::class)
class InputStreamTest {
@get:Rule
val stdin: TextFromStandardInputStream = emptyStandardInputStream()
val fs: FileSystem = Jimfs.newFileSystem(Configuration.unix())

@Test
fun `options can be inputStreams`() {
val file = Files.createFile(fs.getPath("foo"))
Files.write(file, "text".encodeToByteArray())

class C : TestCommand() {
val stream by option().inputStream(fs)

override fun run_() {
stream?.readBytes()?.decodeToString() shouldBe "text"
}
}

C().parse("--stream=foo")
}

@Test
fun `passing explicit -`() {
stdin.provideLines("text")
class C : TestCommand() {
val stream by argument().inputStream(fs)

override fun run_() {
stream.readBytes().decodeToString().replace("\r", "") shouldBe "text\n"
}
}

C().parse("-")
}

@Test
fun `option and arg with defaultStdin`() {
class C : TestCommand() {
val option by option().inputStream(fs).defaultStdin()
val stream by argument().inputStream(fs).defaultStdin()

override fun run_() {
stdin.provideLines("text1")
option.readBytes().decodeToString().replace("\r", "") shouldBe "text1\n"

stdin.provideLines("text2")
stream.readBytes().decodeToString().replace("\r", "") shouldBe "text2\n"
}
}

C().parse("")
}
}
Loading