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:
11
Cargo.lock
generated
11
Cargo.lock
generated
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()) {
|
||||
|
||||
@@ -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::<ShResult<Vec<String>>>()?;
|
||||
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<bool> {
|
||||
if let Some(map) = v.get_map_mut(name) {
|
||||
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());
|
||||
} 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::<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();
|
||||
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<String> = 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)
|
||||
));
|
||||
};
|
||||
|
||||
|
||||
@@ -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<RedirGuard>)>;
|
||||
type SetupReturns = ShResult<(Option<Vec<(String, Span)>>, Option<RedirGuard>)>;
|
||||
pub fn setup_builtin(
|
||||
argv: Vec<Tk>,
|
||||
argv: Option<Vec<Tk>>,
|
||||
job: &mut JobBldr,
|
||||
io_mode: Option<(&mut IoStack, Vec<Redir>)>,
|
||||
) -> 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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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())?;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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())?;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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))?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
113
src/expand.rs
113
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<char> = 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<Vec<String>> {
|
||||
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<char> = 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<String> {
|
||||
let mut current = String::new();
|
||||
let mut chars = inner.chars().peekable();
|
||||
let mut depth = 0;
|
||||
let mut cur_quote: Option<char> = 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<String> {
|
||||
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<Chars<'_>>) -> ShResult<String> {
|
||||
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())
|
||||
|
||||
281
src/parse/lex.rs
281
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<String>,
|
||||
pub cursor: usize,
|
||||
in_quote: bool,
|
||||
quote_state: QuoteState,
|
||||
brc_grp_start: Option<usize>,
|
||||
flags: LexFlags,
|
||||
}
|
||||
@@ -183,11 +223,11 @@ impl LexStream {
|
||||
pub fn new(source: Arc<String>, 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<String> {
|
||||
let mut cursor = 0;
|
||||
let mut splits = vec![];
|
||||
@@ -925,19 +900,71 @@ pub fn split_all_unescaped(slice: &str, pat: &str) -> Vec<String> {
|
||||
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<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
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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::<usize>().unwrap();
|
||||
Ok(Self::FromBack(idx))
|
||||
|
||||
Reference in New Issue
Block a user