Files
sausage/userland/jaq/src/cli.rs
T
surma b9e3e3677d userland: search and exec tools, spawn library (T14)
rg (ripgrep 15.2), grep (ours, on ripgrep's regex crates), find and xargs
(ours), sed (uutils 0.3 wrapper), diff and cmp (uutils diffutils 0.6 with -r),
fd 10.5, jaq 2.3, env and timeout. userland/spawn is a small Command-like
client of the process interface. search-test.nu: 42 cases match native tools.
2026-11-06 17:21:51 +00:00

234 lines
7.6 KiB
Rust

//! Command-line argument parsing
use core::fmt;
use std::env::ArgsOs;
use std::ffi::OsString;
use std::path::PathBuf;
#[derive(Debug, Default)]
pub struct Cli {
// Input options
pub null_input: bool,
/// When the option `--slurp` is used additionally,
/// then the whole input is read into a single string.
pub raw_input: bool,
/// When input is read from files,
/// jaq yields an array for each file, whereas
/// jq produces only a single array.
pub slurp: bool,
// Output options
pub compact_output: bool,
pub raw_output: bool,
/// This flag enables `--raw-output`.
pub join_output: bool,
pub in_place: bool,
pub sort_keys: bool,
pub color_output: bool,
pub monochrome_output: bool,
pub tab: bool,
pub indent: usize,
// Compilation options
pub from_file: bool,
/// If this option is given multiple times, all given directories are searched.
pub library_path: Vec<PathBuf>,
// Key-value options
pub arg: Vec<(String, String)>,
pub argjson: Vec<(String, String)>,
pub slurpfile: Vec<(String, OsString)>,
pub rawfile: Vec<(String, OsString)>,
// Positional arguments
/// If this argument is not given, it is assumed to be `.`, the identity filter.
pub filter: Option<Filter>,
pub files: Vec<PathBuf>,
pub args: Vec<String>,
//pub jsonargs: Vec<String>,
pub run_tests: Option<Vec<PathBuf>>,
/// If there is some last output value `v`,
/// then the exit status code is
/// 1 if `v < true` (that is, if `v` is `false` or `null`) and
/// 0 otherwise.
/// If there is no output value, then the exit status code is 4.
///
/// If any error occurs, then this option has no effect.
pub exit_status: bool,
pub version: bool,
pub help: bool,
}
#[derive(Debug)]
pub enum Filter {
Inline(String),
FromFile(PathBuf),
}
impl Cli {
fn positional(&mut self, mode: &Mode, arg: OsString) -> Result<(), Error> {
if self.filter.is_none() {
self.filter = Some(if self.from_file {
Filter::FromFile(arg.into())
} else {
Filter::Inline(arg.into_string()?)
})
} else {
match mode {
Mode::Files => self.files.push(arg.into()),
Mode::Args => self.args.push(arg.into_string()?),
//Mode::JsonArgs => self.jsonargs.push(arg.into_string()?),
}
}
Ok(())
}
fn long(&mut self, mode: &mut Mode, arg: &str, args: &mut ArgsOs) -> Result<(), Error> {
let int = |s: OsString| s.into_string().ok()?.parse().ok();
match arg {
// handle all arguments after "--"
"" => args.try_for_each(|arg| self.positional(mode, arg))?,
"null-input" => self.short('n', args)?,
"raw-input" => self.short('R', args)?,
"slurp" => self.short('s', args)?,
"compact-output" => self.short('c', args)?,
"raw-output" => self.short('r', args)?,
"join-output" => self.short('j', args)?,
"in-place" => self.short('i', args)?,
"sort-keys" => self.short('S', args)?,
"color-output" => self.short('C', args)?,
"monochrome-output" => self.short('M', args)?,
"tab" => self.tab = true,
"indent" => self.indent = args.next().and_then(int).ok_or(Error::Int("--indent"))?,
"from-file" => self.short('f', args)?,
"library-path" => self.short('L', args)?,
"arg" => {
let (name, value) = parse_key_val("--arg", args)?;
self.arg.push((name, value.into_string()?));
}
"argjson" => {
let (name, value) = parse_key_val("--argjson", args)?;
self.argjson.push((name, value.into_string()?));
}
"slurpfile" => self.slurpfile.push(parse_key_val("--slurpfile", args)?),
"rawfile" => self.rawfile.push(parse_key_val("--rawfile", args)?),
"args" => *mode = Mode::Args,
//"jsonargs" => *mode = Mode::JsonArgs,
"run-tests" => self.run_tests = Some(args.map(PathBuf::from).collect()),
"exit-status" => self.short('e', args)?,
"version" => self.short('V', args)?,
"help" => self.short('h', args)?,
arg => Err(Error::Flag(format!("--{arg}")))?,
}
Ok(())
}
fn short(&mut self, arg: char, args: &mut ArgsOs) -> Result<(), Error> {
match arg {
'n' => self.null_input = true,
'R' => self.raw_input = true,
's' => self.slurp = true,
'c' => self.compact_output = true,
'r' => self.raw_output = true,
'j' => self.join_output = true,
'i' => self.in_place = true,
'S' => self.sort_keys = true,
'C' => self.color_output = true,
'M' => self.monochrome_output = true,
'f' => self.from_file = true,
'L' => self
.library_path
.push(args.next().ok_or(Error::Path("-L"))?.into()),
'e' => self.exit_status = true,
'V' => self.version = true,
'h' => self.help = true,
arg => Err(Error::Flag(format!("-{arg}")))?,
}
Ok(())
}
pub fn parse() -> Result<Self, Error> {
let mut cli = Self {
indent: 2,
..Self::default()
};
let mut mode = Mode::Files;
let mut args = std::env::args_os();
args.next();
while let Some(arg) = args.next() {
match arg.to_str() {
// we've got a valid UTF-8 argument here
Some(s) => match s.strip_prefix("--") {
Some(rest) => cli.long(&mut mode, rest, &mut args)?,
None => match s.strip_prefix("-") {
Some(rest) => rest.chars().try_for_each(|c| cli.short(c, &mut args))?,
None => cli.positional(&mode, arg)?,
},
},
// we've got invalid UTF-8, so it is no valid flag
// note that we do not check here whether arg starts with `-`,
// because this seems to be quite difficult to do in a portable way
None => cli.positional(&mode, arg)?,
}
}
Ok(cli)
}
pub fn color_if(&self, f: impl Fn() -> bool) -> bool {
if self.monochrome_output {
false
} else if self.color_output {
true
} else {
f()
}
}
}
#[derive(Debug)]
pub enum Error {
Flag(String),
Utf8(OsString),
KeyValue(&'static str),
Int(&'static str),
Path(&'static str),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::Flag(s) => write!(f, "unknown flag: {s}"),
Self::Utf8(s) => write!(f, "invalid UTF-8: {s:?}"),
Self::KeyValue(o) => write!(f, "{o} expects a key and a value"),
Self::Int(o) => write!(f, "{o} expects an integer"),
Self::Path(o) => write!(f, "{o} expects a path"),
}
}
}
/// Conversion of errors from [`OsString::into_string`].
impl From<OsString> for Error {
fn from(e: OsString) -> Self {
Self::Utf8(e)
}
}
fn parse_key_val(arg: &'static str, args: &mut ArgsOs) -> Result<(String, OsString), Error> {
let err = || Error::KeyValue(arg);
let key = args.next().ok_or_else(err)?.into_string()?;
let val = args.next().ok_or_else(err)?;
Ok((key, val))
}
/// Interpretation of positional arguments.
enum Mode {
Args,
//JsonArgs,
Files,
}