ArgParser is a simplified argument parser / multiplatform CLI for Kotlin.
With only about 500 LoC implementation, ArgParser has:
- Parsing for params, flags, items(positional args) and subcommands
- Type safe and convince data model (
Tuple4
/OneOrMore
andNamedMap
, you can ignore them and just useArgParserBy
) - No pre-defined command syntax restriction, only prefixed(param, flag) and unprefixed(item) args are distinguished
- No self-made concept, let users define
-v
,-h
(ParseStop
) and--
(PrefixesStop
) behavior themselves
object ReadMap: ArgParser3<String, Pair<String, String>, String>(
arg("name", "store mapping to a name", param = "map_name", default_value = "anonymous") { require(it.isNotBlank()) ; it },
arg("map m", "maps a key to its value", "k v", repeatable = true, convert = multiParam { it[0] to it[1] }),
arg("mode", "mode of this mapping", ""/*default to it's name, "mode"*/, "none").checkOptions("none", "mutable", "linked"),
arg("v", "print version") { println("mapper v1.0") ; throw SwitchParser.ParseStop }
)
And the one using ArgParserBy
wrapper:
class ReadMapUsingBy(ap: ArgParserBy) {
val name by ap+arg("name", "...", "map_name", "anonymous") { require(it.isNotBlank()) ; it }
val map by (ap+arg("map m", "...", "k v", convert = multiParam { it[0] to it[1] })).multiply { it.toMap() }
val mode by ap+arg("mode", "...", "", "none").checkOptions("none", "mutable", "linked")
val showVer by ap+arg("v", "print version") { println("v1.0") ; SwitchParser.stop() }//should not be accessed
val isShowVer/*run later?*/ by ap.flag("v", "print version", "vh") // when 'h' in res.flags, item count/ordering checking are suppressed.
}
Subcommands:
class Pip(ap: ArgParserBy) {
val show = ArgParserBy("show").apply {
flag("files f", "Show the full list of installed files for each package.")
// Use general super class to add general(non subcommand specific) options.
}
val install = ArgParserBy("install").apply {
this+arg("index-url i", "Base URL of Python Package Index", "url")
(this+arg("requirement r", "Install from the given requirements file.", "file")).multiply()
item(arg("target", "target specifier, maybe requirements file / VCS / local / PyPI", "archive url/path"))
}
init {
ap+helpArg ; ap.asParser().addHelpSubCommand("pip")
ap.flag("verbose v", "Give more output. Option is additive, and can be used up to 3 times.") // then check "vvv" in flags, or count { it == "v" }.
ap.addSub("Show information about one or more installed packages.", show)
ap.addSub("Install packages from PyPI/VCS/Local or remote path&archives", install)
ap.addSub("Compute hashes of package archives.", ArgParserBy("hash").apply { items(arg("...", "")) })
}
}
And the one using the old way:
object PipOld: ArgParser1<Unit>(noArg, helpArg) {
init { addSub("show", "...", Show) ; addHelpSubCommand("pip") }
object Show: ArgParser1<Unit>(noArg, arg("files f", "..."))
}
- Supports named args
--a=b
(usingautoSplit=listOf("a=")
) with configurable prefix list - Supports variable number of items and named destruct (both
[src...] [dst]
and[dst] [src...]
are supported) - Extension funs for idiom:
checkOptions(*strings)
,options("a" to 1)
,options(SomeEnum)
,getEnv()
,multiParam {list->}
,defineFlags("enable-oo" to "O")
- Supports repeatable args, multiply params, each can with a converter
- Sub-commands and command alias (like
pip install
,git commit -m
->git cm
) - Extensible using
override
, features like prompt when missing, mutually exclusive args, sequential prefixes(ffmpeg
), @-file expansion, did-you-mean suggestion can be implmented easily - Generates well formatted line-wrapped help message (can be grouped), only with Kotlin's named arguments in
toString
(opened for override) - Supports
backRun
unparsing, so it's also fascinating to build a CLI program binding with this lib - Supports 3 different programming paradigm: procedural
SwitchParser
, OOP&FPArgParser
, reflectArgParserBy
- KISS(keep it simple, stupid) principle, modular programming, "Simplicity is the ultimate sophistication".
- Readable code with reading order guide, <^v>comments and a little line breaks ;)
- TBD: add shell completion gen(not compile time, req. Arg objects) in a separate module
- Doesn't provide basic APIs for building CLIs on Kotlin/Multiplatform
- Use too much puzzling configuration objects that hiding the essence from users (e.g. "token transformer" instead of Kotlin's native approach
ap.run(map {}.toTypedArray())
) - Define too complicated data/mental model for command line args
- Unchangable&invisible pre-defined syntax for args (e.g. GNU/POSIX)
- Boring API that have too much coding conventions(e.g. named parameter) or boilerplate codes
- Doesn't support argument types other than
String
, or expose unchecked types - Treating flags as "boolean params", instead of storing them together
- Made it too complicated to set help-printing format
- Call
exitProcess
instead of throwingParseError
to let users influence error behavior(e.g. to print usage) - Unnecessary reflection / lazy calculation(e.g. runParse)
- Unnecessary design patterns(e.g. validator)/ Exception type definitions
- Too long implementation code that scares newcomers from reading π¨
- Why not add shorthands for types like
Double
,kotlin.Pair
andTriple
?
I think using a general convert
parameter for arg
is sufficient, and those "special case" ain't special enough.
(In fact I checked --help
option in ruby, python, lua, rustc, php, squeak, java, javac, kotlinc; no one have multi-param or float num options... even gcc, clang doesn't have much)
So what's the reason for adding them in org.parserkt.argp
, instead of letting users to define them explicitly in their projects...
- Why not enforce GNU(
-o
,--name
,--name=val
)/POSIX(--
) syntax for cmdline
This repository is licensed under MIT, neither GNU nor UNIX :)
...and it's boring to separate "long option" and "short option"(they are both "prefixed" option), if you wants to build a Minimist Parser
that must distinguish between them, override checkPrefixForName
and save var currentPrefix
(a partially implemented one can be found here).
- The animated banner is low-quality/have slow frame rate
This one is the best I've got, it tooks me at least 5-hrs making that...
- How's the performance of this lib comparing to others?
I don't care, but SwitchParser
will only traverse args
once every time you run
,
and declarative code is always no faster than imperative one (a lot of Map<String,Arg<*>>
is allocated in ArgParser4
)
ArgParserBy
for unusual/subcommand arg definitions requires dirty code
I'v tried my best... forgive me
- Your English grammar is poor
...Sorry for that, and please open an issue if you found my grammar mistakes.
//package org.parserkt.argp
const val SPREAD = "..." //< spread itemArg name
val noArg: Arg<Unit> = arg<Unit>("\u0000\u0000", "") { error("noArg parsed") }
val helpArg = arg("h help", "print this help") { SwitchParser.stop() }
abstract class ArgParserHandlers {
protected open fun printHelp() = println(toString())
protected open fun prefixMessageCaps() = TextCaps.nonePair
protected open fun rescuePrefix(name: String): Arg<*> { throw SwitchParser.ParseError("$name unknown") }
protected open fun rescueMissing(p: Arg<*>): Any? = null
protected open fun checkAutoSplitForName(name: String, param: String) {}
protected open fun checkPrefixForName(name: String, prefix: String) {
if (prefix == "--" && name.length == 1) throw SwitchParser.ParseError("single-char shorthand should like: -$name")
}
/** Un-parse method used in ArgParser4.backRun */
protected open fun showParam(param: Any): String? = param?.toString() //<^ real implementation omitted
}
- rewrite
prefixMessageCaps
if you want to change letter case in message prefix / body - rewrite
rescuePrefix
(yield anArg<*>
with param/defaultValue) if you want to support dynamic-k=v
or-n5
- rewrite
rescueMissing
if you want to prompt for arg value when missing
Ordering & focal point:
- SwitchParser a basic
Array<String>
iterator state machine withonItem
andonPrefix
and pretty error messages. focus:run
,arg
,ParseError
/ParseLoc
,Companion.extractArg
- ArgParser
ArgParser4
~ArgParsrer1
(subclass) functional wrapper toSwitchParser
usingMap
s to make DSL-style available. focus:ParseResult
(tup,named,flags,items),ArgParser4.<init>
(checks),commandAliases
,dynamicNameMap
/flagMap
(non-typed "prefix name" map),inner class Driver: SwitchParser
,argToOMs
+nameMap
,getOrPutOM
/addResult
(dynamic storage),itemArgZ
(an iter);onItem
,onPrefix
,read
,dynamicInterpret
;onArg
(alias&subcmd),run
,assignDefaultOrFail
; andbackRun
/toString
/ArgParserAsSubDelegate
- Helpers Utils & multiplatform CLI and extension for
Arg<String>
/Arg<T>
building. focus:Env
a multiplatform(MP) CLI API set(envvar/prompt/write),arg
/argInt
, (JVM)argFile
,OneOrMore
+NamedMap
,TextCaps
,associateByAll
(used in arg aliasArg.names
),joinToBreakLines
(format --help summary line) - ArgParserBy By delegates and
backRun
/addSub
wrapper to dynamic typedArgParser
. focus:reversed
(for[...] a b
desctruct),By
(used as namespace).res
,By.Flag
/By.ParsedArg<T,R>
(.wrap
),By.Param.multiply
,ArgParser4.reversed
/Arg<Str>.addNopConvert
(in ParserTricks) - ParserTricks Tricks using this framework (contributions are welcome). focus:
addHelpSubcommand
,Arg<T>.env
+Arg<Str>.getEnv
(!overload for JVM),Arg<T>.wrapXXX
Thanks to the creators of Python's argparse module, which provided the initial inspiration for this library.
Thanks also to the language design team behind Kotlin (not kotlinx-cli, which disappointed me!!).
Thanks to xenomachina's argparser, where I copied this section from :P
And to AJ Alt's CliKT, which inspired me to make the banner animation