Developer tools

Command-line arguments

Command-line parsing for Quillo programs and scripts, described as a map and run by clap: flags, counted flags, typed options, positionals, nested subcommands, choices, required, conflicting and dependent arguments, env fallbacks with where each value came from, generated help and usage, and suggestions for misspellings.

Version 1.0.0 · 5 functions · 2 crates

#cli#arguments#scripts#clap

Functions

What use cli_args.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use cli_args_parse, cli_args_help from cli_args (Quillo 0.6).

cli_args_parse(spec: any, args: list) → anyor an error

Parses args (the words after the program's name) by spec. Answers { command, values, sources }, command being the subcommand path as a list and sources saying where each value came from. For --help or --version it answers { exit: 0, output }, for the program to print and stop. A mistake is an error with clap's message and usage.

cli_args_help(spec: any, path: list) → stror an error

The full help text for a spec, or for one subcommand with path (["db", "migrate"]).

cli_args_usage(spec: any) → stror an error

The usage line for a spec.

cli_args_check(spec: any) → boolor an error

Checks a spec without parsing anything: true, or an error naming what is wrong with it. Worth a test: clap's own checks are not all there in a release build.

cli_args_os() → list

The words to parse: those after a -- in the process's command line where the runner lets one through, or else CLI_ARGS from the environment, split on spaces, as quillo run itself refuses anything it does not know. Empty when there are neither.

Get it

$ quillo install cli_args               # cli_args.qwer into this folder$ quillo install cli_args --example     # and its example, cli_args.qwe

Or with curl:

$ curl -O https://quillolang.org/plugins/files/cli_args.qwer$ curl -O https://quillolang.org/plugins/files/cli_args.qwe

Then, in your program, all of it or only what you call:

main.qwe
use cli_args.qweruse cli_args_parse, cli_args_help from cli_args      // Quillo 0.6: only these

It uses these crates, which cargo fetches the first time the plugin is built:

[dependencies]
clap = { version = "=4.6.7", default-features = false, features = ["std", "help", "usage", "error-context", "suggestions", "string", "env"] }serde_json = { version = "1", features = ["preserve_order"] }

Example

cli_args.qwe
// A backup tool's command line, described as data: flags, options with types and env fallbacks,// subcommands, generated help, and mistakes answered the way people expect.////   CLI_ARGS="restore --from /backups/monday --force" quillo run cli_args.qwe//   then open http://localhost:8080/real; the other routes parse words given in the route.//// A Quillo program is not handed its command line, so the words come from `CLI_ARGS`, or from// after a `--` where the runner lets one through.use "cli_args.qwer"let spec = {    name: "backup",    version: "1.4.2",    about: "Backs up and restores a folder",    args: [        { name: "verbose", kind: "count", short: "v", help: "Say more; twice for even more" },        { name: "config", short: "c", type: "path", env: "BACKUP_CONFIG", help: "Config file", value_name: "FILE" },        { name: "dry_run", long: "dry-run", kind: "flag", short: "n", help: "Show what would happen" },        { name: "token", env: "BACKUP_TOKEN", hidden: true },    ],    commands: [        { name: "create", about: "Makes a backup", aliases: ["c"], args: [            { name: "source", kind: "positional", required: true, type: "path", help: "Folder to back up" },            { name: "level", short: "l", type: "int", default: 6, help: "Compression level" },            { name: "ratio", type: "float", default: 0.5 },            { name: "format", choices: ["tar", "zip", "qar"], default: "tar", help: "Archive format" },            { name: "exclude", short: "x", multiple: true, help: "Pattern to leave out" },            { name: "encrypt", type: "bool", env: "BACKUP_ENCRYPT" },        ]},        { name: "restore", about: "Restores a backup", args: [            { name: "from", required: true, type: "path" },            { name: "to", conflicts: ["in_place"] },            { name: "in_place", long: "in-place", kind: "flag" },            { name: "force", kind: "flag", requires: ["from"] },        ]},        { name: "db", about: "Database dumps", command_required: true, commands: [            { name: "dump", about: "Dumps it", args: [{ name: "table", kind: "positional", multiple: true }] },        ]},    ],}fn parse(words) { cli_args_parse(spec, split(words, " ")) }route GET /create {    let r = parse("-vv -c /etc/b.toml create /home/ada --level 9 -x '*.log' -x tmp --format zip")?    { command: r.command, values: r.values, sources: r.sources }}route GET /defaults {    let r = parse("c /data")?    { command: r.command, level: r.values.level, level_from: r.sources.level, ratio: r.values.ratio, format: r.values.format, exclude: r.values.exclude, verbose: r.values.verbose, encrypt: r.values.encrypt, encrypt_from: r.sources.encrypt, config: r.values.config, config_from: r.sources.config, token: r.values.token }}route GET /nested = parse("db dump users orders")?route GET /mistakes = {    bad_number: parse("create /d --level high").error,    bad_choice: parse("create /d --format rar").error,    missing: parse("create").error,    unknown: parse("create /d --levle 3").error,    typo: parse("creat /d").error,    clash: parse("restore --from /b --to /c --in-place").error,}route GET /help = {    top: parse("--help")?,    version: parse("--version")?,    sub: cli_args_help(spec, ["create"])?,    deep: parse("db")?.exit,    usage: cli_args_usage(spec)?,}route GET /specs = {    ok: cli_args_check(spec)?,    unknown_key: cli_args_check({ name: "x", arg: [] }).error,    bad_kind: cli_args_check({ name: "x", args: [{ name: "a", kind: "switch" }] }).error,    bad_type: cli_args_check({ name: "x", args: [{ name: "a", type: "date" }] }).error,    twice: cli_args_check({ name: "x", args: [{ name: "a" }, { name: "a" }] }).error,    long_short: cli_args_check({ name: "x", args: [{ name: "a", short: "ab" }] }).error,    help_taken: cli_args_check({ name: "x", args: [{ name: "a", short: "h" }] }).error,}route GET /real {    let words = cli_args_os()    let r = cli_args_parse(spec, words)    if is_err(r) { return { words: words, mistake: r.error } }    { words: words, command: r.command, from: r.values.from, force: r.values.force }}
$ quillo run cli_args.qwe$ curl 'localhost:8080/create'$ curl 'localhost:8080/defaults'$ curl 'localhost:8080/nested'$ curl 'localhost:8080/mistakes'$ curl 'localhost:8080/help'$ curl 'localhost:8080/specs'$ curl 'localhost:8080/real'

Source

cli_args.qwer
//! Command-line arguments for Quillo programs and scripts, parsed with `clap` from a spec given as a map: flags, counted flags, typed options, positionals, subcommands, choices, required and conflicting arguments, env fallbacks, generated help and usage, and misspellings answered with a suggestion.//! version: 1.0.0//!//! [dependencies]//! clap = { version = "=4.6.7", default-features = false, features = ["std", "help", "usage", "error-context", "suggestions", "string", "env"] }//! serde_json = { version = "1", features = ["preserve_order"] }use clap::builder::{BoolishValueParser, PossibleValuesParser, ValueParser};use clap::error::ErrorKind;use clap::parser::ValueSource;use clap::{Arg, ArgAction, ArgMatches, Command};use serde_json::{json, Map, Value};use std::path::PathBuf;// A spec: { name, version, about, after_help, args: [...], commands: [...], command_required }.// An arg: { name, kind, short, long, help, type, default, env, required, choices, multiple,//           value_name, hidden, conflicts, requires, aliases }.// `kind` is flag, count, option (the default) or positional; `type` is string, int, float, bool or// path. A command is a spec with `aliases`.const SPEC_KEYS: [&str; 9] = ["name", "version", "about", "after_help", "args", "commands", "command_required", "aliases", "hidden"];const ARG_KEYS: [&str; 15] = ["name", "kind", "short", "long", "help", "type", "default", "env", "required", "choices", "multiple", "value_name", "hidden", "conflicts", "requires"];fn s<'a>(o: &'a Value, k: &str) -> Option<&'a str> {    o.get(k).and_then(|v| v.as_str())}fn yes(o: &Value, k: &str) -> bool {    o.get(k).and_then(|v| v.as_bool()).unwrap_or(false)}fn list(o: &Value, k: &str) -> Vec<String> {    match o.get(k) {        Some(Value::Array(a)) => a.iter().filter_map(|x| x.as_str().map(String::from)).collect(),        Some(Value::String(t)) => vec![t.clone()],        _ => vec![],    }}fn text_of(v: &Value) -> String {    match v {        Value::String(t) => t.clone(),        x => x.to_string(),    }}fn arg_of(a: &Value, context: &str) -> Result<Arg, String> {    let m = a.as_object().ok_or(format!("{}: an arg must be a map", context))?;    for k in m.keys() {        if !ARG_KEYS.contains(&k.as_str()) {            return Err(format!("{}: an arg has no `{}`; it takes {}", context, k, ARG_KEYS.join(", ")));        }    }    let name = s(a, "name").ok_or(format!("{}: an arg needs `name`", context))?.to_string();    if name.is_empty() || !name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == '-') {        return Err(format!("{}: `{}` is not an arg name", context, name));    }    let kind = s(a, "kind").unwrap_or("option");    let ty = s(a, "type").unwrap_or("string");    let mut arg = Arg::new(name.clone());    let positional = kind == "positional";    if !positional {        arg = arg.long(s(a, "long").unwrap_or(&name).to_string());    }    if let Some(t) = s(a, "short") {        let mut c = t.chars();        let (Some(ch), None) = (c.next(), c.next()) else { return Err(format!("{}: `short` for `{}` must be one letter", context, name)) };        if positional {            return Err(format!("{}: a positional has no `short`", context));        }        if ch == 'h' {            return Err(format!("{}: `-h` is help", context));        }        arg = arg.short(ch);    }    if let Some(h) = s(a, "help") {        arg = arg.help(h.to_string());    }    if yes(a, "hidden") {        arg = arg.hide(true);    }    if let Some(e) = s(a, "env") {        arg = arg.env(e.to_string());    }    for x in list(a, "conflicts") {        arg = arg.conflicts_with(x);    }    for x in list(a, "requires") {        arg = arg.requires(x);    }    arg = match kind {        "flag" => {            if a.get("default").is_some() {                return Err(format!("{}: the flag `{}` has no default: it is false until given", context, name));            }            arg.action(ArgAction::SetTrue)        }        "count" => arg.action(ArgAction::Count),        "option" | "positional" => {            let multiple = yes(a, "multiple");            arg = arg.action(if multiple { ArgAction::Append } else { ArgAction::Set });            if multiple && positional {                arg = arg.num_args(1..);            }            let choices = list(a, "choices");            let parser: ValueParser = match ty {                "string" if !choices.is_empty() => PossibleValuesParser::new(choices).into(),                "string" => clap::value_parser!(String).into(),                "int" => clap::value_parser!(i64).into(),                "float" => clap::value_parser!(f64).into(),                "bool" => BoolishValueParser::new().into(),                "path" => clap::value_parser!(PathBuf).into(),                t => return Err(format!("{}: `{}` is not a type: string, int, float, bool or path", context, t)),            };            arg = arg.value_parser(parser);            if ty != "string" && a.get("choices").is_some() {                return Err(format!("{}: `choices` is for strings", context));            }            if let Some(d) = a.get("default") {                match d {                    Value::Array(l) => arg = arg.default_values(l.iter().map(text_of).collect::<Vec<_>>()),                    x => arg = arg.default_value(text_of(x)),                }            }            if let Some(n) = s(a, "value_name") {                arg = arg.value_name(n.to_string());            }            if yes(a, "required") {                arg = arg.required(true);            }            arg        }        k => return Err(format!("{}: `{}` is not a kind: flag, count, option or positional", context, k)),    };    Ok(arg)}fn command_of(spec: &Value, context: &str) -> Result<Command, String> {    let m = spec.as_object().ok_or("a spec must be a map")?;    for k in m.keys() {        if !SPEC_KEYS.contains(&k.as_str()) {            return Err(format!("{}: a spec has no `{}`; it takes {}", context, k, SPEC_KEYS.join(", ")));        }    }    let name = s(spec, "name").ok_or(format!("{}: a spec needs `name`", context))?.to_string();    let mut c = Command::new(name.clone());    if let Some(v) = s(spec, "version") {        c = c.version(v.to_string());    }    if let Some(v) = s(spec, "about") {        c = c.about(v.to_string());    }    if let Some(v) = s(spec, "after_help") {        c = c.after_help(v.to_string());    }    for a in list(spec, "aliases") {        c = c.visible_alias(a);    }    if yes(spec, "hidden") {        c = c.hide(true);    }    let here = format!("{} {}", context, name);    let mut seen = vec![];    for a in spec.get("args").and_then(|v| v.as_array()).into_iter().flatten() {        let n = s(a, "name").unwrap_or("").to_string();        if seen.contains(&n) {            return Err(format!("{}: two args named `{}`", here.trim(), n));        }        seen.push(n);        c = c.arg(arg_of(a, here.trim())?);    }    let mut names = vec![];    for x in spec.get("commands").and_then(|v| v.as_array()).into_iter().flatten() {        let n = s(x, "name").unwrap_or("").to_string();        if names.contains(&n) {            return Err(format!("{}: two commands named `{}`", here.trim(), n));        }        names.push(n);        c = c.subcommand(command_of(x, here.trim())?);    }    if yes(spec, "command_required") {        c = c.subcommand_required(true).arg_required_else_help(true);    }    Ok(c)}fn source_name(s: Option<ValueSource>) -> &'static str {    match s {        Some(ValueSource::CommandLine) => "command line",        Some(ValueSource::EnvVariable) => "environment",        Some(ValueSource::DefaultValue) => "default",        _ => "absent",    }}fn collect(spec: &Value, m: &ArgMatches, values: &mut Map<String, Value>, sources: &mut Map<String, Value>) {    for a in spec.get("args").and_then(|v| v.as_array()).into_iter().flatten() {        let name = s(a, "name").unwrap_or("");        let kind = s(a, "kind").unwrap_or("option");        let ty = s(a, "type").unwrap_or("string");        let many = yes(a, "multiple");        let v: Value = match kind {            "flag" => json!(m.get_flag(name)),            "count" => json!(m.get_count(name)),            _ => {                macro_rules! read {                    ($t:ty, $f:expr) => {                        if many {                            json!(m.get_many::<$t>(name).map(|l| l.map($f).collect::<Vec<_>>()).unwrap_or_default())                        } else {                            m.get_one::<$t>(name).map($f).map_or(Value::Null, |x| json!(x))                        }                    };                }                match ty {                    "int" => read!(i64, |x: &i64| *x),                    "float" => read!(f64, |x: &f64| *x),                    "bool" => read!(bool, |x: &bool| *x),                    "path" => read!(PathBuf, |x: &PathBuf| x.display().to_string()),                    _ => read!(String, |x: &String| x.clone()),                }            }        };        values.insert(name.to_string(), v);        sources.insert(name.to_string(), json!(source_name(m.value_source(name))));    }}fn walk(spec: &Value, m: &ArgMatches, path: &mut Vec<String>, values: &mut Map<String, Value>, sources: &mut Map<String, Value>) {    collect(spec, m, values, sources);    if let Some((name, sub)) = m.subcommand() {        path.push(name.to_string());        let next = spec.get("commands").and_then(|v| v.as_array()).into_iter().flatten().find(|c| s(c, "name") == Some(name));        if let Some(c) = next {            walk(c, sub, path, values, sources);        }    }}/// Parses `args` (the words after the program's name) by `spec`. Answers `{ command, values, sources }`, `command` being the subcommand path as a list and `sources` saying where each value came from. For `--help` or `--version` it answers `{ exit: 0, output }`, for the program to print and stop. A mistake is an error with clap's message and usage.pub fn cli_args_parse(spec: serde_json::Value, args: Vec<String>) -> Result<Value, String> {    let name = s(&spec, "name").unwrap_or("program").to_string();    let c = command_of(&spec, "")?;    let mut argv = vec![name];    argv.extend(args);    match c.try_get_matches_from(argv) {        Ok(m) => {            let (mut path, mut values, mut sources) = (vec![], Map::new(), Map::new());            walk(&spec, &m, &mut path, &mut values, &mut sources);            Ok(json!({ "command": path, "values": values, "sources": sources }))        }        Err(e) => match e.kind() {            ErrorKind::DisplayHelp | ErrorKind::DisplayVersion | ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand => {                Ok(json!({ "exit": if e.kind() == ErrorKind::DisplayHelpOnMissingArgumentOrSubcommand { 2 } else { 0 }, "output": e.render().to_string().trim_end() }))            }            _ => Err(e.render().to_string().trim_end().to_string()),        },    }}/// The full help text for a spec, or for one subcommand with `path` (["db", "migrate"]).pub fn cli_args_help(spec: serde_json::Value, path: Vec<String>) -> Result<String, String> {    let mut c = command_of(&spec, "")?;    let mut here = &mut c;    here.build();    for p in &path {        here = here.find_subcommand_mut(p).ok_or(format!("no subcommand `{}`", p))?;    }    Ok(here.render_long_help().to_string().trim_end().to_string())}/// The usage line for a spec.pub fn cli_args_usage(spec: serde_json::Value) -> Result<String, String> {    let mut c = command_of(&spec, "")?;    Ok(c.render_usage().to_string())}/// Checks a spec without parsing anything: true, or an error naming what is wrong with it. Worth a test: clap's own checks are not all there in a release build.pub fn cli_args_check(spec: serde_json::Value) -> Result<bool, String> {    command_of(&spec, "")?;    Ok(true)}/// The words to parse: those after a `--` in the process's command line where the runner lets one through, or else `CLI_ARGS` from the environment, split on spaces, as `quillo run` itself refuses anything it does not know. Empty when there are neither.pub fn cli_args_os() -> Vec<String> {    let all: Vec<String> = std::env::args().collect();    if let Some(i) = all.iter().position(|a| a == "--") {        return all[i + 1..].to_vec();    }    std::env::var("CLI_ARGS").map(|t| t.split_whitespace().map(String::from).collect()).unwrap_or_default()}
Thread
38Useful? Like it, or add to its thread below.

Thread 0 posts

On Command-line arguments: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.