From 9d8d8901d70221b76defeefbe65bb95571b0aeb5 Mon Sep 17 00:00:00 2001 From: pagedmov Date: Sat, 28 Feb 2026 15:51:09 -0500 Subject: [PATCH] Add array support for local/export/readonly builtins Add array length syntax ${arr[#]} Map read path now expands variables before splitting on ., fixing map "$node" with dotted paths Map assignment path uses quote-aware token splitting, enabling quoted keys like "--type=" Completion errors now display above prompt instead of being overwritten Fix nested if/fi parser bug when closing keywords appear on separate lines Add QuoteState enum, replacing ad-hoc quote tracking booleans across lexer, highlighter, and expansion Add split_tk_at/split_tk for quote-aware token splitting with span preservation Refactor setup_builtin to accept optional argv for deferred expansion Add ariadne dependency (not yet wired up) --- Cargo.lock | 11 ++ Cargo.toml | 1 + src/builtin/alias.rs | 6 +- src/builtin/arrops.rs | 11 +- src/builtin/cd.rs | 3 +- src/builtin/complete.rs | 5 +- src/builtin/dirstack.rs | 9 +- src/builtin/echo.rs | 3 +- src/builtin/eval.rs | 3 +- src/builtin/exec.rs | 3 +- src/builtin/jobctl.rs | 9 +- src/builtin/map.rs | 45 ++++--- src/builtin/mod.rs | 28 ++-- src/builtin/pwd.rs | 2 +- src/builtin/read.rs | 3 +- src/builtin/shift.rs | 3 +- src/builtin/shopt.rs | 3 +- src/builtin/source.rs | 3 +- src/builtin/trap.rs | 3 +- src/builtin/varcmds.rs | 62 ++++++--- src/builtin/zoltraak.rs | 3 +- src/expand.rs | 113 +++++++--------- src/parse/lex.rs | 281 ++++++++++++++++++++++------------------ src/parse/mod.rs | 2 + src/readline/mod.rs | 39 +++--- src/state.rs | 2 + 26 files changed, 375 insertions(+), 281 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5d33eb4..f76851e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,6 +61,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ariadne" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8454c8a44ce2cb9cc7e7fae67fc6128465b343b92c6631e94beca3c8d1524ea5" +dependencies = [ + "unicode-width", + "yansi", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -451,6 +461,7 @@ dependencies = [ name = "shed" version = "0.3.0" dependencies = [ + "ariadne", "bitflags", "clap", "env_logger", diff --git a/Cargo.toml b/Cargo.toml index 7cbffec..a42e787 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ edition = "2024" debug = true [dependencies] +ariadne = "0.6.0" bitflags = "2.8.0" clap = { version = "4.5.38", features = ["derive"] } env_logger = "0.11.9" diff --git a/src/builtin/alias.rs b/src/builtin/alias.rs index f56f491..dd93a3e 100644 --- a/src/builtin/alias.rs +++ b/src/builtin/alias.rs @@ -18,7 +18,8 @@ pub fn alias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult< unreachable!() }; - let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; + let argv = argv.unwrap(); if argv.is_empty() { // Display the environment variables @@ -67,7 +68,8 @@ pub fn unalias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResul unreachable!() }; - let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; + let argv = argv.unwrap(); if argv.is_empty() { // Display the environment variables diff --git a/src/builtin/arrops.rs b/src/builtin/arrops.rs index bd3f882..88ff6f0 100644 --- a/src/builtin/arrops.rs +++ b/src/builtin/arrops.rs @@ -1,3 +1,5 @@ +use std::iter::Peekable; + 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} }; @@ -51,7 +53,8 @@ fn arr_pop_inner(node: Node, io_stack: &mut IoStack, job: &mut JobBldr, end: End 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 (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; + let argv = argv.unwrap(); let stdout = borrow_fd(STDOUT_FILENO); let mut status = 0; @@ -91,7 +94,8 @@ fn arr_push_inner(node: Node, io_stack: &mut IoStack, job: &mut JobBldr, end: En 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 (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; + let argv = argv.unwrap(); let mut argv = argv.into_iter(); let Some((name, _)) = argv.next() else { @@ -140,7 +144,8 @@ pub fn arr_rotate(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShRe 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 (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; + let argv = argv.unwrap(); for (arg, _) in argv { write_vars(|v| -> ShResult<()> { diff --git a/src/builtin/cd.rs b/src/builtin/cd.rs index af5dce3..3f19a93 100644 --- a/src/builtin/cd.rs +++ b/src/builtin/cd.rs @@ -18,7 +18,8 @@ pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> { unreachable!() }; - let (argv, _) = setup_builtin(argv, job, None)?; + let (argv, _) = setup_builtin(Some(argv), job, None)?; + let argv = argv.unwrap(); let new_dir = if let Some((arg, _)) = argv.into_iter().next() { PathBuf::from(arg) diff --git a/src/builtin/complete.rs b/src/builtin/complete.rs index 41ae715..56f6f0c 100644 --- a/src/builtin/complete.rs +++ b/src/builtin/complete.rs @@ -168,7 +168,8 @@ pub fn complete_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) - let (argv, opts) = get_opts_from_tokens(argv, &COMP_OPTS)?; let comp_opts = get_comp_opts(opts)?; - let (argv, _) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (argv, _) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; + let argv = argv.unwrap(); if comp_opts.flags.contains(CompFlags::PRINT) { if argv.is_empty() { @@ -242,7 +243,7 @@ pub fn compgen_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> let (argv, opts) = get_opts_from_tokens(argv, &COMPGEN_OPTS)?; let prefix = argv.clone().into_iter().nth(1).unwrap_or_default(); let comp_opts = get_comp_opts(opts)?; - let (_, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (_, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; let comp_spec = BashCompSpec::from_comp_opts(comp_opts).with_source(src); diff --git a/src/builtin/dirstack.rs b/src/builtin/dirstack.rs index 110ceb1..840c10a 100644 --- a/src/builtin/dirstack.rs +++ b/src/builtin/dirstack.rs @@ -127,7 +127,8 @@ pub fn pushd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult< unreachable!() }; - let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; + let argv = argv.unwrap(); let mut dir = None; let mut rotate_idx = None; @@ -213,7 +214,8 @@ pub fn popd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<( unreachable!() }; - let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; + let argv = argv.unwrap(); let mut remove_idx = None; let mut no_cd = false; @@ -316,7 +318,8 @@ pub fn dirs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<( unreachable!() }; - let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; + let argv = argv.unwrap(); let mut abbreviate_home = true; let mut one_per_line = false; diff --git a/src/builtin/echo.rs b/src/builtin/echo.rs index b535f91..2f5c4c9 100644 --- a/src/builtin/echo.rs +++ b/src/builtin/echo.rs @@ -51,7 +51,8 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<( assert!(!argv.is_empty()); let (argv, opts) = get_opts_from_tokens(argv, &ECHO_OPTS)?; let flags = get_echo_flags(opts).blame(blame)?; - let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; + let argv = argv.unwrap(); let output_channel = if flags.contains(EchoFlags::USE_STDERR) { borrow_fd(STDERR_FILENO) diff --git a/src/builtin/eval.rs b/src/builtin/eval.rs index ed8c971..37e26b3 100644 --- a/src/builtin/eval.rs +++ b/src/builtin/eval.rs @@ -16,7 +16,8 @@ pub fn eval(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<( unreachable!() }; - let (expanded_argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (expanded_argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; + let expanded_argv = expanded_argv.unwrap(); if expanded_argv.is_empty() { state::set_status(0); diff --git a/src/builtin/exec.rs b/src/builtin/exec.rs index ff82db4..56de57a 100644 --- a/src/builtin/exec.rs +++ b/src/builtin/exec.rs @@ -18,7 +18,8 @@ pub fn exec_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> Sh unreachable!() }; - let (expanded_argv, guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (expanded_argv, guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; + let expanded_argv = expanded_argv.unwrap(); if let Some(g) = guard { // Persist redirections so they affect the entire shell, // not just this command call diff --git a/src/builtin/jobctl.rs b/src/builtin/jobctl.rs index 40777f7..bf1c009 100644 --- a/src/builtin/jobctl.rs +++ b/src/builtin/jobctl.rs @@ -28,7 +28,8 @@ pub fn continue_job(node: Node, job: &mut JobBldr, behavior: JobBehavior) -> ShR unreachable!() }; - let (argv, _) = setup_builtin(argv, job, None)?; + let (argv, _) = setup_builtin(Some(argv), job, None)?; + let argv = argv.unwrap(); let mut argv = argv.into_iter(); if read_jobs(|j| j.get_fg().is_some()) { @@ -143,7 +144,8 @@ pub fn jobs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<( unreachable!() }; - let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; + let argv = argv.unwrap(); let mut flags = JobCmdFlags::empty(); for (arg, span) in argv { @@ -190,7 +192,8 @@ pub fn disown(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult unreachable!() }; - let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; + let argv = argv.unwrap(); let mut argv = argv.into_iter(); let curr_job_id = if let Some(id) = read_jobs(|j| j.curr_job()) { diff --git a/src/builtin/map.rs b/src/builtin/map.rs index 8e3b810..00c8bd1 100644 --- a/src/builtin/map.rs +++ b/src/builtin/map.rs @@ -5,7 +5,7 @@ use nix::{libc::STDOUT_FILENO, unistd::write}; use serde_json::{Map, Value}; use crate::{ - expand::expand_cmd_sub, 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} + expand::expand_cmd_sub, getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node, lex::{split_tk, split_tk_at}}, procio::{IoStack, borrow_fd}, state::{self, read_vars, write_vars} }; #[derive(Debug, Clone)] @@ -252,17 +252,23 @@ pub fn map(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<() unreachable!() }; - let (argv, opts) = get_opts_from_tokens(argv, &map_opts_spec())?; + let (mut 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)))?; + let (_, _guard) = setup_builtin(None, job, Some((io_stack, node.redirs)))?; + if !argv.is_empty() { + argv.remove(0); // remove "map" command from argv + } - for (arg,_) in argv { - if let Some((lhs,rhs)) = split_at_unescaped(&arg, "=") { - let path = split_all_unescaped(&lhs, "."); + for arg in argv { + if let Some((lhs,rhs)) = split_tk_at(&arg, "=") { + let path = split_tk(&lhs, ".") + .into_iter() + .map(|s| s.expand().map(|exp| exp.get_words().join(" "))) + .collect::>>()?; let Some(name) = path.first() else { return Err(ShErr::simple( ShErrKind::InternalErr, - format!("invalid map path: {}", lhs) + format!("invalid map path: {}", lhs.as_str()) )); }; @@ -271,39 +277,42 @@ pub fn map(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<() let make_leaf = |s: String| { if is_func { MapNode::DynamicLeaf(s) } else { MapNode::StaticLeaf(s) } }; - let found = write_vars(|v| { + let expanded = rhs.expand()?.get_words().join(" "); + let found = write_vars(|v| -> ShResult { if let Some(map) = v.get_map_mut(name) { if is_json { - if let Ok(parsed) = serde_json::from_str::(&rhs) { + if let Ok(parsed) = serde_json::from_str::(expanded.as_str()) { map.set(&path[1..], parsed.into()); } else { - map.set(&path[1..], make_leaf(rhs.clone())); + map.set(&path[1..], make_leaf(expanded.clone())); } } else { - map.set(&path[1..], make_leaf(rhs.clone())); + map.set(&path[1..], make_leaf(expanded.clone())); } - true + Ok(true) } else { - false + Ok(false) } }); - if !found { + if !found? { let mut new = MapNode::default(); - if is_json && let Ok(parsed) = serde_json::from_str::(&rhs) { + if is_json /*&& let Ok(parsed) = serde_json::from_str::(rhs.as_str()) */{ + let parsed = serde_json::from_str::(expanded.as_str()).unwrap(); let node: MapNode = parsed.into(); new.set(&path[1..], node); } else { - new.set(&path[1..], make_leaf(rhs)); + new.set(&path[1..], make_leaf(expanded)); } write_vars(|v| v.set_map(name, new, map_opts.flags.contains(MapFlags::LOCAL))); } } else { - let path = split_all_unescaped(&arg, "."); + let expanded = arg.expand()?.get_words().join(" "); + let path: Vec = expanded.split('.').map(|s| s.to_string()).collect(); let Some(name) = path.first() else { return Err(ShErr::simple( ShErrKind::InternalErr, - format!("invalid map path: {}", &arg) + format!("invalid map path: {}", expanded) )); }; diff --git a/src/builtin/mod.rs b/src/builtin/mod.rs index 8a7ef10..f55426e 100644 --- a/src/builtin/mod.rs +++ b/src/builtin/mod.rs @@ -67,13 +67,13 @@ pub const BUILTINS: [&str; 41] = [ /// * If redirections are given, the second field of the resulting tuple will /// *always* be `Some()` /// * If no redirections are given, the second field will *always* be `None` -type SetupReturns = ShResult<(Vec<(String, Span)>, Option)>; +type SetupReturns = ShResult<(Option>, Option)>; pub fn setup_builtin( - argv: Vec, + argv: Option>, job: &mut JobBldr, io_mode: Option<(&mut IoStack, Vec)>, ) -> SetupReturns { - let mut argv: Vec<(String, Span)> = prepare_argv(argv)?; + let mut argv = argv.map(|argv| prepare_argv(argv)).transpose()?; let child_pgid = if let Some(pgid) = job.pgid() { pgid @@ -81,18 +81,22 @@ pub fn setup_builtin( job.set_pgid(Pid::this()); Pid::this() }; - let cmd_name = argv.remove(0).0; + let cmd_name = argv + .as_mut() + .and_then(|argv| { + if argv.is_empty() { + None + } else { + Some(argv.remove(0).0) + } + }).unwrap_or_else(|| String::new()); let child = ChildProc::new(Pid::this(), Some(&cmd_name), Some(child_pgid))?; job.push_child(child); - let guard = if let Some((io_stack, redirs)) = io_mode { - io_stack.append_to_frame(redirs); - let io_frame = io_stack.pop_frame(); - let guard = io_frame.redirect()?; - Some(guard) - } else { - None - }; + let guard = io_mode.map(|(io,rdrs)| { + io.append_to_frame(rdrs); + io.pop_frame().redirect() + }).transpose()?; // We return the io_frame because the caller needs to also call // io_frame.restore() diff --git a/src/builtin/pwd.rs b/src/builtin/pwd.rs index 6e37dde..6d3978a 100644 --- a/src/builtin/pwd.rs +++ b/src/builtin/pwd.rs @@ -18,7 +18,7 @@ pub fn pwd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<() unreachable!() }; - let (_, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (_, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; let stdout = borrow_fd(STDOUT_FILENO); diff --git a/src/builtin/read.rs b/src/builtin/read.rs index cdbf982..a0d943c 100644 --- a/src/builtin/read.rs +++ b/src/builtin/read.rs @@ -75,7 +75,8 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S let (argv, opts) = get_opts_from_tokens(argv, &READ_OPTS)?; let read_opts = get_read_flags(opts).blame(blame.clone())?; - let (argv, _) = setup_builtin(argv, job, None).blame(blame.clone())?; + let (argv, _) = setup_builtin(Some(argv), job, None).blame(blame.clone())?; + let argv = argv.unwrap(); if let Some(prompt) = read_opts.prompt { write(borrow_fd(STDOUT_FILENO), prompt.as_bytes())?; diff --git a/src/builtin/shift.rs b/src/builtin/shift.rs index 40b65a5..e6c90b1 100644 --- a/src/builtin/shift.rs +++ b/src/builtin/shift.rs @@ -16,7 +16,8 @@ pub fn shift(node: Node, job: &mut JobBldr) -> ShResult<()> { unreachable!() }; - let (argv, _) = setup_builtin(argv, job, None)?; + let (argv, _) = setup_builtin(Some(argv), job, None)?; + let argv = argv.unwrap(); let mut argv = argv.into_iter(); if let Some((arg, span)) = argv.next() { diff --git a/src/builtin/shopt.rs b/src/builtin/shopt.rs index 56a5c1b..41945ff 100644 --- a/src/builtin/shopt.rs +++ b/src/builtin/shopt.rs @@ -18,7 +18,8 @@ pub fn shopt(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult< unreachable!() }; - let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; + let argv = argv.unwrap(); if argv.is_empty() { let mut output = write_shopts(|s| s.display_opts())?; diff --git a/src/builtin/source.rs b/src/builtin/source.rs index f3b3c88..976be2e 100644 --- a/src/builtin/source.rs +++ b/src/builtin/source.rs @@ -17,7 +17,8 @@ pub fn source(node: Node, job: &mut JobBldr) -> ShResult<()> { unreachable!() }; - let (argv, _) = setup_builtin(argv, job, None)?; + let (argv, _) = setup_builtin(Some(argv), job, None)?; + let argv = argv.unwrap(); for (arg, span) in argv { let path = PathBuf::from(arg); diff --git a/src/builtin/trap.rs b/src/builtin/trap.rs index c4cac8c..b4bea83 100644 --- a/src/builtin/trap.rs +++ b/src/builtin/trap.rs @@ -123,7 +123,8 @@ pub fn trap(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<( unreachable!() }; - let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; + let argv = argv.unwrap(); if argv.is_empty() { let stdout = borrow_fd(STDOUT_FILENO); diff --git a/src/builtin/varcmds.rs b/src/builtin/varcmds.rs index 5dc6c81..62922d3 100644 --- a/src/builtin/varcmds.rs +++ b/src/builtin/varcmds.rs @@ -1,7 +1,7 @@ use crate::{ jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, - parse::{NdRule, Node}, + parse::{NdRule, Node, lex::split_tk_at}, prelude::*, procio::{IoStack, borrow_fd}, state::{self, VarFlags, VarKind, read_vars, write_vars}, @@ -18,7 +18,10 @@ pub fn readonly(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu unreachable!() }; - let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (_, _guard) = setup_builtin(None, job, Some((io_stack, node.redirs)))?; + + // Remove "readonly" from argv + let argv = if !argv.is_empty() { &argv[1..] } else { &argv[..] }; if argv.is_empty() { // Display the local variables @@ -38,10 +41,17 @@ pub fn readonly(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu let stdout = borrow_fd(STDOUT_FILENO); write(stdout, vars_output.as_bytes())?; // Write it } else { - for (arg, _) in argv { - if let Some((var, val)) = arg.split_once('=') { - write_vars(|v| v.set_var(var, VarKind::Str(val.to_string()), VarFlags::READONLY))?; + for tk in argv { + if let Some((var_tk, val_tk)) = split_tk_at(tk, "=") { + let var = var_tk.expand()?.get_words().join(" "); + let val = if val_tk.as_str().starts_with('(') && val_tk.as_str().ends_with(')') { + VarKind::arr_from_tk(val_tk.clone())? + } else { + VarKind::Str(val_tk.expand()?.get_words().join(" ")) + }; + write_vars(|v| v.set_var(&var, val, VarFlags::READONLY))?; } else { + let arg = tk.clone().expand()?.get_words().join(" "); write_vars(|v| v.set_var(&arg, VarKind::Str(String::new()), VarFlags::READONLY))?; } } @@ -61,7 +71,8 @@ pub fn unset(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult< unreachable!() }; - let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; + let argv = argv.unwrap(); if argv.is_empty() { return Err(ShErr::full( @@ -95,7 +106,10 @@ pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult unreachable!() }; - let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (_, _guard) = setup_builtin(None, job, Some((io_stack, node.redirs)))?; + + // Remove "export" from argv + let argv = if !argv.is_empty() { &argv[1..] } else { &argv[..] }; if argv.is_empty() { // Display the environment variables @@ -109,12 +123,18 @@ pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult let stdout = borrow_fd(STDOUT_FILENO); write(stdout, env_output.as_bytes())?; // Write it } else { - for (arg, _) in argv { - if let Some((var, val)) = arg.split_once('=') { - write_vars(|v| v.set_var(var, VarKind::Str(val.to_string()), VarFlags::EXPORT))?; + for tk in argv { + if let Some((var_tk, val_tk)) = split_tk_at(tk, "=") { + let var = var_tk.expand()?.get_words().join(" "); + let val = if val_tk.as_str().starts_with('(') && val_tk.as_str().ends_with(')') { + VarKind::arr_from_tk(val_tk.clone())? + } else { + VarKind::Str(val_tk.expand()?.get_words().join(" ")) + }; + write_vars(|v| v.set_var(&var, val, VarFlags::EXPORT))?; } else { - write_vars(|v| v.export_var(&arg)); // Export an existing variable, if - // any + let arg = tk.clone().expand()?.get_words().join(" "); + write_vars(|v| v.export_var(&arg)); // Export an existing variable, if any } } } @@ -131,7 +151,10 @@ pub fn local(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult< unreachable!() }; - let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (_, _guard) = setup_builtin(None, job, Some((io_stack, node.redirs)))?; + + // Remove "local" from argv + let argv = if !argv.is_empty() { &argv[1..] } else { &argv[..] }; if argv.is_empty() { // Display the local variables @@ -150,10 +173,17 @@ pub fn local(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult< let stdout = borrow_fd(STDOUT_FILENO); write(stdout, vars_output.as_bytes())?; // Write it } else { - for (arg, _) in argv { - if let Some((var, val)) = arg.split_once('=') { - write_vars(|v| v.set_var(var, VarKind::Str(val.to_string()), VarFlags::LOCAL))?; + for tk in argv { + if let Some((var_tk, val_tk)) = split_tk_at(tk, "=") { + let var = var_tk.expand()?.get_words().join(" "); + let val = if val_tk.as_str().starts_with('(') && val_tk.as_str().ends_with(')') { + VarKind::arr_from_tk(val_tk.clone())? + } else { + VarKind::Str(val_tk.expand()?.get_words().join(" ")) + }; + write_vars(|v| v.set_var(&var, val, VarFlags::LOCAL))?; } else { + let arg = tk.clone().expand()?.get_words().join(" "); write_vars(|v| v.set_var(&arg, VarKind::Str(String::new()), VarFlags::LOCAL))?; } } diff --git a/src/builtin/zoltraak.rs b/src/builtin/zoltraak.rs index 3498fd6..c29e195 100644 --- a/src/builtin/zoltraak.rs +++ b/src/builtin/zoltraak.rs @@ -106,7 +106,8 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu } } - let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (argv, _guard) = setup_builtin(Some(argv), job, Some((io_stack, node.redirs)))?; + let argv = argv.unwrap(); for (arg, span) in argv { if &arg == "/" && !flags.contains(ZoltFlags::NO_PRESERVE_ROOT) { diff --git a/src/expand.rs b/src/expand.rs index 3799de3..8608fb2 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -7,7 +7,7 @@ use regex::Regex; use crate::libsh::error::{ShErr, ShErrKind, ShResult}; use crate::parse::execute::exec_input; -use crate::parse::lex::{LexFlags, LexStream, Tk, TkFlags, TkRule, is_hard_sep}; +use crate::parse::lex::{LexFlags, LexStream, QuoteState, Tk, TkFlags, TkRule, is_hard_sep}; use crate::parse::{Redir, RedirType}; use crate::procio::{IoBuf, IoFrame, IoMode, IoStack}; use crate::readline::markers; @@ -130,18 +130,16 @@ fn has_braces(s: &str) -> bool { let mut found_open = false; let mut has_comma = false; let mut has_range = false; - let mut cur_quote: Option = None; + let mut qt_state = QuoteState::default(); while let Some(ch) = chars.next() { match ch { '\\' => { chars.next(); } // skip escaped char - '\'' if cur_quote.is_none() => cur_quote = Some('\''), - '\'' if cur_quote == Some('\'') => cur_quote = None, - '"' if cur_quote.is_none() => cur_quote = Some('"'), - '"' if cur_quote == Some('"') => cur_quote = None, - '{' if cur_quote.is_none() => { + '\'' => qt_state.toggle_single(), + '"' => qt_state.toggle_double(), + '{' if qt_state.in_quote() => { if depth == 0 { found_open = true; has_comma = false; @@ -149,16 +147,16 @@ fn has_braces(s: &str) -> bool { } depth += 1; } - '}' if cur_quote.is_none() && depth > 0 => { + '}' if qt_state.outside() && depth > 0 => { depth -= 1; if depth == 0 && found_open && (has_comma || has_range) { return true; } } - ',' if cur_quote.is_none() && depth == 1 => { + ',' if qt_state.outside() && depth == 1 => { has_comma = true; } - '.' if cur_quote.is_none() && depth == 1 => { + '.' if qt_state.outside() && depth == 1 => { if chars.peek() == Some(&'.') { chars.next(); has_range = true; @@ -239,7 +237,7 @@ fn expand_one_brace(word: &str) -> ShResult> { fn get_brace_parts(word: &str) -> Option<(String, String, String)> { let mut chars = word.chars().peekable(); let mut prefix = String::new(); - let mut cur_quote: Option = None; + let mut qt_state = QuoteState::default(); // Find the opening brace while let Some(ch) = chars.next() { @@ -250,23 +248,15 @@ fn get_brace_parts(word: &str) -> Option<(String, String, String)> { prefix.push(next); } } - '\'' if cur_quote.is_none() => { - cur_quote = Some('\''); - prefix.push(ch); - } - '\'' if cur_quote == Some('\'') => { - cur_quote = None; - prefix.push(ch); - } - '"' if cur_quote.is_none() => { - cur_quote = Some('"'); - prefix.push(ch); - } - '"' if cur_quote == Some('"') => { - cur_quote = None; - prefix.push(ch); - } - '{' if cur_quote.is_none() => { + '\'' => { + qt_state.toggle_single(); + prefix.push(ch); + } + '"' => { + qt_state.toggle_double(); + prefix.push(ch); + } + '{' if qt_state.outside() => { break; } _ => prefix.push(ch), @@ -276,7 +266,7 @@ fn get_brace_parts(word: &str) -> Option<(String, String, String)> { // Find matching closing brace let mut depth = 1; let mut inner = String::new(); - cur_quote = None; + qt_state = QuoteState::default(); while let Some(ch) = chars.next() { match ch { @@ -286,27 +276,19 @@ fn get_brace_parts(word: &str) -> Option<(String, String, String)> { inner.push(next); } } - '\'' if cur_quote.is_none() => { - cur_quote = Some('\''); - inner.push(ch); - } - '\'' if cur_quote == Some('\'') => { - cur_quote = None; - inner.push(ch); - } - '"' if cur_quote.is_none() => { - cur_quote = Some('"'); - inner.push(ch); - } - '"' if cur_quote == Some('"') => { - cur_quote = None; - inner.push(ch); - } - '{' if cur_quote.is_none() => { + '\'' => { + qt_state.toggle_single(); + inner.push(ch); + } + '"' => { + qt_state.toggle_double(); + inner.push(ch); + } + '{' if qt_state.outside() => { depth += 1; inner.push(ch); } - '}' if cur_quote.is_none() => { + '}' if qt_state.outside() => { depth -= 1; if depth == 0 { break; @@ -335,7 +317,7 @@ fn split_brace_inner(inner: &str) -> Vec { let mut current = String::new(); let mut chars = inner.chars().peekable(); let mut depth = 0; - let mut cur_quote: Option = None; + let mut qt_state = QuoteState::default(); while let Some(ch) = chars.next() { match ch { @@ -345,31 +327,23 @@ fn split_brace_inner(inner: &str) -> Vec { current.push(next); } } - '\'' if cur_quote.is_none() => { - cur_quote = Some('\''); - current.push(ch); - } - '\'' if cur_quote == Some('\'') => { - cur_quote = None; - current.push(ch); - } - '"' if cur_quote.is_none() => { - cur_quote = Some('"'); - current.push(ch); - } - '"' if cur_quote == Some('"') => { - cur_quote = None; - current.push(ch); - } - '{' if cur_quote.is_none() => { + '\'' => { + qt_state.toggle_single(); + current.push(ch); + } + '"' => { + qt_state.toggle_double(); + current.push(ch); + } + '{' if qt_state.outside() => { depth += 1; current.push(ch); } - '}' if cur_quote.is_none() => { + '}' if qt_state.outside() => { depth -= 1; current.push(ch); } - ',' if cur_quote.is_none() && depth == 0 => { + ',' if qt_state.outside() && depth == 0 => { parts.push(std::mem::take(&mut current)); } _ => current.push(ch), @@ -556,6 +530,11 @@ pub fn expand_var(chars: &mut Peekable>) -> ShResult { let arg_sep = markers::ARG_SEP.to_string(); read_vars(|v| v.get_arr_elems(&var_name))?.join(&arg_sep) } + ArrIndex::ArgCount => { + read_vars(|v| v.get_arr_elems(&var_name)) + .map(|elems| elems.len().to_string()) + .unwrap_or_else(|_| "0".to_string()) + } ArrIndex::AllJoined => { let ifs = read_vars(|v| v.try_get_var("IFS")) .unwrap_or_else(|| " \t\n".to_string()) diff --git a/src/parse/lex.rs b/src/parse/lex.rs index 77c31a5..07b91a8 100644 --- a/src/parse/lex.rs +++ b/src/parse/lex.rs @@ -24,6 +24,46 @@ pub const KEYWORDS: [&str; 16] = [ pub const OPENERS: [&str; 6] = ["if", "while", "until", "for", "select", "case"]; +/// Used to track whether the lexer is currently inside a quote, and if so, which type +#[derive(Default,Debug)] +pub enum QuoteState { + #[default] + Outside, + Single, + Double +} + +impl QuoteState { + pub fn outside(&self) -> bool { + matches!(self, QuoteState::Outside) + } + pub fn in_single(&self) -> bool { + matches!(self, QuoteState::Single) + } + pub fn in_double(&self) -> bool { + matches!(self, QuoteState::Double) + } + pub fn in_quote(&self) -> bool { + !self.outside() + } + /// Toggles whether we are in a double quote. If self = QuoteState::Single, this does nothing, since double quotes inside single quotes are just literal characters + pub fn toggle_double(&mut self) { + match self { + QuoteState::Outside => *self = QuoteState::Double, + QuoteState::Double => *self = QuoteState::Outside, + _ => {} + } + } + /// Toggles whether we are in a single quote. If self == QuoteState::Double, this does nothing, since single quotes are not interpreted inside double quotes + pub fn toggle_single(&mut self) { + match self { + QuoteState::Outside => *self = QuoteState::Single, + QuoteState::Single => *self = QuoteState::Outside, + _ => {} + } + } +} + /// Span::new(10..20) #[derive(Clone, PartialEq, Default, Debug)] pub struct Span { @@ -150,7 +190,7 @@ bitflags! { pub struct LexStream { source: Arc, pub cursor: usize, - in_quote: bool, + quote_state: QuoteState, brc_grp_start: Option, flags: LexFlags, } @@ -183,11 +223,11 @@ impl LexStream { pub fn new(source: Arc, flags: LexFlags) -> Self { let flags = flags | LexFlags::FRESH | LexFlags::NEXT_IS_CMD; Self { + flags, source, cursor: 0, - in_quote: false, + quote_state: QuoteState::default(), brc_grp_start: None, - flags, } } /// Returns a slice of the source input using the given range @@ -352,6 +392,47 @@ impl LexStream { if let Some(ch) = chars.next() { pos += ch.len_utf8(); } + } + '\'' => { + pos += 1; + self.quote_state.toggle_single(); + } + _ if self.quote_state.in_single() => pos += ch.len_utf8(), + '$' if chars.peek() == Some(&'(') => { + pos += 2; + chars.next(); + let mut paren_count = 1; + let paren_pos = pos; + while let Some(ch) = chars.next() { + match ch { + '\\' => { + pos += 1; + if let Some(next_ch) = chars.next() { + pos += next_ch.len_utf8(); + } + } + '(' => { + pos += 1; + paren_count += 1; + } + ')' => { + pos += 1; + paren_count -= 1; + if paren_count <= 0 { + break; + } + } + _ => pos += ch.len_utf8(), + } + } + if !paren_count == 0 && !self.flags.contains(LexFlags::LEX_UNFINISHED) { + self.cursor = pos; + return Err(ShErr::full( + ShErrKind::ParseErr, + "Unclosed subshell", + Span::new(paren_pos..paren_pos + 1, self.source.clone()), + )); + } } '$' if chars.peek() == Some(&'{') => { pos += 2; @@ -380,6 +461,11 @@ impl LexStream { } } } + '"' => { + pos += 1; + self.quote_state.toggle_double(); + } + _ if self.quote_state.in_double() => pos += ch.len_utf8(), '<' if chars.peek() == Some(&'(') => { pos += 2; chars.next(); @@ -452,42 +538,6 @@ impl LexStream { )); } } - '$' if chars.peek() == Some(&'(') => { - pos += 2; - chars.next(); - let mut paren_count = 1; - let paren_pos = pos; - while let Some(ch) = chars.next() { - match ch { - '\\' => { - pos += 1; - if let Some(next_ch) = chars.next() { - pos += next_ch.len_utf8(); - } - } - '(' => { - pos += 1; - paren_count += 1; - } - ')' => { - pos += 1; - paren_count -= 1; - if paren_count <= 0 { - break; - } - } - _ => pos += ch.len_utf8(), - } - } - if !paren_count == 0 && !self.flags.contains(LexFlags::LEX_UNFINISHED) { - self.cursor = pos; - return Err(ShErr::full( - ShErrKind::ParseErr, - "Unclosed subshell", - Span::new(paren_pos..paren_pos + 1, self.source.clone()), - )); - } - } '(' if self.next_is_cmd() && can_be_subshell => { pos += 1; let mut paren_count = 1; @@ -547,82 +597,6 @@ impl LexStream { self.cursor = pos; return Ok(tk); } - '\'' => { - self.in_quote = true; - pos += 1; - while let Some(q_ch) = chars.next() { - match q_ch { - '\\' => { - pos += 1; - if chars.next().is_some() { - pos += 1; - } - } - _ if q_ch == '\'' => { - pos += 1; - self.in_quote = false; - break; - } - // Any time an ambiguous character is found - // we must push the cursor by the length of the character - // instead of just assuming a length of 1. - // Allows spans to work for wide characters - _ => pos += q_ch.len_utf8(), - } - } - } - '"' => { - self.in_quote = true; - pos += 1; - while let Some(q_ch) = chars.next() { - match q_ch { - '\\' => { - pos += 1; - if chars.next().is_some() { - pos += 1; - } - } - '$' if chars.peek() == Some(&'(') => { - pos += 2; - chars.next(); - let mut cmdsub_count = 1; - while let Some(cmdsub_ch) = chars.next() { - match cmdsub_ch { - '\\' => { - pos += 1; - if chars.next().is_some() { - pos += 1; - } - } - '$' if chars.peek() == Some(&'(') => { - cmdsub_count += 1; - pos += 2; - chars.next(); - } - ')' => { - cmdsub_count -= 1; - pos += 1; - if cmdsub_count <= 0 { - break; - } - } - _ => pos += cmdsub_ch.len_utf8(), - } - } - } - _ if q_ch == '"' => { - pos += 1; - self.in_quote = false; - break; - } - // Any time an ambiguous character is found - // we must push the cursor by the length of the character - // instead of just assuming a length of 1. - // Allows spans to work for wide characters - _ => pos += q_ch.len_utf8(), - } - } - } '=' if chars.peek() == Some(&'(') => { pos += 1; // '=' let mut depth = 1; @@ -652,13 +626,12 @@ impl LexStream { } } } - _ if !self.in_quote && is_op(ch) => break, _ if is_hard_sep(ch) => break, _ => pos += ch.len_utf8(), } } let mut new_tk = self.get_token(self.cursor..pos, TkRule::Str); - if self.in_quote && !self.flags.contains(LexFlags::LEX_UNFINISHED) { + if self.quote_state.in_quote() && !self.flags.contains(LexFlags::LEX_UNFINISHED) { self.cursor = pos; return Err(ShErr::full( ShErrKind::ParseErr, @@ -912,6 +885,8 @@ pub fn ends_with_unescaped(slice: &str, pat: &str) -> bool { slice.ends_with(pat) && !pos_is_escaped(slice, slice.len() - pat.len()) } +/// Splits a string by a pattern, but only if the pattern is not escaped by a backslash +/// and not in quotes. pub fn split_all_unescaped(slice: &str, pat: &str) -> Vec { let mut cursor = 0; let mut splits = vec![]; @@ -925,19 +900,71 @@ pub fn split_all_unescaped(slice: &str, pat: &str) -> Vec { splits } +/// Splits a string at the first occurrence of a pattern, but only if the pattern is not escaped by a backslash +/// and not in quotes. Returns None if the pattern is not found or only found escaped. 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())); + let mut chars = slice.char_indices().peekable(); + let mut qt_state = QuoteState::default(); + + while let Some((i, ch)) = chars.next() { + match ch { + '\\' => { chars.next(); continue; } + '\'' => qt_state.toggle_single(), + '"' => qt_state.toggle_double(), + _ if qt_state.in_quote() => continue, + _ => {} + } + + if slice[i..].starts_with(pat) { + let before = slice[..i].to_string(); + let after = slice[i + pat.len()..].to_string(); + return Some((before, after)); } - window_start += 1; - window_end += 1; } + + + None +} + +pub fn split_tk(tk: &Tk, pat: &str) -> Vec { + let slice = tk.as_str(); + let mut cursor = 0; + let mut splits = vec![]; + while let Some(split) = split_at_unescaped(&slice[cursor..], pat) { + let before_span = Span::new(tk.span.start + cursor..tk.span.start + cursor + split.0.len(), tk.source().clone()); + splits.push(Tk::new(tk.class.clone(), before_span)); + cursor += split.0.len() + pat.len(); + } + if slice.get(cursor..).is_some_and(|s| !s.is_empty()) { + let remaining_span = Span::new(tk.span.start + cursor..tk.span.end, tk.source().clone()); + splits.push(Tk::new(tk.class.clone(), remaining_span)); + } + splits +} + +pub fn split_tk_at(tk: &Tk, pat: &str) -> Option<(Tk, Tk)> { + let slice = tk.as_str(); + let mut chars = slice.char_indices().peekable(); + let mut qt_state = QuoteState::default(); + + while let Some((i, ch)) = chars.next() { + match ch { + '\\' => { chars.next(); continue; } + '\'' => qt_state.toggle_single(), + '"' => qt_state.toggle_double(), + _ if qt_state.in_quote() => continue, + _ => {} + } + + if slice[i..].starts_with(pat) { + let before_span = Span::new(tk.span.start..tk.span.start + i, tk.source().clone()); + let after_span = Span::new(tk.span.start + i + pat.len()..tk.span.end, tk.source().clone()); + let before_tk = Tk::new(tk.class.clone(), before_span); + let after_tk = Tk::new(tk.class.clone(), after_span); + return Some((before_tk, after_tk)); + } + } + None } diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 8d6d7a1..937eb23 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -1035,6 +1035,7 @@ impl ParseStream { }; cond_nodes.push(cond_node); + self.catch_separator(&mut node_tks); if !self.check_keyword("elif") || !self.next_tk_is_some() { break; } else { @@ -1043,6 +1044,7 @@ impl ParseStream { } } + self.catch_separator(&mut node_tks); if self.check_keyword("else") { node_tks.push(self.next_tk().unwrap()); self.catch_separator(&mut node_tks); diff --git a/src/readline/mod.rs b/src/readline/mod.rs index 0d0c742..34e8f37 100644 --- a/src/readline/mod.rs +++ b/src/readline/mod.rs @@ -8,7 +8,7 @@ use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVis use crate::expand::expand_prompt; use crate::libsh::sys::TTY_FILENO; -use crate::parse::lex::LexStream; +use crate::parse::lex::{LexStream, QuoteState}; use crate::prelude::*; use crate::readline::term::{Pos, calc_str_width}; use crate::state::read_shopts; @@ -339,8 +339,14 @@ impl ShedVi { let line = self.editor.as_str().to_string(); let cursor_pos = self.editor.cursor_byte_pos(); - match self.completer.complete(line, cursor_pos, direction)? { - Some(line) => { + match self.completer.complete(line, cursor_pos, direction) { + Err(e) => { + self.writer.flush_write(&format!("\n{e}\n\n"))?; + + // Printing the error invalidates the layout + self.old_layout = None; + } + Ok(Some(line)) => { let span_start = self.completer.token_span.0; let new_cursor = span_start + self @@ -361,7 +367,7 @@ impl ShedVi { let hint = self.history.get_hint(); self.editor.set_hint(hint); } - None => { + Ok(None) => { self.writer.send_bell().ok(); } } @@ -1005,8 +1011,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> { let span_start = token.span.start; - let mut in_dub_qt = false; - let mut in_sng_qt = false; + let mut qt_state = QuoteState::default(); let mut cmd_sub_depth = 0; let mut proc_sub_depth = 0; @@ -1045,7 +1050,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> { } } } - '$' if !in_sng_qt => { + '$' if !qt_state.in_single() => { let dollar_pos = index; token_chars.next(); // consume the dollar if let Some((_, dollar_ch)) = token_chars.peek() { @@ -1115,13 +1120,13 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> { token_chars.next(); // consume the char with no special handling } - '\\' if !in_sng_qt => { + '\\' if !qt_state.in_single() => { token_chars.next(); // consume the backslash if token_chars.peek().is_some() { token_chars.next(); // consume the escaped char } } - '<' | '>' if !in_dub_qt && !in_sng_qt && cmd_sub_depth == 0 && proc_sub_depth == 0 => { + '<' | '>' if !qt_state.in_quote() && cmd_sub_depth == 0 && proc_sub_depth == 0 => { token_chars.next(); if let Some((_, proc_sub_ch)) = token_chars.peek() && *proc_sub_ch == '(' @@ -1133,25 +1138,25 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> { } } } - '"' if !in_sng_qt => { - if in_dub_qt { + '"' if !qt_state.in_single() => { + if qt_state.in_double() { insertions.push((span_start + *i + 1, markers::STRING_DQ_END)); } else { insertions.push((span_start + *i, markers::STRING_DQ)); } - in_dub_qt = !in_dub_qt; + qt_state.toggle_double(); token_chars.next(); // consume the quote } - '\'' if !in_dub_qt => { - if in_sng_qt { + '\'' if !qt_state.in_double() => { + if qt_state.in_single() { insertions.push((span_start + *i + 1, markers::STRING_SQ_END)); } else { insertions.push((span_start + *i, markers::STRING_SQ)); } - in_sng_qt = !in_sng_qt; + qt_state.toggle_single(); token_chars.next(); // consume the quote } - '[' if !in_dub_qt && !in_sng_qt && !token.flags.contains(TkFlags::ASSIGN) => { + '[' if !qt_state.in_quote() && !token.flags.contains(TkFlags::ASSIGN) => { token_chars.next(); // consume the opening bracket let start_pos = span_start + index; let mut is_glob_pat = false; @@ -1177,7 +1182,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> { insertions.push((start_pos, markers::GLOB)); } } - '*' | '?' if (!in_dub_qt && !in_sng_qt) => { + '*' | '?' if !qt_state.in_quote() => { let glob_ch = *ch; token_chars.next(); // consume the first glob char if !in_context(markers::COMMAND, &insertions) { diff --git a/src/state.rs b/src/state.rs index ae1c3ac..8c25dab 100644 --- a/src/state.rs +++ b/src/state.rs @@ -620,6 +620,7 @@ impl VarFlags { pub enum ArrIndex { Literal(usize), FromBack(usize), + ArgCount, AllJoined, AllSplit, } @@ -630,6 +631,7 @@ impl FromStr for ArrIndex { match s { "@" => Ok(Self::AllSplit), "*" => Ok(Self::AllJoined), + "#" => Ok(Self::ArgCount), _ if s.starts_with('-') && s[1..].chars().all(|c| c.is_digit(1)) => { let idx = s[1..].parse::().unwrap(); Ok(Self::FromBack(idx))