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)
This commit is contained in:
2026-02-28 15:51:09 -05:00
parent 4cda68e635
commit 9d8d8901d7
26 changed files with 375 additions and 281 deletions

11
Cargo.lock generated
View File

@@ -61,6 +61,16 @@ dependencies = [
"windows-sys 0.61.2", "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]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.7.6" version = "0.7.6"
@@ -451,6 +461,7 @@ dependencies = [
name = "shed" name = "shed"
version = "0.3.0" version = "0.3.0"
dependencies = [ dependencies = [
"ariadne",
"bitflags", "bitflags",
"clap", "clap",
"env_logger", "env_logger",

View File

@@ -10,6 +10,7 @@ edition = "2024"
debug = true debug = true
[dependencies] [dependencies]
ariadne = "0.6.0"
bitflags = "2.8.0" bitflags = "2.8.0"
clap = { version = "4.5.38", features = ["derive"] } clap = { version = "4.5.38", features = ["derive"] }
env_logger = "0.11.9" env_logger = "0.11.9"

View File

@@ -18,7 +18,8 @@ pub fn alias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
unreachable!() 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() { if argv.is_empty() {
// Display the environment variables // Display the environment variables
@@ -67,7 +68,8 @@ pub fn unalias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResul
unreachable!() 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() { if argv.is_empty() {
// Display the environment variables // Display the environment variables

View File

@@ -1,3 +1,5 @@
use std::iter::Peekable;
use crate::{ 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} 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 (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?;
let arr_op_opts = get_arr_op_opts(opts)?; 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 stdout = borrow_fd(STDOUT_FILENO);
let mut status = 0; 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 (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?;
let _arr_op_opts = get_arr_op_opts(opts)?; 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 mut argv = argv.into_iter();
let Some((name, _)) = argv.next() else { 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 (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?;
let arr_op_opts = get_arr_op_opts(opts)?; 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 { for (arg, _) in argv {
write_vars(|v| -> ShResult<()> { write_vars(|v| -> ShResult<()> {

View File

@@ -18,7 +18,8 @@ pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> {
unreachable!() 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() { let new_dir = if let Some((arg, _)) = argv.into_iter().next() {
PathBuf::from(arg) PathBuf::from(arg)

View File

@@ -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 (argv, opts) = get_opts_from_tokens(argv, &COMP_OPTS)?;
let comp_opts = get_comp_opts(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 comp_opts.flags.contains(CompFlags::PRINT) {
if argv.is_empty() { 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 (argv, opts) = get_opts_from_tokens(argv, &COMPGEN_OPTS)?;
let prefix = argv.clone().into_iter().nth(1).unwrap_or_default(); let prefix = argv.clone().into_iter().nth(1).unwrap_or_default();
let comp_opts = get_comp_opts(opts)?; 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); let comp_spec = BashCompSpec::from_comp_opts(comp_opts).with_source(src);

View File

@@ -127,7 +127,8 @@ pub fn pushd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
unreachable!() 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 dir = None;
let mut rotate_idx = None; let mut rotate_idx = None;
@@ -213,7 +214,8 @@ pub fn popd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
unreachable!() 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 remove_idx = None;
let mut no_cd = false; let mut no_cd = false;
@@ -316,7 +318,8 @@ pub fn dirs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
unreachable!() 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 abbreviate_home = true;
let mut one_per_line = false; let mut one_per_line = false;

View File

@@ -51,7 +51,8 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
assert!(!argv.is_empty()); assert!(!argv.is_empty());
let (argv, opts) = get_opts_from_tokens(argv, &ECHO_OPTS)?; let (argv, opts) = get_opts_from_tokens(argv, &ECHO_OPTS)?;
let flags = get_echo_flags(opts).blame(blame)?; 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) { let output_channel = if flags.contains(EchoFlags::USE_STDERR) {
borrow_fd(STDERR_FILENO) borrow_fd(STDERR_FILENO)

View File

@@ -16,7 +16,8 @@ pub fn eval(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
unreachable!() 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() { if expanded_argv.is_empty() {
state::set_status(0); state::set_status(0);

View File

@@ -18,7 +18,8 @@ pub fn exec_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> Sh
unreachable!() 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 { if let Some(g) = guard {
// Persist redirections so they affect the entire shell, // Persist redirections so they affect the entire shell,
// not just this command call // not just this command call

View File

@@ -28,7 +28,8 @@ pub fn continue_job(node: Node, job: &mut JobBldr, behavior: JobBehavior) -> ShR
unreachable!() 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(); let mut argv = argv.into_iter();
if read_jobs(|j| j.get_fg().is_some()) { 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!() 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(); let mut flags = JobCmdFlags::empty();
for (arg, span) in argv { for (arg, span) in argv {
@@ -190,7 +192,8 @@ pub fn disown(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult
unreachable!() 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 mut argv = argv.into_iter();
let curr_job_id = if let Some(id) = read_jobs(|j| j.curr_job()) { let curr_job_id = if let Some(id) = read_jobs(|j| j.curr_job()) {

View File

@@ -5,7 +5,7 @@ use nix::{libc::STDOUT_FILENO, unistd::write};
use serde_json::{Map, Value}; use serde_json::{Map, Value};
use crate::{ 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)] #[derive(Debug, Clone)]
@@ -252,17 +252,23 @@ pub fn map(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()
unreachable!() 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 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 { for arg in argv {
if let Some((lhs,rhs)) = split_at_unescaped(&arg, "=") { if let Some((lhs,rhs)) = split_tk_at(&arg, "=") {
let path = split_all_unescaped(&lhs, "."); let path = split_tk(&lhs, ".")
.into_iter()
.map(|s| s.expand().map(|exp| exp.get_words().join(" ")))
.collect::<ShResult<Vec<String>>>()?;
let Some(name) = path.first() else { let Some(name) = path.first() else {
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::InternalErr, 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| { let make_leaf = |s: String| {
if is_func { MapNode::DynamicLeaf(s) } else { MapNode::StaticLeaf(s) } 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<bool> {
if let Some(map) = v.get_map_mut(name) { if let Some(map) = v.get_map_mut(name) {
if is_json { if is_json {
if let Ok(parsed) = serde_json::from_str::<Value>(&rhs) { if let Ok(parsed) = serde_json::from_str::<Value>(expanded.as_str()) {
map.set(&path[1..], parsed.into()); map.set(&path[1..], parsed.into());
} else { } else {
map.set(&path[1..], make_leaf(rhs.clone())); map.set(&path[1..], make_leaf(expanded.clone()));
} }
} else { } else {
map.set(&path[1..], make_leaf(rhs.clone())); map.set(&path[1..], make_leaf(expanded.clone()));
} }
true Ok(true)
} else { } else {
false Ok(false)
} }
}); });
if !found { if !found? {
let mut new = MapNode::default(); let mut new = MapNode::default();
if is_json && let Ok(parsed) = serde_json::from_str::<Value>(&rhs) { if is_json /*&& let Ok(parsed) = serde_json::from_str::<Value>(rhs.as_str()) */{
let parsed = serde_json::from_str::<Value>(expanded.as_str()).unwrap();
let node: MapNode = parsed.into(); let node: MapNode = parsed.into();
new.set(&path[1..], node); new.set(&path[1..], node);
} else { } 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))); write_vars(|v| v.set_map(name, new, map_opts.flags.contains(MapFlags::LOCAL)));
} }
} else { } else {
let path = split_all_unescaped(&arg, "."); let expanded = arg.expand()?.get_words().join(" ");
let path: Vec<String> = expanded.split('.').map(|s| s.to_string()).collect();
let Some(name) = path.first() else { let Some(name) = path.first() else {
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::InternalErr, ShErrKind::InternalErr,
format!("invalid map path: {}", &arg) format!("invalid map path: {}", expanded)
)); ));
}; };

View File

@@ -67,13 +67,13 @@ pub const BUILTINS: [&str; 41] = [
/// * If redirections are given, the second field of the resulting tuple will /// * If redirections are given, the second field of the resulting tuple will
/// *always* be `Some()` /// *always* be `Some()`
/// * If no redirections are given, the second field will *always* be `None` /// * If no redirections are given, the second field will *always* be `None`
type SetupReturns = ShResult<(Vec<(String, Span)>, Option<RedirGuard>)>; type SetupReturns = ShResult<(Option<Vec<(String, Span)>>, Option<RedirGuard>)>;
pub fn setup_builtin( pub fn setup_builtin(
argv: Vec<Tk>, argv: Option<Vec<Tk>>,
job: &mut JobBldr, job: &mut JobBldr,
io_mode: Option<(&mut IoStack, Vec<Redir>)>, io_mode: Option<(&mut IoStack, Vec<Redir>)>,
) -> SetupReturns { ) -> 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() { let child_pgid = if let Some(pgid) = job.pgid() {
pgid pgid
@@ -81,18 +81,22 @@ pub fn setup_builtin(
job.set_pgid(Pid::this()); job.set_pgid(Pid::this());
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))?; let child = ChildProc::new(Pid::this(), Some(&cmd_name), Some(child_pgid))?;
job.push_child(child); job.push_child(child);
let guard = if let Some((io_stack, redirs)) = io_mode { let guard = io_mode.map(|(io,rdrs)| {
io_stack.append_to_frame(redirs); io.append_to_frame(rdrs);
let io_frame = io_stack.pop_frame(); io.pop_frame().redirect()
let guard = io_frame.redirect()?; }).transpose()?;
Some(guard)
} else {
None
};
// We return the io_frame because the caller needs to also call // We return the io_frame because the caller needs to also call
// io_frame.restore() // io_frame.restore()

View File

@@ -18,7 +18,7 @@ pub fn pwd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()
unreachable!() 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); let stdout = borrow_fd(STDOUT_FILENO);

View File

@@ -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 (argv, opts) = get_opts_from_tokens(argv, &READ_OPTS)?;
let read_opts = get_read_flags(opts).blame(blame.clone())?; 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 { if let Some(prompt) = read_opts.prompt {
write(borrow_fd(STDOUT_FILENO), prompt.as_bytes())?; write(borrow_fd(STDOUT_FILENO), prompt.as_bytes())?;

View File

@@ -16,7 +16,8 @@ pub fn shift(node: Node, job: &mut JobBldr) -> ShResult<()> {
unreachable!() 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(); let mut argv = argv.into_iter();
if let Some((arg, span)) = argv.next() { if let Some((arg, span)) = argv.next() {

View File

@@ -18,7 +18,8 @@ pub fn shopt(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
unreachable!() 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() { if argv.is_empty() {
let mut output = write_shopts(|s| s.display_opts())?; let mut output = write_shopts(|s| s.display_opts())?;

View File

@@ -17,7 +17,8 @@ pub fn source(node: Node, job: &mut JobBldr) -> ShResult<()> {
unreachable!() 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 { for (arg, span) in argv {
let path = PathBuf::from(arg); let path = PathBuf::from(arg);

View File

@@ -123,7 +123,8 @@ pub fn trap(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
unreachable!() 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() { if argv.is_empty() {
let stdout = borrow_fd(STDOUT_FILENO); let stdout = borrow_fd(STDOUT_FILENO);

View File

@@ -1,7 +1,7 @@
use crate::{ use crate::{
jobs::JobBldr, jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult}, libsh::error::{ShErr, ShErrKind, ShResult},
parse::{NdRule, Node}, parse::{NdRule, Node, lex::split_tk_at},
prelude::*, prelude::*,
procio::{IoStack, borrow_fd}, procio::{IoStack, borrow_fd},
state::{self, VarFlags, VarKind, read_vars, write_vars}, 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!() 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() { if argv.is_empty() {
// Display the local variables // 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); let stdout = borrow_fd(STDOUT_FILENO);
write(stdout, vars_output.as_bytes())?; // Write it write(stdout, vars_output.as_bytes())?; // Write it
} else { } else {
for (arg, _) in argv { for tk in argv {
if let Some((var, val)) = arg.split_once('=') { if let Some((var_tk, val_tk)) = split_tk_at(tk, "=") {
write_vars(|v| v.set_var(var, VarKind::Str(val.to_string()), VarFlags::READONLY))?; 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 { } 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))?; 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!() 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() { if argv.is_empty() {
return Err(ShErr::full( return Err(ShErr::full(
@@ -95,7 +106,10 @@ pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult
unreachable!() 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() { if argv.is_empty() {
// Display the environment variables // 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); let stdout = borrow_fd(STDOUT_FILENO);
write(stdout, env_output.as_bytes())?; // Write it write(stdout, env_output.as_bytes())?; // Write it
} else { } else {
for (arg, _) in argv { for tk in argv {
if let Some((var, val)) = arg.split_once('=') { if let Some((var_tk, val_tk)) = split_tk_at(tk, "=") {
write_vars(|v| v.set_var(var, VarKind::Str(val.to_string()), VarFlags::EXPORT))?; 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 { } else {
write_vars(|v| v.export_var(&arg)); // Export an existing variable, if VarKind::Str(val_tk.expand()?.get_words().join(" "))
// any };
write_vars(|v| v.set_var(&var, val, VarFlags::EXPORT))?;
} else {
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!() 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() { if argv.is_empty() {
// Display the local variables // 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); let stdout = borrow_fd(STDOUT_FILENO);
write(stdout, vars_output.as_bytes())?; // Write it write(stdout, vars_output.as_bytes())?; // Write it
} else { } else {
for (arg, _) in argv { for tk in argv {
if let Some((var, val)) = arg.split_once('=') { if let Some((var_tk, val_tk)) = split_tk_at(tk, "=") {
write_vars(|v| v.set_var(var, VarKind::Str(val.to_string()), VarFlags::LOCAL))?; 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 { } 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))?; write_vars(|v| v.set_var(&arg, VarKind::Str(String::new()), VarFlags::LOCAL))?;
} }
} }

View File

@@ -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 { for (arg, span) in argv {
if &arg == "/" && !flags.contains(ZoltFlags::NO_PRESERVE_ROOT) { if &arg == "/" && !flags.contains(ZoltFlags::NO_PRESERVE_ROOT) {

View File

@@ -7,7 +7,7 @@ use regex::Regex;
use crate::libsh::error::{ShErr, ShErrKind, ShResult}; use crate::libsh::error::{ShErr, ShErrKind, ShResult};
use crate::parse::execute::exec_input; 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::parse::{Redir, RedirType};
use crate::procio::{IoBuf, IoFrame, IoMode, IoStack}; use crate::procio::{IoBuf, IoFrame, IoMode, IoStack};
use crate::readline::markers; use crate::readline::markers;
@@ -130,18 +130,16 @@ fn has_braces(s: &str) -> bool {
let mut found_open = false; let mut found_open = false;
let mut has_comma = false; let mut has_comma = false;
let mut has_range = false; let mut has_range = false;
let mut cur_quote: Option<char> = None; let mut qt_state = QuoteState::default();
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
match ch { match ch {
'\\' => { '\\' => {
chars.next(); chars.next();
} // skip escaped char } // skip escaped char
'\'' if cur_quote.is_none() => cur_quote = Some('\''), '\'' => qt_state.toggle_single(),
'\'' if cur_quote == Some('\'') => cur_quote = None, '"' => qt_state.toggle_double(),
'"' if cur_quote.is_none() => cur_quote = Some('"'), '{' if qt_state.in_quote() => {
'"' if cur_quote == Some('"') => cur_quote = None,
'{' if cur_quote.is_none() => {
if depth == 0 { if depth == 0 {
found_open = true; found_open = true;
has_comma = false; has_comma = false;
@@ -149,16 +147,16 @@ fn has_braces(s: &str) -> bool {
} }
depth += 1; depth += 1;
} }
'}' if cur_quote.is_none() && depth > 0 => { '}' if qt_state.outside() && depth > 0 => {
depth -= 1; depth -= 1;
if depth == 0 && found_open && (has_comma || has_range) { if depth == 0 && found_open && (has_comma || has_range) {
return true; return true;
} }
} }
',' if cur_quote.is_none() && depth == 1 => { ',' if qt_state.outside() && depth == 1 => {
has_comma = true; has_comma = true;
} }
'.' if cur_quote.is_none() && depth == 1 => { '.' if qt_state.outside() && depth == 1 => {
if chars.peek() == Some(&'.') { if chars.peek() == Some(&'.') {
chars.next(); chars.next();
has_range = true; has_range = true;
@@ -239,7 +237,7 @@ fn expand_one_brace(word: &str) -> ShResult<Vec<String>> {
fn get_brace_parts(word: &str) -> Option<(String, String, String)> { fn get_brace_parts(word: &str) -> Option<(String, String, String)> {
let mut chars = word.chars().peekable(); let mut chars = word.chars().peekable();
let mut prefix = String::new(); let mut prefix = String::new();
let mut cur_quote: Option<char> = None; let mut qt_state = QuoteState::default();
// Find the opening brace // Find the opening brace
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
@@ -250,23 +248,15 @@ fn get_brace_parts(word: &str) -> Option<(String, String, String)> {
prefix.push(next); prefix.push(next);
} }
} }
'\'' if cur_quote.is_none() => { '\'' => {
cur_quote = Some('\''); qt_state.toggle_single();
prefix.push(ch); prefix.push(ch);
} }
'\'' if cur_quote == Some('\'') => { '"' => {
cur_quote = None; qt_state.toggle_double();
prefix.push(ch); prefix.push(ch);
} }
'"' if cur_quote.is_none() => { '{' if qt_state.outside() => {
cur_quote = Some('"');
prefix.push(ch);
}
'"' if cur_quote == Some('"') => {
cur_quote = None;
prefix.push(ch);
}
'{' if cur_quote.is_none() => {
break; break;
} }
_ => prefix.push(ch), _ => prefix.push(ch),
@@ -276,7 +266,7 @@ fn get_brace_parts(word: &str) -> Option<(String, String, String)> {
// Find matching closing brace // Find matching closing brace
let mut depth = 1; let mut depth = 1;
let mut inner = String::new(); let mut inner = String::new();
cur_quote = None; qt_state = QuoteState::default();
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
match ch { match ch {
@@ -286,27 +276,19 @@ fn get_brace_parts(word: &str) -> Option<(String, String, String)> {
inner.push(next); inner.push(next);
} }
} }
'\'' if cur_quote.is_none() => { '\'' => {
cur_quote = Some('\''); qt_state.toggle_single();
inner.push(ch); inner.push(ch);
} }
'\'' if cur_quote == Some('\'') => { '"' => {
cur_quote = None; qt_state.toggle_double();
inner.push(ch); inner.push(ch);
} }
'"' if cur_quote.is_none() => { '{' if qt_state.outside() => {
cur_quote = Some('"');
inner.push(ch);
}
'"' if cur_quote == Some('"') => {
cur_quote = None;
inner.push(ch);
}
'{' if cur_quote.is_none() => {
depth += 1; depth += 1;
inner.push(ch); inner.push(ch);
} }
'}' if cur_quote.is_none() => { '}' if qt_state.outside() => {
depth -= 1; depth -= 1;
if depth == 0 { if depth == 0 {
break; break;
@@ -335,7 +317,7 @@ fn split_brace_inner(inner: &str) -> Vec<String> {
let mut current = String::new(); let mut current = String::new();
let mut chars = inner.chars().peekable(); let mut chars = inner.chars().peekable();
let mut depth = 0; let mut depth = 0;
let mut cur_quote: Option<char> = None; let mut qt_state = QuoteState::default();
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
match ch { match ch {
@@ -345,31 +327,23 @@ fn split_brace_inner(inner: &str) -> Vec<String> {
current.push(next); current.push(next);
} }
} }
'\'' if cur_quote.is_none() => { '\'' => {
cur_quote = Some('\''); qt_state.toggle_single();
current.push(ch); current.push(ch);
} }
'\'' if cur_quote == Some('\'') => { '"' => {
cur_quote = None; qt_state.toggle_double();
current.push(ch); current.push(ch);
} }
'"' if cur_quote.is_none() => { '{' if qt_state.outside() => {
cur_quote = Some('"');
current.push(ch);
}
'"' if cur_quote == Some('"') => {
cur_quote = None;
current.push(ch);
}
'{' if cur_quote.is_none() => {
depth += 1; depth += 1;
current.push(ch); current.push(ch);
} }
'}' if cur_quote.is_none() => { '}' if qt_state.outside() => {
depth -= 1; depth -= 1;
current.push(ch); current.push(ch);
} }
',' if cur_quote.is_none() && depth == 0 => { ',' if qt_state.outside() && depth == 0 => {
parts.push(std::mem::take(&mut current)); parts.push(std::mem::take(&mut current));
} }
_ => current.push(ch), _ => current.push(ch),
@@ -556,6 +530,11 @@ pub fn expand_var(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
let arg_sep = markers::ARG_SEP.to_string(); let arg_sep = markers::ARG_SEP.to_string();
read_vars(|v| v.get_arr_elems(&var_name))?.join(&arg_sep) 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 => { ArrIndex::AllJoined => {
let ifs = read_vars(|v| v.try_get_var("IFS")) let ifs = read_vars(|v| v.try_get_var("IFS"))
.unwrap_or_else(|| " \t\n".to_string()) .unwrap_or_else(|| " \t\n".to_string())

View File

@@ -24,6 +24,46 @@ pub const KEYWORDS: [&str; 16] = [
pub const OPENERS: [&str; 6] = ["if", "while", "until", "for", "select", "case"]; 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) /// Span::new(10..20)
#[derive(Clone, PartialEq, Default, Debug)] #[derive(Clone, PartialEq, Default, Debug)]
pub struct Span { pub struct Span {
@@ -150,7 +190,7 @@ bitflags! {
pub struct LexStream { pub struct LexStream {
source: Arc<String>, source: Arc<String>,
pub cursor: usize, pub cursor: usize,
in_quote: bool, quote_state: QuoteState,
brc_grp_start: Option<usize>, brc_grp_start: Option<usize>,
flags: LexFlags, flags: LexFlags,
} }
@@ -183,11 +223,11 @@ impl LexStream {
pub fn new(source: Arc<String>, flags: LexFlags) -> Self { pub fn new(source: Arc<String>, flags: LexFlags) -> Self {
let flags = flags | LexFlags::FRESH | LexFlags::NEXT_IS_CMD; let flags = flags | LexFlags::FRESH | LexFlags::NEXT_IS_CMD;
Self { Self {
flags,
source, source,
cursor: 0, cursor: 0,
in_quote: false, quote_state: QuoteState::default(),
brc_grp_start: None, brc_grp_start: None,
flags,
} }
} }
/// Returns a slice of the source input using the given range /// Returns a slice of the source input using the given range
@@ -352,6 +392,47 @@ impl LexStream {
if let Some(ch) = chars.next() { if let Some(ch) = chars.next() {
pos += ch.len_utf8(); 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(&'{') => { '$' if chars.peek() == Some(&'{') => {
pos += 2; 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(&'(') => { '<' if chars.peek() == Some(&'(') => {
pos += 2; pos += 2;
chars.next(); 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 => { '(' if self.next_is_cmd() && can_be_subshell => {
pos += 1; pos += 1;
let mut paren_count = 1; let mut paren_count = 1;
@@ -547,82 +597,6 @@ impl LexStream {
self.cursor = pos; self.cursor = pos;
return Ok(tk); 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(&'(') => { '=' if chars.peek() == Some(&'(') => {
pos += 1; // '=' pos += 1; // '='
let mut depth = 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, _ if is_hard_sep(ch) => break,
_ => pos += ch.len_utf8(), _ => pos += ch.len_utf8(),
} }
} }
let mut new_tk = self.get_token(self.cursor..pos, TkRule::Str); 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; self.cursor = pos;
return Err(ShErr::full( return Err(ShErr::full(
ShErrKind::ParseErr, 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()) 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<String> { pub fn split_all_unescaped(slice: &str, pat: &str) -> Vec<String> {
let mut cursor = 0; let mut cursor = 0;
let mut splits = vec![]; let mut splits = vec![];
@@ -925,19 +900,71 @@ pub fn split_all_unescaped(slice: &str, pat: &str) -> Vec<String> {
splits 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)> { pub fn split_at_unescaped(slice: &str, pat: &str) -> Option<(String,String)> {
let mut window_start = 0; let mut chars = slice.char_indices().peekable();
let mut window_end = pat.len(); let mut qt_state = QuoteState::default();
if window_end > slice.len() {
return None; 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,
_ => {}
} }
while window_end <= slice.len() {
if &slice[window_start..window_end] == pat && !pos_is_escaped(slice, window_start) { if slice[i..].starts_with(pat) {
return Some((slice[..window_start].to_string(), slice[window_end..].to_string())); 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<Tk> {
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 None
} }

View File

@@ -1035,6 +1035,7 @@ impl ParseStream {
}; };
cond_nodes.push(cond_node); cond_nodes.push(cond_node);
self.catch_separator(&mut node_tks);
if !self.check_keyword("elif") || !self.next_tk_is_some() { if !self.check_keyword("elif") || !self.next_tk_is_some() {
break; break;
} else { } else {
@@ -1043,6 +1044,7 @@ impl ParseStream {
} }
} }
self.catch_separator(&mut node_tks);
if self.check_keyword("else") { if self.check_keyword("else") {
node_tks.push(self.next_tk().unwrap()); node_tks.push(self.next_tk().unwrap());
self.catch_separator(&mut node_tks); self.catch_separator(&mut node_tks);

View File

@@ -8,7 +8,7 @@ use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVis
use crate::expand::expand_prompt; use crate::expand::expand_prompt;
use crate::libsh::sys::TTY_FILENO; use crate::libsh::sys::TTY_FILENO;
use crate::parse::lex::LexStream; use crate::parse::lex::{LexStream, QuoteState};
use crate::prelude::*; use crate::prelude::*;
use crate::readline::term::{Pos, calc_str_width}; use crate::readline::term::{Pos, calc_str_width};
use crate::state::read_shopts; use crate::state::read_shopts;
@@ -339,8 +339,14 @@ impl ShedVi {
let line = self.editor.as_str().to_string(); let line = self.editor.as_str().to_string();
let cursor_pos = self.editor.cursor_byte_pos(); let cursor_pos = self.editor.cursor_byte_pos();
match self.completer.complete(line, cursor_pos, direction)? { match self.completer.complete(line, cursor_pos, direction) {
Some(line) => { 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 span_start = self.completer.token_span.0;
let new_cursor = span_start let new_cursor = span_start
+ self + self
@@ -361,7 +367,7 @@ impl ShedVi {
let hint = self.history.get_hint(); let hint = self.history.get_hint();
self.editor.set_hint(hint); self.editor.set_hint(hint);
} }
None => { Ok(None) => {
self.writer.send_bell().ok(); 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 span_start = token.span.start;
let mut in_dub_qt = false; let mut qt_state = QuoteState::default();
let mut in_sng_qt = false;
let mut cmd_sub_depth = 0; let mut cmd_sub_depth = 0;
let mut proc_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; let dollar_pos = index;
token_chars.next(); // consume the dollar token_chars.next(); // consume the dollar
if let Some((_, dollar_ch)) = token_chars.peek() { 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 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 token_chars.next(); // consume the backslash
if token_chars.peek().is_some() { if token_chars.peek().is_some() {
token_chars.next(); // consume the escaped char 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(); token_chars.next();
if let Some((_, proc_sub_ch)) = token_chars.peek() if let Some((_, proc_sub_ch)) = token_chars.peek()
&& *proc_sub_ch == '(' && *proc_sub_ch == '('
@@ -1133,25 +1138,25 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
} }
} }
} }
'"' if !in_sng_qt => { '"' if !qt_state.in_single() => {
if in_dub_qt { if qt_state.in_double() {
insertions.push((span_start + *i + 1, markers::STRING_DQ_END)); insertions.push((span_start + *i + 1, markers::STRING_DQ_END));
} else { } else {
insertions.push((span_start + *i, markers::STRING_DQ)); insertions.push((span_start + *i, markers::STRING_DQ));
} }
in_dub_qt = !in_dub_qt; qt_state.toggle_double();
token_chars.next(); // consume the quote token_chars.next(); // consume the quote
} }
'\'' if !in_dub_qt => { '\'' if !qt_state.in_double() => {
if in_sng_qt { if qt_state.in_single() {
insertions.push((span_start + *i + 1, markers::STRING_SQ_END)); insertions.push((span_start + *i + 1, markers::STRING_SQ_END));
} else { } else {
insertions.push((span_start + *i, markers::STRING_SQ)); insertions.push((span_start + *i, markers::STRING_SQ));
} }
in_sng_qt = !in_sng_qt; qt_state.toggle_single();
token_chars.next(); // consume the quote 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 token_chars.next(); // consume the opening bracket
let start_pos = span_start + index; let start_pos = span_start + index;
let mut is_glob_pat = false; 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)); insertions.push((start_pos, markers::GLOB));
} }
} }
'*' | '?' if (!in_dub_qt && !in_sng_qt) => { '*' | '?' if !qt_state.in_quote() => {
let glob_ch = *ch; let glob_ch = *ch;
token_chars.next(); // consume the first glob char token_chars.next(); // consume the first glob char
if !in_context(markers::COMMAND, &insertions) { if !in_context(markers::COMMAND, &insertions) {

View File

@@ -620,6 +620,7 @@ impl VarFlags {
pub enum ArrIndex { pub enum ArrIndex {
Literal(usize), Literal(usize),
FromBack(usize), FromBack(usize),
ArgCount,
AllJoined, AllJoined,
AllSplit, AllSplit,
} }
@@ -630,6 +631,7 @@ impl FromStr for ArrIndex {
match s { match s {
"@" => Ok(Self::AllSplit), "@" => Ok(Self::AllSplit),
"*" => Ok(Self::AllJoined), "*" => Ok(Self::AllJoined),
"#" => Ok(Self::ArgCount),
_ if s.starts_with('-') && s[1..].chars().all(|c| c.is_digit(1)) => { _ if s.starts_with('-') && s[1..].chars().all(|c| c.is_digit(1)) => {
let idx = s[1..].parse::<usize>().unwrap(); let idx = s[1..].parse::<usize>().unwrap();
Ok(Self::FromBack(idx)) Ok(Self::FromBack(idx))