From deafab9c930ab092c4ee8abd4cbe09c7eb34aa22 Mon Sep 17 00:00:00 2001 From: Jonas Maier <> Date: Sun, 8 Mar 2026 08:28:57 +0100 Subject: argument parsing --- Cargo.lock | 45 +++++++++++++++++++++++ Cargo.toml | 1 + pish_derive/.gitignore | 1 + pish_derive/Cargo.lock | 47 ++++++++++++++++++++++++ pish_derive/Cargo.toml | 12 +++++++ pish_derive/src/lib.rs | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/run/builtin.rs | 48 +++++++++++++++++++++++++ src/run/mod.rs | 2 ++ 8 files changed, 252 insertions(+) create mode 100644 pish_derive/.gitignore create mode 100644 pish_derive/Cargo.lock create mode 100644 pish_derive/Cargo.toml create mode 100644 pish_derive/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index b8af854..1c8a615 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -29,16 +29,44 @@ name = "pish" version = "0.3.0" dependencies = [ "libc", + "pish_derive", "sqlite", "termios", ] +[[package]] +name = "pish_derive" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "pkg-config" version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + [[package]] name = "shlex" version = "1.3.0" @@ -73,6 +101,17 @@ dependencies = [ "sqlite3-src", ] +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "termios" version = "0.3.3" @@ -81,3 +120,9 @@ checksum = "411c5bf740737c7918b8b1fe232dca4dc9f8e754b8ad5e20966814001ed0ac6b" dependencies = [ "libc", ] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/Cargo.toml b/Cargo.toml index d29df84..3707d2a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,3 +9,4 @@ license-file = "LICENSE" libc = "0.2.182" sqlite = "0.37.0" termios = "0.3" +pish_derive = { path = "./pish_derive" } diff --git a/pish_derive/.gitignore b/pish_derive/.gitignore new file mode 100644 index 0000000..c41cc9e --- /dev/null +++ b/pish_derive/.gitignore @@ -0,0 +1 @@ +/target \ No newline at end of file diff --git a/pish_derive/Cargo.lock b/pish_derive/Cargo.lock new file mode 100644 index 0000000..af838d6 --- /dev/null +++ b/pish_derive/Cargo.lock @@ -0,0 +1,47 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "pish_derive" +version = "0.1.0" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" diff --git a/pish_derive/Cargo.toml b/pish_derive/Cargo.toml new file mode 100644 index 0000000..11fea7e --- /dev/null +++ b/pish_derive/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "pish_derive" +version = "0.1.0" +edition = "2024" + +[lib] +proc-macro = true + +[dependencies] +proc-macro2 = "1.0.106" +quote = "1.0.45" +syn = "2.0.117" diff --git a/pish_derive/src/lib.rs b/pish_derive/src/lib.rs new file mode 100644 index 0000000..2ac98f7 --- /dev/null +++ b/pish_derive/src/lib.rs @@ -0,0 +1,96 @@ +use proc_macro::{Literal, TokenStream}; +use quote::{ToTokens, quote}; +use syn::{Data, DeriveInput, Fields, parse_macro_input}; + +#[proc_macro_derive(FromArgs)] +pub fn derive_cli(input: TokenStream) -> TokenStream { + let input = parse_macro_input!(input as DeriveInput); + + let name = input.ident; + + let fields = match input.data { + Data::Struct(data) => data.fields, + _ => panic!("Cli can only be derived for structs"), + }; + + let mut field_parsers_init = Vec::new(); + let mut field_parsers = Vec::new(); + let mut field_parsers_post = Vec::new(); + let mut field_names = Vec::new(); + + if let Fields::Named(fields_named) = fields { + for field in fields_named.named { + let ident = field.ident.unwrap(); + let name_str = ident.to_string(); + + let mut long_name = b"--".to_vec(); + long_name.extend_from_slice(name_str.as_bytes()); + let long_name = proc_macro2::Literal::byte_string(&long_name); + + field_names.push(ident.clone()); + + // initialization + field_parsers_init.push(quote! { + let mut #ident = None; + }); + + let is_bool = field.ty.to_token_stream().to_string() == String::from("bool"); + let is_option = field.ty.to_token_stream().to_string().starts_with("Option"); // bad bad detection + + // in the loop + if is_bool { + field_parsers.push(quote! { + if arg == #long_name { + #ident = Some(true); + continue; + } + }); + } else { + field_parsers.push(quote! { + if arg == #long_name { + let Some(val) = iter.next() else { + return Err(ArgParseError::MissingArgValue(#name_str)); + }; + match String::from_utf8_lossy(val).parse() { + Ok(parsed) => { + #ident = Some(parsed); + continue; + } + Err(err) => { + return Err(ArgParseError::ArgValueParseError(#name_str, format!("{err:?}"))); + } + } + } + }); + } + + // after loop + if is_bool { + field_parsers_post.push(quote!{ let #ident = #ident.unwrap_or(false); }); + } else if !is_option { + field_parsers_post.push(quote!{ let Some(#ident) = #ident else { return Err(ArgParseError::MissingArg(#name_str)) }; }); + } + } + } + + let expanded = quote! { + impl ArgParse for #name { + fn parse<'a>(args: &'a [BString]) -> std::result::Result> { + let mut iter = args.iter().skip(1); + + #(#field_parsers_init)* + + while let Some(arg) = iter.next() { + #(#field_parsers)*; + return Err(ArgParseError::LeftoverArg(arg)); + } + + #(#field_parsers_post)* + + Ok(Self { #( #field_names ),* }) + } + } + }; + + TokenStream::from(expanded) +} diff --git a/src/run/builtin.rs b/src/run/builtin.rs index c9456cd..366aaa5 100644 --- a/src/run/builtin.rs +++ b/src/run/builtin.rs @@ -3,10 +3,49 @@ use std::sync::{Arc, Mutex}; use std::{env::*, fs::OpenOptions, path::PathBuf}; +use pish_derive::FromArgs; + use super::{Builtin, BuiltinError as Error, BuiltinResult as Result}; use crate::parse::CmdDisplay; use crate::*; +pub enum ArgParseError<'a> { + LeftoverArg(&'a [u8]), + MissingArg(&'static str), + MissingArgValue(&'static str), + ArgValueParseError(&'static str, String), +} + +pub trait ArgParse: Sized { + fn parse<'a>(args: &'a [BString]) -> std::result::Result>; +} + +fn args(args: &[BString], w: &mut dyn Write) -> std::result::Result { + let err = match T::parse(args) { + Ok(t) => return Ok(t), + Err(e) => e, + }; + + match err { + ArgParseError::LeftoverArg(items) => { + w.write_all(b"leftover argument: ")?; + w.write_all(items)?; + w.write_all(b"\n")?; + } + ArgParseError::MissingArg(arg) => { + write!(w, "argument `{arg}` is missing\n")?; + } + ArgParseError::MissingArgValue(arg) => { + write!(w, "argument `{arg}` is missing its value\n")?; + } + ArgParseError::ArgValueParseError(arg, err) => { + write!(w, "failed to parse value of `{arg}`: {err}")?; + } + } + + Err(Error::Exit(-2)) +} + pub struct cd; impl Builtin for cd { fn name(&self) -> &str { @@ -206,6 +245,15 @@ impl Builtin for builtins { } } +#[derive(FromArgs)] +struct HistoryArgs { + local: bool, + + here: bool, + at: Option, + // TODO: temporal control, i.e. before & after +} + pub struct history; impl Builtin for history { fn name(&self) -> &str { diff --git a/src/run/mod.rs b/src/run/mod.rs index 0b9aef8..d386590 100644 --- a/src/run/mod.rs +++ b/src/run/mod.rs @@ -155,6 +155,7 @@ impl Executor { Ok(Err(e)) => match e { BuiltinError::IO(_) => code = -1, BuiltinError::Exit(c) => code = c, + BuiltinError::ParseError(_) => code = -2, }, Err(_) => code = 127, } @@ -290,6 +291,7 @@ pub fn run(se: Arc>, cmd: Vec) { #[derive(Debug)] pub enum BuiltinError { IO(std::io::Error), + ParseError(&'static str), Exit(i32), } -- cgit v1.2.3