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
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 errorParses 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 errorThe full help text for a spec, or for one subcommand with path (["db", "migrate"]).
cli_args_usage(spec: any) → stror an errorThe usage line for a spec.
cli_args_check(spec: any) → boolor an errorChecks 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() → listThe 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.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/cli_args.qwer$ curl -O https://quillolang.org/plugins/files/cli_args.qweThen, in your program, all of it or only what you call:
use cli_args.qweruse cli_args_parse, cli_args_help from cli_args // Quillo 0.6: only theseIt uses these crates, which cargo fetches the first time the plugin is built:
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
// 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
//! 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()}Related
ffmpeg_cli.qwer · 1.0.0ffmpeg and ffprobe through argument lists: a file probed to a map, thumbnails, trims, transcodes and HLS segments, a test video made from lavfi, progress read from -progress with a background job followed by percent, and everything checked first: paths forced to `file:`, so URLs and concat: stay plain names, and codecs, times and sizes refused unless they are what they should be.3 views · 6 downloadsGit commands git_cli.qwer · 1.0.0The git command driven through argument lists and parsed to maps: status --porcelain=v2 with staged, unstaged, untracked and conflicted files, branches with upstreams, tags, diff --numstat with renames and binaries, blame --porcelain, show, and a file at a revision. Unlike git_log, it reads working trees; a repository's config cannot run commands through it.2 views · 5 downloadsRun a command cli_run.qwer · 1.0.0Other programs run from Quillo with arguments as a list, never a shell string: a timeout that ends the whole process group, SIGTERM then SIGKILL, an output limit that kills or truncates, stdin, environment and working folder, exit status and signal reported apart, shell-free pipelines with pipefail, and background jobs watched and stopped.4 views · 6 downloadsThread 0 posts
On Command-line arguments: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.