From 83630d16d3e274fb046a0dfc8849a49c7fb8ff36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20F=C3=A4rber?= <01mf02@gmail.com> Date: Tue, 19 Nov 2024 16:58:21 +0100 Subject: [PATCH] Make a CLI by hand. The motivation is to correctly parse `--args`. As a nice side effect, this eliminates the dependency on clap as well. See: --- Cargo.lock | 117 +------------------------ jaq/Cargo.toml | 1 - jaq/src/cli.rs | 219 +++++++++++++++++++++++++++++++++++++++++++++++ jaq/src/help.txt | 38 ++++++++ jaq/src/main.rs | 219 +++++++++-------------------------------------- 5 files changed, 301 insertions(+), 293 deletions(-) create mode 100644 jaq/src/cli.rs create mode 100644 jaq/src/help.txt diff --git a/Cargo.lock b/Cargo.lock index 877ad535..220cdf90 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -83,43 +83,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "clap" -version = "4.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91b9970d7505127a162fdaa9b96428d28a479ba78c9ec7550a63a5d9863db682" -dependencies = [ - "atty", - "bitflags", - "clap_derive", - "clap_lex", - "once_cell", - "strsim", - "termcolor", -] - -[[package]] -name = "clap_derive" -version = "4.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0177313f9f02afc995627906bbd8967e2be069f5261954222dac78290c2b9014" -dependencies = [ - "heck", - "proc-macro-error", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "clap_lex" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "350b9cf31731f9957399229e9b2adc51eeabdfbe9d71d9a0552275fd12710d09" -dependencies = [ - "os_str_bytes", -] - [[package]] name = "codesnake" version = "0.2.1" @@ -186,12 +149,6 @@ version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - [[package]] name = "hermit-abi" version = "0.1.19" @@ -237,7 +194,6 @@ name = "jaq" version = "2.0.0-epsilon" dependencies = [ "atty", - "clap", "codesnake", "env_logger", "hifijson", @@ -387,36 +343,6 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" -[[package]] -name = "os_str_bytes" -version = "6.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn 1.0.109", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - [[package]] name = "proc-macro2" version = "1.0.89" @@ -482,7 +408,7 @@ checksum = "ad1e866f866923f252f05c889987993144fb74e722403468a4ebd70c3cd756c0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn", ] [[package]] @@ -503,23 +429,6 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - [[package]] name = "syn" version = "2.0.87" @@ -545,15 +454,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "termcolor" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" -dependencies = [ - "winapi-util", -] - [[package]] name = "typed-arena" version = "2.0.2" @@ -612,7 +512,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.87", + "syn", "wasm-bindgen-shared", ] @@ -634,7 +534,7 @@ checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -671,15 +571,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - [[package]] name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" @@ -709,5 +600,5 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.87", + "syn", ] diff --git a/jaq/Cargo.toml b/jaq/Cargo.toml index 3b2e0d1d..69f75662 100644 --- a/jaq/Cargo.toml +++ b/jaq/Cargo.toml @@ -21,7 +21,6 @@ jaq-json = { version = "1.0.0-epsilon", path = "../jaq-json" } atty = "0.2" codesnake = { version = "0.2" } -clap = { version = "4.0.0", features = ["derive"] } env_logger = { version = "0.10.0", default-features = false } hifijson = "0.2.0" log = { version = "0.4.17" } diff --git a/jaq/src/cli.rs b/jaq/src/cli.rs new file mode 100644 index 00000000..9679a31b --- /dev/null +++ b/jaq/src/cli.rs @@ -0,0 +1,219 @@ +//! 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 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, + + // Key-value options + pub arg: 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, + pub files: Vec, + pub args: Vec, + //pub jsonargs: Vec, + pub run_tests: Option, + /// 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| Some(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)?, + "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"))?, + "library-path" => self.short('L', args)?, + "arg" => { + let (name, value) = parse_key_val("--arg", args)?; + self.arg.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.next().ok_or(Error::Path("--run-tests"))?.into()) + } + "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, + '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 { + let mut cli = Self::default(); + cli.indent = 2; + let mut mode = Mode::Files; + let mut args = std::env::args_os(); + args.next(); + while let Some(arg) = args.next() { + match arg.to_str() { + 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)?, + }, + }, + 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 an integer"), + } + } +} + +/// Conversion of errors from [`OsString::into_string`]. +impl From 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)) +} + +enum Mode { + Args, + //JsonArgs, + Files, +} diff --git a/jaq/src/help.txt b/jaq/src/help.txt new file mode 100644 index 00000000..44d73951 --- /dev/null +++ b/jaq/src/help.txt @@ -0,0 +1,38 @@ +Just Another Query Tool + +Usage: jaq [OPTION]... [FILTER] [ARG]... + +Arguments: + [FILTER] Filter to execute + [ARG]... Positional arguments, by default used as input files + +Input options: + -n, --null-input Use null as single input value + -R, --raw-input Read lines of the input as sequence of strings + -s, --slurp Read (slurp) all input values into one array + +Output options: + -c, --compact-output Print JSON compactly, omitting whitespace + -r, --raw-output Write strings without escaping them with quotes + -j, --join-output Do not print a newline after each value + -i, --in-place Overwrite input file with its output + -C, --color-output Always color output + -M, --monochrome-output Do not color output + --tab Use tabs for indentation rather than spaces + --indent Use N spaces for indentation [default: 2] + +Compilation options: + -f, --from-file Read filter from a file given by filter argument + -L, --library-path Search for modules and data in given directory + +Variable options: + --arg Set variable `$A` to string `V` + --slurpfile Set variable `$A` to array containing the JSON values in file `F` + --rawfile Set variable `$A` to string containing the contents of file `F` + --args Collect remaining positional arguments into `$ARGS.positional` + +Remaining options: + --run-tests Run tests from a file + -e, --exit-status Use the last output value as exit status code + -V, --version Print version + -h, --help Print help diff --git a/jaq/src/main.rs b/jaq/src/main.rs index 665e821b..f1738cf2 100644 --- a/jaq/src/main.rs +++ b/jaq/src/main.rs @@ -1,8 +1,9 @@ -use clap::{Parser, ValueEnum}; +mod cli; + +use cli::Cli; use core::fmt::{self, Display, Formatter}; use jaq_core::{compile, load, Ctx, Native, RcIter}; use jaq_json::Val; -use std::ffi::{OsStr, OsString}; use std::io::{self, BufRead, Write}; use std::path::{Path, PathBuf}; use std::process::{ExitCode, Termination}; @@ -13,134 +14,6 @@ type Filter = jaq_core::Filter>; #[global_allocator] static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; -/// Just Another Query Tool -#[derive(Parser)] -#[command(version)] -struct Cli { - /// Use null as single input value - #[arg(short, long)] - null_input: bool, - - /// Use the last output value as exit status code - /// - /// 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. - #[arg(short, long)] - exit_status: bool, - - /// Read (slurp) all input values into one array - /// - /// When input is read from files, - /// jaq yields an array for each file, whereas - /// jq produces only a single array. - #[arg(short, long)] - slurp: bool, - - /// Overwrite input file with its output - #[arg(short, long)] - in_place: bool, - - /// Write strings without escaping them with quotes - #[arg(short, long)] - raw_output: bool, - - /// Read lines of the input as sequence of strings - /// - /// When the option `--slurp` is used additionally, - /// then the whole input is read into a single string. - #[arg(short = 'R', long)] - raw_input: bool, - - /// Print JSON compactly, omitting whitespace - #[arg(short, long)] - compact_output: bool, - - /// Use n spaces for indentation - #[arg(long, value_name = "n", default_value_t = 2)] - indent: usize, - - /// Use tabs for indentation rather than spaces - #[arg(long)] - tab: bool, - - /// Do not print a newline after each value - /// - /// This flag enables `--raw-output`. - #[arg(short, long)] - join_output: bool, - - /// Color output - /// - /// When this is set to `auto`, colors are enabled if - /// output is written to a terminal and - /// the `NO_COLOR` environment variable is not set. - #[arg(long, value_name = "WHEN", default_value = "auto")] - color: ColorWhen, - - /// Read filter from a file given by filter argument - #[arg(short, long)] - from_file: bool, - - /// Search for modules and data in given directory - /// - /// If this option is given multiple times, all given directories are searched. - #[arg(short = 'L', long, value_name = "DIR")] - library_path: Vec, - - /// Set variable `$` to string `` - #[arg(long, value_names = &["a", "v"])] - arg: Vec, - - /// Set variable `$` to string containing the contents of file `f` - #[arg(long, value_names = &["a", "f"])] - rawfile: Vec, - - /// Set variable `$` to array containing the JSON values in file `f` - #[arg(long, value_names = &["a", "f"])] - slurpfile: Vec, - - /// Run tests from a file - #[arg(long, value_name = "FILE")] - run_tests: Option, - - /// Collect positional arguments into `$ARGS.positional` - /// - /// When using this flag, positional arguments are not used as input files. - #[arg(long)] - args: bool, - - /// Filter to execute - /// - /// If this argument is not given, it is assumed to be `.`, the identity filter. - filter: Option, - - /// Positional arguments, by default used as input files - #[arg(name = "ARG")] - posargs: Vec, -} - -#[derive(Clone, ValueEnum)] -enum ColorWhen { - Always, - Auto, - Never, -} - -impl ColorWhen { - fn use_if(&self, f: impl Fn() -> bool) -> bool { - match self { - Self::Always => true, - Self::Auto => f(), - Self::Never => false, - } - } -} - fn main() -> ExitCode { use env_logger::Env; env_logger::Builder::from_env(Env::default().filter_or("LOG", "debug")) @@ -152,7 +25,21 @@ fn main() -> ExitCode { }) .init(); - let cli = Cli::parse(); + let cli = match Cli::parse() { + Ok(cli) => cli, + Err(e) => { + eprintln!("{e}"); + return ExitCode::from(2); + } + }; + + if cli.version { + println!("{} {}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")); + return ExitCode::SUCCESS; + } else if cli.help { + println!("{}", include_str!("help.txt")); + return ExitCode::SUCCESS; + } let no_color = std::env::var("NO_COLOR").map_or(false, |v| !v.is_empty()); let detect_color = |stream| atty::is(stream) && !no_color; @@ -164,12 +51,12 @@ fn main() -> ExitCode { } }; - set_color(!cli.in_place && cli.color.use_if(|| detect_color(atty::Stream::Stdout))); + set_color(!cli.in_place && cli.color_if(|| detect_color(atty::Stream::Stdout))); match real_main(&cli) { Ok(exit) => exit, Err(e) => { - set_color(cli.color.use_if(|| detect_color(atty::Stream::Stderr))); + set_color(cli.color_if(|| detect_color(atty::Stream::Stderr))); e.report() } } @@ -185,25 +72,22 @@ fn real_main(cli: &Cli) -> Result { let (vals, filter) = match &cli.filter { None => (Vec::new(), Filter::default()), Some(filter) => { - let (path, code) = if cli.from_file { - (filter.into(), std::fs::read_to_string(filter)?) - } else { - ("".into(), filter.clone().into_string()?) + let (path, code) = match filter { + cli::Filter::FromFile(path) => (path.into(), std::fs::read_to_string(path)?), + cli::Filter::Inline(filter) => ("".into(), filter.clone()), }; - parse(&path, &code, &vars, &cli.library_path).map_err(Error::Report)? } }; ctx.extend(vals); //println!("Filter: {:?}", filter); - let files = if cli.args { &[] } else { &*cli.posargs }; - let last = if files.is_empty() { + let last = if cli.files.is_empty() { let inputs = read_buffered(cli, io::stdin().lock()); with_stdout(|out| run(cli, &filter, ctx, inputs, |v| print(out, cli, &v)))? } else { let mut last = None; - for file in files { + for file in &cli.files { let path = Path::new(file); let file = load_file(path).map_err(|e| Error::Io(Some(path.display().to_string()), e))?; @@ -242,37 +126,26 @@ fn real_main(cli: &Cli) -> Result { } } -fn bind(var_val: &mut Vec<(String, Val)>, args: &[OsString], f: F) -> Result<(), Error> -where - F: Fn(&OsStr) -> Result, -{ - for arg_val in args.chunks(2) { - if let [arg, val] = arg_val { - var_val.push((arg.to_os_string().into_string()?, f(val)?)); - } - } - Ok(()) -} - fn binds(cli: &Cli) -> Result, Error> { - let mut var_val = Vec::new(); - - bind(&mut var_val, &cli.arg, |v| { - Ok(Val::Str(v.to_os_string().into_string()?.into())) - })?; - bind(&mut var_val, &cli.rawfile, |path| { + let arg = cli.arg.iter().map(|(k, s)| { + let s = s.to_owned(); + Ok((k.to_owned(), Val::Str(s.into()))) + }); + let rawfile = cli.rawfile.iter().map(|(k, path)| { let s = std::fs::read_to_string(path).map_err(|e| Error::Io(Some(format!("{path:?}")), e)); - Ok(Val::Str(s?.into())) - })?; - bind(&mut var_val, &cli.slurpfile, |path| { - json_array(path).map_err(|e| Error::Io(Some(format!("{path:?}")), e)) - })?; + Ok((k.to_owned(), Val::Str(s?.into()))) + }); + let slurpfile = cli.rawfile.iter().map(|(k, path)| { + let a = json_array(path).map_err(|e| Error::Io(Some(format!("{path:?}")), e)); + Ok((k.to_owned(), a?)) + }); - let positional = if cli.args { &*cli.posargs } else { &[] }; - let positional = positional.iter().cloned(); - let positional = positional.map(|s| Ok(Val::from(s.into_string()?))); + let positional = cli.args.iter().cloned().map(|s| Ok(Val::from(s))); let positional = positional.collect::, Error>>()?; + let var_val = arg.chain(rawfile).chain(slurpfile); + let mut var_val = var_val.collect::, Error>>()?; + var_val.push(("ARGS".to_string(), args(&positional, &var_val))); let env = std::env::vars().map(|(k, v)| (k.into(), Val::from(v))); var_val.push(("ENV".to_string(), Val::obj(env.collect()))); @@ -435,7 +308,6 @@ type FileReports = (load::File, Vec); enum Error { Io(Option, io::Error), Report(Vec), - Utf8(OsString), Parse(String), Jaq(jaq_core::Error), Persist(tempfile::PersistError), @@ -472,10 +344,6 @@ impl Termination for Error { 3 } Self::NoOutput => 4, - Self::Utf8(s) => { - eprintln!("Error: failed to interpret as UTF-8: {s:?}"); - 5 - } Self::Parse(e) => { eprintln!("Error: failed to parse: {e}"); 5 @@ -495,13 +363,6 @@ impl From for Error { } } -/// Conversion of errors from [`OsString::into_string`]. -impl From for Error { - fn from(e: OsString) -> Self { - Self::Utf8(e) - } -} - /// Run a filter with given input values and run `f` for every value output. /// /// This function cannot return an `Iterator` because it creates an `RcIter`.