feat(features): added file options, fixed some things
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
package org.kargs
|
||||
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* Sealed class representing different argument types with conversion and validation logic
|
||||
*/
|
||||
@@ -84,6 +86,42 @@ sealed class ArgType<T>(val typeName: String) {
|
||||
override fun getValidationDescription(): String = "optional value or flag"
|
||||
}
|
||||
|
||||
/**
|
||||
* File path type with existence and permision validation
|
||||
*/
|
||||
class FilePath(
|
||||
val mustExist: Boolean = false,
|
||||
val mustBeFile: Boolean = false,
|
||||
val mustBeDirectory: Boolean = false,
|
||||
val mustBeReadable: Boolean = false,
|
||||
val mustBeWritable: Boolean = false
|
||||
) : ArgType<File>("File") {
|
||||
override fun convert(value: String): File = File(value)
|
||||
|
||||
override fun validate(value: File): Boolean {
|
||||
return when {
|
||||
mustExist && !value.exists() -> false
|
||||
mustBeFile && !value.isFile -> false
|
||||
mustBeDirectory && !value.isDirectory -> false
|
||||
mustBeReadable && !value.canRead() -> false
|
||||
mustBeWritable && !value.canWrite() -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
override fun getValidationDescription(): String {
|
||||
val conditions = mutableListOf<String>()
|
||||
if (mustExist) conditions.add("must exist")
|
||||
if (mustBeFile) conditions.add("must be a file")
|
||||
if (mustBeDirectory) conditions.add("must be a directory")
|
||||
if (mustBeReadable) conditions.add("must be readable")
|
||||
if (mustBeWritable) conditions.add("must be writable")
|
||||
|
||||
return if (conditions.isEmpty()) "file path" else "file path (${conditions.joinToString(", ")})"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
val String: ArgType<kotlin.String> = StringType
|
||||
val Int: ArgType<kotlin.Int> = IntType
|
||||
@@ -104,5 +142,30 @@ sealed class ArgType<T>(val typeName: String) {
|
||||
* Create an optional value or flag
|
||||
*/
|
||||
fun optionalValue(defaultWhenPresent: String = "true") = OptionalValue(defaultWhenPresent)
|
||||
|
||||
/**
|
||||
* File that must exist and be a regular file
|
||||
*/
|
||||
fun existingFile() = FilePath(mustExist = true, mustBeFile = true)
|
||||
|
||||
/**
|
||||
* Directory that must exist
|
||||
*/
|
||||
fun existingDirectory() = FilePath(mustExist = true, mustBeDirectory = true)
|
||||
|
||||
/**
|
||||
* File that must exist and be readable
|
||||
*/
|
||||
fun readableFile() = FilePath(mustExist = true, mustBeFile = true, mustBeReadable = true)
|
||||
|
||||
/**
|
||||
* File that must be writable (can be created if doesn't exist)
|
||||
*/
|
||||
fun writableFile() = FilePath(mustBeWritable = true)
|
||||
|
||||
/**
|
||||
* Any file path (no validation)
|
||||
*/
|
||||
fun filePath() = FilePath()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,10 @@ class Parser(
|
||||
* @param args Array of command-line arguments
|
||||
*/
|
||||
fun parse(args: Array<String>) {
|
||||
if (handleVersionFlag(args)) {
|
||||
return // Exit after showing version
|
||||
}
|
||||
|
||||
if (args.isEmpty()) {
|
||||
if (config.helpOnEmpty) {
|
||||
printGlobalHelp()
|
||||
@@ -254,7 +258,8 @@ class Parser(
|
||||
* Print global help menu
|
||||
*/
|
||||
private fun printGlobalHelp() {
|
||||
println(colorize("Usage: $programName <command> [options]", Color.BOLD))
|
||||
val versionInfo = config.programVersion?.let { " (v$it)" } ?: ""
|
||||
println(colorize("Usage: $programName$versionInfo <command> [options]", Color.BOLD))
|
||||
println()
|
||||
println(colorize("Commands:", Color.BOLD))
|
||||
commands.forEach { cmd ->
|
||||
@@ -321,6 +326,31 @@ class Parser(
|
||||
// Exit gracefully instead of throwing
|
||||
// kotlin.system.exitProcess(1)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if version flag is present and handle it
|
||||
*/
|
||||
private fun handleVersionFlag(args: Array<String>): Boolean {
|
||||
if (config.programVersion == null) return false
|
||||
|
||||
val versionFlags = listOf("--${config.versionLongName}", "-${config.versionShortName}")
|
||||
|
||||
if (args.any { it in versionFlags }) {
|
||||
printVersion()
|
||||
return true // Indicates version was shown, should exit
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Print version information
|
||||
*/
|
||||
private fun printVersion() {
|
||||
config.programVersion?.let { version ->
|
||||
println("$programName version $version")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -9,7 +9,11 @@ data class ParserConfig(
|
||||
val helpOnEmpty: Boolean = true, // Show help when no args provided
|
||||
val caseSensitive: Boolean = true,
|
||||
val allowAbbreviations: Boolean = false, // Allow partial option matching
|
||||
val programVersion: String? = null // Adds --version and -v flag if set
|
||||
val programVersion: String? = null, // Adds --version and -v flag if set
|
||||
|
||||
// Optional customize version flag names
|
||||
val versionLongName: String = "version",
|
||||
val versionShortName: String = "V" // Capital V to avoid conflicts with -v (verbose)
|
||||
) {
|
||||
companion object {
|
||||
val DEFAULT = ParserConfig()
|
||||
|
||||
@@ -139,6 +139,7 @@ abstract class Subcommand(
|
||||
is ArgType.IntRange -> " <${type.min}-${type.max}>"
|
||||
is ArgType.Choice -> " <${type.choices.joinToString("|")}>"
|
||||
is ArgType.OptionalValue -> " [string]"
|
||||
is ArgType.FilePath -> " <path>"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import org.junit.jupiter.params.provider.ValueSource
|
||||
import org.kargs.ArgType
|
||||
import org.kargs.ArgumentParseException
|
||||
import kotlin.test.assertEquals
|
||||
import kotlin.io.path.createTempFile
|
||||
import kotlin.io.path.createTempDirectory
|
||||
|
||||
class ArgTypeTest {
|
||||
|
||||
@@ -103,5 +105,192 @@ class ArgTypeTest {
|
||||
val type = ArgType.choice("apple", "banana", "cherry")
|
||||
assertEquals("one of: apple, banana, cherry", type.getValidationDescription())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `FilePath converts string to File`() {
|
||||
val type = ArgType.filePath()
|
||||
val result = type.convert("/path/to/file.txt")
|
||||
assertEquals("/path/to/file.txt", result.path)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `FilePath with no constraints allows any path`() {
|
||||
val type = ArgType.filePath()
|
||||
// Should not throw for non-existent files
|
||||
val result = type.convert("/non/existent/path")
|
||||
assertEquals("/non/existent/path", result.path)
|
||||
assertEquals(true, type.validate(result))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `existingFile validates file exists and is file`() {
|
||||
val type = ArgType.existingFile()
|
||||
|
||||
// Create a temporary file for testing
|
||||
val tempFile = kotlin.io.path.createTempFile().toFile()
|
||||
tempFile.writeText("test content")
|
||||
|
||||
try {
|
||||
// Should work with existing file
|
||||
val result = type.convert(tempFile.absolutePath)
|
||||
assertEquals(true, type.validate(result))
|
||||
|
||||
// Should fail with non-existent file
|
||||
val nonExistent = type.convert("/non/existent/file.txt")
|
||||
assertEquals(false, type.validate(nonExistent))
|
||||
|
||||
} finally {
|
||||
tempFile.delete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `existingFile fails validation on directory`() {
|
||||
val type = ArgType.existingFile()
|
||||
|
||||
// Create a temporary directory
|
||||
val tempDir = kotlin.io.path.createTempDirectory().toFile()
|
||||
|
||||
try {
|
||||
val result = type.convert(tempDir.absolutePath)
|
||||
assertEquals(false, type.validate(result)) // Should fail because it's a directory, not a file
|
||||
|
||||
} finally {
|
||||
tempDir.delete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `existingDirectory validates directory exists and is directory`() {
|
||||
val type = ArgType.existingDirectory()
|
||||
|
||||
// Create a temporary directory
|
||||
val tempDir = kotlin.io.path.createTempDirectory().toFile()
|
||||
|
||||
try {
|
||||
// Should work with existing directory
|
||||
val result = type.convert(tempDir.absolutePath)
|
||||
assertEquals(true, type.validate(result))
|
||||
|
||||
// Should fail with non-existent directory
|
||||
val nonExistent = type.convert("/non/existent/directory")
|
||||
assertEquals(false, type.validate(nonExistent))
|
||||
|
||||
} finally {
|
||||
tempDir.delete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `existingDirectory fails validation on file`() {
|
||||
val type = ArgType.existingDirectory()
|
||||
|
||||
// Create a temporary file
|
||||
val tempFile = kotlin.io.path.createTempFile().toFile()
|
||||
tempFile.writeText("test")
|
||||
|
||||
try {
|
||||
val result = type.convert(tempFile.absolutePath)
|
||||
assertEquals(false, type.validate(result)) // Should fail because it's a file, not a directory
|
||||
|
||||
} finally {
|
||||
tempFile.delete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `readableFile validates file is readable`() {
|
||||
val type = ArgType.readableFile()
|
||||
|
||||
// Create a temporary file
|
||||
val tempFile = kotlin.io.path.createTempFile().toFile()
|
||||
tempFile.writeText("test content")
|
||||
|
||||
try {
|
||||
// Should work with readable file
|
||||
val result = type.convert(tempFile.absolutePath)
|
||||
assertEquals(true, type.validate(result))
|
||||
|
||||
// Make file unreadable (this might not work on all systems)
|
||||
tempFile.setReadable(false)
|
||||
assertEquals(false, type.validate(result))
|
||||
|
||||
} finally {
|
||||
tempFile.setReadable(true) // Restore permissions
|
||||
tempFile.delete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `writableFile validates file is writable`() {
|
||||
val type = ArgType.writableFile()
|
||||
|
||||
// Create a temporary file
|
||||
val tempFile = kotlin.io.path.createTempFile().toFile()
|
||||
tempFile.writeText("test content")
|
||||
|
||||
try {
|
||||
// Should work with writable file
|
||||
val result = type.convert(tempFile.absolutePath)
|
||||
assertEquals(true, type.validate(result))
|
||||
|
||||
// Make file read-only (this might not work on all systems)
|
||||
tempFile.setWritable(false)
|
||||
assertEquals(false, type.validate(result))
|
||||
|
||||
} finally {
|
||||
tempFile.setWritable(true) // Restore permissions
|
||||
tempFile.delete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `FilePath validation description includes constraints`() {
|
||||
assertEquals("file path", ArgType.filePath().getValidationDescription())
|
||||
assertEquals("file path (must exist, must be a file)", ArgType.existingFile().getValidationDescription())
|
||||
assertEquals("file path (must exist, must be a directory)", ArgType.existingDirectory().getValidationDescription())
|
||||
assertEquals("file path (must exist, must be a file, must be readable)", ArgType.readableFile().getValidationDescription())
|
||||
assertEquals("file path (must be writable)", ArgType.writableFile().getValidationDescription())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `FilePath with multiple constraints`() {
|
||||
val type = ArgType.FilePath(
|
||||
mustExist = true,
|
||||
mustBeFile = true,
|
||||
mustBeReadable = true,
|
||||
mustBeWritable = true
|
||||
)
|
||||
|
||||
val tempFile = kotlin.io.path.createTempFile().toFile()
|
||||
tempFile.writeText("test")
|
||||
|
||||
try {
|
||||
val result = type.convert(tempFile.absolutePath)
|
||||
assertEquals(true, type.validate(result))
|
||||
|
||||
assertEquals(
|
||||
"file path (must exist, must be a file, must be readable, must be writable)",
|
||||
type.getValidationDescription()
|
||||
)
|
||||
|
||||
} finally {
|
||||
tempFile.delete()
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `FilePath handles non-existent parent directories`() {
|
||||
val type = ArgType.writableFile()
|
||||
|
||||
// Path with non-existent parent directory
|
||||
val result = type.convert("/non/existent/parent/file.txt")
|
||||
|
||||
// Should convert fine (validation happens separately)
|
||||
assertEquals("/non/existent/parent/file.txt", result.path)
|
||||
|
||||
// Validation should fail because parent doesn't exist
|
||||
assertEquals(false, type.validate(result))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user