An easy-to-use command-line argument parser for Kotlin apps. Interprets an Array<String>, args, formatted in a familiar syntax and serializes a valid, stable object of arguments to be coveniently read in any given program.
- Declare required, optional, flag, and positional args
- Supports multiple key-value delimiters for options, eg:
-n 3,-n=3, or-n3 - Set default values for optional and flags args
- Convenient mapping arg for restricted values
- Transform parameter to convert
Stringvalue to arbitrary type - Stacked flag arguments eg,
-iand-v=>-iv - Concise one-line feedback for parsing errors
- Builtin
--helpcommand to print neatly-formatted and comprehensive usage statement - Builtin
--versionand--quitcommands --delimiter to separate options and positional args- Subcommands support for implementing commands like
git commit [options]orgit add [options]
- Argument nomenclature
- Game args example walkthrough
- Subcommands
- Exceptions and error logs
- Importing the library
- Todo
- Contributing
- Aknowledgements
| Term | Definition |
|---|---|
| Options | Generic term for the args declared before the positional args in the command |
| Optional args | Key-value option that may or may not be declared in the command |
| Required args | Key-value option that must be provided in the command |
| Flag args | Optional arg without a key that maps to a Boolean value |
| Positional args | Argument(s) that are declared after the options in the command and their 'position' in the command matters |
Example creating and parsing args for a game program
To begin, create a custom class with only CmdArgsParser in the constructor.
class MyGameArgs(parser: CmdArgsParser)We are now ready to start defining the args as member properties on this class.
Say we wanted a 'seed' argument for the program, where the user may or may not specify it. This can look something like this:
val seed: String? by parser.optionalArg(
"-s", "--seed",
valueLabel = "SEED",
help = "Seed for the game instance. Uses random seed if not set.",
)Breaking this down, the optionalArg method returns a modified Lazy delegate, CmdArgNullable.KeyValue, whose initializer resolves to a nullable generic type. So we explicitly define the return type as String? and not String. The value of the help parameter here indicates that if the user does not specify a --seed then null will be set and the program can later interpret that to mean generating a random seed for the game instance. The vararg param keys allows us to accept either -s or its verbose form --seed as keys in args. Lastly, the valueLabel parameter is used by the --help command to demonstrate usage of the command eg, [-s SEED]
If we wanted an optional arg to fallback to some default value instead of null, we can change the return type to a non-nullable type Int and include a default parameter:
val numLives: Int by parser.optionalArg(
"-l", "--num-lives",
valueLabel = "COUNT",
help = "Set count of player lives",
default = 3
) Note in the previous example that numLives is an Int. In its current state the app would return a Result.Failure with a CmdArgsParseException and print error: Failed casting value for member 'numLives' with type kotlin.Int. Did you include the initializer() parameter?. By default the parser does not handle casting the value of the arg. You must explicity cast them from String to the desired type and return it in the initializer parameter.
val numLives: Int by parser.optionalArg(
"-l", "--num-lives",
valueLabel = "COUNT",
help = "Set count of player lives",
default = 3,
initializer = { argString ->
argString.toInt().also {
require(it > 0) { "Lives must be > 0" }
}
}
) Specifying required args is very similar to specifying optional arguments. Make the seed arg required by changing the return type to a non-null String and calling requiredArg:
val seed: String by parser.requiredArg(
"-s", "--seed",
valueLabel = "SEED",
help = "Seed for the game instance"
)Flags are optionals whose value is either true or false. These are parsed as false if not found in args, and true otherwise.
Setup a flag that when declared, enables the use of cheat codes:
val cheatsEnabled: Boolean by parser.flagArg(
"-c", "--cheats-enabled",
help = "Enable use of cheat codes"
)The behavior can be reversed to set cheats enabled by default by specifying the default parameter:
val cheatsEnabled: Boolean by parser.flagArg(
"-c", "--no-cheats",
help = "Disable use of cheat codes",
default = true
)Args with a restricted value set can use the methods optionalMapArg or requiredMapArg. The following defines a required arg which maps values "easy", "medium", and "hard" to the enums Mode.EASY, Mode.MEDIUM, and Mode.HARD respectively:
val mode: Mode by parser.requiredMapArg(
"-m", "--mode",
valueLabel = "MODE",
help = "Set game mode difficulty",
map = mapOf(
"easy" to Mode.EASY,
"medium" to Mode.MEDIUM,
"hard" to Mode.HARD
)
)Positional args are declared with the positionalArg method. Here we define player speed and the path of the path of the save file:
val playerSpeed: Double by parser.positionalArg(
valueLabel = "SPEED",
help = "Player speed"
) { argString ->
argString.toDouble().also {
require(it >= 0.0)
}
}
val saveFile: File by parser.positionalArg(
valueLabel = "FILE",
help = "Save file location"
) { File(it) }The order in which they are declared in the args class matters. For example, the command line should should specify SPEED and then FILE.
Formatting the --help output is limited in the project's current state. However, you may set a prologue or an epilogue statement like so:
class MyGameArgs(parser: CmdArgsParser): CmdArgHelpConfigHolder {
override val cmdArgHelpConfig: CmdArgHelpConfig
get() = CmdArgHelpConfig(
prologue = "Prologue - A challenging puzzle game all about life",
epilogue = "Epilogue - Have fun!"
}
// ...We will see the usage and output of the --help command shortly.
class MyGameArgs(parser: CmdArgsParser): CmdArgHelpConfigHolder {
override val cmdArgHelpConfig: CmdArgHelpConfig
get() = CmdArgHelpConfig(
prologue = "Prologue - A challenging puzzle game all about life",
epilogue = "Epilogue - Have fun!"
}
val seed: String? by parser.optionalArg(
"-s", "--seed",
valueLabel = "SEED",
help = "Seed for the game instance. Uses random seed if not set.",
)
val numLives: Int by parser.optionalArg(
"-l", "--num-lives",
valueLabel = "COUNT",
help = "Set count of player lives",
default = 3
) { argString ->
argString.toInt().also {
require(it > 0) { "Lives must be > 0" }
}
}
val cheatsEnabled: Boolean by parser.flagArg(
"-c", "--cheats-enabled",
help = "Enable use of cheat codes"
)
val mode: Mode by parser.requiredMapArg(
"-m", "--mode",
valueLabel = "MODE",
help = "Set game mode difficulty",
map = mapOf(
"easy" to Mode.EASY,
"medium" to Mode.MEDIUM,
"hard" to Mode.HARD
)
)
val playerSpeed: Double by parser.positionalArg(
valueLabel = "SPEED",
help = "Player speed"
) { argString ->
argString.toDouble().also {
require(it >= 0.0)
}
}
val saveFile: File by parser.positionalArg(
valueLabel = "FILE",
help = "Save file location"
) { File(it) }
enum class Mode { EASY, MEDIUM, HARD }
}A basic usage looks like this:
val args = arrayOf(
"-l", "9",
"--cheats-enabled",
"--mode=medium",
"--",
"100.50",
"C:\\Users\\User\\MyGame\\saves"
)
CmdArgsParser(args, programName = "MyGame.jar").parse(::MyGameArgs)
.onSuccess { parsedArgs ->
handleParsedArgs(parsedArgs)
}.onFailure {
// Optionally handle parse failure
}Observe that an Array<String> args has been defined where, according to the configuration of MyGameArgs, the player has 9 lives (-l 9), cheats are enabled (--cheats-enabled), medium difficulty is set (--mode=medium), the player has 100.50 speed (SPEED 100.50), and the save file path is (FILE C:\\Users\\User\\MyGame\\saves). These args have been passed into the CmdArgsParser along with a programName which is referenced in the output of the --help command. Then, the parse(::MyGameArgs) call returns a Kotlin.Result where Result.Success is only returned when the args are validated and parsed successfully.
Running the --help command is as simple as providing "--help" as the only arg:
val args = arrayOf("--help")Output
Usage: MyGame.jar
-m=MODE
[-s=SEED] [-l=COUNT]
[-c]
[--] SPEED FILE
Prologue - A challenging puzzle game all about life
Required args:
-m MODE, --mode MODE : Set game mode difficulty
MODE={easy,medium,hard}
Positional args:
SPEED : Player speed
FILE : Save file location
Optional args:
-s SEED, --seed SEED : Seed for the game instance. Uses random seed if not set.
-l COUNT, --num-lives COUNT : Set count of player lives (Default 3)
Flag args:
-c, --cheats-enabled : Enable use of cheat codes (Default false)
Epilogue - Have fun!
Supply "--version" as the only arg. This will print out the value of the String version param provided in the CmdArgsParser's constructor.
val args = arrayOf("--version")
CmdArgsParser(args, programName = "MyGame.jar", version = "MyGame version 1.0").parse(::MyGameArgs)Output
MyGame version 1.0
Often times it is useful to support separate commands within the same program, allowing us to defining different parsing behavior unique to each subcommand. The following example shows one such use case.
Example of an args class for a file encryption program supporting 'encrypt' and 'decrypt' subcommands:
class FileEncryptorArgs(parser: CmdArgsParser) {
val encryptionArgs: EncryptionArgs? by parser.subparser(
subcommand = "encrypt",
help = "Encryption mode",
creator = ::EncryptionArgs
)
val decryptionArgs: DecryptionArgs? by parser.subparser(
subcommand = "decrypt",
help = "Decryption mode",
creator = ::DecryptionArgs
)
}The method for defining a subcommand is subparser. The return type of this method is a non-nullable Lazy delegate Subcommand which initializes to another custom args class on parsing.
Subcommand args classes
class EncryptionArgs(subparser: CmdArgsParser): SharedArgs(subparser) {
val encFileExcludeRegex: Regex? by subparser.optionalArg(
"-f", "--enc-filereg",
valueLabel = "REGEX",
help = "Exclude file regex for encryption"
) { it.toRegex() }
val encDirExcludeRegex: Regex? by subparser.optionalArg(
"-d", "--enc-dirreg",
valueLabel = "REGEX",
help = "Exclude directory regex for encryption"
) { it.toRegex() }
}
class DecryptionArgs(subparser: CmdArgsParser): SharedArgs(subparser)
open class SharedArgs(parser: CmdArgsParser) {
val srcDir: File by parser.positionalArg(
valueLabel = "SRC",
help = "Source directory"
) { File(it) }
val destDir: File by parser.positionalArg(
valueLabel = "DEST",
help = "Destination directory"
) { File(it) }
}Observe that EncryptionArgs supports the unique optionals encFileExcludeRegex and encDirExcludeRegex. This allows us to optionally exclude certain files or directories when encrypting some srcDir. Conversely, both EncryptionArgs and DecryptionArgs extend the SharedArgs custom args class because they both need the positionals srcDir and destDir. This can be one handy way to share args amongst subcommands and cut down on code duplication.
Usage
Encrypt the contents of the directory enc_in ignoring all .txt files and the 'videos' directory and output the contents to enc_out:
val args = arrayOf(
"encrypt",
"-f", "^.*\\.txt$",
"--enc-dirreg", "^videos$",
"enc_in",
"enc_out"
)
val parsedArgs = CmdArgsParser(args, "FileEncryptor.jar").parse(::FileEncryptorArgs).getOrThrow()
if (parsedArgs.encryptionArgs != null) {
runEncryption(parsedArgs.encryptionArgs!!)
} else runDecryption(parsedArgs.decryptionArgs!!)Decrypt the contents of dec_in and output to dec_out:
val args = arrayOf(
"decrypt",
"dec_in",
"dec_out"
)--help is supported for both the root args and subcommand args.
FileEncryptor.jar --help output:
Usage: FileEncryptor.jar
SUBCOMMAND [ARGS]
Subcommands:
encrypt : Encryption mode
decrypt : Decryption mode
FileEncryptor.jar encrypt --help output:
Usage: FileEncryptor.jar encrypt
[-f=REGEX] [-d=REGEX]
[--] SRC DEST
Positional args:
SRC : Source file
DEST : Destination file
Optional args:
-f REGEX, --enc-filereg REGEX : Exclude file regex for encryption
-d REGEX, --enc-dirreg REGEX : Exclude directory regex for encryption
The following CmdArgsParserExceptions are used for error handling and debugging and occur in the order provided:
1.CmdArgsParserInitiaizationException occurs when there is some issue with creating the args, eg: declaring the same key twice, or providing a key with an invalid format.
Caution
This is the only Exception that is thrown at runtime and will not be returned in Result.Failure from the parse method.
2.CmdArgsBuiltinCommandException is returned when a builtin command like --help or --version has been processed.
3.CmdArgsMalformedException is returned when the args is malformed in some way, eg: unrecognized key, missing arg value, or too many positionals declared.
4.CmdArgsParseException occurs at the last step in parsing and is returned when there is an issue with parsing one of the args from args, eg: required arg not found, casting failure, or some other error thrown from the initializer param.
If either CmdArgsMalformedException or CmdArgsParseException occur, the program will print out a concise single-line error statement. Some example logs:
error: [-i, --include] Required value not found
error: No value specified for arg -x
error: Positional arg(s) not provided: DEST
repositories {
mavenCentral()
maven {
url 'https://jitpack.io'
content { includeGroup 'com.github.sircjarr.cmdargsparser' }
}
}
dependencies {
implementation 'com.github.sircjarr.cmdargsparser:kt-cmd-args-parser:1.0.1'
}repositories {
mavenCentral()
maven {
url = uri("https://jitpack.io")
content { includeGroup("com.github.sircjarr.cmdargsparser") }
}
}
dependencies {
implementation("com.github.sircjarr.cmdargsparser:kt-cmd-args-parser:1.0.1")
}<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>
<dependency>
<groupId>com.github.sircjarr.cmdargsparser</groupId>
<artifactId>kt-cmd-args-parser</artifactId>
<version>1.0.1</version>
</dependency>- Optional positonals
- Arbitrary # of positional args
- Arbitrary # of optional values
- Bash-style regex string parsing
- Customizable
--helpcommand formatting - Range or other arg validation info in
--help - Embedded subcommands
- Documentation in code comments
MRs and creating issues are more than welcome
Thanks to kotlin-argparser for the inspiration of this project.
If this project caught your interest enough to star or contribute in any way — thank you!