From e3f3e3dcdc8936a505348ab2ca7673d27396e3ad Mon Sep 17 00:00:00 2001 From: pagedmov Date: Sat, 22 Mar 2025 20:10:47 -0400 Subject: [PATCH] Implemented an abstraction for extracting flags from builtins --- src/builtin/echo.rs | 55 +++++- src/fern.rs | 2 + src/getopt.rs | 89 ++++++++++ src/jobs.rs | 3 - src/parse/lex.rs | 3 - src/parse/mod.rs | 89 +++++++++- src/state.rs | 2 - src/tests/expand.rs | 16 +- src/tests/getopt.rs | 37 ++++ src/tests/mod.rs | 27 +++ src/tests/parser.rs | 21 +++ ...rn__tests__getopt__getopt_from_argv-2.snap | 12 ++ ...fern__tests__getopt__getopt_from_argv.snap | 26 +++ ...ests__getopt__getopt_multiple_short-2.snap | 15 ++ ..._tests__getopt__getopt_multiple_short.snap | 8 + .../fern__tests__getopt__getopt_simple-2.snap | 9 + .../fern__tests__getopt__getopt_simple.snap | 8 + .../fern__tests__parser__node_operation.snap | 162 ++++++++++++++++++ 18 files changed, 567 insertions(+), 17 deletions(-) create mode 100644 src/getopt.rs create mode 100644 src/tests/getopt.rs create mode 100644 src/tests/snapshots/fern__tests__getopt__getopt_from_argv-2.snap create mode 100644 src/tests/snapshots/fern__tests__getopt__getopt_from_argv.snap create mode 100644 src/tests/snapshots/fern__tests__getopt__getopt_multiple_short-2.snap create mode 100644 src/tests/snapshots/fern__tests__getopt__getopt_multiple_short.snap create mode 100644 src/tests/snapshots/fern__tests__getopt__getopt_simple-2.snap create mode 100644 src/tests/snapshots/fern__tests__getopt__getopt_simple.snap create mode 100644 src/tests/snapshots/fern__tests__parser__node_operation.snap diff --git a/src/builtin/echo.rs b/src/builtin/echo.rs index 89125f7..5b972b4 100644 --- a/src/builtin/echo.rs +++ b/src/builtin/echo.rs @@ -1,25 +1,70 @@ -use crate::{builtin::setup_builtin, jobs::{ChildProc, JobBldr}, libsh::error::ShResult, parse::{execute::prepare_argv, NdRule, Node}, prelude::*, procio::{borrow_fd, IoStack}, state}; +use std::sync::Arc; + +use crate::{builtin::setup_builtin, getopt::{get_opts_from_tokens, Opt, ECHO_OPTS}, jobs::{ChildProc, JobBldr}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{execute::prepare_argv, NdRule, Node}, prelude::*, procio::{borrow_fd, IoStack}, state}; + +bitflags! { + pub struct EchoFlags: u32 { + const NO_NEWLINE = 0b000001; + const USE_STDERR = 0b000010; + const USE_ESCAPE = 0b000100; + } +} pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { + let blame = node.get_span().clone(); let NdRule::Command { assignments: _, argv } = node.class else { unreachable!() }; assert!(!argv.is_empty()); - + let (argv,opts) = get_opts_from_tokens(argv); + let flags = get_echo_flags(opts).blame(blame)?; let (argv,io_frame) = setup_builtin(argv, job, Some((io_stack,node.redirs)))?; - let stdout = borrow_fd(STDOUT_FILENO); + let output_channel = if flags.contains(EchoFlags::USE_STDERR) { + borrow_fd(STDERR_FILENO) + } else { + borrow_fd(STDOUT_FILENO) + }; let mut echo_output = argv.into_iter() .map(|a| a.0) // Extract the String from the tuple of (String,Span) .collect::>() .join(" "); - echo_output.push('\n'); + if !flags.contains(EchoFlags::NO_NEWLINE) { + echo_output.push('\n') + } - write(stdout, echo_output.as_bytes())?; + write(output_channel, echo_output.as_bytes())?; io_frame.unwrap().restore()?; state::set_status(0); Ok(()) } + +pub fn get_echo_flags(mut opts: Vec) -> ShResult { + let mut flags = EchoFlags::empty(); + + while let Some(opt) = opts.pop() { + if !ECHO_OPTS.contains(&opt) { + return Err( + ShErr::simple( + ShErrKind::ExecFail, + format!("echo: Unexpected flag '{opt}'"), + ) + ) + } + let Opt::Short(opt) = opt else { + unreachable!() + }; + + match opt { + 'n' => flags |= EchoFlags::NO_NEWLINE, + 'r' => flags |= EchoFlags::USE_STDERR, + 'e' => flags |= EchoFlags::USE_ESCAPE, + _ => unreachable!() + } + } + + Ok(flags) +} diff --git a/src/fern.rs b/src/fern.rs index f29a460..509ea8c 100644 --- a/src/fern.rs +++ b/src/fern.rs @@ -10,10 +10,12 @@ pub mod jobs; pub mod signal; #[cfg(test)] pub mod tests; +pub mod getopt; use std::collections::HashSet; use expand::expand_aliases; +use getopt::get_opts; use libsh::error::ShResult; use parse::{execute::Dispatcher, lex::{LexFlags, LexStream, Tk}, Ast, ParseStream, ParsedSrc}; use procio::IoFrame; diff --git a/src/getopt.rs b/src/getopt.rs new file mode 100644 index 0000000..f170bbe --- /dev/null +++ b/src/getopt.rs @@ -0,0 +1,89 @@ +use std::{ops::Deref, str::FromStr, sync::{Arc, LazyLock}}; + +use fmt::Display; + +use crate::{libsh::error::ShResult, parse::lex::Tk, prelude::*}; + +type OptSet = Arc<[Opt]>; + +pub static ECHO_OPTS: LazyLock = LazyLock::new(|| {[ + Opt::Short('n'), + Opt::Short('E'), + Opt::Short('e'), + Opt::Short('r'), +].into()}); + +#[derive(Clone,PartialEq,Eq,Debug)] +pub enum Opt { + Long(String), + Short(char) +} + +impl Opt { + pub fn parse(s: &str) -> Vec { + flog!(DEBUG, s); + let mut opts = vec![]; + + if s.starts_with("--") { + opts.push(Opt::Long(s.trim_start_matches('-').to_string())) + } else if s.starts_with('-') { + let mut chars = s.trim_start_matches('-').chars(); + while let Some(ch) = chars.next() { + opts.push(Self::Short(ch)) + } + } + flog!(DEBUG,opts); + + opts + } +} + +impl Display for Opt { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Long(opt) => write!(f,"--{}",opt), + Self::Short(opt) => write!(f,"-{}",opt), + } + } +} + +pub fn get_opts(words: Vec) -> (Vec,Vec) { + let mut words_iter = words.into_iter(); + let mut opts = vec![]; + let mut non_opts = vec![]; + + while let Some(word) = words_iter.next() { + flog!(DEBUG, opts,non_opts); + if &word == "--" { + non_opts.extend(words_iter); + break + } + let parsed_opts = Opt::parse(&word); + if parsed_opts.is_empty() { + non_opts.push(word) + } else { + opts.extend(parsed_opts); + } + } + (non_opts,opts) +} + +pub fn get_opts_from_tokens(tokens: Vec) -> (Vec, Vec) { + let mut tokens_iter = tokens.into_iter(); + let mut opts = vec![]; + let mut non_opts = vec![]; + + while let Some(token) = tokens_iter.next() { + if &token.to_string() == "--" { + non_opts.extend(tokens_iter); + break + } + let parsed_opts = Opt::parse(&token.to_string()); + if parsed_opts.is_empty() { + non_opts.push(token) + } else { + opts.extend(parsed_opts); + } + } + (non_opts,opts) +} diff --git a/src/jobs.rs b/src/jobs.rs index ba5e142..bbd01e8 100644 --- a/src/jobs.rs +++ b/src/jobs.rs @@ -461,7 +461,6 @@ impl Job { if child.pid == Pid::this() { // TODO: figure out some way to get the exit code of builtins let code = state::get_status(); - flog!(DEBUG,code); stats.push(WtStat::Exited(child.pid, code)); continue } @@ -621,9 +620,7 @@ pub fn wait_fg(job: Job) -> ShResult<()> { attach_tty(job.pgid())?; disable_reaping()?; let statuses = write_jobs(|j| j.new_fg(job))?; - flog!(DEBUG,statuses); for status in statuses { - flog!(DEBUG,status); match status { WtStat::Exited(_, exit_code) => { code = exit_code; diff --git a/src/parse/lex.rs b/src/parse/lex.rs index 3fb052a..32ee57b 100644 --- a/src/parse/lex.rs +++ b/src/parse/lex.rs @@ -412,7 +412,6 @@ impl LexStream { subsh_tk.flags |= TkFlags::IS_SUBSH; self.cursor = pos; self.set_next_is_cmd(true); - flog!(DEBUG, subsh_tk); return Ok(subsh_tk) } '{' if pos == self.cursor && self.next_is_cmd() => { @@ -464,7 +463,6 @@ impl LexStream { } } let mut new_tk = self.get_token(self.cursor..pos, TkRule::Str); - flog!(DEBUG,new_tk); if self.in_quote && !self.flags.contains(LexFlags::LEX_UNFINISHED) { return Err( ShErr::full( @@ -509,7 +507,6 @@ impl LexStream { } } self.cursor = pos; - flog!(DEBUG, self.slice_from_cursor()); Ok(new_tk) } pub fn get_token(&self, range: Range, class: TkRule) -> Tk { diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 896f109..127ea4d 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -42,7 +42,6 @@ impl ParsedSrc { for token in LexStream::new(self.src.clone(), LexFlags::empty()) { tokens.push(token?); } - flog!(DEBUG,tokens); let mut nodes = vec![]; for result in ParseStream::new(tokens) { @@ -1130,3 +1129,91 @@ fn is_func_name(tk: Option<&Tk>) -> bool { (tk.span.as_str().ends_with("()") && !tk.span.as_str().ends_with("\\()")) }) } + +/// Perform an operation on the child nodes of a given node +/// +/// # Parameters +/// node: A mutable reference to a node to be operated on +/// filter: A closure or function which checks an attribute of a child node and returns a boolean +/// operation: The closure or function to apply to a child node which matches on the filter +/// +/// Very useful for testing, i.e. needing to extract specific types of nodes from the AST to inspect values +pub fn node_operation(node: &mut Node, filter: &F1, operation: &mut F2) + where + F1: Fn(&Node) -> bool, + F2: FnMut(&mut Node) +{ + let check_node = |node: &mut Node, filter: &F1, operation: &mut F2| { + if filter(&node) { + operation(node); + } else { + node_operation::(node, filter, operation); + } + }; + + if filter(node) { + operation(node); + } + + match node.class { + NdRule::IfNode { ref mut cond_nodes, ref mut else_block } => { + for node in cond_nodes { + let CondNode { cond, body } = node; + check_node(cond,filter,operation); + for body_node in body { + check_node(body_node,filter,operation); + } + } + + for else_node in else_block { + check_node(else_node,filter,operation); + } + + } + NdRule::LoopNode { kind: _, ref mut cond_node } => { + let CondNode { cond, body } = cond_node; + check_node(cond,filter,operation); + for body_node in body { + check_node(body_node,filter,operation); + } + } + NdRule::ForNode { vars: _, arr: _, ref mut body } => { + for body_node in body { + check_node(body_node,filter,operation); + } + } + NdRule::CaseNode { pattern: _, ref mut case_blocks } => { + for block in case_blocks { + let CaseNode { pattern: _, body } = block; + for body_node in body { + check_node(body_node,filter,operation); + } + } + } + NdRule::Command { ref mut assignments, argv: _ } => { + for assign_node in assignments { + check_node(assign_node,filter,operation); + } + } + NdRule::Pipeline { ref mut cmds, pipe_err: _ } => { + for cmd_node in cmds { + check_node(cmd_node,filter,operation); + } + } + NdRule::Conjunction { ref mut elements } => { + for node in elements.iter_mut() { + let ConjunctNode { cmd, operator: _ } = node; + check_node(cmd,filter,operation); + } + } + NdRule::Assignment { kind: _, var: _, val: _ } => return, // No nodes to check + NdRule::BraceGrp { ref mut body } => { + for body_node in body { + check_node(body_node,filter,operation); + } + } + NdRule::FuncDef { name: _, ref mut body } => { + check_node(body,filter,operation) + } + } +} diff --git a/src/state.rs b/src/state.rs index c66ae8b..b8a0bfa 100644 --- a/src/state.rs +++ b/src/state.rs @@ -317,8 +317,6 @@ pub fn get_status() -> i32 { } #[track_caller] pub fn set_status(code: i32) { - flog!(DEBUG,std::panic::Location::caller()); - flog!(DEBUG,code); write_vars(|v| v.set_param('?', &code.to_string())) } diff --git a/src/tests/expand.rs b/src/tests/expand.rs index f006a2d..0d873d6 100644 --- a/src/tests/expand.rs +++ b/src/tests/expand.rs @@ -17,7 +17,7 @@ fn simple_expansion() { let var_tk = tokens.pop().unwrap(); let var_span = var_tk.span.clone(); - let exp_tk = var_tk.expand(var_span, TkFlags::empty()); + let exp_tk = var_tk.expand(var_span, TkFlags::empty()).unwrap(); write_vars(|v| v.vars_mut().clear()); insta::assert_debug_snapshot!(exp_tk.get_words()) } @@ -62,6 +62,16 @@ fn expand_multiple_aliases() { assert_eq!(result.as_str(),"echo foo; echo bar; echo biz") } +#[test] +fn alias_in_arg_position() { + write_logic(|l| l.insert_alias("foo", "echo foo")); + + let input = String::from("echo foo"); + + let result = expand_aliases(input.clone(), HashSet::new()); + assert_eq!(input,result) +} + #[test] fn expand_recursive_alias() { write_logic(|l| l.insert_alias("foo", "echo foo")); @@ -74,9 +84,9 @@ fn expand_recursive_alias() { #[test] fn test_infinite_recursive_alias() { - write_logic(|l| l.insert_alias("foo", "foo")); + write_logic(|l| l.insert_alias("foo", "foo bar")); let input = String::from("foo"); let result = expand_aliases(input, HashSet::new()); - assert_eq!(result.as_str(),"foo") + assert_eq!(result.as_str(),"foo bar") } diff --git a/src/tests/getopt.rs b/src/tests/getopt.rs new file mode 100644 index 0000000..7545472 --- /dev/null +++ b/src/tests/getopt.rs @@ -0,0 +1,37 @@ +use getopt::get_opts_from_tokens; +use parse::NdRule; +use tests::get_nodes; + +use super::super::*; + +#[test] +fn getopt_from_argv() { + let node = get_nodes("echo -n -e foo", |node| matches!(node.class, NdRule::Command {..})) + .pop() + .unwrap(); + let NdRule::Command { assignments, argv } = node.class else { + panic!() + }; + + let (words,opts) = get_opts_from_tokens(argv); + insta::assert_debug_snapshot!(words); + insta::assert_debug_snapshot!(opts) +} + +#[test] +fn getopt_simple() { + let raw = "echo -n foo".split_whitespace().map(|s| s.to_string()).collect::>(); + + let (words,opts) = get_opts(raw); + insta::assert_debug_snapshot!(words); + insta::assert_debug_snapshot!(opts); +} + +#[test] +fn getopt_multiple_short() { + let raw = "echo -nre foo".split_whitespace().map(|s| s.to_string()).collect::>(); + + let (words,opts) = get_opts(raw); + insta::assert_debug_snapshot!(words); + insta::assert_debug_snapshot!(opts); +} diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 00ecc9d..5f2184f 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -1,5 +1,32 @@ +use std::rc::Rc; + +use crate::parse::{lex::{LexFlags, LexStream}, node_operation, Node, ParseStream}; + pub mod lexer; pub mod parser; pub mod expand; pub mod term; pub mod error; +pub mod getopt; + +/// Unsafe to use outside of tests +pub fn get_nodes(input: &str, filter: F1) -> Vec + where + F1: Fn(&Node) -> bool +{ + let mut nodes = vec![]; + let tokens = LexStream::new(Rc::new(input.into()), LexFlags::empty()) + .map(|tk| tk.unwrap()) + .collect::>(); + let mut parsed_nodes = ParseStream::new(tokens) + .map(|nd| nd.unwrap()) + .collect::>(); + + for node in parsed_nodes.iter_mut() { + node_operation(node, + &filter, + &mut |node: &mut Node| nodes.push(node.clone()) + ); + } + nodes +} diff --git a/src/tests/parser.rs b/src/tests/parser.rs index 165be39..7d880ef 100644 --- a/src/tests/parser.rs +++ b/src/tests/parser.rs @@ -1,3 +1,5 @@ +use parse::{node_operation, NdRule, Node}; + use super::super::*; #[test] @@ -196,3 +198,22 @@ fn parse_cursed() { // 15,000 line snapshot file btw insta::assert_debug_snapshot!(nodes) } +#[test] +fn test_node_operation() { + let input = String::from("echo hello world; echo foo bar"); + let mut check_nodes = vec![]; + let mut tokens: Vec = LexStream::new(input.into(), LexFlags::empty()) + .map(|tk| tk.unwrap()) + .collect(); + + let nodes = ParseStream::new(tokens) + .map(|nd| nd.unwrap()); + + for mut node in nodes { + node_operation(&mut node, + &|node: &Node| matches!(node.class, NdRule::Command {..}), + &mut |node: &mut Node| check_nodes.push(node.clone()), + ); + } + insta::assert_debug_snapshot!(check_nodes) +} diff --git a/src/tests/snapshots/fern__tests__getopt__getopt_from_argv-2.snap b/src/tests/snapshots/fern__tests__getopt__getopt_from_argv-2.snap new file mode 100644 index 0000000..4314c4a --- /dev/null +++ b/src/tests/snapshots/fern__tests__getopt__getopt_from_argv-2.snap @@ -0,0 +1,12 @@ +--- +source: src/tests/getopt.rs +expression: opts +--- +[ + Short( + 'n', + ), + Short( + 'e', + ), +] diff --git a/src/tests/snapshots/fern__tests__getopt__getopt_from_argv.snap b/src/tests/snapshots/fern__tests__getopt__getopt_from_argv.snap new file mode 100644 index 0000000..4b51fdb --- /dev/null +++ b/src/tests/snapshots/fern__tests__getopt__getopt_from_argv.snap @@ -0,0 +1,26 @@ +--- +source: src/tests/getopt.rs +expression: words +--- +[ + Tk { + class: Str, + span: Span { + range: 0..4, + source: "echo -n -e foo", + }, + flags: TkFlags( + IS_CMD | BUILTIN, + ), + }, + Tk { + class: Str, + span: Span { + range: 11..14, + source: "echo -n -e foo", + }, + flags: TkFlags( + 0x0, + ), + }, +] diff --git a/src/tests/snapshots/fern__tests__getopt__getopt_multiple_short-2.snap b/src/tests/snapshots/fern__tests__getopt__getopt_multiple_short-2.snap new file mode 100644 index 0000000..1a46305 --- /dev/null +++ b/src/tests/snapshots/fern__tests__getopt__getopt_multiple_short-2.snap @@ -0,0 +1,15 @@ +--- +source: src/tests/getopt.rs +expression: opts +--- +[ + Short( + 'n', + ), + Short( + 'r', + ), + Short( + 'e', + ), +] diff --git a/src/tests/snapshots/fern__tests__getopt__getopt_multiple_short.snap b/src/tests/snapshots/fern__tests__getopt__getopt_multiple_short.snap new file mode 100644 index 0000000..98bb71f --- /dev/null +++ b/src/tests/snapshots/fern__tests__getopt__getopt_multiple_short.snap @@ -0,0 +1,8 @@ +--- +source: src/tests/getopt.rs +expression: words +--- +[ + "echo", + "foo", +] diff --git a/src/tests/snapshots/fern__tests__getopt__getopt_simple-2.snap b/src/tests/snapshots/fern__tests__getopt__getopt_simple-2.snap new file mode 100644 index 0000000..fd6335c --- /dev/null +++ b/src/tests/snapshots/fern__tests__getopt__getopt_simple-2.snap @@ -0,0 +1,9 @@ +--- +source: src/tests/getopt.rs +expression: opts +--- +[ + Short( + 'n', + ), +] diff --git a/src/tests/snapshots/fern__tests__getopt__getopt_simple.snap b/src/tests/snapshots/fern__tests__getopt__getopt_simple.snap new file mode 100644 index 0000000..98bb71f --- /dev/null +++ b/src/tests/snapshots/fern__tests__getopt__getopt_simple.snap @@ -0,0 +1,8 @@ +--- +source: src/tests/getopt.rs +expression: words +--- +[ + "echo", + "foo", +] diff --git a/src/tests/snapshots/fern__tests__parser__node_operation.snap b/src/tests/snapshots/fern__tests__parser__node_operation.snap new file mode 100644 index 0000000..f1f46d5 --- /dev/null +++ b/src/tests/snapshots/fern__tests__parser__node_operation.snap @@ -0,0 +1,162 @@ +--- +source: src/tests/parser.rs +expression: check_nodes +--- +[ + Node { + class: Command { + assignments: [], + argv: [ + Tk { + class: Str, + span: Span { + range: 0..4, + source: "echo hello world; echo foo bar", + }, + flags: TkFlags( + IS_CMD | BUILTIN, + ), + }, + Tk { + class: Str, + span: Span { + range: 5..10, + source: "echo hello world; echo foo bar", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Str, + span: Span { + range: 11..16, + source: "echo hello world; echo foo bar", + }, + flags: TkFlags( + 0x0, + ), + }, + ], + }, + flags: NdFlags( + 0x0, + ), + redirs: [], + tokens: [ + Tk { + class: Str, + span: Span { + range: 0..4, + source: "echo hello world; echo foo bar", + }, + flags: TkFlags( + IS_CMD | BUILTIN, + ), + }, + Tk { + class: Str, + span: Span { + range: 5..10, + source: "echo hello world; echo foo bar", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Str, + span: Span { + range: 11..16, + source: "echo hello world; echo foo bar", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Sep, + span: Span { + range: 16..18, + source: "echo hello world; echo foo bar", + }, + flags: TkFlags( + 0x0, + ), + }, + ], + }, + Node { + class: Command { + assignments: [], + argv: [ + Tk { + class: Str, + span: Span { + range: 18..22, + source: "echo hello world; echo foo bar", + }, + flags: TkFlags( + IS_CMD | BUILTIN, + ), + }, + Tk { + class: Str, + span: Span { + range: 23..26, + source: "echo hello world; echo foo bar", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Str, + span: Span { + range: 27..30, + source: "echo hello world; echo foo bar", + }, + flags: TkFlags( + 0x0, + ), + }, + ], + }, + flags: NdFlags( + 0x0, + ), + redirs: [], + tokens: [ + Tk { + class: Str, + span: Span { + range: 18..22, + source: "echo hello world; echo foo bar", + }, + flags: TkFlags( + IS_CMD | BUILTIN, + ), + }, + Tk { + class: Str, + span: Span { + range: 23..26, + source: "echo hello world; echo foo bar", + }, + flags: TkFlags( + 0x0, + ), + }, + Tk { + class: Str, + span: Span { + range: 27..30, + source: "echo hello world; echo foo bar", + }, + flags: TkFlags( + 0x0, + ), + }, + ], + }, +]