diff --git a/Cargo.lock b/Cargo.lock index 8d53151..5d33eb4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -236,6 +236,12 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + [[package]] name = "jiff" version = "0.2.20" @@ -399,6 +405,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" @@ -419,6 +434,19 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + [[package]] name = "shed" version = "0.3.0" @@ -432,6 +460,7 @@ dependencies = [ "nix", "pretty_assertions", "regex", + "serde_json", "tempfile", "unicode-segmentation", "unicode-width", @@ -616,3 +645,9 @@ name = "yansi" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index 15e27e2..7cbffec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ glob = "0.3.2" log = "0.4.29" nix = { version = "0.29.0", features = ["uio", "term", "user", "hostname", "fs", "default", "signal", "process", "event", "ioctl", "poll"] } regex = "1.11.1" +serde_json = "1.0.149" unicode-segmentation = "1.12.0" unicode-width = "0.2.0" vte = "0.15" diff --git a/src/builtin/arrops.rs b/src/builtin/arrops.rs new file mode 100644 index 0000000..bd3f882 --- /dev/null +++ b/src/builtin/arrops.rs @@ -0,0 +1,188 @@ +use crate::{ + getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node}, prelude::*, procio::{IoStack, borrow_fd}, state::{self, VarFlags, VarKind, write_vars} +}; + +use super::setup_builtin; + +fn arr_op_optspec() -> Vec { + vec![ + OptSpec { + opt: Opt::Short('c'), + takes_arg: true + }, + OptSpec { + opt: Opt::Short('r'), + takes_arg: false + }, + OptSpec { + opt: Opt::Short('v'), + takes_arg: true + } + ] +} + +pub struct ArrOpOpts { + count: usize, + reverse: bool, + var: Option, +} + +impl Default for ArrOpOpts { + fn default() -> Self { + Self { + count: 1, + reverse: false, + var: None, + } + } +} + +#[derive(Clone, Copy)] +enum End { Front, Back } + +fn arr_pop_inner(node: Node, io_stack: &mut IoStack, job: &mut JobBldr, end: End) -> ShResult<()> { + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; + + let (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?; + let arr_op_opts = get_arr_op_opts(opts)?; + let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let stdout = borrow_fd(STDOUT_FILENO); + let mut status = 0; + + for (arg,_) in argv { + for _ in 0..arr_op_opts.count { + let pop = |arr: &mut std::collections::VecDeque| match end { + End::Front => arr.pop_front(), + End::Back => arr.pop_back(), + }; + let Some(popped) = write_vars(|v| v.get_arr_mut(&arg).ok().and_then(pop)) else { + status = 1; + break; + }; + status = 0; + + if let Some(ref var) = arr_op_opts.var { + write_vars(|v| v.set_var(var, VarKind::Str(popped), VarFlags::NONE))?; + } else { + write(stdout, popped.as_bytes())?; + write(stdout, b"\n")?; + } + } + } + + state::set_status(status); + Ok(()) +} + +fn arr_push_inner(node: Node, io_stack: &mut IoStack, job: &mut JobBldr, end: End) -> ShResult<()> { + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; + + let (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?; + let _arr_op_opts = get_arr_op_opts(opts)?; + let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + + let mut argv = argv.into_iter(); + let Some((name, _)) = argv.next() else { + return Err(ShErr::simple(ShErrKind::ExecFail, "push: missing array name".to_string())); + }; + + for (val, _) in argv { + let push_val = val.clone(); + if let Err(e) = write_vars(|v| v.get_arr_mut(&name).map(|arr| match end { + End::Front => arr.push_front(push_val), + End::Back => arr.push_back(push_val), + })) { + state::set_status(1); + return Err(e); + }; + } + + state::set_status(0); + Ok(()) +} + +pub fn arr_pop(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { + arr_pop_inner(node, io_stack, job, End::Back) +} + +pub fn arr_fpop(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { + arr_pop_inner(node, io_stack, job, End::Front) +} + +pub fn arr_push(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { + arr_push_inner(node, io_stack, job, End::Back) +} + +pub fn arr_fpush(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { + arr_push_inner(node, io_stack, job, End::Front) +} + +pub fn arr_rotate(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; + + let (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?; + let arr_op_opts = get_arr_op_opts(opts)?; + let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + + for (arg, _) in argv { + write_vars(|v| -> ShResult<()> { + let arr = v.get_arr_mut(&arg)?; + if arr_op_opts.reverse { + arr.rotate_right(arr_op_opts.count.min(arr.len())); + } else { + arr.rotate_left(arr_op_opts.count.min(arr.len())); + } + Ok(()) + })?; + } + + state::set_status(0); + Ok(()) +} + +pub fn get_arr_op_opts(opts: Vec) -> ShResult { + let mut arr_op_opts = ArrOpOpts::default(); + for opt in opts { + match opt { + Opt::ShortWithArg('c', count) => { + arr_op_opts.count = count.parse::().map_err(|_| { + ShErr::simple(ShErrKind::ParseErr, format!("invalid count: {}", count)) + })?; + } + Opt::Short('c') => { + return Err(ShErr::simple(ShErrKind::ParseErr, "missing count for -c".to_string())); + } + Opt::Short('r') => { + arr_op_opts.reverse = true; + } + Opt::ShortWithArg('v', var) => { + arr_op_opts.var = Some(var); + } + Opt::Short('v') => { + return Err(ShErr::simple(ShErrKind::ParseErr, "missing variable name for -v".to_string())); + } + _ => { + return Err(ShErr::simple(ShErrKind::ParseErr, format!("invalid option: {}", opt))); + } + } + } + Ok(arr_op_opts) +} diff --git a/src/builtin/map.rs b/src/builtin/map.rs new file mode 100644 index 0000000..4bad1ab --- /dev/null +++ b/src/builtin/map.rs @@ -0,0 +1,369 @@ +use std::collections::HashMap; + +use bitflags::bitflags; +use nix::{libc::STDOUT_FILENO, unistd::write}; +use serde_json::{Map, Value}; + +use crate::{ + getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node, lex::{split_all_unescaped, split_at_unescaped}}, procio::{IoStack, borrow_fd}, state::{self, read_vars, write_vars} +}; + +#[derive(Debug, Clone)] +pub enum MapNode { + Leaf(String), + Array(Vec), + Branch(HashMap), +} + +impl Default for MapNode { + fn default() -> Self { + Self::Branch(HashMap::new()) + } +} + +impl From for serde_json::Value { + fn from(val: MapNode) -> Self { + match val { + MapNode::Branch(map) => { + let val_map = map.into_iter() + .map(|(k,v)| { + (k,v.into()) + }) + .collect::>(); + + Value::Object(val_map) + } + MapNode::Array(nodes) => { + let arr = nodes + .into_iter() + .map(|node| node.into()) + .collect(); + Value::Array(arr) + } + MapNode::Leaf(leaf) => { + Value::String(leaf) + } + } + } +} + +impl From for MapNode { + fn from(value: Value) -> Self { + match value { + Value::Object(map) => { + let node_map = map.into_iter() + .map(|(k,v)| { + (k, v.into()) + }) + .collect::>(); + + MapNode::Branch(node_map) + } + Value::Array(arr) => { + let nodes = arr + .into_iter() + .map(|v| v.into()) + .collect(); + MapNode::Array(nodes) + } + Value::String(s) => MapNode::Leaf(s), + v => MapNode::Leaf(v.to_string()) + } + } +} + +impl MapNode { + fn get(&self, path: &[String]) -> Option<&MapNode> { + match path { + [] => Some(self), + [key, rest @ ..] => match self { + MapNode::Leaf(_) => None, + MapNode::Array(map_nodes) => { + let idx: usize = key.parse().ok()?; + map_nodes.get(idx)?.get(rest) + } + MapNode::Branch(map) => map.get(key)?.get(rest) + } + } + } + + fn set(&mut self, path: &[String], value: MapNode) { + match path { + [] => *self = value, + [key, rest @ ..] => { + if matches!(self, MapNode::Leaf(_)) { + // promote leaf to branch if we still have path left to traverse + *self = Self::default(); + } + match self { + MapNode::Branch(map) => { + let child = map + .entry(key.to_string()) + .or_insert_with(Self::default); + child.set(rest, value); + } + MapNode::Array(map_nodes) => { + let idx: usize = key.parse().expect("expected array index"); + if idx >= map_nodes.len() { + map_nodes.resize(idx + 1, Self::default()); + } + map_nodes[idx].set(rest, value); + } + _ => unreachable!() + } + } + } + } + + fn remove(&mut self, path: &[String]) -> Option { + match path { + [] => None, + [key] => match self { + MapNode::Branch(map) => map.remove(key), + MapNode::Array(nodes) => { + let idx: usize = key.parse().ok()?; + if idx >= nodes.len() { + return None; + } + Some(nodes.remove(idx)) + } + _ => None + } + [key, rest @ ..] => match self { + MapNode::Branch(map) => map.get_mut(key)?.remove(rest), + MapNode::Array(nodes) => { + let idx: usize = key.parse().ok()?; + if idx >= nodes.len() { + return None; + } + nodes[idx].remove(rest) + } + _ => None + } + } + } + + fn keys(&self) -> Vec { + match self { + MapNode::Branch(map) => map.keys().map(|k| k.to_string()).collect(), + MapNode::Array(nodes) => nodes.iter().filter_map(|n| n.display(false, false).ok()).collect(), + MapNode::Leaf(s) => vec![s.clone()], + } + } + + fn display(&self, json: bool, pretty: bool) -> ShResult { + if json || matches!(self, MapNode::Branch(_)) { + let val: Value = self.clone().into(); + if pretty { + match serde_json::to_string_pretty(&val) { + Ok(s) => Ok(s), + Err(e) => Err(ShErr::simple( + ShErrKind::InternalErr, + format!("failed to serialize map: {e}") + )) + } + } else { + match serde_json::to_string(&val) { + Ok(s) => Ok(s), + Err(e) => Err(ShErr::simple( + ShErrKind::InternalErr, + format!("failed to serialize map: {e}") + )) + } + } + } else { + match self { + MapNode::Leaf(leaf) => Ok(leaf.clone()), + MapNode::Array(nodes) => { + let mut s = String::new(); + for node in nodes { + let display = node.display(json, pretty)?; + if matches!(node, MapNode::Branch(_)) { + s.push_str(&format!("'{}'", display)); + } else { + s.push_str(&node.display(json, pretty)?); + } + s.push('\n'); + } + Ok(s.trim_end_matches('\n').to_string()) + } + _ => unreachable!() + } + } + } +} + +use super::setup_builtin; + +fn map_opts_spec() -> [OptSpec; 5] { + [ + OptSpec { + opt: Opt::Short('r'), + takes_arg: false + }, + OptSpec { + opt: Opt::Short('j'), + takes_arg: false + }, + OptSpec { + opt: Opt::Short('k'), + takes_arg: false + }, + OptSpec { + opt: Opt::Long("pretty".into()), + takes_arg: false + }, + OptSpec { + opt: Opt::Short('l'), + takes_arg: false + }, + ] +} + +#[derive(Debug, Clone, Copy)] +pub struct MapOpts { + flags: MapFlags, +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct MapFlags: u32 { + const REMOVE = 0b000001; + const KEYS = 0b000010; + const JSON = 0b000100; + const LOCAL = 0b001000; + const PRETTY = 0b010000; + } +} + +pub fn map(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; + + let (argv, opts) = get_opts_from_tokens(argv, &map_opts_spec())?; + let map_opts = get_map_opts(opts); + let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + + for (arg,_) in argv { + if let Some((lhs,rhs)) = split_at_unescaped(&arg, "=") { + let path = split_all_unescaped(&lhs, "."); + let Some(name) = path.first() else { + return Err(ShErr::simple( + ShErrKind::InternalErr, + format!("invalid map path: {}", lhs) + )); + }; + + let is_json = map_opts.flags.contains(MapFlags::JSON); + let found = write_vars(|v| { + if let Some(map) = v.get_map_mut(name) { + if is_json { + if let Ok(parsed) = serde_json::from_str::(&rhs) { + map.set(&path[1..], parsed.into()); + } else { + map.set(&path[1..], MapNode::Leaf(rhs.clone())); + } + } else { + map.set(&path[1..], MapNode::Leaf(rhs.clone())); + } + true + } else { + false + } + }); + + if !found { + let mut new = MapNode::default(); + if is_json && let Ok(parsed) = serde_json::from_str::(&rhs) { + let node: MapNode = parsed.into(); + new.set(&path[1..], node); + } else { + new.set(&path[1..], MapNode::Leaf(rhs)); + } + write_vars(|v| v.set_map(name, new, map_opts.flags.contains(MapFlags::LOCAL))); + } + } else { + let path = split_all_unescaped(&arg, "."); + let Some(name) = path.first() else { + return Err(ShErr::simple( + ShErrKind::InternalErr, + format!("invalid map path: {}", &arg) + )); + }; + + if map_opts.flags.contains(MapFlags::REMOVE) { + write_vars(|v| { + if path.len() == 1 { + v.remove_map(name); + } else { + let Some(map) = v.get_map_mut(name) else { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("map not found: {}", name) + )); + }; + map.remove(&path[1..]); + } + + Ok(()) + })?; + continue; + } + + let json = map_opts.flags.contains(MapFlags::JSON); + let pretty = map_opts.flags.contains(MapFlags::PRETTY); + let keys = map_opts.flags.contains(MapFlags::KEYS); + let has_map = read_vars(|v| v.get_map(name).is_some()); + if !has_map { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("map not found: {}", name) + )); + } + let Some(output) = read_vars(|v| { + v.get_map(name) + .and_then(|map| map.get(&path[1..]) + .and_then(|n| { + if keys { + Some(n.keys().join(" ")) + } else { + n.display(json, pretty).ok() + } + })) + }) else { + state::set_status(1); + continue; + }; + + let stdout = borrow_fd(STDOUT_FILENO); + write(stdout, output.as_bytes())?; + write(stdout, b"\n")?; + } + } + + state::set_status(0); + Ok(()) +} + +pub fn get_map_opts(opts: Vec) -> MapOpts { + let mut map_opts = MapOpts { + flags: MapFlags::empty() + }; + + for opt in opts { + match opt { + Opt::Short('r') => map_opts.flags |= MapFlags::REMOVE, + Opt::Short('j') => map_opts.flags |= MapFlags::JSON, + Opt::Short('k') => map_opts.flags |= MapFlags::KEYS, + Opt::Short('l') => map_opts.flags |= MapFlags::LOCAL, + Opt::Long(ref s) if s == "pretty" => map_opts.flags |= MapFlags::PRETTY, + _ => unreachable!() + } + } + map_opts +} diff --git a/src/builtin/mod.rs b/src/builtin/mod.rs index c85a8d6..8a7ef10 100644 --- a/src/builtin/mod.rs +++ b/src/builtin/mod.rs @@ -30,12 +30,14 @@ pub mod test; // [[ ]] thing pub mod trap; pub mod varcmds; pub mod zoltraak; +pub mod map; +pub mod arrops; -pub const BUILTINS: [&str; 35] = [ +pub const BUILTINS: [&str; 41] = [ "echo", "cd", "read", "export", "local", "pwd", "source", "shift", "jobs", "fg", "bg", "disown", "alias", "unalias", "return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly", - "unset", "complete", "compgen", + "unset", "complete", "compgen", "map", "pop", "fpop", "push", "fpush", "rotate" ]; /// Sets up a builtin command diff --git a/src/parse/execute.rs b/src/parse/execute.rs index 26b5747..e7e711b 100644 --- a/src/parse/execute.rs +++ b/src/parse/execute.rs @@ -5,23 +5,7 @@ use std::{ use crate::{ builtin::{ - alias::{alias, unalias}, - cd::cd, - complete::{compgen_builtin, complete_builtin}, - dirstack::{dirs, popd, pushd}, - echo::echo, - eval, exec, - flowctl::flowctl, - jobctl::{JobBehavior, continue_job, disown, jobs}, - pwd::pwd, - read::read_builtin, - shift::shift, - shopt::shopt, - source::source, - test::double_bracket_test, - trap::{TrapTarget, trap}, - varcmds::{export, local, readonly, unset}, - zoltraak::zoltraak, + alias::{alias, unalias}, arrops::{arr_pop, arr_fpop, arr_push, arr_fpush, arr_rotate}, cd::cd, complete::{compgen_builtin, complete_builtin}, dirstack::{dirs, popd, pushd}, echo::echo, eval, exec, flowctl::flowctl, jobctl::{JobBehavior, continue_job, disown, jobs}, map, pwd::pwd, read::read_builtin, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, varcmds::{export, local, readonly, unset}, zoltraak::zoltraak }, expand::{expand_aliases, glob_to_regex}, jobs::{ChildProc, JobStack, dispatch_job}, @@ -817,6 +801,12 @@ impl Dispatcher { "unset" => unset(cmd, io_stack_mut, curr_job_mut), "complete" => complete_builtin(cmd, io_stack_mut, curr_job_mut), "compgen" => compgen_builtin(cmd, io_stack_mut, curr_job_mut), + "map" => map::map(cmd, io_stack_mut, curr_job_mut), + "pop" => arr_pop(cmd, io_stack_mut, curr_job_mut), + "fpop" => arr_fpop(cmd, io_stack_mut, curr_job_mut), + "push" => arr_push(cmd, io_stack_mut, curr_job_mut), + "fpush" => arr_fpush(cmd, io_stack_mut, curr_job_mut), + "rotate" => arr_rotate(cmd, io_stack_mut, curr_job_mut), "true" | ":" => { state::set_status(0); Ok(()) diff --git a/src/parse/lex.rs b/src/parse/lex.rs index 0ef1edb..77c31a5 100644 --- a/src/parse/lex.rs +++ b/src/parse/lex.rs @@ -912,6 +912,35 @@ pub fn ends_with_unescaped(slice: &str, pat: &str) -> bool { slice.ends_with(pat) && !pos_is_escaped(slice, slice.len() - pat.len()) } +pub fn split_all_unescaped(slice: &str, pat: &str) -> Vec { + let mut cursor = 0; + let mut splits = vec![]; + while let Some(split) = split_at_unescaped(&slice[cursor..], pat) { + cursor += split.0.len() + pat.len(); + splits.push(split.0); + } + if let Some(remaining) = slice.get(cursor..) { + splits.push(remaining.to_string()); + } + splits +} + +pub fn split_at_unescaped(slice: &str, pat: &str) -> Option<(String,String)> { + let mut window_start = 0; + let mut window_end = pat.len(); + if window_end > slice.len() { + return None; + } + while window_end <= slice.len() { + if &slice[window_start..window_end] == pat && !pos_is_escaped(slice, window_start) { + return Some((slice[..window_start].to_string(), slice[window_end..].to_string())); + } + window_start += 1; + window_end += 1; + } + None +} + pub fn pos_is_escaped(slice: &str, pos: usize) -> bool { let bytes = slice.as_bytes(); let mut escaped = false; diff --git a/src/parse/mod.rs b/src/parse/mod.rs index f745793..8d6d7a1 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -955,6 +955,7 @@ impl ParseStream { }; case_blocks.push(case_node); + self.catch_separator(&mut node_tks); if self.check_keyword("esac") { node_tks.push(self.next_tk().unwrap()); self.assert_separator(&mut node_tks)?; @@ -1057,6 +1058,7 @@ impl ParseStream { } } + self.catch_separator(&mut node_tks); if !self.check_keyword("fi") || !self.next_tk_is_some() { self.panic_mode(&mut node_tks); return Err(parse_err_full( @@ -1139,6 +1141,7 @@ impl ParseStream { body.push(node) } + self.catch_separator(&mut node_tks); if !self.check_keyword("done") || !self.next_tk_is_some() { self.panic_mode(&mut node_tks); return Err(parse_err_full( @@ -1210,6 +1213,7 @@ impl ParseStream { )); }; + self.catch_separator(&mut node_tks); if !self.check_keyword("done") || !self.next_tk_is_some() { self.panic_mode(&mut node_tks); return Err(parse_err_full( diff --git a/src/readline/linebuf.rs b/src/readline/linebuf.rs index bb8a633..7d3fd15 100644 --- a/src/readline/linebuf.rs +++ b/src/readline/linebuf.rs @@ -1947,13 +1947,23 @@ impl LineBuf { return; }; let mut level: usize = 0; + let mut last_keyword: Option = None; for tk in tokens { if tk.flags.contains(TkFlags::KEYWORD) { match tk.as_str() { - "then" | "do" | "in" => level += 1, + "in" => { + if last_keyword.as_deref() == Some("case") { + level += 1; + } else { + // 'in' is also used in for loops, but we already increment level on 'do' for those + // so we just skip it here + } + } + "then" | "do" => level += 1, "done" | "fi" | "esac" => level = level.saturating_sub(1), _ => { /* Continue */ } } + last_keyword = Some(tk.to_string()); } else if tk.class == TkRule::BraceGrpStart { level += 1; } else if tk.class == TkRule::BraceGrpEnd { diff --git a/src/state.rs b/src/state.rs index b893470..f610bff 100644 --- a/src/state.rs +++ b/src/state.rs @@ -11,7 +11,7 @@ use std::{ use nix::unistd::{User, gethostname, getppid}; use crate::{ - builtin::{BUILTINS, trap::TrapTarget}, + builtin::{BUILTINS, map::MapNode, trap::TrapTarget}, exec_input, jobs::JobTab, libsh::{ @@ -257,11 +257,7 @@ impl ScopeStack { { match var.kind() { VarKind::Arr(items) => { - let mut item_vec = items.clone().into_iter().collect::>(); - - item_vec.sort_by_key(|(idx, _)| *idx); // sort by index - - return Ok(item_vec.into_iter().map(|(_, s)| s).collect()); + return Ok(items.iter().cloned().collect()); } _ => { return Err(ShErr::simple( @@ -277,6 +273,25 @@ impl ScopeStack { format!("Variable '{}' not found", var_name), )) } + pub fn get_arr_mut(&mut self, var_name: &str) -> ShResult<&mut VecDeque> { + for scope in self.scopes.iter_mut().rev() { + if scope.var_exists(var_name) + && let Some(var) = scope.vars_mut().get_mut(var_name) + { + match var.kind_mut() { + VarKind::Arr(items) => return Ok(items), + _ => return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("Variable '{}' is not an array", var_name), + )), + } + } + } + Err(ShErr::simple( + ShErrKind::ExecFail, + format!("Variable '{}' not found", var_name), + )) + } pub fn index_var(&self, var_name: &str, idx: ArrIndex) -> ShResult { for scope in self.scopes.iter().rev() { if scope.var_exists(var_name) @@ -304,7 +319,7 @@ impl ScopeStack { } }; - if let Some(item) = items.get(&idx) { + if let Some(item) = items.get(idx) { return Ok(item.clone()); } else { return Err(ShErr::simple( @@ -324,6 +339,38 @@ impl ScopeStack { } Ok("".into()) } + pub fn remove_map(&mut self, map_name: &str) -> Option { + for scope in self.scopes.iter_mut().rev() { + if scope.get_map(map_name).is_some() { + return scope.remove_map(map_name); + } + } + None + } + pub fn get_map(&self, map_name: &str) -> Option<&MapNode> { + for scope in self.scopes.iter().rev() { + if let Some(map) = scope.get_map(map_name) { + return Some(map) + } + } + None + } + pub fn get_map_mut(&mut self, map_name: &str) -> Option<&mut MapNode> { + for scope in self.scopes.iter_mut().rev() { + if let Some(map) = scope.get_map_mut(map_name) { + return Some(map) + } + } + None + } + pub fn set_map(&mut self, map_name: &str, map: MapNode, local: bool) { + if local + && let Some(scope) = self.scopes.last_mut() { + scope.set_map(map_name, map); + } else if let Some(scope) = self.scopes.first_mut() { + scope.set_map(map_name, map); + } + } pub fn try_get_var(&self, var_name: &str) -> Option { // This version of get_var() is mainly used internally // so that we have access to Option methods @@ -588,18 +635,11 @@ impl FromStr for ArrIndex { } } -pub fn hashmap_to_vec(map: HashMap) -> Vec { - let mut items = map.into_iter().collect::>(); - items.sort_by_key(|(idx, _)| *idx); - - items.into_iter().map(|(_, i)| i).collect() -} - #[derive(Clone, Debug)] pub enum VarKind { Str(String), Int(i32), - Arr(HashMap), + Arr(VecDeque), AssocArr(Vec<(String, String)>), } @@ -614,7 +654,7 @@ impl VarKind { } let raw = raw[1..raw.len() - 1].to_string(); - let tokens: HashMap = LexStream::new(Arc::new(raw), LexFlags::empty()) + let tokens: VecDeque = LexStream::new(Arc::new(raw), LexFlags::empty()) .map(|tk| tk.and_then(|tk| tk.expand()).map(|tk| tk.get_words())) .try_fold(vec![], |mut acc, wrds| { match wrds { @@ -624,16 +664,13 @@ impl VarKind { Ok(acc) })? .into_iter() - .enumerate() .collect(); Ok(Self::Arr(tokens)) } pub fn arr_from_vec(vec: Vec) -> Self { - let tokens: HashMap = vec.into_iter().enumerate().collect(); - - Self::Arr(tokens) + Self::Arr(VecDeque::from(vec)) } } @@ -643,7 +680,6 @@ impl Display for VarKind { VarKind::Str(s) => write!(f, "{s}"), VarKind::Int(i) => write!(f, "{i}"), VarKind::Arr(items) => { - let items = hashmap_to_vec(items.clone()); let mut item_iter = items.iter().peekable(); while let Some(item) = item_iter.next() { write!(f, "{item}")?; @@ -702,8 +738,9 @@ impl Display for Var { pub struct VarTab { vars: HashMap, params: HashMap, - sh_argv: VecDeque, /* Using a VecDeque makes the implementation of `shift` - * straightforward */ + sh_argv: VecDeque, /* Using a VecDeque makes the implementation of `shift` straightforward */ + + maps: HashMap } impl VarTab { @@ -715,6 +752,7 @@ impl VarTab { vars, params, sh_argv: VecDeque::new(), + maps: HashMap::new(), }; var_tab.init_sh_argv(); var_tab @@ -833,6 +871,24 @@ impl VarTab { self.update_arg_params(); arg } + pub fn set_map(&mut self, map_name: &str, map: MapNode) { + self.maps.insert(map_name.to_string(), map); + } + pub fn remove_map(&mut self, map_name: &str) -> Option { + self.maps.remove(map_name) + } + pub fn get_map(&self, map_name: &str) -> Option<&MapNode> { + self.maps.get(map_name) + } + pub fn get_map_mut(&mut self, map_name: &str) -> Option<&mut MapNode> { + self.maps.get_mut(map_name) + } + pub fn maps(&self) -> &HashMap { + &self.maps + } + pub fn maps_mut(&mut self) -> &mut HashMap { + &mut self.maps + } pub fn vars(&self) -> &HashMap { &self.vars } @@ -906,7 +962,10 @@ impl VarTab { } }; - items.insert(idx, val); + if idx >= items.len() { + items.resize(idx + 1, String::new()); + } + items[idx] = val; return Ok(()); } _ => { @@ -945,6 +1004,9 @@ impl VarTab { } Ok(()) } + pub fn map_exists(&self, map_name: &str) -> bool { + self.maps.contains_key(map_name) + } pub fn var_exists(&self, var_name: &str) -> bool { if let Ok(param) = var_name.parse::() { return self.params.contains_key(¶m);