From c5081802282a7bf3fa1290ba88b0d9a170c12397 Mon Sep 17 00:00:00 2001 From: pagedmov Date: Fri, 27 Feb 2026 11:03:56 -0500 Subject: [PATCH] Added -j flag to 'complete' for completing job names/pids --- src/builtin/alias.rs | 2 +- src/builtin/complete.rs | 464 ++++++++++--------- src/builtin/dirstack.rs | 705 ++++++++++++++-------------- src/builtin/echo.rs | 8 +- src/builtin/eval.rs | 2 +- src/builtin/exec.rs | 2 +- src/builtin/flowctl.rs | 4 +- src/builtin/jobctl.rs | 4 +- src/builtin/mod.rs | 32 +- src/builtin/pwd.rs | 2 +- src/builtin/read.rs | 15 +- src/builtin/shopt.rs | 13 +- src/builtin/test.rs | 2 +- src/builtin/trap.rs | 12 +- src/builtin/varcmds.rs | 48 +- src/builtin/zoltraak.rs | 4 +- src/expand.rs | 201 ++++---- src/getopt.rs | 13 +- src/jobs.rs | 5 +- src/libsh/error.rs | 9 +- src/libsh/utils.rs | 39 +- src/main.rs | 417 ++++++++--------- src/parse/execute.rs | 779 ++++++++++++++++--------------- src/parse/lex.rs | 137 +++--- src/parse/mod.rs | 33 +- src/prelude.rs | 10 +- src/procio.rs | 10 +- src/readline/complete.rs | 878 +++++++++++++++++++---------------- src/readline/highlight.rs | 204 +++++---- src/readline/linebuf.rs | 454 +++++++++--------- src/readline/mod.rs | 440 ++++++++++-------- src/readline/term.rs | 68 +-- src/readline/vicmd.rs | 9 +- src/readline/vimode.rs | 20 +- src/shopt.rs | 19 +- src/signal.rs | 12 +- src/state.rs | 940 +++++++++++++++++++++----------------- src/tests/complete.rs | 22 +- src/tests/error.rs | 4 +- src/tests/expand.rs | 38 +- src/tests/mod.rs | 3 +- src/tests/readline.rs | 23 +- src/tests/redir.rs | 2 +- src/tests/state.rs | 4 +- 44 files changed, 3259 insertions(+), 2853 deletions(-) diff --git a/src/builtin/alias.rs b/src/builtin/alias.rs index 412726c..f56f491 100644 --- a/src/builtin/alias.rs +++ b/src/builtin/alias.rs @@ -3,7 +3,7 @@ use crate::{ libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node}, prelude::*, - procio::{borrow_fd, IoStack}, + procio::{IoStack, borrow_fd}, state::{self, read_logic, write_logic}, }; diff --git a/src/builtin/complete.rs b/src/builtin/complete.rs index 6da5d5f..7435bd5 100644 --- a/src/builtin/complete.rs +++ b/src/builtin/complete.rs @@ -1,120 +1,138 @@ use bitflags::bitflags; use nix::{libc::STDOUT_FILENO, unistd::write}; -use crate::{builtin::setup_builtin, getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node}, procio::{IoStack, borrow_fd}, readline::complete::{BashCompSpec, CompContext, CompSpec}, state::{self, read_meta, write_meta}}; +use crate::{ + builtin::setup_builtin, + getopt::{Opt, OptSpec, get_opts_from_tokens}, + jobs::JobBldr, + libsh::error::{ShErr, ShErrKind, ShResult}, + parse::{NdRule, Node}, + procio::{IoStack, borrow_fd}, + readline::complete::{BashCompSpec, CompContext, CompSpec}, + state::{self, read_meta, write_meta}, +}; -pub const COMPGEN_OPTS: [OptSpec;8] = [ - OptSpec { - opt: Opt::Short('F'), - takes_arg: true - }, - OptSpec { - opt: Opt::Short('W'), - takes_arg: true - }, - OptSpec { - opt: Opt::Short('f'), - takes_arg: false - }, - OptSpec { - opt: Opt::Short('d'), - takes_arg: false - }, - OptSpec { - opt: Opt::Short('c'), - takes_arg: false - }, - OptSpec { - opt: Opt::Short('u'), - takes_arg: false - }, - OptSpec { - opt: Opt::Short('v'), - takes_arg: false - }, - OptSpec { - opt: Opt::Short('o'), - takes_arg: true - } +pub const COMPGEN_OPTS: [OptSpec; 9] = [ + OptSpec { + opt: Opt::Short('F'), + takes_arg: true, + }, + OptSpec { + opt: Opt::Short('W'), + takes_arg: true, + }, + OptSpec { + opt: Opt::Short('j'), + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('f'), + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('d'), + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('c'), + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('u'), + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('v'), + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('o'), + takes_arg: true, + }, ]; -pub const COMP_OPTS: [OptSpec;11] = [ - OptSpec { - opt: Opt::Short('F'), - takes_arg: true - }, - OptSpec { - opt: Opt::Short('W'), - takes_arg: true - }, - OptSpec { - opt: Opt::Short('A'), - takes_arg: true - }, - OptSpec { - opt: Opt::Short('p'), - takes_arg: false - }, - OptSpec { - opt: Opt::Short('r'), - takes_arg: false - }, - OptSpec { - opt: Opt::Short('f'), - takes_arg: false - }, - OptSpec { - opt: Opt::Short('d'), - takes_arg: false - }, - OptSpec { - opt: Opt::Short('c'), - takes_arg: false - }, - OptSpec { - opt: Opt::Short('u'), - takes_arg: false - }, - OptSpec { - opt: Opt::Short('v'), - takes_arg: false - }, - OptSpec { - opt: Opt::Short('o'), - takes_arg: true - } +pub const COMP_OPTS: [OptSpec; 12] = [ + OptSpec { + opt: Opt::Short('F'), + takes_arg: true, + }, + OptSpec { + opt: Opt::Short('W'), + takes_arg: true, + }, + OptSpec { + opt: Opt::Short('A'), + takes_arg: true, + }, + OptSpec { + opt: Opt::Short('j'), + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('p'), + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('r'), + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('f'), + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('d'), + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('c'), + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('u'), + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('v'), + takes_arg: false, + }, + OptSpec { + opt: Opt::Short('o'), + takes_arg: true, + }, ]; bitflags! { - #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] - pub struct CompFlags: u32 { - const FILES = 0b0000000001; - const DIRS = 0b0000000010; - const CMDS = 0b0000000100; - const USERS = 0b0000001000; - const VARS = 0b0000010000; - const PRINT = 0b0000100000; - const REMOVE = 0b0001000000; - } - #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] - pub struct CompOptFlags: u32 { - const DEFAULT = 0b0000000001; - const DIRNAMES = 0b0000000010; - const NOSPACE = 0b0000000100; - } + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct CompFlags: u32 { + const FILES = 0b0000000001; + const DIRS = 0b0000000010; + const CMDS = 0b0000000100; + const USERS = 0b0000001000; + const VARS = 0b0000010000; + const JOBS = 0b0000100000; + const PRINT = 0b0001000000; + const REMOVE = 0b0010000000; + } + #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] + pub struct CompOptFlags: u32 { + const DEFAULT = 0b0000000001; + const DIRNAMES = 0b0000000010; + const NOSPACE = 0b0000000100; + } } #[derive(Default, Debug, Clone)] pub struct CompOpts { - pub func: Option, - pub wordlist: Option>, - pub action: Option, - pub flags: CompFlags, - pub opt_flags: CompOptFlags, + pub func: Option, + pub wordlist: Option>, + pub action: Option, + pub flags: CompFlags, + pub opt_flags: CompOptFlags, } pub fn complete_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { - let blame = node.get_span().clone(); + let blame = node.get_span().clone(); let NdRule::Command { assignments: _, argv, @@ -123,152 +141,150 @@ pub fn complete_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) - unreachable!() }; assert!(!argv.is_empty()); - let src = argv.clone() - .into_iter() - .map(|tk| tk.expand().map(|tk| tk.get_words().join(" "))) - .collect::>>()? - .join(" "); + let src = argv + .clone() + .into_iter() + .map(|tk| tk.expand().map(|tk| tk.get_words().join(" "))) + .collect::>>()? + .join(" "); 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 comp_opts = get_comp_opts(opts)?; + let (argv, _) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; - if comp_opts.flags.contains(CompFlags::PRINT) { - if argv.is_empty() { - read_meta(|m| { - let specs = m.comp_specs().values(); - for spec in specs { - println!("{}", spec.source()); - } - }) - } else { - read_meta(|m| { - for (cmd,_) in &argv { - if let Some(spec) = m.comp_specs().get(cmd) { - println!("{}", spec.source()); - } - } - }) - } + if comp_opts.flags.contains(CompFlags::PRINT) { + if argv.is_empty() { + read_meta(|m| { + let specs = m.comp_specs().values(); + for spec in specs { + println!("{}", spec.source()); + } + }) + } else { + read_meta(|m| { + for (cmd, _) in &argv { + if let Some(spec) = m.comp_specs().get(cmd) { + println!("{}", spec.source()); + } + } + }) + } - state::set_status(0); - return Ok(()); - } + state::set_status(0); + return Ok(()); + } - if comp_opts.flags.contains(CompFlags::REMOVE) { - write_meta(|m| { - for (cmd,_) in &argv { - m.remove_comp_spec(cmd); - } - }); + if comp_opts.flags.contains(CompFlags::REMOVE) { + write_meta(|m| { + for (cmd, _) in &argv { + m.remove_comp_spec(cmd); + } + }); - state::set_status(0); - return Ok(()); - } + state::set_status(0); + return Ok(()); + } - if argv.is_empty() { - state::set_status(1); - return Err(ShErr::full(ShErrKind::ExecFail, "complete: no command specified", blame)); - } + if argv.is_empty() { + state::set_status(1); + return Err(ShErr::full( + ShErrKind::ExecFail, + "complete: no command specified", + blame, + )); + } - let comp_spec = BashCompSpec::from_comp_opts(comp_opts) - .with_source(src); + let comp_spec = BashCompSpec::from_comp_opts(comp_opts).with_source(src); - for (cmd,_) in argv { - write_meta(|m| m.set_comp_spec(cmd, Box::new(comp_spec.clone()))); - } + for (cmd, _) in argv { + write_meta(|m| m.set_comp_spec(cmd, Box::new(comp_spec.clone()))); + } - state::set_status(0); - Ok(()) + state::set_status(0); + Ok(()) } pub fn compgen_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { - let blame = node.get_span().clone(); - let NdRule::Command { - assignments: _, - argv, - } = node.class - else { - unreachable!() - }; - assert!(!argv.is_empty()); - let src = argv.clone() - .into_iter() - .map(|tk| tk.expand().map(|tk| tk.get_words().join(" "))) - .collect::>>()? - .join(" "); + let blame = node.get_span().clone(); + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; + assert!(!argv.is_empty()); + let src = argv + .clone() + .into_iter() + .map(|tk| tk.expand().map(|tk| tk.get_words().join(" "))) + .collect::>>()? + .join(" "); - 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 (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 comp_spec = BashCompSpec::from_comp_opts(comp_opts).with_source(src); - let comp_spec = BashCompSpec::from_comp_opts(comp_opts) - .with_source(src); + let dummy_ctx = CompContext { + words: vec![prefix.clone()], + cword: 0, + line: prefix.to_string(), + cursor_pos: prefix.as_str().len(), + }; - let dummy_ctx = CompContext { - words: vec![prefix.clone()], - cword: 0, - line: prefix.to_string(), - cursor_pos: prefix.as_str().len() - }; + let results = comp_spec.complete(&dummy_ctx)?; - let results = comp_spec.complete(&dummy_ctx)?; + let stdout = borrow_fd(STDOUT_FILENO); + for result in &results { + write(stdout, result.as_bytes())?; + write(stdout, b"\n")?; + } - let stdout = borrow_fd(STDOUT_FILENO); - for result in &results { - write(stdout, result.as_bytes())?; - write(stdout, b"\n")?; - } - - state::set_status(0); - Ok(()) + state::set_status(0); + Ok(()) } pub fn get_comp_opts(opts: Vec) -> ShResult { - let mut comp_opts = CompOpts::default(); + let mut comp_opts = CompOpts::default(); - for opt in opts { - match opt { - Opt::ShortWithArg('F',func) => { - comp_opts.func = Some(func); - }, - Opt::ShortWithArg('W',wordlist) => { - comp_opts.wordlist = Some(wordlist.split_whitespace().map(|s| s.to_string()).collect()); - }, - Opt::ShortWithArg('A',action) => { - comp_opts.action = Some(action); - } - Opt::ShortWithArg('o', opt_flag) => { - match opt_flag.as_str() { - "default" => comp_opts.opt_flags |= CompOptFlags::DEFAULT, - "dirnames" => comp_opts.opt_flags |= CompOptFlags::DIRNAMES, - "nospace" => comp_opts.opt_flags |= CompOptFlags::NOSPACE, - _ => { - return Err(ShErr::full( - ShErrKind::InvalidOpt, - format!("complete: invalid option: {}", opt_flag), - Default::default() - )); - } - } - } + for opt in opts { + match opt { + Opt::ShortWithArg('F', func) => { + comp_opts.func = Some(func); + } + Opt::ShortWithArg('W', wordlist) => { + comp_opts.wordlist = Some(wordlist.split_whitespace().map(|s| s.to_string()).collect()); + } + Opt::ShortWithArg('A', action) => { + comp_opts.action = Some(action); + } + Opt::ShortWithArg('o', opt_flag) => match opt_flag.as_str() { + "default" => comp_opts.opt_flags |= CompOptFlags::DEFAULT, + "dirnames" => comp_opts.opt_flags |= CompOptFlags::DIRNAMES, + "nospace" => comp_opts.opt_flags |= CompOptFlags::NOSPACE, + _ => { + return Err(ShErr::full( + ShErrKind::InvalidOpt, + format!("complete: invalid option: {}", opt_flag), + Default::default(), + )); + } + }, - Opt::Short('r') => comp_opts.flags |= CompFlags::REMOVE, - Opt::Short('p') => comp_opts.flags |= CompFlags::PRINT, - Opt::Short('f') => comp_opts.flags |= CompFlags::FILES, - Opt::Short('d') => comp_opts.flags |= CompFlags::DIRS, - Opt::Short('c') => comp_opts.flags |= CompFlags::CMDS, - Opt::Short('u') => comp_opts.flags |= CompFlags::USERS, - Opt::Short('v') => comp_opts.flags |= CompFlags::VARS, - _ => unreachable!() - } - } + Opt::Short('r') => comp_opts.flags |= CompFlags::REMOVE, + Opt::Short('j') => comp_opts.flags |= CompFlags::JOBS, + Opt::Short('p') => comp_opts.flags |= CompFlags::PRINT, + Opt::Short('f') => comp_opts.flags |= CompFlags::FILES, + Opt::Short('d') => comp_opts.flags |= CompFlags::DIRS, + Opt::Short('c') => comp_opts.flags |= CompFlags::CMDS, + Opt::Short('u') => comp_opts.flags |= CompFlags::USERS, + Opt::Short('v') => comp_opts.flags |= CompFlags::VARS, + _ => unreachable!(), + } + } - Ok(comp_opts) + Ok(comp_opts) } diff --git a/src/builtin/dirstack.rs b/src/builtin/dirstack.rs index a1c23bc..110ceb1 100644 --- a/src/builtin/dirstack.rs +++ b/src/builtin/dirstack.rs @@ -2,396 +2,427 @@ use std::{env, path::PathBuf}; use nix::{libc::STDOUT_FILENO, unistd::write}; -use crate::{builtin::setup_builtin, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node, lex::Span}, procio::{IoStack, borrow_fd}, state::{self, read_meta, write_meta}}; +use crate::{ + builtin::setup_builtin, + jobs::JobBldr, + libsh::error::{ShErr, ShErrKind, ShResult}, + parse::{NdRule, Node, lex::Span}, + procio::{IoStack, borrow_fd}, + state::{self, read_meta, write_meta}, +}; enum StackIdx { - FromTop(usize), - FromBottom(usize), + FromTop(usize), + FromBottom(usize), } fn print_dirs() -> ShResult<()> { - let current_dir = env::current_dir()?; - let dirs_iter = read_meta(|m| { - m.dirs() - .clone() - .into_iter() - }); - let all_dirs = [current_dir].into_iter().chain(dirs_iter) - .map(|d| d.to_string_lossy().to_string()) - .map(|d| { - let Ok(home) = env::var("HOME") else { - return d; - }; + let current_dir = env::current_dir()?; + let dirs_iter = read_meta(|m| m.dirs().clone().into_iter()); + let all_dirs = [current_dir] + .into_iter() + .chain(dirs_iter) + .map(|d| d.to_string_lossy().to_string()) + .map(|d| { + let Ok(home) = env::var("HOME") else { + return d; + }; - if d.starts_with(&home) { - let new = d.strip_prefix(&home).unwrap(); - format!("~{new}") - } else { - d - } - }).collect::>() - .join(" "); + if d.starts_with(&home) { + let new = d.strip_prefix(&home).unwrap(); + format!("~{new}") + } else { + d + } + }) + .collect::>() + .join(" "); - let stdout = borrow_fd(STDOUT_FILENO); - write(stdout, all_dirs.as_bytes())?; - write(stdout, b"\n")?; + let stdout = borrow_fd(STDOUT_FILENO); + write(stdout, all_dirs.as_bytes())?; + write(stdout, b"\n")?; - Ok(()) + Ok(()) } fn change_directory(target: &PathBuf, blame: Span) -> ShResult<()> { - if !target.is_dir() { - return Err(ShErr::full( - ShErrKind::ExecFail, - format!("not a directory: {}", target.display()), - blame, - )); - } + if !target.is_dir() { + return Err(ShErr::full( + ShErrKind::ExecFail, + format!("not a directory: {}", target.display()), + blame, + )); + } - if let Err(e) = env::set_current_dir(target) { - return Err(ShErr::full( - ShErrKind::ExecFail, - format!("Failed to change directory: {}", e), - blame, - )); - } - let new_dir = env::current_dir().map_err(|e| { - ShErr::full( - ShErrKind::ExecFail, - format!("Failed to get current directory: {}", e), - blame, - ) - })?; - unsafe { env::set_var("PWD", new_dir) }; - Ok(()) + if let Err(e) = env::set_current_dir(target) { + return Err(ShErr::full( + ShErrKind::ExecFail, + format!("Failed to change directory: {}", e), + blame, + )); + } + let new_dir = env::current_dir().map_err(|e| { + ShErr::full( + ShErrKind::ExecFail, + format!("Failed to get current directory: {}", e), + blame, + ) + })?; + unsafe { env::set_var("PWD", new_dir) }; + Ok(()) } fn parse_stack_idx(arg: &str, blame: Span, cmd: &str) -> ShResult { - let (from_top, digits) = if let Some(rest) = arg.strip_prefix('+') { - (true, rest) - } else if let Some(rest) = arg.strip_prefix('-') { - (false, rest) - } else { - unreachable!() - }; + let (from_top, digits) = if let Some(rest) = arg.strip_prefix('+') { + (true, rest) + } else if let Some(rest) = arg.strip_prefix('-') { + (false, rest) + } else { + unreachable!() + }; - if digits.is_empty() { - return Err(ShErr::full( - ShErrKind::ExecFail, - format!("{cmd}: missing index after '{}'", if from_top { "+" } else { "-" }), - blame, - )); - } + if digits.is_empty() { + return Err(ShErr::full( + ShErrKind::ExecFail, + format!( + "{cmd}: missing index after '{}'", + if from_top { "+" } else { "-" } + ), + blame, + )); + } - for ch in digits.chars() { - if !ch.is_ascii_digit() { - return Err(ShErr::full( - ShErrKind::ExecFail, - format!("{cmd}: invalid argument: {arg}"), - blame, - )); - } - } + for ch in digits.chars() { + if !ch.is_ascii_digit() { + return Err(ShErr::full( + ShErrKind::ExecFail, + format!("{cmd}: invalid argument: {arg}"), + blame, + )); + } + } - let n = digits.parse::().map_err(|e| { - ShErr::full( - ShErrKind::ExecFail, - format!("{cmd}: invalid index: {e}"), - blame, - ) - })?; + let n = digits.parse::().map_err(|e| { + ShErr::full( + ShErrKind::ExecFail, + format!("{cmd}: invalid index: {e}"), + blame, + ) + })?; - if from_top { - Ok(StackIdx::FromTop(n)) - } else { - Ok(StackIdx::FromBottom(n)) - } + if from_top { + Ok(StackIdx::FromTop(n)) + } else { + Ok(StackIdx::FromBottom(n)) + } } pub fn pushd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { - let blame = node.get_span().clone(); - let NdRule::Command { - assignments: _, - argv - } = node.class else { unreachable!() }; + let blame = node.get_span().clone(); + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; - let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; - let mut dir = None; - let mut rotate_idx = None; - let mut no_cd = false; + let mut dir = None; + let mut rotate_idx = None; + let mut no_cd = false; - for (arg, _) in argv { - if arg.starts_with('+') || (arg.starts_with('-') && arg.len() > 1 && arg.as_bytes()[1].is_ascii_digit()) { - rotate_idx = Some(parse_stack_idx(&arg, blame.clone(), "pushd")?); - } else if arg == "-n" { - no_cd = true; - } else if arg.starts_with('-') { - return Err(ShErr::full( - ShErrKind::ExecFail, - format!("pushd: invalid option: {arg}"), - blame.clone(), - )); - } else { - if dir.is_some() { - return Err(ShErr::full( - ShErrKind::ExecFail, - "pushd: too many arguments".to_string(), - blame.clone(), - )); - } - let target = PathBuf::from(&arg); - if !target.is_dir() { - return Err(ShErr::full( - ShErrKind::ExecFail, - format!("pushd: not a directory: {arg}"), - blame.clone(), - )); - } - dir = Some(target); - } - } + for (arg, _) in argv { + if arg.starts_with('+') + || (arg.starts_with('-') && arg.len() > 1 && arg.as_bytes()[1].is_ascii_digit()) + { + rotate_idx = Some(parse_stack_idx(&arg, blame.clone(), "pushd")?); + } else if arg == "-n" { + no_cd = true; + } else if arg.starts_with('-') { + return Err(ShErr::full( + ShErrKind::ExecFail, + format!("pushd: invalid option: {arg}"), + blame.clone(), + )); + } else { + if dir.is_some() { + return Err(ShErr::full( + ShErrKind::ExecFail, + "pushd: too many arguments".to_string(), + blame.clone(), + )); + } + let target = PathBuf::from(&arg); + if !target.is_dir() { + return Err(ShErr::full( + ShErrKind::ExecFail, + format!("pushd: not a directory: {arg}"), + blame.clone(), + )); + } + dir = Some(target); + } + } - if let Some(idx) = rotate_idx { - let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("/")); - let new_cwd = write_meta(|m| { - let dirs = m.dirs_mut(); - dirs.push_front(cwd); - match idx { - StackIdx::FromTop(n) => dirs.rotate_left(n), - StackIdx::FromBottom(n) => dirs.rotate_right(n + 1), - } - dirs.pop_front() - }); + if let Some(idx) = rotate_idx { + let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("/")); + let new_cwd = write_meta(|m| { + let dirs = m.dirs_mut(); + dirs.push_front(cwd); + match idx { + StackIdx::FromTop(n) => dirs.rotate_left(n), + StackIdx::FromBottom(n) => dirs.rotate_right(n + 1), + } + dirs.pop_front() + }); - if let Some(dir) = new_cwd - && !no_cd { - change_directory(&dir, blame)?; - print_dirs()?; - } - } else if let Some(dir) = dir { - let old_dir = env::current_dir()?; - if old_dir != dir { - write_meta(|m| m.push_dir(old_dir)); - } + if let Some(dir) = new_cwd + && !no_cd + { + change_directory(&dir, blame)?; + print_dirs()?; + } + } else if let Some(dir) = dir { + let old_dir = env::current_dir()?; + if old_dir != dir { + write_meta(|m| m.push_dir(old_dir)); + } - if no_cd { - state::set_status(0); - return Ok(()); - } + if no_cd { + state::set_status(0); + return Ok(()); + } - change_directory(&dir, blame)?; - print_dirs()?; - } + change_directory(&dir, blame)?; + print_dirs()?; + } - state::set_status(0); - Ok(()) + state::set_status(0); + Ok(()) } pub fn popd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { - let blame = node.get_span().clone(); - let NdRule::Command { - assignments: _, - argv - } = node.class else { unreachable!() }; + let blame = node.get_span().clone(); + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; - let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; - let mut remove_idx = None; - let mut no_cd = false; + let mut remove_idx = None; + let mut no_cd = false; - for (arg, _) in argv { - if arg.starts_with('+') || (arg.starts_with('-') && arg.len() > 1 && arg.as_bytes()[1].is_ascii_digit()) { - remove_idx = Some(parse_stack_idx(&arg, blame.clone(), "popd")?); - } else if arg == "-n" { - no_cd = true; - } else if arg.starts_with('-') { - return Err(ShErr::full( - ShErrKind::ExecFail, - format!("popd: invalid option: {arg}"), - blame.clone(), - )); - } - } + for (arg, _) in argv { + if arg.starts_with('+') + || (arg.starts_with('-') && arg.len() > 1 && arg.as_bytes()[1].is_ascii_digit()) + { + remove_idx = Some(parse_stack_idx(&arg, blame.clone(), "popd")?); + } else if arg == "-n" { + no_cd = true; + } else if arg.starts_with('-') { + return Err(ShErr::full( + ShErrKind::ExecFail, + format!("popd: invalid option: {arg}"), + blame.clone(), + )); + } + } - if let Some(idx) = remove_idx { - match idx { - StackIdx::FromTop(0) => { - // +0 is same as plain popd: pop top, cd to it - let dir = write_meta(|m| m.pop_dir()); - if !no_cd { - if let Some(dir) = dir { - change_directory(&dir, blame.clone())?; - } else { - return Err(ShErr::full( - ShErrKind::ExecFail, - "popd: directory stack empty".to_string(), - blame.clone(), - )); - } - } - } - StackIdx::FromTop(n) => { - // +N (N>0): remove (N-1)th stored entry, no cd - write_meta(|m| { - let dirs = m.dirs_mut(); - let idx = n - 1; - if idx >= dirs.len() { - return Err(ShErr::full( - ShErrKind::ExecFail, - format!("popd: directory index out of range: +{n}"), - blame.clone(), - )); - } - dirs.remove(idx); - Ok(()) - })?; - } - StackIdx::FromBottom(n) => { - write_meta(|m| -> ShResult<()> { - let dirs = m.dirs_mut(); - let actual = dirs.len().checked_sub(n + 1).ok_or_else(|| { - ShErr::full( - ShErrKind::ExecFail, - format!("popd: directory index out of range: -{n}"), - blame.clone(), - ) - })?; - dirs.remove(actual); - Ok(()) - })?; - } - } - print_dirs()?; - } else { - let dir = write_meta(|m| m.pop_dir()); + if let Some(idx) = remove_idx { + match idx { + StackIdx::FromTop(0) => { + // +0 is same as plain popd: pop top, cd to it + let dir = write_meta(|m| m.pop_dir()); + if !no_cd { + if let Some(dir) = dir { + change_directory(&dir, blame.clone())?; + } else { + return Err(ShErr::full( + ShErrKind::ExecFail, + "popd: directory stack empty".to_string(), + blame.clone(), + )); + } + } + } + StackIdx::FromTop(n) => { + // +N (N>0): remove (N-1)th stored entry, no cd + write_meta(|m| { + let dirs = m.dirs_mut(); + let idx = n - 1; + if idx >= dirs.len() { + return Err(ShErr::full( + ShErrKind::ExecFail, + format!("popd: directory index out of range: +{n}"), + blame.clone(), + )); + } + dirs.remove(idx); + Ok(()) + })?; + } + StackIdx::FromBottom(n) => { + write_meta(|m| -> ShResult<()> { + let dirs = m.dirs_mut(); + let actual = dirs.len().checked_sub(n + 1).ok_or_else(|| { + ShErr::full( + ShErrKind::ExecFail, + format!("popd: directory index out of range: -{n}"), + blame.clone(), + ) + })?; + dirs.remove(actual); + Ok(()) + })?; + } + } + print_dirs()?; + } else { + let dir = write_meta(|m| m.pop_dir()); - if no_cd { - state::set_status(0); - return Ok(()); - } + if no_cd { + state::set_status(0); + return Ok(()); + } - if let Some(dir) = dir { - change_directory(&dir, blame.clone())?; - print_dirs()?; - } else { - return Err(ShErr::full( - ShErrKind::ExecFail, - "popd: directory stack empty".to_string(), - blame.clone(), - )); - } - } + if let Some(dir) = dir { + change_directory(&dir, blame.clone())?; + print_dirs()?; + } else { + return Err(ShErr::full( + ShErrKind::ExecFail, + "popd: directory stack empty".to_string(), + blame.clone(), + )); + } + } - Ok(()) + Ok(()) } pub fn dirs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { - let blame = node.get_span().clone(); - let NdRule::Command { - assignments: _, - argv - } = node.class else { unreachable!() }; + let blame = node.get_span().clone(); + let NdRule::Command { + assignments: _, + argv, + } = node.class + else { + unreachable!() + }; - let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; - let mut abbreviate_home = true; - let mut one_per_line = false; - let mut one_per_line_indexed = false; - let mut clear_stack = false; - let mut target_idx: Option = None; + let mut abbreviate_home = true; + let mut one_per_line = false; + let mut one_per_line_indexed = false; + let mut clear_stack = false; + let mut target_idx: Option = None; - for (arg,_) in argv { - match arg.as_str() { - "-p" => one_per_line = true, - "-v" => one_per_line_indexed = true, - "-c" => clear_stack = true, - "-l" => abbreviate_home = false, - _ if (arg.starts_with('+') || arg.starts_with('-')) && arg.len() > 1 && arg.as_bytes()[1].is_ascii_digit() => { - target_idx = Some(parse_stack_idx(&arg, blame.clone(), "dirs")?); - } - _ if arg.starts_with('-') => { - return Err(ShErr::full( - ShErrKind::ExecFail, - format!("dirs: invalid option: {arg}"), - blame.clone(), - )); - } - _ => { - return Err(ShErr::full( - ShErrKind::ExecFail, - format!("dirs: unexpected argument: {arg}"), - blame.clone(), - )); - } - } - } + for (arg, _) in argv { + match arg.as_str() { + "-p" => one_per_line = true, + "-v" => one_per_line_indexed = true, + "-c" => clear_stack = true, + "-l" => abbreviate_home = false, + _ if (arg.starts_with('+') || arg.starts_with('-')) + && arg.len() > 1 + && arg.as_bytes()[1].is_ascii_digit() => + { + target_idx = Some(parse_stack_idx(&arg, blame.clone(), "dirs")?); + } + _ if arg.starts_with('-') => { + return Err(ShErr::full( + ShErrKind::ExecFail, + format!("dirs: invalid option: {arg}"), + blame.clone(), + )); + } + _ => { + return Err(ShErr::full( + ShErrKind::ExecFail, + format!("dirs: unexpected argument: {arg}"), + blame.clone(), + )); + } + } + } - if clear_stack { - write_meta(|m| m.dirs_mut().clear()); - return Ok(()) - } + if clear_stack { + write_meta(|m| m.dirs_mut().clear()); + return Ok(()); + } + let mut dirs: Vec = read_meta(|m| { + let current_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from("/")); + let stack = [current_dir] + .into_iter() + .chain(m.dirs().clone()) + .map(|d| d.to_string_lossy().to_string()); - let mut dirs: Vec = read_meta(|m| { - let current_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from("/")); - let stack = [current_dir].into_iter() - .chain(m.dirs().clone()) - .map(|d| d.to_string_lossy().to_string()); + if abbreviate_home { + let Ok(home) = env::var("HOME") else { + return stack.collect(); + }; + stack + .map(|d| { + if d.starts_with(&home) { + let new = d.strip_prefix(&home).unwrap(); + format!("~{new}") + } else { + d + } + }) + .collect() + } else { + stack.collect() + } + }); - if abbreviate_home { - let Ok(home) = env::var("HOME") else { - return stack.collect(); - }; - stack.map(|d| { - if d.starts_with(&home) { - let new = d.strip_prefix(&home).unwrap(); - format!("~{new}") - } else { - d - } - }).collect() - } else { - stack.collect() - } - }); + if let Some(idx) = target_idx { + let target = match idx { + StackIdx::FromTop(n) => dirs.get(n), + StackIdx::FromBottom(n) => dirs.get(dirs.len().saturating_sub(n + 1)), + }; - if let Some(idx) = target_idx { - let target = match idx { - StackIdx::FromTop(n) => dirs.get(n), - StackIdx::FromBottom(n) => dirs.get(dirs.len().saturating_sub(n + 1)), - }; + if let Some(dir) = target { + dirs = vec![dir.clone()]; + } else { + return Err(ShErr::full( + ShErrKind::ExecFail, + format!( + "dirs: directory index out of range: {}", + match idx { + StackIdx::FromTop(n) => format!("+{n}"), + StackIdx::FromBottom(n) => format!("-{n}"), + } + ), + blame.clone(), + )); + } + } - if let Some(dir) = target { - dirs = vec![dir.clone()]; - } else { - return Err(ShErr::full( - ShErrKind::ExecFail, - format!("dirs: directory index out of range: {}", match idx { - StackIdx::FromTop(n) => format!("+{n}"), - StackIdx::FromBottom(n) => format!("-{n}"), - }), - blame.clone(), - )); - } - } + let mut output = String::new(); - let mut output = String::new(); + if one_per_line { + output = dirs.join("\n"); + } else if one_per_line_indexed { + for (i, dir) in dirs.iter_mut().enumerate() { + *dir = format!("{i}\t{dir}"); + } + output = dirs.join("\n"); + output.push('\n'); + } else { + print_dirs()?; + } - if one_per_line { - output = dirs.join("\n"); - } else if one_per_line_indexed { - for (i, dir) in dirs.iter_mut().enumerate() { - *dir = format!("{i}\t{dir}"); - } - output = dirs.join("\n"); - output.push('\n'); - } else { - print_dirs()?; - } + let stdout = borrow_fd(STDOUT_FILENO); + write(stdout, output.as_bytes())?; - let stdout = borrow_fd(STDOUT_FILENO); - write(stdout, output.as_bytes())?; - - Ok(()) + Ok(()) } diff --git a/src/builtin/echo.rs b/src/builtin/echo.rs index 049b93a..b535f91 100644 --- a/src/builtin/echo.rs +++ b/src/builtin/echo.rs @@ -1,12 +1,12 @@ use crate::{ builtin::setup_builtin, expand::expand_prompt, - getopt::{get_opts_from_tokens, Opt, OptSpec}, + getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node}, prelude::*, - procio::{borrow_fd, IoStack}, + procio::{IoStack, borrow_fd}, state, }; @@ -30,7 +30,7 @@ pub const ECHO_OPTS: [OptSpec; 4] = [ ]; bitflags! { - #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] + #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct EchoFlags: u32 { const NO_NEWLINE = 0b000001; const USE_STDERR = 0b000010; @@ -59,7 +59,6 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<( borrow_fd(STDOUT_FILENO) }; - let mut echo_output = prepare_echo_args( argv .into_iter() @@ -197,7 +196,6 @@ pub fn prepare_echo_args( prepared_args.push(prepared_arg); } - Ok(prepared_args) } diff --git a/src/builtin/eval.rs b/src/builtin/eval.rs index a3b87a4..ed8c971 100644 --- a/src/builtin/eval.rs +++ b/src/builtin/eval.rs @@ -2,7 +2,7 @@ use crate::{ builtin::setup_builtin, jobs::JobBldr, libsh::error::ShResult, - parse::{execute::exec_input, NdRule, Node}, + parse::{NdRule, Node, execute::exec_input}, procio::IoStack, state, }; diff --git a/src/builtin/exec.rs b/src/builtin/exec.rs index da15335..ff82db4 100644 --- a/src/builtin/exec.rs +++ b/src/builtin/exec.rs @@ -4,7 +4,7 @@ use crate::{ builtin::setup_builtin, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, - parse::{execute::ExecArgs, NdRule, Node}, + parse::{NdRule, Node, execute::ExecArgs}, procio::IoStack, state, }; diff --git a/src/builtin/flowctl.rs b/src/builtin/flowctl.rs index e08e66d..ae565b0 100644 --- a/src/builtin/flowctl.rs +++ b/src/builtin/flowctl.rs @@ -1,6 +1,6 @@ use crate::{ libsh::error::{ShErr, ShErrKind, ShResult}, - parse::{execute::prepare_argv, NdRule, Node}, + parse::{NdRule, Node, execute::prepare_argv}, }; pub fn flowctl(node: Node, kind: ShErrKind) -> ShResult<()> { @@ -31,7 +31,7 @@ pub fn flowctl(node: Node, kind: ShErrKind) -> ShResult<()> { code = status; } - let (kind,message) = match kind { + let (kind, message) = match kind { LoopContinue(_) => (LoopContinue(code), "'continue' found outside of loop"), LoopBreak(_) => (LoopBreak(code), "'break' found outside of loop"), FuncReturn(_) => (FuncReturn(code), "'return' found outside of function"), diff --git a/src/builtin/jobctl.rs b/src/builtin/jobctl.rs index 47229d2..40777f7 100644 --- a/src/builtin/jobctl.rs +++ b/src/builtin/jobctl.rs @@ -1,9 +1,9 @@ use crate::{ jobs::{JobBldr, JobCmdFlags, JobID}, libsh::error::{ShErr, ShErrKind, ShResult}, - parse::{lex::Span, NdRule, Node}, + parse::{NdRule, Node, lex::Span}, prelude::*, - procio::{borrow_fd, IoStack}, + procio::{IoStack, borrow_fd}, state::{self, read_jobs, write_jobs}, }; diff --git a/src/builtin/mod.rs b/src/builtin/mod.rs index cf280ae..c85a8d6 100644 --- a/src/builtin/mod.rs +++ b/src/builtin/mod.rs @@ -8,13 +8,17 @@ use crate::{ execute::prepare_argv, lex::{Span, Tk}, }, - procio::{IoStack, RedirGuard}, state, + procio::{IoStack, RedirGuard}, + state, }; pub mod alias; pub mod cd; +pub mod complete; +pub mod dirstack; pub mod echo; -pub mod varcmds; +pub mod eval; +pub mod exec; pub mod flowctl; pub mod jobctl; pub mod pwd; @@ -24,16 +28,14 @@ pub mod shopt; pub mod source; pub mod test; // [[ ]] thing pub mod trap; +pub mod varcmds; pub mod zoltraak; -pub mod dirstack; -pub mod exec; -pub mod eval; -pub mod complete; pub const BUILTINS: [&str; 35] = [ - "echo", "cd", "read", "export", "local", "pwd", "source", "shift", "jobs", "fg", "bg", "disown", "alias", "unalias", - "return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "command", "trap", - "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly", "unset", "complete", "compgen" + "echo", "cd", "read", "export", "local", "pwd", "source", "shift", "jobs", "fg", "bg", "disown", + "alias", "unalias", "return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", + "command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly", + "unset", "complete", "compgen", ]; /// Sets up a builtin command @@ -96,16 +98,16 @@ pub fn setup_builtin( } pub fn true_builtin() -> ShResult<()> { - state::set_status(0); - Ok(()) + state::set_status(0); + Ok(()) } pub fn false_builtin() -> ShResult<()> { - state::set_status(1); - Ok(()) + state::set_status(1); + Ok(()) } pub fn noop_builtin() -> ShResult<()> { - state::set_status(0); - Ok(()) + state::set_status(0); + Ok(()) } diff --git a/src/builtin/pwd.rs b/src/builtin/pwd.rs index 3cc4acf..6e37dde 100644 --- a/src/builtin/pwd.rs +++ b/src/builtin/pwd.rs @@ -3,7 +3,7 @@ use crate::{ libsh::error::ShResult, parse::{NdRule, Node}, prelude::*, - procio::{borrow_fd, IoStack}, + procio::{IoStack, borrow_fd}, state, }; diff --git a/src/builtin/read.rs b/src/builtin/read.rs index 44369a2..cdbf982 100644 --- a/src/builtin/read.rs +++ b/src/builtin/read.rs @@ -7,13 +7,13 @@ use nix::{ use crate::{ builtin::setup_builtin, - getopt::{get_opts_from_tokens, Opt, OptSpec}, + getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node}, - procio::{borrow_fd, IoStack}, + procio::{IoStack, borrow_fd}, readline::term::RawModeGuard, - state::{self, read_vars, write_vars, VarFlags, VarKind}, + state::{self, VarFlags, VarKind, read_vars, write_vars}, }; pub const READ_OPTS: [OptSpec; 7] = [ @@ -81,7 +81,10 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S write(borrow_fd(STDOUT_FILENO), prompt.as_bytes())?; } - log::info!("read_builtin: starting read with delim={}", read_opts.delim as char); + log::info!( + "read_builtin: starting read with delim={}", + read_opts.delim as char + ); let input = if isatty(STDIN_FILENO)? { // Restore default terminal settings @@ -182,9 +185,7 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S }; if argv.is_empty() { - write_vars(|v| { - v.set_var("REPLY", VarKind::Str(input.clone()), VarFlags::NONE) - })?; + write_vars(|v| v.set_var("REPLY", VarKind::Str(input.clone()), VarFlags::NONE))?; } else { // get our field separator let mut field_sep = read_vars(|v| v.get_var("IFS")); diff --git a/src/builtin/shopt.rs b/src/builtin/shopt.rs index 4541d57..56a5c1b 100644 --- a/src/builtin/shopt.rs +++ b/src/builtin/shopt.rs @@ -20,16 +20,16 @@ pub fn shopt(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult< let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; - if argv.is_empty() { - let mut output = write_shopts(|s| s.display_opts())?; + if argv.is_empty() { + let mut output = write_shopts(|s| s.display_opts())?; let output_channel = borrow_fd(STDOUT_FILENO); output.push('\n'); write(output_channel, output.as_bytes())?; - state::set_status(0); - return Ok(()) - } + state::set_status(0); + return Ok(()); + } for (arg, span) in argv { let Some(mut output) = write_shopts(|s| s.query(&arg)).blame(span)? else { @@ -42,7 +42,6 @@ pub fn shopt(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult< write(output_channel, output.as_bytes())?; } - - state::set_status(0); + state::set_status(0); Ok(()) } diff --git a/src/builtin/test.rs b/src/builtin/test.rs index dfeea4d..8a39bc6 100644 --- a/src/builtin/test.rs +++ b/src/builtin/test.rs @@ -8,7 +8,7 @@ use regex::Regex; use crate::{ libsh::error::{ShErr, ShErrKind, ShResult}, - parse::{ConjunctOp, NdRule, Node, TestCase, TEST_UNARY_OPS}, + parse::{ConjunctOp, NdRule, Node, TEST_UNARY_OPS, TestCase}, }; #[derive(Debug, Clone)] diff --git a/src/builtin/trap.rs b/src/builtin/trap.rs index 2f86821..c4cac8c 100644 --- a/src/builtin/trap.rs +++ b/src/builtin/trap.rs @@ -11,7 +11,7 @@ use crate::{ jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node}, - procio::{borrow_fd, IoStack}, + procio::{IoStack, borrow_fd}, state::{self, read_logic, write_logic}, }; @@ -59,12 +59,10 @@ impl FromStr for TrapTarget { "IO" => Ok(TrapTarget::Signal(Signal::SIGIO)), "PWR" => Ok(TrapTarget::Signal(Signal::SIGPWR)), "SYS" => Ok(TrapTarget::Signal(Signal::SIGSYS)), - _ => { - Err(ShErr::simple( - ShErrKind::ExecFail, - format!("invalid trap target '{}'", s), - )) - } + _ => Err(ShErr::simple( + ShErrKind::ExecFail, + format!("invalid trap target '{}'", s), + )), } } } diff --git a/src/builtin/varcmds.rs b/src/builtin/varcmds.rs index 106fe93..5dc6c81 100644 --- a/src/builtin/varcmds.rs +++ b/src/builtin/varcmds.rs @@ -26,7 +26,7 @@ pub fn readonly(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu let mut vars = v .flatten_vars() .into_iter() - .filter(|(_, v)| v.flags().contains(VarFlags::READONLY)) + .filter(|(_, v)| v.flags().contains(VarFlags::READONLY)) .map(|(k, v)| format!("{}={}", k, v)) .collect::>(); vars.sort(); @@ -47,12 +47,12 @@ pub fn readonly(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu } } - state::set_status(0); - Ok(()) + state::set_status(0); + Ok(()) } pub fn unset(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { - let blame = node.get_span().clone(); + let blame = node.get_span().clone(); let NdRule::Command { assignments: _, argv, @@ -63,27 +63,27 @@ pub fn unset(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult< let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; - if argv.is_empty() { - return Err(ShErr::full( - ShErrKind::SyntaxErr, - "unset: Expected at least one argument", - blame - )); - } + if argv.is_empty() { + return Err(ShErr::full( + ShErrKind::SyntaxErr, + "unset: Expected at least one argument", + blame, + )); + } - for (arg,span) in argv { - if !read_vars(|v| v.var_exists(&arg)) { - return Err(ShErr::full( - ShErrKind::ExecFail, - format!("unset: No such variable '{arg}'"), - span - )); - } - write_vars(|v| v.unset_var(&arg))?; - } + for (arg, span) in argv { + if !read_vars(|v| v.var_exists(&arg)) { + return Err(ShErr::full( + ShErrKind::ExecFail, + format!("unset: No such variable '{arg}'"), + span, + )); + } + write_vars(|v| v.unset_var(&arg))?; + } - state::set_status(0); - Ok(()) + state::set_status(0); + Ok(()) } pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { @@ -114,7 +114,7 @@ pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult write_vars(|v| v.set_var(var, VarKind::Str(val.to_string()), VarFlags::EXPORT))?; } else { write_vars(|v| v.export_var(&arg)); // Export an existing variable, if - // any + // any } } } diff --git a/src/builtin/zoltraak.rs b/src/builtin/zoltraak.rs index f38d90d..3498fd6 100644 --- a/src/builtin/zoltraak.rs +++ b/src/builtin/zoltraak.rs @@ -1,12 +1,12 @@ use std::os::unix::fs::OpenOptionsExt; use crate::{ - getopt::{get_opts_from_tokens, Opt, OptSpec}, + getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{Note, ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node}, prelude::*, - procio::{borrow_fd, IoStack}, + procio::{IoStack, borrow_fd}, }; use super::setup_builtin; diff --git a/src/expand.rs b/src/expand.rs index 4c00229..38575be 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -12,13 +12,13 @@ use crate::parse::{Redir, RedirType}; use crate::procio::{IoBuf, IoFrame, IoMode, IoStack}; use crate::readline::markers; use crate::state::{ - ArrIndex, LogTab, VarFlags, VarKind, read_jobs, read_logic, read_vars, write_jobs, write_meta, write_vars + ArrIndex, LogTab, VarFlags, VarKind, read_jobs, read_logic, read_vars, write_jobs, write_meta, + write_vars, }; use crate::{jobs, prelude::*}; const PARAMETERS: [char; 7] = ['@', '*', '#', '$', '?', '!', '0']; - impl Tk { /// Create a new expanded token pub fn expand(self) -> ShResult { @@ -80,16 +80,16 @@ impl Expander { let mut chars = self.raw.chars(); let mut cur_word = String::new(); let mut was_quoted = false; - let ifs = env::var("IFS").unwrap_or_else(|_| " \t\n".to_string()); + let ifs = env::var("IFS").unwrap_or_else(|_| " \t\n".to_string()); 'outer: while let Some(ch) = chars.next() { match ch { markers::DUB_QUOTE | markers::SNG_QUOTE | markers::SUBSH => { while let Some(q_ch) = chars.next() { match q_ch { - markers::ARG_SEP if ch == markers::DUB_QUOTE => { - words.push(mem::take(&mut cur_word)); - } + markers::ARG_SEP if ch == markers::DUB_QUOTE => { + words.push(mem::take(&mut cur_word)); + } _ if q_ch == ch => { was_quoted = true; continue 'outer; // Isn't rust cool @@ -518,11 +518,11 @@ pub fn expand_raw(chars: &mut Peekable>) -> ShResult { pub fn expand_var(chars: &mut Peekable>) -> ShResult { let mut var_name = String::new(); let mut brace_depth: i32 = 0; - let mut inner_brace_depth: i32 = 0; - let mut bracket_depth: i32 = 0; - let mut idx_brace_depth: i32 = 0; - let mut idx_raw = String::new(); - let mut idx = None; + let mut inner_brace_depth: i32 = 0; + let mut bracket_depth: i32 = 0; + let mut idx_brace_depth: i32 = 0; + let mut idx_raw = String::new(); + let mut idx = None; while let Some(&ch) = chars.peek() { match ch { markers::SUBSH if var_name.is_empty() => { @@ -551,51 +551,63 @@ pub fn expand_var(chars: &mut Peekable>) -> ShResult { '}' if brace_depth > 0 && bracket_depth == 0 && inner_brace_depth == 0 => { chars.next(); // consume the brace let val = if let Some(idx) = idx { - match idx { - ArrIndex::AllSplit => { - let arg_sep = markers::ARG_SEP.to_string(); - read_vars(|v| v.get_arr_elems(&var_name))?.join(&arg_sep) - } - ArrIndex::AllJoined => { - let ifs = read_vars(|v| v.try_get_var("IFS")) - .unwrap_or_else(|| " \t\n".to_string()) - .chars() - .next() - .unwrap_or(' ') - .to_string(); + match idx { + ArrIndex::AllSplit => { + let arg_sep = markers::ARG_SEP.to_string(); + read_vars(|v| v.get_arr_elems(&var_name))?.join(&arg_sep) + } + ArrIndex::AllJoined => { + let ifs = read_vars(|v| v.try_get_var("IFS")) + .unwrap_or_else(|| " \t\n".to_string()) + .chars() + .next() + .unwrap_or(' ') + .to_string(); - read_vars(|v| v.get_arr_elems(&var_name))?.join(&ifs) - }, - _ => read_vars(|v| v.index_var(&var_name, idx))? - } - - } else { - perform_param_expansion(&var_name)? - }; + read_vars(|v| v.get_arr_elems(&var_name))?.join(&ifs) + } + _ => read_vars(|v| v.index_var(&var_name, idx))?, + } + } else { + perform_param_expansion(&var_name)? + }; return Ok(val); } - '[' if brace_depth > 0 && bracket_depth == 0 && inner_brace_depth == 0 => { - chars.next(); // consume the bracket - bracket_depth += 1; - } - ']' if bracket_depth > 0 && idx_brace_depth == 0 => { - bracket_depth -= 1; - chars.next(); // consume the bracket - if bracket_depth == 0 { - let expanded_idx = expand_raw(&mut idx_raw.chars().peekable())?; - idx = Some(expanded_idx.parse::().map_err(|_| ShErr::simple(ShErrKind::ParseErr, format!("Array index must be a number, got '{expanded_idx}'")))?); - } - } - ch if bracket_depth > 0 => { - chars.next(); // safe to consume - if ch == '{' { idx_brace_depth += 1; } - if ch == '}' { idx_brace_depth -= 1; } - idx_raw.push(ch); - } + '[' if brace_depth > 0 && bracket_depth == 0 && inner_brace_depth == 0 => { + chars.next(); // consume the bracket + bracket_depth += 1; + } + ']' if bracket_depth > 0 && idx_brace_depth == 0 => { + bracket_depth -= 1; + chars.next(); // consume the bracket + if bracket_depth == 0 { + let expanded_idx = expand_raw(&mut idx_raw.chars().peekable())?; + idx = Some(expanded_idx.parse::().map_err(|_| { + ShErr::simple( + ShErrKind::ParseErr, + format!("Array index must be a number, got '{expanded_idx}'"), + ) + })?); + } + } + ch if bracket_depth > 0 => { + chars.next(); // safe to consume + if ch == '{' { + idx_brace_depth += 1; + } + if ch == '}' { + idx_brace_depth -= 1; + } + idx_raw.push(ch); + } ch if brace_depth > 0 => { chars.next(); // safe to consume - if ch == '{' { inner_brace_depth += 1; } - if ch == '}' { inner_brace_depth -= 1; } + if ch == '{' { + inner_brace_depth += 1; + } + if ch == '}' { + inner_brace_depth -= 1; + } var_name.push(ch); } ch if var_name.is_empty() && PARAMETERS.contains(&ch) => { @@ -1411,12 +1423,10 @@ pub fn perform_param_expansion(raw: &str) -> ShResult { None => expand_raw(&mut default.chars().peekable()), } } - ParamExp::DefaultUnset(default) => { - match vars.try_get_var(&var_name) { - Some(val) => Ok(val), - None => expand_raw(&mut default.chars().peekable()), - } - } + ParamExp::DefaultUnset(default) => match vars.try_get_var(&var_name) { + Some(val) => Ok(val), + None => expand_raw(&mut default.chars().peekable()), + }, ParamExp::SetDefaultUnsetOrNull(default) => { match vars.try_get_var(&var_name).filter(|v| !v.is_empty()) { Some(val) => Ok(val), @@ -1427,28 +1437,22 @@ pub fn perform_param_expansion(raw: &str) -> ShResult { } } } - ParamExp::SetDefaultUnset(default) => { - match vars.try_get_var(&var_name) { - Some(val) => Ok(val), - None => { - let expanded = expand_raw(&mut default.chars().peekable())?; - write_vars(|v| v.set_var(&var_name, VarKind::Str(expanded.clone()), VarFlags::NONE))?; - Ok(expanded) - } + ParamExp::SetDefaultUnset(default) => match vars.try_get_var(&var_name) { + Some(val) => Ok(val), + None => { + let expanded = expand_raw(&mut default.chars().peekable())?; + write_vars(|v| v.set_var(&var_name, VarKind::Str(expanded.clone()), VarFlags::NONE))?; + Ok(expanded) } - } - ParamExp::AltSetNotNull(alt) => { - match vars.try_get_var(&var_name).filter(|v| !v.is_empty()) { - Some(_) => expand_raw(&mut alt.chars().peekable()), - None => Ok("".into()), - } - } - ParamExp::AltNotNull(alt) => { - match vars.try_get_var(&var_name) { - Some(_) => expand_raw(&mut alt.chars().peekable()), - None => Ok("".into()), - } - } + }, + ParamExp::AltSetNotNull(alt) => match vars.try_get_var(&var_name).filter(|v| !v.is_empty()) { + Some(_) => expand_raw(&mut alt.chars().peekable()), + None => Ok("".into()), + }, + ParamExp::AltNotNull(alt) => match vars.try_get_var(&var_name) { + Some(_) => expand_raw(&mut alt.chars().peekable()), + None => Ok("".into()), + }, ParamExp::ErrUnsetOrNull(err) => { match vars.try_get_var(&var_name).filter(|v| !v.is_empty()) { Some(val) => Ok(val), @@ -1462,19 +1466,17 @@ pub fn perform_param_expansion(raw: &str) -> ShResult { } } } - ParamExp::ErrUnset(err) => { - match vars.try_get_var(&var_name) { - Some(val) => Ok(val), - None => { - let expanded = expand_raw(&mut err.chars().peekable())?; - Err(ShErr::Simple { - kind: ShErrKind::ExecFail, - msg: expanded, - notes: vec![], - }) - } + ParamExp::ErrUnset(err) => match vars.try_get_var(&var_name) { + Some(val) => Ok(val), + None => { + let expanded = expand_raw(&mut err.chars().peekable())?; + Err(ShErr::Simple { + kind: ShErrKind::ExecFail, + msg: expanded, + notes: vec![], + }) } - } + }, ParamExp::Substr(pos) => { let value = vars.get_var(&var_name); if let Some(substr) = value.get(pos..) { @@ -1861,7 +1863,7 @@ fn tokenize_prompt(raw: &str) -> Vec { 'n' => tokens.push(PromptTk::Text("\n".into())), 'r' => tokens.push(PromptTk::Text("\r".into())), 't' => tokens.push(PromptTk::RuntimeMillis), - 'j' => tokens.push(PromptTk::JobCount), + 'j' => tokens.push(PromptTk::JobCount), 'T' => tokens.push(PromptTk::RuntimeFormatted), '\\' => tokens.push(PromptTk::Text("\\".into())), '"' => tokens.push(PromptTk::Text("\"".into())), @@ -2058,9 +2060,20 @@ pub fn expand_prompt(raw: &str) -> ShResult { PromptTk::SuccessSymbol => todo!(), PromptTk::FailureSymbol => todo!(), PromptTk::JobCount => { - let count = read_jobs(|j| j.jobs().iter().filter(|j| j.as_ref().is_some_and(|j| j.get_stats().iter().all(|st| matches!(st, WtStat::StillAlive)))).count()); - result.push_str(&count.to_string()); - } + let count = read_jobs(|j| { + j.jobs() + .iter() + .filter(|j| { + j.as_ref().is_some_and(|j| { + j.get_stats() + .iter() + .all(|st| matches!(st, WtStat::StillAlive)) + }) + }) + .count() + }); + result.push_str(&count.to_string()); + } PromptTk::Function(f) => { let output = expand_cmd_sub(&f)?; result.push_str(&output); diff --git a/src/getopt.rs b/src/getopt.rs index 4e853ec..495e8e3 100644 --- a/src/getopt.rs +++ b/src/getopt.rs @@ -67,12 +67,15 @@ pub fn get_opts(words: Vec) -> (Vec, Vec) { (non_opts, opts) } -pub fn get_opts_from_tokens(tokens: Vec, opt_specs: &[OptSpec]) -> ShResult<(Vec, Vec)> { +pub fn get_opts_from_tokens( + tokens: Vec, + opt_specs: &[OptSpec], +) -> ShResult<(Vec, Vec)> { let mut tokens_iter = tokens - .into_iter() - .map(|t| t.expand()) - .collect::>>()? - .into_iter(); + .into_iter() + .map(|t| t.expand()) + .collect::>>()? + .into_iter(); let mut opts = vec![]; let mut non_opts = vec![]; diff --git a/src/jobs.rs b/src/jobs.rs index cff97ec..5f9d4f7 100644 --- a/src/jobs.rs +++ b/src/jobs.rs @@ -4,7 +4,7 @@ use crate::{ term::{Style, Styled}, }, prelude::*, - procio::{borrow_fd, IoMode}, + procio::{IoMode, borrow_fd}, signal::{disable_reaping, enable_reaping}, state::{self, read_jobs, set_status, write_jobs}, }; @@ -632,6 +632,9 @@ impl Job { } Ok(()) } + pub fn name(&self) -> Option<&str> { + self.children().first().and_then(|child| child.cmd()) + } pub fn display(&self, job_order: &[usize], flags: JobCmdFlags) -> String { let long = flags.contains(JobCmdFlags::LONG); let init = flags.contains(JobCmdFlags::INIT); diff --git a/src/libsh/error.rs b/src/libsh/error.rs index a427b31..712c431 100644 --- a/src/libsh/error.rs +++ b/src/libsh/error.rs @@ -1,7 +1,10 @@ use std::fmt::Display; use crate::{ - getopt::Opt, libsh::term::{Style, Styled}, parse::lex::Span, prelude::* + getopt::Opt, + libsh::term::{Style, Styled}, + parse::lex::Span, + prelude::*, }; pub type ShResult = Result; @@ -393,7 +396,7 @@ impl From for ShErr { #[derive(Debug, Clone)] pub enum ShErrKind { IoErr(io::ErrorKind), - InvalidOpt, + InvalidOpt, SyntaxErr, ParseErr, InternalErr, @@ -420,7 +423,7 @@ impl Display for ShErrKind { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let output = match self { Self::IoErr(e) => &format!("I/O Error: {e}"), - Self::InvalidOpt => &format!("Invalid option"), + Self::InvalidOpt => &format!("Invalid option"), Self::SyntaxErr => "Syntax Error", Self::ParseErr => "Parse Error", Self::InternalErr => "Internal Error", diff --git a/src/libsh/utils.rs b/src/libsh/utils.rs index 538c078..8f56362 100644 --- a/src/libsh/utils.rs +++ b/src/libsh/utils.rs @@ -17,7 +17,7 @@ pub trait CharDequeUtils { pub trait TkVecUtils { fn get_span(&self) -> Option; fn debug_tokens(&self); - fn split_at_separators(&self) -> Vec>; + fn split_at_separators(&self) -> Vec>; } pub trait RedirVecUtils { @@ -86,29 +86,24 @@ impl TkVecUtils for Vec { fn debug_tokens(&self) { for token in self {} } - fn split_at_separators(&self) -> Vec> { - let mut splits = vec![]; - let mut cur_split = vec![]; - for tk in self { - match tk.class { - TkRule::Pipe | - TkRule::ErrPipe | - TkRule::And | - TkRule::Or | - TkRule::Bg | - TkRule::Sep => { - splits.push(std::mem::take(&mut cur_split)); - } - _ => cur_split.push(tk.clone()), - } - } + fn split_at_separators(&self) -> Vec> { + let mut splits = vec![]; + let mut cur_split = vec![]; + for tk in self { + match tk.class { + TkRule::Pipe | TkRule::ErrPipe | TkRule::And | TkRule::Or | TkRule::Bg | TkRule::Sep => { + splits.push(std::mem::take(&mut cur_split)); + } + _ => cur_split.push(tk.clone()), + } + } - if !cur_split.is_empty() { - splits.push(cur_split); - } + if !cur_split.is_empty() { + splits.push(cur_split); + } - splits - } + splits + } } impl RedirVecUtils for Vec { diff --git a/src/main.rs b/src/main.rs index f3689db..f91956e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ #![allow( - clippy::derivable_impls, - clippy::tabs_in_doc_comments, - clippy::while_let_on_iterator + clippy::derivable_impls, + clippy::tabs_in_doc_comments, + clippy::while_let_on_iterator )] pub mod builtin; pub mod expand; @@ -40,22 +40,22 @@ use state::{read_vars, write_vars}; #[derive(Parser, Debug)] struct ShedArgs { - script: Option, + script: Option, - #[arg(short)] - command: Option, + #[arg(short)] + command: Option, - #[arg(trailing_var_arg = true)] - script_args: Vec, + #[arg(trailing_var_arg = true)] + script_args: Vec, - #[arg(long)] - version: bool, + #[arg(long)] + version: bool, - #[arg(short)] - interactive: bool, + #[arg(short)] + interactive: bool, - #[arg(long,short)] - login_shell: bool, + #[arg(long, short)] + login_shell: bool, } /// Force evaluation of lazily-initialized values early in shell startup. @@ -69,227 +69,236 @@ struct ShedArgs { /// closure, which forces access to the variable table and causes its `LazyLock` /// constructor to run. fn kickstart_lazy_evals() { - read_vars(|_| {}); + read_vars(|_| {}); } /// We need to make sure that even if we panic, our child processes get sighup fn setup_panic_handler() { - let default_panic_hook = std::panic::take_hook(); - std::panic::set_hook(Box::new(move |info| { - let _ = state::SHED.try_with(|shed| { - if let Ok(mut jobs) = shed.jobs.try_borrow_mut() { - jobs.hang_up(); - } - }); + let default_panic_hook = std::panic::take_hook(); + std::panic::set_hook(Box::new(move |info| { + let _ = state::SHED.try_with(|shed| { + if let Ok(mut jobs) = shed.jobs.try_borrow_mut() { + jobs.hang_up(); + } + }); - default_panic_hook(info); - })); + default_panic_hook(info); + })); } fn main() -> ExitCode { - env_logger::init(); - kickstart_lazy_evals(); - setup_panic_handler(); + env_logger::init(); + kickstart_lazy_evals(); + setup_panic_handler(); - let mut args = ShedArgs::parse(); - if env::args().next().is_some_and(|a| a.starts_with('-')) { - // first arg is '-shed' - // meaning we are in a login shell - args.login_shell = true; - } - if args.version { - println!("shed {} ({} {})", env!("CARGO_PKG_VERSION"), std::env::consts::ARCH, std::env::consts::OS); - return ExitCode::SUCCESS; - } + let mut args = ShedArgs::parse(); + if env::args().next().is_some_and(|a| a.starts_with('-')) { + // first arg is '-shed' + // meaning we are in a login shell + args.login_shell = true; + } + if args.version { + println!( + "shed {} ({} {})", + env!("CARGO_PKG_VERSION"), + std::env::consts::ARCH, + std::env::consts::OS + ); + return ExitCode::SUCCESS; + } - if let Err(e) = if let Some(path) = args.script { - run_script(path, args.script_args) - } else if let Some(cmd) = args.command { - exec_input(cmd, None, false) - } else { - shed_interactive() - } { - eprintln!("shed: {e}"); - }; + if let Err(e) = if let Some(path) = args.script { + run_script(path, args.script_args) + } else if let Some(cmd) = args.command { + exec_input(cmd, None, false) + } else { + shed_interactive() + } { + eprintln!("shed: {e}"); + }; - if let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Exit)) - && let Err(e) = exec_input(trap, None, false) { - eprintln!("shed: error running EXIT trap: {e}"); - } + if let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Exit)) + && let Err(e) = exec_input(trap, None, false) + { + eprintln!("shed: error running EXIT trap: {e}"); + } - write_jobs(|j| j.hang_up()); - ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8) + write_jobs(|j| j.hang_up()); + ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8) } fn run_script>(path: P, args: Vec) -> ShResult<()> { - let path = path.as_ref(); - if !path.is_file() { - eprintln!("shed: Failed to open input file: {}", path.display()); - QUIT_CODE.store(1, Ordering::SeqCst); - return Err(ShErr::simple( - ShErrKind::CleanExit(1), - "input file not found", - )); - } - let Ok(input) = fs::read_to_string(path) else { - eprintln!("shed: Failed to read input file: {}", path.display()); - QUIT_CODE.store(1, Ordering::SeqCst); - return Err(ShErr::simple( - ShErrKind::CleanExit(1), - "failed to read input file", - )); - }; + let path = path.as_ref(); + if !path.is_file() { + eprintln!("shed: Failed to open input file: {}", path.display()); + QUIT_CODE.store(1, Ordering::SeqCst); + return Err(ShErr::simple( + ShErrKind::CleanExit(1), + "input file not found", + )); + } + let Ok(input) = fs::read_to_string(path) else { + eprintln!("shed: Failed to read input file: {}", path.display()); + QUIT_CODE.store(1, Ordering::SeqCst); + return Err(ShErr::simple( + ShErrKind::CleanExit(1), + "failed to read input file", + )); + }; - write_vars(|v| { - v.cur_scope_mut() - .bpush_arg(path.to_string_lossy().to_string()) - }); - for arg in args { - write_vars(|v| v.cur_scope_mut().bpush_arg(arg)) - } + write_vars(|v| { + v.cur_scope_mut() + .bpush_arg(path.to_string_lossy().to_string()) + }); + for arg in args { + write_vars(|v| v.cur_scope_mut().bpush_arg(arg)) + } - exec_input(input, None, false) + exec_input(input, None, false) } fn shed_interactive() -> ShResult<()> { - let _raw_mode = raw_mode(); // sets raw mode, restores termios on drop - sig_setup(); + let _raw_mode = raw_mode(); // sets raw mode, restores termios on drop + sig_setup(); - if let Err(e) = source_rc() { - eprintln!("{e}"); - } + if let Err(e) = source_rc() { + eprintln!("{e}"); + } - // Create readline instance with initial prompt - let mut readline = match ShedVi::new(Prompt::new(), *TTY_FILENO) { - Ok(rl) => rl, - Err(e) => { - eprintln!("Failed to initialize readline: {e}"); - QUIT_CODE.store(1, Ordering::SeqCst); - return Err(ShErr::simple( - ShErrKind::CleanExit(1), - "readline initialization failed", - )); - } - }; + // Create readline instance with initial prompt + let mut readline = match ShedVi::new(Prompt::new(), *TTY_FILENO) { + Ok(rl) => rl, + Err(e) => { + eprintln!("Failed to initialize readline: {e}"); + QUIT_CODE.store(1, Ordering::SeqCst); + return Err(ShErr::simple( + ShErrKind::CleanExit(1), + "readline initialization failed", + )); + } + }; - // Main poll loop - loop { - write_meta(|m| { - m.try_rehash_commands(); - m.try_rehash_cwd_listing(); - }); + // Main poll loop + loop { + write_meta(|m| { + m.try_rehash_commands(); + m.try_rehash_cwd_listing(); + }); - // Handle any pending signals - while signals_pending() { - if let Err(e) = check_signals() { - match e.kind() { - ShErrKind::ClearReadline => { - // Ctrl+C - clear current input and show new prompt - readline.reset(false)?; - } - ShErrKind::CleanExit(code) => { - QUIT_CODE.store(*code, Ordering::SeqCst); - return Ok(()); - } - _ => eprintln!("{e}"), - } - } - } + // Handle any pending signals + while signals_pending() { + if let Err(e) = check_signals() { + match e.kind() { + ShErrKind::ClearReadline => { + // Ctrl+C - clear current input and show new prompt + readline.reset(false)?; + } + ShErrKind::CleanExit(code) => { + QUIT_CODE.store(*code, Ordering::SeqCst); + return Ok(()); + } + _ => eprintln!("{e}"), + } + } + } - if GOT_SIGWINCH.swap(false, Ordering::SeqCst) { - log::info!("Window size change detected, updating readline dimensions"); - readline.writer.update_t_cols(); - readline.prompt_mut().refresh()?; - } + if GOT_SIGWINCH.swap(false, Ordering::SeqCst) { + log::info!("Window size change detected, updating readline dimensions"); + readline.writer.update_t_cols(); + readline.prompt_mut().refresh()?; + } - if JOB_DONE.swap(false, Ordering::SeqCst) { - // update the prompt so any job count escape sequences update dynamically - readline.prompt_mut().refresh()?; - } + if JOB_DONE.swap(false, Ordering::SeqCst) { + // update the prompt so any job count escape sequences update dynamically + readline.prompt_mut().refresh()?; + } - readline.print_line(false)?; + readline.print_line(false)?; - // Poll for stdin input - let mut fds = [PollFd::new( - unsafe { BorrowedFd::borrow_raw(*TTY_FILENO) }, - PollFlags::POLLIN, - )]; + // Poll for stdin input + let mut fds = [PollFd::new( + unsafe { BorrowedFd::borrow_raw(*TTY_FILENO) }, + PollFlags::POLLIN, + )]; - match poll(&mut fds, PollTimeout::MAX) { - Ok(_) => {} - Err(Errno::EINTR) => { - // Interrupted by signal, loop back to handle it - continue; - } - Err(e) => { - eprintln!("poll error: {e}"); - break; - } - } + match poll(&mut fds, PollTimeout::MAX) { + Ok(_) => {} + Err(Errno::EINTR) => { + // Interrupted by signal, loop back to handle it + continue; + } + Err(e) => { + eprintln!("poll error: {e}"); + break; + } + } - // Check if stdin has data - if fds[0].revents().is_some_and(|r| r.contains(PollFlags::POLLIN)) { - let mut buffer = [0u8; 1024]; - match read(*TTY_FILENO, &mut buffer) { - Ok(0) => { - // EOF - break; - } - Ok(n) => { - readline.feed_bytes(&buffer[..n]); - } - Err(Errno::EINTR) => { - // Interrupted, continue to handle signals - continue; - } - Err(e) => { - eprintln!("read error: {e}"); - break; - } - } - } + // Check if stdin has data + if fds[0] + .revents() + .is_some_and(|r| r.contains(PollFlags::POLLIN)) + { + let mut buffer = [0u8; 1024]; + match read(*TTY_FILENO, &mut buffer) { + Ok(0) => { + // EOF + break; + } + Ok(n) => { + readline.feed_bytes(&buffer[..n]); + } + Err(Errno::EINTR) => { + // Interrupted, continue to handle signals + continue; + } + Err(e) => { + eprintln!("read error: {e}"); + break; + } + } + } - // Process any available input - match readline.process_input() { - Ok(ReadlineEvent::Line(input)) => { - let start = Instant::now(); - write_meta(|m| m.start_timer()); - if let Err(e) = RawModeGuard::with_cooked_mode(|| exec_input(input, None, true)) { - match e.kind() { - ShErrKind::CleanExit(code) => { - QUIT_CODE.store(*code, Ordering::SeqCst); - return Ok(()); - } - _ => eprintln!("{e}"), - } - } - let command_run_time = start.elapsed(); - log::info!("Command executed in {:.2?}", command_run_time); - write_meta(|m| m.stop_timer()); - readline.writer.flush_write("\n")?; + // Process any available input + match readline.process_input() { + Ok(ReadlineEvent::Line(input)) => { + let start = Instant::now(); + write_meta(|m| m.start_timer()); + if let Err(e) = RawModeGuard::with_cooked_mode(|| exec_input(input, None, true)) { + match e.kind() { + ShErrKind::CleanExit(code) => { + QUIT_CODE.store(*code, Ordering::SeqCst); + return Ok(()); + } + _ => eprintln!("{e}"), + } + } + let command_run_time = start.elapsed(); + log::info!("Command executed in {:.2?}", command_run_time); + write_meta(|m| m.stop_timer()); + readline.writer.flush_write("\n")?; - // Reset for next command with fresh prompt - readline.reset(true)?; - let real_end = start.elapsed(); - log::info!("Total round trip time: {:.2?}", real_end); - } - Ok(ReadlineEvent::Eof) => { - // Ctrl+D on empty line - QUIT_CODE.store(0, Ordering::SeqCst); - return Ok(()); - } - Ok(ReadlineEvent::Pending) => { - // No complete input yet, keep polling - } - Err(e) => match e.kind() { - ShErrKind::CleanExit(code) => { - QUIT_CODE.store(*code, Ordering::SeqCst); - return Ok(()); - } - _ => eprintln!("{e}"), - } - } - } + // Reset for next command with fresh prompt + readline.reset(true)?; + let real_end = start.elapsed(); + log::info!("Total round trip time: {:.2?}", real_end); + } + Ok(ReadlineEvent::Eof) => { + // Ctrl+D on empty line + QUIT_CODE.store(0, Ordering::SeqCst); + return Ok(()); + } + Ok(ReadlineEvent::Pending) => { + // No complete input yet, keep polling + } + Err(e) => match e.kind() { + ShErrKind::CleanExit(code) => { + QUIT_CODE.store(*code, Ordering::SeqCst); + return Ok(()); + } + _ => eprintln!("{e}"), + }, + } + } - Ok(()) + Ok(()) } diff --git a/src/parse/execute.rs b/src/parse/execute.rs index 8e50b0a..39a2fc1 100644 --- a/src/parse/execute.rs +++ b/src/parse/execute.rs @@ -1,15 +1,40 @@ -use std::{collections::{HashSet, VecDeque}, os::unix::fs::PermissionsExt}; +use std::{ + collections::{HashSet, VecDeque}, + os::unix::fs::PermissionsExt, +}; use crate::{ builtin::{ - alias::{alias, unalias}, cd::cd, complete::{compgen_builtin, complete_builtin}, dirstack::{dirs, popd, pushd}, echo::echo, eval, exec, flowctl::flowctl, jobctl::{JobBehavior, continue_job, disown, jobs}, pwd::pwd, read::read_builtin, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, true_builtin, varcmds::{export, local, readonly, unset}, zoltraak::zoltraak + alias::{alias, unalias}, + cd::cd, + complete::{compgen_builtin, complete_builtin}, + dirstack::{dirs, popd, pushd}, + echo::echo, + eval, exec, + flowctl::flowctl, + jobctl::{JobBehavior, continue_job, disown, jobs}, + pwd::pwd, + read::read_builtin, + shift::shift, + shopt::shopt, + source::source, + test::double_bracket_test, + trap::{TrapTarget, trap}, + true_builtin, + varcmds::{export, local, readonly, unset}, + zoltraak::zoltraak, }, expand::{expand_aliases, glob_to_regex}, jobs::{ChildProc, JobStack, dispatch_job}, - libsh::{error::{ShErr, ShErrKind, ShResult, ShResultExt}, utils::RedirVecUtils}, + libsh::{ + error::{ShErr, ShErrKind, ShResult, ShResultExt}, + utils::RedirVecUtils, + }, prelude::*, procio::{IoMode, IoStack}, - state::{self, ShFunc, VarFlags, VarKind, read_logic, read_shopts, write_jobs, write_logic, write_vars}, + state::{ + self, ShFunc, VarFlags, VarKind, read_logic, read_shopts, write_jobs, write_logic, write_vars, + }, }; use super::{ @@ -23,35 +48,37 @@ thread_local! { } pub fn is_in_path(name: &str) -> bool { - if name.starts_with("./") || name.starts_with("../") || name.starts_with('/') { - let path = Path::new(name); - if path.exists() && path.is_file() && !path.is_dir() { - let meta = match path.metadata() { - Ok(m) => m, - Err(_) => return false, - }; - if meta.permissions().mode() & 0o111 != 0 { - return true; - } - } - false - } else { - let Ok(path) = env::var("PATH") else { return false }; - let paths = path.split(':'); - for path in paths { - let full_path = Path::new(path).join(name); - if full_path.exists() && full_path.is_file() && !full_path.is_dir() { - let meta = match full_path.metadata() { - Ok(m) => m, - Err(_) => continue, - }; - if meta.permissions().mode() & 0o111 != 0 { - return true; - } - } - } - false - } + if name.starts_with("./") || name.starts_with("../") || name.starts_with('/') { + let path = Path::new(name); + if path.exists() && path.is_file() && !path.is_dir() { + let meta = match path.metadata() { + Ok(m) => m, + Err(_) => return false, + }; + if meta.permissions().mode() & 0o111 != 0 { + return true; + } + } + false + } else { + let Ok(path) = env::var("PATH") else { + return false; + }; + let paths = path.split(':'); + for path in paths { + let full_path = Path::new(path).join(name); + if full_path.exists() && full_path.is_file() && !full_path.is_dir() { + let meta = match full_path.metadata() { + Ok(m) => m, + Err(_) => continue, + }; + if meta.permissions().mode() & 0o111 != 0 { + return true; + } + } + } + false + } } pub struct ScopeGuard; @@ -155,7 +182,7 @@ pub fn exec_input(input: String, io_stack: Option, interactive: bool) - return Ok(()); } - let nodes = parser.extract_nodes(); + let nodes = parser.extract_nodes(); let mut dispatcher = Dispatcher::new(nodes, interactive); if let Some(mut stack) = io_stack { @@ -225,8 +252,9 @@ impl Dispatcher { } else if is_subsh(node.get_command().cloned()) { self.exec_subsh(node) } else if read_shopts(|s| s.core.autocd) - && Path::new(cmd.span.as_str()).is_dir() - && !is_in_path(cmd.span.as_str()) { + && Path::new(cmd.span.as_str()).is_dir() + && !is_in_path(cmd.span.as_str()) + { let dir = cmd.span.as_str().to_string(); let stack = IoStack { stack: self.io_stack.clone(), @@ -305,27 +333,27 @@ impl Dispatcher { unreachable!() }; - self.run_fork("anonymous_subshell", |s| { - if let Err(e) = s.set_assignments(assignments, AssignBehavior::Export) { - eprintln!("{e}"); - return; - }; - s.io_stack.append_to_frame(subsh.redirs); - let mut argv = match prepare_argv(argv) { - Ok(argv) => argv, - Err(e) => { - eprintln!("{e}"); - return; - } - }; + self.run_fork("anonymous_subshell", |s| { + if let Err(e) = s.set_assignments(assignments, AssignBehavior::Export) { + eprintln!("{e}"); + return; + }; + s.io_stack.append_to_frame(subsh.redirs); + let mut argv = match prepare_argv(argv) { + Ok(argv) => argv, + Err(e) => { + eprintln!("{e}"); + return; + } + }; - let subsh = argv.remove(0); - let subsh_body = subsh.0.to_string(); + let subsh = argv.remove(0); + let subsh_body = subsh.0.to_string(); - if let Err(e) = exec_input(subsh_body, None, s.interactive) { - eprintln!("{e}"); - }; - }) + if let Err(e) = exec_input(subsh_body, None, s.interactive) { + eprintln!("{e}"); + }; + }) } fn exec_func(&mut self, func: Node) -> ShResult<()> { let blame = func.get_span().clone(); @@ -362,7 +390,7 @@ impl Dispatcher { let result = if let Some(ref mut func_body) = read_logic(|l| l.get_func(&func_name)) { let _guard = ScopeGuard::exclusive_scope(Some(argv)); - func_body.body_mut().flags = func.flags; + func_body.body_mut().flags = func.flags; if let Err(e) = self.exec_brc_grp(func_body.body().clone()) { match e.kind() { @@ -390,33 +418,32 @@ impl Dispatcher { let NdRule::BraceGrp { body } = brc_grp.class else { unreachable!() }; - let fork_builtins = brc_grp.flags.contains(NdFlags::FORK_BUILTINS); + let fork_builtins = brc_grp.flags.contains(NdFlags::FORK_BUILTINS); self.io_stack.append_to_frame(brc_grp.redirs); - let _guard = self.io_stack.pop_frame().redirect()?; - let brc_grp_logic = |s: &mut Self| -> ShResult<()> { + let _guard = self.io_stack.pop_frame().redirect()?; + let brc_grp_logic = |s: &mut Self| -> ShResult<()> { + for node in body { + let blame = node.get_span(); + s.dispatch_node(node).try_blame(blame)?; + } - for node in body { - let blame = node.get_span(); - s.dispatch_node(node).try_blame(blame)?; - } + Ok(()) + }; - Ok(()) - }; - - if fork_builtins { - log::trace!("Forking brace group"); - self.run_fork("brace group", |s| { - if let Err(e) = brc_grp_logic(s) { - eprintln!("{e}"); - } - }) - } else { - brc_grp_logic(self) - } + if fork_builtins { + log::trace!("Forking brace group"); + self.run_fork("brace group", |s| { + if let Err(e) = brc_grp_logic(s) { + eprintln!("{e}"); + } + }) + } else { + brc_grp_logic(self) + } } fn exec_case(&mut self, case_stmt: Node) -> ShResult<()> { - let blame = case_stmt.get_span().clone(); + let blame = case_stmt.get_span().clone(); let NdRule::CaseNode { pattern, case_blocks, @@ -425,52 +452,52 @@ impl Dispatcher { unreachable!() }; - let fork_builtins = case_stmt.flags.contains(NdFlags::FORK_BUILTINS); + let fork_builtins = case_stmt.flags.contains(NdFlags::FORK_BUILTINS); - self.io_stack.append_to_frame(case_stmt.redirs); - let _guard = self.io_stack.pop_frame().redirect()?; + self.io_stack.append_to_frame(case_stmt.redirs); + let _guard = self.io_stack.pop_frame().redirect()?; - let case_logic = |s: &mut Self| -> ShResult<()> { - let exp_pattern = pattern.clone().expand()?; - let pattern_raw = exp_pattern - .get_words() - .first() - .map(|s| s.to_string()) - .unwrap_or_default(); + let case_logic = |s: &mut Self| -> ShResult<()> { + let exp_pattern = pattern.clone().expand()?; + let pattern_raw = exp_pattern + .get_words() + .first() + .map(|s| s.to_string()) + .unwrap_or_default(); - 'outer: for block in case_blocks { - let CaseNode { pattern, body } = block; - let block_pattern_raw = pattern.span.as_str().trim_end_matches(')').trim(); - // Split at '|' to allow for multiple patterns like `foo|bar)` - let block_patterns = block_pattern_raw.split('|'); + 'outer: for block in case_blocks { + let CaseNode { pattern, body } = block; + let block_pattern_raw = pattern.span.as_str().trim_end_matches(')').trim(); + // Split at '|' to allow for multiple patterns like `foo|bar)` + let block_patterns = block_pattern_raw.split('|'); - for pattern in block_patterns { - let pattern_regex = glob_to_regex(pattern, false); - if pattern_regex.is_match(&pattern_raw) { - for node in &body { - s.dispatch_node(node.clone())?; - } - break 'outer; - } - } - } + for pattern in block_patterns { + let pattern_regex = glob_to_regex(pattern, false); + if pattern_regex.is_match(&pattern_raw) { + for node in &body { + s.dispatch_node(node.clone())?; + } + break 'outer; + } + } + } - Ok(()) - }; + Ok(()) + }; - if fork_builtins { - log::trace!("Forking builtin: case"); - self.run_fork("case", |s| { - if let Err(e) = case_logic(s) { - eprintln!("{e}"); - } - }) - } else { - case_logic(self).try_blame(blame) - } + if fork_builtins { + log::trace!("Forking builtin: case"); + self.run_fork("case", |s| { + if let Err(e) = case_logic(s) { + eprintln!("{e}"); + } + }) + } else { + case_logic(self).try_blame(blame) + } } fn exec_loop(&mut self, loop_stmt: Node) -> ShResult<()> { - let blame = loop_stmt.get_span().clone(); + let blame = loop_stmt.get_span().clone(); let NdRule::LoopNode { kind, cond_node } = loop_stmt.class else { unreachable!(); }; @@ -481,64 +508,64 @@ impl Dispatcher { } }; - let fork_builtins = loop_stmt.flags.contains(NdFlags::FORK_BUILTINS); + let fork_builtins = loop_stmt.flags.contains(NdFlags::FORK_BUILTINS); - self.io_stack.append_to_frame(loop_stmt.redirs); - let _guard = self.io_stack.pop_frame().redirect()?; + self.io_stack.append_to_frame(loop_stmt.redirs); + let _guard = self.io_stack.pop_frame().redirect()?; - let loop_logic = |s: &mut Self| -> ShResult<()> { - let CondNode { cond, body } = cond_node; - 'outer: loop { - if let Err(e) = s.dispatch_node(*cond.clone()) { - state::set_status(1); - return Err(e); - } + let loop_logic = |s: &mut Self| -> ShResult<()> { + let CondNode { cond, body } = cond_node; + 'outer: loop { + if let Err(e) = s.dispatch_node(*cond.clone()) { + state::set_status(1); + return Err(e); + } - let status = state::get_status(); - if keep_going(kind, status) { - for node in &body { - if let Err(e) = s.dispatch_node(node.clone()) { - match e.kind() { - ShErrKind::LoopBreak(code) => { - state::set_status(*code); - break 'outer; - } - ShErrKind::LoopContinue(code) => { - state::set_status(*code); - continue 'outer; - } - _ => { - return Err(e); - } - } - } - } - } else { - break; - } - } + let status = state::get_status(); + if keep_going(kind, status) { + for node in &body { + if let Err(e) = s.dispatch_node(node.clone()) { + match e.kind() { + ShErrKind::LoopBreak(code) => { + state::set_status(*code); + break 'outer; + } + ShErrKind::LoopContinue(code) => { + state::set_status(*code); + continue 'outer; + } + _ => { + return Err(e); + } + } + } + } + } else { + break; + } + } - Ok(()) - }; + Ok(()) + }; - if fork_builtins { - log::trace!("Forking builtin: loop"); - self.run_fork("loop", |s| { - if let Err(e) = loop_logic(s) { - eprintln!("{e}"); - } - }) - } else { - loop_logic(self).try_blame(blame) - } + if fork_builtins { + log::trace!("Forking builtin: loop"); + self.run_fork("loop", |s| { + if let Err(e) = loop_logic(s) { + eprintln!("{e}"); + } + }) + } else { + loop_logic(self).try_blame(blame) + } } fn exec_for(&mut self, for_stmt: Node) -> ShResult<()> { - let blame = for_stmt.get_span().clone(); + let blame = for_stmt.get_span().clone(); let NdRule::ForNode { vars, arr, body } = for_stmt.class else { unreachable!(); }; - let fork_builtins = for_stmt.flags.contains(NdFlags::FORK_BUILTINS); + let fork_builtins = for_stmt.flags.contains(NdFlags::FORK_BUILTINS); let to_expanded_strings = |tks: Vec| -> ShResult> { Ok( @@ -552,60 +579,66 @@ impl Dispatcher { ) }; - self.io_stack.append_to_frame(for_stmt.redirs); - let _guard = self.io_stack.pop_frame().redirect()?; + self.io_stack.append_to_frame(for_stmt.redirs); + let _guard = self.io_stack.pop_frame().redirect()?; - let for_logic = |s: &mut Self| -> ShResult<()> { - // Expand all array variables - let arr: Vec = to_expanded_strings(arr)?; - let vars: Vec = to_expanded_strings(vars)?; + let for_logic = |s: &mut Self| -> ShResult<()> { + // Expand all array variables + let arr: Vec = to_expanded_strings(arr)?; + let vars: Vec = to_expanded_strings(vars)?; - let mut for_guard = VarCtxGuard::new(vars.iter().map(|v| v.to_string()).collect()); + let mut for_guard = VarCtxGuard::new(vars.iter().map(|v| v.to_string()).collect()); - 'outer: for chunk in arr.chunks(vars.len()) { - let empty = String::new(); - let chunk_iter = vars - .iter() - .zip(chunk.iter().chain(std::iter::repeat(&empty))); + 'outer: for chunk in arr.chunks(vars.len()) { + let empty = String::new(); + let chunk_iter = vars + .iter() + .zip(chunk.iter().chain(std::iter::repeat(&empty))); - for (var, val) in chunk_iter { - write_vars(|v| v.set_var(&var.to_string(), VarKind::Str(val.to_string()), VarFlags::NONE))?; - for_guard.vars.insert(var.to_string()); - } + for (var, val) in chunk_iter { + write_vars(|v| { + v.set_var( + &var.to_string(), + VarKind::Str(val.to_string()), + VarFlags::NONE, + ) + })?; + for_guard.vars.insert(var.to_string()); + } - for node in body.clone() { - if let Err(e) = s.dispatch_node(node) { - match e.kind() { - ShErrKind::LoopBreak(code) => { - state::set_status(*code); - break 'outer; - } - ShErrKind::LoopContinue(code) => { - state::set_status(*code); - continue 'outer; - } - _ => return Err(e), - } - } - } - } + for node in body.clone() { + if let Err(e) = s.dispatch_node(node) { + match e.kind() { + ShErrKind::LoopBreak(code) => { + state::set_status(*code); + break 'outer; + } + ShErrKind::LoopContinue(code) => { + state::set_status(*code); + continue 'outer; + } + _ => return Err(e), + } + } + } + } - Ok(()) - }; + Ok(()) + }; - if fork_builtins { - log::trace!("Forking builtin: for"); - self.run_fork("for", |s| { - if let Err(e) = for_logic(s) { - eprintln!("{e}"); - } - }) - } else { - for_logic(self).try_blame(blame) - } + if fork_builtins { + log::trace!("Forking builtin: for"); + self.run_fork("for", |s| { + if let Err(e) = for_logic(s) { + eprintln!("{e}"); + } + }) + } else { + for_logic(self).try_blame(blame) + } } fn exec_if(&mut self, if_stmt: Node) -> ShResult<()> { - let blame = if_stmt.get_span().clone(); + let blame = if_stmt.get_span().clone(); let NdRule::IfNode { cond_nodes, else_block, @@ -613,61 +646,61 @@ impl Dispatcher { else { unreachable!(); }; - let fork_builtins = if_stmt.flags.contains(NdFlags::FORK_BUILTINS); + let fork_builtins = if_stmt.flags.contains(NdFlags::FORK_BUILTINS); self.io_stack.append_to_frame(if_stmt.redirs); - let _guard = self.io_stack.pop_frame().redirect()?; + let _guard = self.io_stack.pop_frame().redirect()?; - let if_logic = |s: &mut Self| -> ShResult<()> { - let mut matched = false; - for node in cond_nodes { - let CondNode { cond, body } = node; + let if_logic = |s: &mut Self| -> ShResult<()> { + let mut matched = false; + for node in cond_nodes { + let CondNode { cond, body } = node; - if let Err(e) = s.dispatch_node(*cond) { - state::set_status(1); - return Err(e); - } + if let Err(e) = s.dispatch_node(*cond) { + state::set_status(1); + return Err(e); + } - match state::get_status() { - 0 => { - matched = true; - for body_node in body { - s.dispatch_node(body_node)?; - } - break; // Don't check remaining elif conditions - } - _ => continue, - } - } + match state::get_status() { + 0 => { + matched = true; + for body_node in body { + s.dispatch_node(body_node)?; + } + break; // Don't check remaining elif conditions + } + _ => continue, + } + } - if !matched && !else_block.is_empty() { - for node in else_block { - s.dispatch_node(node)?; - } - } + if !matched && !else_block.is_empty() { + for node in else_block { + s.dispatch_node(node)?; + } + } - Ok(()) - }; + Ok(()) + }; - if fork_builtins { - log::trace!("Forking builtin: if"); - self.run_fork("if", |s| { - if let Err(e) = if_logic(s) { - eprintln!("{e}"); - state::set_status(1); - } - }) - } else { - if_logic(self).try_blame(blame) - } + if fork_builtins { + log::trace!("Forking builtin: if"); + self.run_fork("if", |s| { + if let Err(e) = if_logic(s) { + eprintln!("{e}"); + state::set_status(1); + } + }) + } else { + if_logic(self).try_blame(blame) + } } fn exec_pipeline(&mut self, pipeline: Node) -> ShResult<()> { let NdRule::Pipeline { cmds, pipe_err: _ } = pipeline.class else { unreachable!() }; self.job_stack.new_job(); - let fork_builtin = cmds.len() > 1; // If there's more than one command, we need to fork builtins - let (mut in_redirs, mut out_redirs) = self.io_stack.pop_frame().redirs.split_by_channel(); + let fork_builtin = cmds.len() > 1; // If there's more than one command, we need to fork builtins + let (mut in_redirs, mut out_redirs) = self.io_stack.pop_frame().redirs.split_by_channel(); // Zip the commands and their respective pipes into an iterator let pipes_and_cmds = get_pipe_stack(cmds.len()).into_iter().zip(cmds); @@ -676,21 +709,21 @@ impl Dispatcher { if let Some(pipe) = rpipe { self.io_stack.push_to_frame(pipe); } else { - for redir in std::mem::take(&mut in_redirs) { - self.io_stack.push_to_frame(redir); - } - } + for redir in std::mem::take(&mut in_redirs) { + self.io_stack.push_to_frame(redir); + } + } if let Some(pipe) = wpipe { self.io_stack.push_to_frame(pipe); } else { - for redir in std::mem::take(&mut out_redirs) { - self.io_stack.push_to_frame(redir); - } - } + for redir in std::mem::take(&mut out_redirs) { + self.io_stack.push_to_frame(redir); + } + } - if fork_builtin { - cmd.flags |= NdFlags::FORK_BUILTINS; - } + if fork_builtin { + cmd.flags |= NdFlags::FORK_BUILTINS; + } self.dispatch_node(cmd)?; } let job = self.job_stack.finalize_job().unwrap(); @@ -699,32 +732,35 @@ impl Dispatcher { Ok(()) } fn exec_builtin(&mut self, cmd: Node) -> ShResult<()> { - let fork_builtins = cmd.flags.contains(NdFlags::FORK_BUILTINS); - let cmd_raw = cmd.get_command().unwrap_or_else(|| panic!("expected command NdRule, got {:?}", &cmd.class)).to_string(); + let fork_builtins = cmd.flags.contains(NdFlags::FORK_BUILTINS); + let cmd_raw = cmd + .get_command() + .unwrap_or_else(|| panic!("expected command NdRule, got {:?}", &cmd.class)) + .to_string(); - if fork_builtins { - log::trace!("Forking builtin: {}", cmd_raw); - let _guard = self.io_stack.pop_frame().redirect()?; - self.run_fork(&cmd_raw, |s| { - if let Err(e) = s.dispatch_builtin(cmd) { - eprintln!("{e}"); - } - }) - } else { - let result = self.dispatch_builtin(cmd); + if fork_builtins { + log::trace!("Forking builtin: {}", cmd_raw); + let _guard = self.io_stack.pop_frame().redirect()?; + self.run_fork(&cmd_raw, |s| { + if let Err(e) = s.dispatch_builtin(cmd) { + eprintln!("{e}"); + } + }) + } else { + let result = self.dispatch_builtin(cmd); - if let Err(e) = result { - let code = state::get_status(); - if code == 0 { - state::set_status(1); - } - return Err(e); - } - Ok(()) - } + if let Err(e) = result { + let code = state::get_status(); + if code == 0 { + state::set_status(1); + } + return Err(e); + } + Ok(()) + } } - fn dispatch_builtin(&mut self, mut cmd: Node) -> ShResult<()> { - let cmd_raw = cmd.get_command().unwrap().to_string(); + fn dispatch_builtin(&mut self, mut cmd: Node) -> ShResult<()> { + let cmd_raw = cmd.get_command().unwrap().to_string(); let NdRule::Command { assignments, argv } = &mut cmd.class else { unreachable!() }; @@ -751,51 +787,48 @@ impl Dispatcher { } return self.exec_cmd(cmd); } - match cmd_raw.as_str() { - "echo" => echo(cmd, io_stack_mut, curr_job_mut), - "cd" => cd(cmd, curr_job_mut), - "export" => export(cmd, io_stack_mut, curr_job_mut), - "local" => local(cmd, io_stack_mut, curr_job_mut), - "pwd" => pwd(cmd, io_stack_mut, curr_job_mut), - "source" => source(cmd, curr_job_mut), - "shift" => shift(cmd, curr_job_mut), - "fg" => continue_job(cmd, curr_job_mut, JobBehavior::Foregound), - "bg" => continue_job(cmd, curr_job_mut, JobBehavior::Background), - "disown" => disown(cmd, io_stack_mut, curr_job_mut), - "jobs" => jobs(cmd, io_stack_mut, curr_job_mut), - "alias" => alias(cmd, io_stack_mut, curr_job_mut), - "unalias" => unalias(cmd, io_stack_mut, curr_job_mut), - "return" => flowctl(cmd, ShErrKind::FuncReturn(0)), - "break" => flowctl(cmd, ShErrKind::LoopBreak(0)), - "continue" => flowctl(cmd, ShErrKind::LoopContinue(0)), - "exit" => flowctl(cmd, ShErrKind::CleanExit(0)), - "zoltraak" => zoltraak(cmd, io_stack_mut, curr_job_mut), - "shopt" => shopt(cmd, io_stack_mut, curr_job_mut), - "read" => read_builtin(cmd, io_stack_mut, curr_job_mut), - "trap" => trap(cmd, io_stack_mut, curr_job_mut), - "pushd" => pushd(cmd, io_stack_mut, curr_job_mut), - "popd" => popd(cmd, io_stack_mut, curr_job_mut), - "dirs" => dirs(cmd, io_stack_mut, curr_job_mut), - "exec" => exec::exec_builtin(cmd, io_stack_mut, curr_job_mut), - "eval" => eval::eval(cmd, io_stack_mut, curr_job_mut), - "readonly" => readonly(cmd, io_stack_mut, curr_job_mut), - "unset" => unset(cmd, io_stack_mut, curr_job_mut), - "complete" => complete_builtin(cmd, io_stack_mut, curr_job_mut), - "compgen" => compgen_builtin(cmd, io_stack_mut, curr_job_mut), - "true" | ":" => { - state::set_status(0); - Ok(()) - }, - "false" => { - state::set_status(1); - Ok(()) - }, - _ => unimplemented!( - "Have not yet added support for builtin '{}'", - cmd_raw - ), - } - } + match cmd_raw.as_str() { + "echo" => echo(cmd, io_stack_mut, curr_job_mut), + "cd" => cd(cmd, curr_job_mut), + "export" => export(cmd, io_stack_mut, curr_job_mut), + "local" => local(cmd, io_stack_mut, curr_job_mut), + "pwd" => pwd(cmd, io_stack_mut, curr_job_mut), + "source" => source(cmd, curr_job_mut), + "shift" => shift(cmd, curr_job_mut), + "fg" => continue_job(cmd, curr_job_mut, JobBehavior::Foregound), + "bg" => continue_job(cmd, curr_job_mut, JobBehavior::Background), + "disown" => disown(cmd, io_stack_mut, curr_job_mut), + "jobs" => jobs(cmd, io_stack_mut, curr_job_mut), + "alias" => alias(cmd, io_stack_mut, curr_job_mut), + "unalias" => unalias(cmd, io_stack_mut, curr_job_mut), + "return" => flowctl(cmd, ShErrKind::FuncReturn(0)), + "break" => flowctl(cmd, ShErrKind::LoopBreak(0)), + "continue" => flowctl(cmd, ShErrKind::LoopContinue(0)), + "exit" => flowctl(cmd, ShErrKind::CleanExit(0)), + "zoltraak" => zoltraak(cmd, io_stack_mut, curr_job_mut), + "shopt" => shopt(cmd, io_stack_mut, curr_job_mut), + "read" => read_builtin(cmd, io_stack_mut, curr_job_mut), + "trap" => trap(cmd, io_stack_mut, curr_job_mut), + "pushd" => pushd(cmd, io_stack_mut, curr_job_mut), + "popd" => popd(cmd, io_stack_mut, curr_job_mut), + "dirs" => dirs(cmd, io_stack_mut, curr_job_mut), + "exec" => exec::exec_builtin(cmd, io_stack_mut, curr_job_mut), + "eval" => eval::eval(cmd, io_stack_mut, curr_job_mut), + "readonly" => readonly(cmd, io_stack_mut, curr_job_mut), + "unset" => unset(cmd, io_stack_mut, curr_job_mut), + "complete" => complete_builtin(cmd, io_stack_mut, curr_job_mut), + "compgen" => compgen_builtin(cmd, io_stack_mut, curr_job_mut), + "true" | ":" => { + state::set_status(0); + Ok(()) + } + "false" => { + state::set_status(1); + Ok(()) + } + _ => unimplemented!("Have not yet added support for builtin '{}'", cmd_raw), + } + } fn exec_cmd(&mut self, cmd: Node) -> ShResult<()> { let NdRule::Command { assignments, argv } = cmd.class else { unreachable!() @@ -810,10 +843,10 @@ impl Dispatcher { env_vars_to_unset = self.set_assignments(assignments, assign_behavior)?; } - let no_fork = cmd.flags.contains(NdFlags::NO_FORK); + let no_fork = cmd.flags.contains(NdFlags::NO_FORK); if argv.is_empty() { - state::set_status(0); + state::set_status(0); return Ok(()); } @@ -823,30 +856,30 @@ impl Dispatcher { let _guard = self.io_stack.pop_frame().redirect()?; let job = self.job_stack.curr_job_mut().unwrap(); - let child_logic = || -> ! { - let cmd = &exec_args.cmd.0; - let span = exec_args.cmd.1; + let child_logic = || -> ! { + let cmd = &exec_args.cmd.0; + let span = exec_args.cmd.1; - let Err(e) = execvpe(cmd, &exec_args.argv, &exec_args.envp); + let Err(e) = execvpe(cmd, &exec_args.argv, &exec_args.envp); - // execvpe only returns on error - let cmd_str = cmd.to_str().unwrap().to_string(); - match e { - Errno::ENOENT => { - let err = ShErr::full(ShErrKind::CmdNotFound(cmd_str), "", span); - eprintln!("{err}"); - } - _ => { - let err = ShErr::full(ShErrKind::Errno(e), format!("{e}"), span); - eprintln!("{err}"); - } - } - exit(e as i32) - }; + // execvpe only returns on error + let cmd_str = cmd.to_str().unwrap().to_string(); + match e { + Errno::ENOENT => { + let err = ShErr::full(ShErrKind::CmdNotFound(cmd_str), "", span); + eprintln!("{err}"); + } + _ => { + let err = ShErr::full(ShErrKind::Errno(e), format!("{e}"), span); + eprintln!("{err}"); + } + } + exit(e as i32) + }; - if no_fork { - child_logic(); - } + if no_fork { + child_logic(); + } match unsafe { fork()? } { ForkResult::Child => child_logic(), @@ -875,27 +908,27 @@ impl Dispatcher { Ok(()) } - fn run_fork(&mut self, name: &str, f: impl FnOnce(&mut Self)) -> ShResult<()> { - match unsafe { fork()? } { - ForkResult::Child => { - f(self); - exit(state::get_status()) - } - ForkResult::Parent { child } => { - write_jobs(|j| j.drain_registered_fds()); - let job = self.job_stack.curr_job_mut().unwrap(); - let child_pgid = if let Some(pgid) = job.pgid() { - pgid - } else { - job.set_pgid(child); - child - }; - let child_proc = ChildProc::new(child, Some(name), Some(child_pgid))?; - job.push_child(child_proc); - Ok(()) - } - } - } + fn run_fork(&mut self, name: &str, f: impl FnOnce(&mut Self)) -> ShResult<()> { + match unsafe { fork()? } { + ForkResult::Child => { + f(self); + exit(state::get_status()) + } + ForkResult::Parent { child } => { + write_jobs(|j| j.drain_registered_fds()); + let job = self.job_stack.curr_job_mut().unwrap(); + let child_pgid = if let Some(pgid) = job.pgid() { + pgid + } else { + job.set_pgid(child); + child + }; + let child_proc = ChildProc::new(child, Some(name), Some(child_pgid))?; + job.push_child(child_proc); + Ok(()) + } + } + } fn set_assignments(&self, assigns: Vec, behavior: AssignBehavior) -> ShResult> { let mut new_env_vars = vec![]; let flags = match behavior { @@ -917,9 +950,7 @@ impl Dispatcher { // Parse and expand array index BEFORE entering write_vars borrow let indexed = state::parse_arr_bracket(var) - .map(|(name, idx_raw)| { - state::expand_arr_index(&idx_raw).map(|idx| (name, idx)) - }) + .map(|(name, idx_raw)| state::expand_arr_index(&idx_raw).map(|idx| (name, idx))) .transpose()?; match kind { diff --git a/src/parse/lex.rs b/src/parse/lex.rs index 5b38637..b53933f 100644 --- a/src/parse/lex.rs +++ b/src/parse/lex.rs @@ -152,7 +152,7 @@ pub struct LexStream { source: Arc, pub cursor: usize, in_quote: bool, - brc_grp_start: Option, + brc_grp_start: Option, flags: LexFlags, } @@ -187,7 +187,7 @@ impl LexStream { source, cursor: 0, in_quote: false, - brc_grp_start: None, + brc_grp_start: None, flags, } } @@ -222,10 +222,10 @@ impl LexStream { pub fn set_in_brc_grp(&mut self, is: bool) { if is { self.flags |= LexFlags::IN_BRC_GRP; - self.brc_grp_start = Some(self.cursor); + self.brc_grp_start = Some(self.cursor); } else { self.flags &= !LexFlags::IN_BRC_GRP; - self.brc_grp_start = None; + self.brc_grp_start = None; } } pub fn next_is_cmd(&self) -> bool { @@ -269,8 +269,8 @@ impl LexStream { } if !found_fd && !self.flags.contains(LexFlags::LEX_UNFINISHED) { - let span_start = self.cursor; - self.cursor = pos; + let span_start = self.cursor; + self.cursor = pos; return Some(Err(ShErr::full( ShErrKind::ParseErr, "Invalid redirection", @@ -624,35 +624,35 @@ impl LexStream { } } } - '=' if chars.peek() == Some(&'(') => { - pos += 1; // '=' - let mut depth = 1; - chars.next(); - pos += 1; // '(' - // looks like an array - while let Some(arr_ch) = chars.next() { - match arr_ch { - '\\' => { - pos += 1; - if let Some(next_ch) = chars.next() { - pos += next_ch.len_utf8(); - } - } - '(' => { - depth += 1; - pos += 1; - } - ')' => { - depth -= 1; - pos += 1; - if depth == 0 { - break; - } - } - _ => pos += arr_ch.len_utf8(), - } - } - } + '=' if chars.peek() == Some(&'(') => { + pos += 1; // '=' + let mut depth = 1; + chars.next(); + pos += 1; // '(' + // looks like an array + while let Some(arr_ch) = chars.next() { + match arr_ch { + '\\' => { + pos += 1; + if let Some(next_ch) = chars.next() { + pos += next_ch.len_utf8(); + } + } + '(' => { + depth += 1; + pos += 1; + } + ')' => { + depth -= 1; + pos += 1; + if depth == 0 { + break; + } + } + _ => pos += arr_ch.len_utf8(), + } + } + } _ if !self.in_quote && is_op(ch) => break, _ if is_hard_sep(ch) => break, _ => pos += ch.len_utf8(), @@ -660,7 +660,7 @@ impl LexStream { } let mut new_tk = self.get_token(self.cursor..pos, TkRule::Str); if self.in_quote && !self.flags.contains(LexFlags::LEX_UNFINISHED) { - self.cursor = pos; + self.cursor = pos; return Err(ShErr::full( ShErrKind::ParseErr, "Unterminated quote", @@ -692,9 +692,9 @@ impl LexStream { } _ if is_cmd_sub(text) => { new_tk.mark(TkFlags::IS_CMDSUB); - if self.next_is_cmd() { - new_tk.mark(TkFlags::IS_CMD); - } + if self.next_is_cmd() { + new_tk.mark(TkFlags::IS_CMD); + } self.set_next_is_cmd(false); } _ => { @@ -731,15 +731,16 @@ impl Iterator for LexStream { return None; } else { // Return the EOI token - if self.in_brc_grp() && !self.flags.contains(LexFlags::LEX_UNFINISHED) { - let start = self.brc_grp_start.unwrap_or(self.cursor.saturating_sub(1)); - self.flags |= LexFlags::STALE; - return Err(ShErr::full( - ShErrKind::ParseErr, - "Unclosed brace group", - Span::new(start..self.cursor, self.source.clone()), - )).into(); - } + if self.in_brc_grp() && !self.flags.contains(LexFlags::LEX_UNFINISHED) { + let start = self.brc_grp_start.unwrap_or(self.cursor.saturating_sub(1)); + self.flags |= LexFlags::STALE; + return Err(ShErr::full( + ShErrKind::ParseErr, + "Unclosed brace group", + Span::new(start..self.cursor, self.source.clone()), + )) + .into(); + } let token = self.get_token(self.cursor..self.cursor, TkRule::EOI); self.flags |= LexFlags::STALE; return Some(Ok(token)); @@ -770,14 +771,15 @@ impl Iterator for LexStream { } if self.cursor == self.source.len() { - if self.in_brc_grp() && !self.flags.contains(LexFlags::LEX_UNFINISHED) { - let start = self.brc_grp_start.unwrap_or(self.cursor.saturating_sub(1)); - return Err(ShErr::full( - ShErrKind::ParseErr, - "Unclosed brace group", - Span::new(start..self.cursor, self.source.clone()), - )).into(); - } + if self.in_brc_grp() && !self.flags.contains(LexFlags::LEX_UNFINISHED) { + let start = self.brc_grp_start.unwrap_or(self.cursor.saturating_sub(1)); + return Err(ShErr::full( + ShErrKind::ParseErr, + "Unclosed brace group", + Span::new(start..self.cursor, self.source.clone()), + )) + .into(); + } return None; } @@ -899,26 +901,27 @@ pub fn is_field_sep(ch: char) -> bool { } pub fn is_keyword(slice: &str) -> bool { - KEYWORDS.contains(&slice) || (ends_with_unescaped(slice, "()") && !ends_with_unescaped(slice, "=()")) + KEYWORDS.contains(&slice) + || (ends_with_unescaped(slice, "()") && !ends_with_unescaped(slice, "=()")) } pub fn is_cmd_sub(slice: &str) -> bool { - slice.starts_with("$(") && ends_with_unescaped(slice,")") + slice.starts_with("$(") && ends_with_unescaped(slice, ")") } 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()) } pub fn pos_is_escaped(slice: &str, pos: usize) -> bool { - let bytes = slice.as_bytes(); - let mut escaped = false; - let mut i = pos; - while i > 0 && bytes[i - 1] == b'\\' { - escaped = !escaped; - i -= 1; - } - escaped + let bytes = slice.as_bytes(); + let mut escaped = false; + let mut i = pos; + while i > 0 && bytes[i - 1] == b'\\' { + escaped = !escaped; + i -= 1; + } + escaped } pub fn lookahead(pat: &str, mut chars: Chars) -> Option { diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 36b897f..7ad95c3 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -117,9 +117,10 @@ impl Node { if let NdRule::Command { assignments: _, argv, - } = &self.class { - argv.iter().next() - } else { + } = &self.class + { + argv.iter().next() + } else { None } } @@ -142,9 +143,9 @@ bitflags! { #[derive(Clone,Copy,Debug)] pub struct NdFlags: u32 { const BACKGROUND = 0b000001; - const FORK_BUILTINS = 0b000010; - const NO_FORK = 0b000100; - const ARR_ASSIGN = 0b001000; + const FORK_BUILTINS = 0b000010; + const NO_FORK = 0b000100; + const ARR_ASSIGN = 0b001000; } } @@ -1380,7 +1381,7 @@ impl ParseStream { redirs.push(redir); } } - TkRule::Comment => { /* Skip comments in command position */ } + TkRule::Comment => { /* Skip comments in command position */ } _ => unimplemented!("Unexpected token rule `{:?}` in parse_cmd()", tk.class), } } @@ -1473,14 +1474,16 @@ impl ParseStream { } } } - if let Some(assign_kind) = assign_kind && !var_name.is_empty() { + if let Some(assign_kind) = assign_kind + && !var_name.is_empty() + { let var = Tk::new(TkRule::Str, Span::new(name_range, token.source())); let val = Tk::new(TkRule::Str, Span::new(val_range, token.source())); - let flags = if var_val.starts_with('(') && var_val.ends_with(')') { - NdFlags::ARR_ASSIGN - } else { - NdFlags::empty() - }; + let flags = if var_val.starts_with('(') && var_val.ends_with(')') { + NdFlags::ARR_ASSIGN + } else { + NdFlags::empty() + }; Some(Node { class: NdRule::Assignment { @@ -1493,8 +1496,8 @@ impl ParseStream { redirs: vec![], }) } else { - None - } + None + } } } diff --git a/src/prelude.rs b/src/prelude.rs index aa9a2c2..72664f7 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -19,17 +19,17 @@ pub use std::os::unix::io::{AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd, pub use bitflags::bitflags; pub use nix::{ errno::Errno, - fcntl::{open, OFlag}, + fcntl::{OFlag, open}, libc::{self, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO}, sys::{ - signal::{self, kill, killpg, pthread_sigmask, signal, SigHandler, SigSet, SigmaskHow, Signal}, + signal::{self, SigHandler, SigSet, SigmaskHow, Signal, kill, killpg, pthread_sigmask, signal}, stat::Mode, termios::{self}, - wait::{waitpid, WaitPidFlag as WtFlag, WaitStatus as WtStat}, + wait::{WaitPidFlag as WtFlag, WaitStatus as WtStat, waitpid}, }, unistd::{ - close, dup, dup2, execvpe, fork, getpgid, getpgrp, isatty, pipe, read, setpgid, tcgetpgrp, - tcsetpgrp, write, ForkResult, Pid, + ForkResult, Pid, close, dup, dup2, execvpe, fork, getpgid, getpgrp, isatty, pipe, read, + setpgid, tcgetpgrp, tcsetpgrp, write, }, }; diff --git a/src/procio.rs b/src/procio.rs index d0eac79..25a0fc4 100644 --- a/src/procio.rs +++ b/src/procio.rs @@ -9,7 +9,7 @@ use crate::{ error::{ShErr, ShErrKind, ShResult}, utils::RedirVecUtils, }, - parse::{get_redir_file, Redir, RedirType}, + parse::{Redir, RedirType, get_redir_file}, prelude::*, }; @@ -79,7 +79,7 @@ impl IoMode { let path_raw = path.as_os_str().to_str().unwrap_or_default().to_string(); let expanded_path = Expander::from_raw(&path_raw)?.expand()?.join(" "); // should just be one string, will have to find some way to handle a return of - // multiple + // multiple let expanded_pathbuf = PathBuf::from(expanded_path); @@ -343,9 +343,9 @@ impl DerefMut for IoStack { } impl From> for IoStack { - fn from(frames: Vec) -> Self { - Self { stack: frames } - } + fn from(frames: Vec) -> Self { + Self { stack: frames } + } } pub fn borrow_fd<'f>(fd: i32) -> BorrowedFd<'f> { diff --git a/src/readline/complete.rs b/src/readline/complete.rs index 39340c8..f5d0630 100644 --- a/src/readline/complete.rs +++ b/src/readline/complete.rs @@ -1,374 +1,452 @@ -use std::{collections::HashSet, env, fmt::Debug, os::unix::fs::PermissionsExt, path::PathBuf, sync::Arc}; +use std::{ + collections::HashSet, env, fmt::Debug, os::unix::fs::PermissionsExt, path::PathBuf, sync::Arc, +}; use crate::{ - builtin::{BUILTINS, complete::{CompFlags, CompOptFlags, CompOpts}}, - libsh::{error::{ShErr, ShErrKind, ShResult}, utils::TkVecUtils}, - parse::{execute::{VarCtxGuard, exec_input}, lex::{self, LexFlags, Tk, TkFlags, TkRule, ends_with_unescaped}}, + builtin::{ + BUILTINS, + complete::{CompFlags, CompOptFlags, CompOpts}, + }, + libsh::{ + error::{ShErr, ShErrKind, ShResult}, + utils::TkVecUtils, + }, + parse::{ + execute::{VarCtxGuard, exec_input}, + lex::{self, LexFlags, Tk, TkFlags, TkRule, ends_with_unescaped}, + }, readline::{ Marker, annotate_input, annotate_input_recursive, get_insertions, markers::{self, is_marker}, }, - state::{VarFlags, VarKind, read_logic, read_meta, read_vars, write_vars}, + state::{VarFlags, VarKind, read_jobs, read_logic, read_meta, read_vars, write_vars}, }; +pub fn complete_jobs(start: &str) -> Vec { + if let Some(prefix) = start.strip_prefix('%') { + read_jobs(|j| { + j.jobs() + .iter() + .filter_map(|j| j.as_ref()) + .filter_map(|j| j.name()) + .filter(|name| name.starts_with(prefix)) + .map(|name| format!("%{name}")) + .collect() + }) + } else { + read_jobs(|j| { + j.jobs() + .iter() + .filter_map(|j| j.as_ref()) + .map(|j| j.pgid().to_string()) + .filter(|pgid| pgid.starts_with(start)) + .collect() + }) + } +} + pub fn complete_users(start: &str) -> Vec { - let Ok(passwd) = std::fs::read_to_string("/etc/passwd") else { - return vec![]; - }; - passwd - .lines() - .filter_map(|line| line.split(':').next()) - .filter(|username| username.starts_with(start)) - .map(|s| s.to_string()) - .collect() + let Ok(passwd) = std::fs::read_to_string("/etc/passwd") else { + return vec![]; + }; + passwd + .lines() + .filter_map(|line| line.split(':').next()) + .filter(|username| username.starts_with(start)) + .map(|s| s.to_string()) + .collect() } pub fn complete_vars(start: &str) -> Vec { - let Some((var_name, name_start, _end)) = extract_var_name(start) else { - return vec![] - }; - if !read_vars(|v| v.get_var(&var_name)).is_empty() { - return vec![] - } - // if we are here, we have a variable substitution that isn't complete - // so let's try to complete it - let prefix = &start[..name_start]; // e.g. "$" or "${" - read_vars(|v| { - v.flatten_vars() - .keys() - .filter(|k| k.starts_with(&var_name) && *k != &var_name) - .map(|k| format!("{prefix}{k}")) - .collect::>() - - }) + let Some((var_name, name_start, _end)) = extract_var_name(start) else { + return vec![]; + }; + if !read_vars(|v| v.get_var(&var_name)).is_empty() { + return vec![]; + } + // if we are here, we have a variable substitution that isn't complete + // so let's try to complete it + let prefix = &start[..name_start]; // e.g. "$" or "${" + read_vars(|v| { + v.flatten_vars() + .keys() + .filter(|k| k.starts_with(&var_name) && *k != &var_name) + .map(|k| format!("{prefix}{k}")) + .collect::>() + }) } pub fn extract_var_name(text: &str) -> Option<(String, usize, usize)> { - let mut chars = text.chars().peekable(); - let mut name = String::new(); - let mut reading_name = false; - let mut pos = 0; - let mut name_start = 0; - let mut name_end = 0; + let mut chars = text.chars().peekable(); + let mut name = String::new(); + let mut reading_name = false; + let mut pos = 0; + let mut name_start = 0; + let mut name_end = 0; - while let Some(ch) = chars.next() { - match ch { - '$' => { - if chars.peek() == Some(&'{') { - continue; - } + while let Some(ch) = chars.next() { + match ch { + '$' => { + if chars.peek() == Some(&'{') { + continue; + } - reading_name = true; - name_start = pos + 1; // Start after the '$' - } - '{' if !reading_name => { - reading_name = true; - name_start = pos + 1; - } - ch if ch.is_alphanumeric() || ch == '_' => { - if reading_name { - name.push(ch); - } - } - _ => { - if reading_name { - name_end = pos; // End before the non-alphanumeric character - break; - } - } - } - pos += 1; - } + reading_name = true; + name_start = pos + 1; // Start after the '$' + } + '{' if !reading_name => { + reading_name = true; + name_start = pos + 1; + } + ch if ch.is_alphanumeric() || ch == '_' => { + if reading_name { + name.push(ch); + } + } + _ => { + if reading_name { + name_end = pos; // End before the non-alphanumeric character + break; + } + } + } + pos += 1; + } - if !reading_name { - return None; - } + if !reading_name { + return None; + } - if name_end == 0 { - name_end = pos; - } + if name_end == 0 { + name_end = pos; + } - Some((name, name_start, name_end)) + Some((name, name_start, name_end)) } fn complete_commands(start: &str) -> Vec { - let mut candidates: Vec = read_meta(|m| { - m.cached_cmds() - .iter() - .filter(|c| c.starts_with(start)) - .cloned() - .collect() - }); + let mut candidates: Vec = read_meta(|m| { + m.cached_cmds() + .iter() + .filter(|c| c.starts_with(start)) + .cloned() + .collect() + }); - candidates.sort(); - candidates + candidates.sort(); + candidates } fn complete_dirs(start: &str) -> Vec { - let filenames = complete_filename(start); - filenames.into_iter().filter(|f| std::fs::metadata(f).map(|m| m.is_dir()).unwrap_or(false)).collect() + let filenames = complete_filename(start); + filenames + .into_iter() + .filter(|f| std::fs::metadata(f).map(|m| m.is_dir()).unwrap_or(false)) + .collect() } fn complete_filename(start: &str) -> Vec { - let mut candidates = vec![]; - let has_dotslash = start.starts_with("./"); + let mut candidates = vec![]; + let has_dotslash = start.starts_with("./"); - // Split path into directory and filename parts - // Use "." if start is empty (e.g., after "foo=") - let path = PathBuf::from(if start.is_empty() { "." } else { start }); - let (dir, prefix) = if start.ends_with('/') || start.is_empty() { - // Completing inside a directory: "src/" → dir="src/", prefix="" - (path, "") - } else if let Some(parent) = path.parent() - && !parent.as_os_str().is_empty() - { - // Has directory component: "src/ma" → dir="src", prefix="ma" - ( - parent.to_path_buf(), - path.file_name().unwrap().to_str().unwrap_or(""), - ) - } else { - // No directory: "fil" → dir=".", prefix="fil" - (PathBuf::from("."), start) - }; + // Split path into directory and filename parts + // Use "." if start is empty (e.g., after "foo=") + let path = PathBuf::from(if start.is_empty() { "." } else { start }); + let (dir, prefix) = if start.ends_with('/') || start.is_empty() { + // Completing inside a directory: "src/" → dir="src/", prefix="" + (path, "") + } else if let Some(parent) = path.parent() + && !parent.as_os_str().is_empty() + { + // Has directory component: "src/ma" → dir="src", prefix="ma" + ( + parent.to_path_buf(), + path.file_name().unwrap().to_str().unwrap_or(""), + ) + } else { + // No directory: "fil" → dir=".", prefix="fil" + (PathBuf::from("."), start) + }; - let Ok(entries) = std::fs::read_dir(&dir) else { - return candidates; - }; + let Ok(entries) = std::fs::read_dir(&dir) else { + return candidates; + }; - for entry in entries.flatten() { - let file_name = entry.file_name(); - let file_str = file_name.to_string_lossy(); + for entry in entries.flatten() { + let file_name = entry.file_name(); + let file_str = file_name.to_string_lossy(); - // Skip hidden files unless explicitly requested - if !prefix.starts_with('.') && file_str.starts_with('.') { - continue; - } + // Skip hidden files unless explicitly requested + if !prefix.starts_with('.') && file_str.starts_with('.') { + continue; + } - if file_str.starts_with(prefix) { - // Reconstruct full path - let mut full_path = dir.join(&file_name); + if file_str.starts_with(prefix) { + // Reconstruct full path + let mut full_path = dir.join(&file_name); - // Add trailing slash for directories - if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) { - full_path.push(""); // adds trailing / - } + // Add trailing slash for directories + if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) { + full_path.push(""); // adds trailing / + } - let mut path_raw = full_path.to_string_lossy().to_string(); - if path_raw.starts_with("./") && !has_dotslash { - path_raw = path_raw.trim_start_matches("./").to_string(); - } + let mut path_raw = full_path.to_string_lossy().to_string(); + if path_raw.starts_with("./") && !has_dotslash { + path_raw = path_raw.trim_start_matches("./").to_string(); + } - candidates.push(path_raw); - } - } + candidates.push(path_raw); + } + } - candidates.sort(); - candidates + candidates.sort(); + candidates } pub enum CompSpecResult { - NoSpec, // No compspec registered - NoMatch { flags: CompOptFlags }, // Compspec found but no candidates matched, returns behavior flags - Match(CompResult) // Compspec found and candidates returned + NoSpec, // No compspec registered + NoMatch { flags: CompOptFlags }, /* Compspec found but no candidates matched, returns + * behavior flags */ + Match(CompResult), // Compspec found and candidates returned } -#[derive(Default,Debug,Clone)] +#[derive(Default, Debug, Clone)] pub struct BashCompSpec { - /// -F: The name of a function to generate the possible completions. - pub function: Option, - /// -W: The list of words - pub wordlist: Option>, - /// -f: complete file names - pub files: bool, - /// -d: complete directory names - pub dirs: bool, - /// -c: complete command names - pub commands: bool, - /// -u: complete user names - pub users: bool, - /// -v complete variable names - pub vars: bool, - /// -A signal: complete signal names - pub signals: bool, + /// -F: The name of a function to generate the possible completions. + pub function: Option, + /// -W: The list of words + pub wordlist: Option>, + /// -f: complete file names + pub files: bool, + /// -d: complete directory names + pub dirs: bool, + /// -c: complete command names + pub commands: bool, + /// -u: complete user names + pub users: bool, + /// -v: complete variable names + pub vars: bool, + /// -A signal: complete signal names + pub signals: bool, + /// -j: complete job pids or names + pub jobs: bool, - pub flags: CompOptFlags, - /// The original command - pub source: String + pub flags: CompOptFlags, + /// The original command + pub source: String, } impl BashCompSpec { - pub fn new() -> Self { - Self::default() - } - pub fn with_func(mut self, func: String) -> Self { - self.function = Some(func); - self - } - pub fn with_wordlist(mut self, wordlist: Vec) -> Self { - self.wordlist = Some(wordlist); - self - } - pub fn with_source(mut self, source: String) -> Self { - self.source = source; - self - } - pub fn files(mut self, enable: bool) -> Self { - self.files = enable; - self - } - pub fn dirs(mut self, enable: bool) -> Self { - self.dirs = enable; - self - } - pub fn commands(mut self, enable: bool) -> Self { - self.commands = enable; - self - } - pub fn users(mut self, enable: bool) -> Self { - self.users = enable; - self - } - pub fn vars(mut self, enable: bool) -> Self { - self.vars = enable; - self - } - pub fn signals(mut self, enable: bool) -> Self { - self.signals = enable; - self - } - pub fn from_comp_opts(opts: CompOpts) -> Self { - let CompOpts { func, wordlist, action: _, flags, opt_flags } = opts; - Self { - function: func, - wordlist, - files: flags.contains(CompFlags::FILES), - dirs: flags.contains(CompFlags::DIRS), - commands: flags.contains(CompFlags::CMDS), - users: flags.contains(CompFlags::USERS), - vars: flags.contains(CompFlags::VARS), - flags: opt_flags, - signals: false, // TODO: implement signal completion - source: String::new() - } - } - pub fn exec_comp_func(&self, ctx: &CompContext) -> ShResult> { - let mut vars_to_unset = HashSet::new(); - for var in [ "COMP_WORDS", "COMP_CWORD", "COMP_LINE", "COMP_POINT", "COMPREPLY" ] { - vars_to_unset.insert(var.to_string()); - } - let _guard = VarCtxGuard::new(vars_to_unset); + pub fn new() -> Self { + Self::default() + } + pub fn with_func(mut self, func: String) -> Self { + self.function = Some(func); + self + } + pub fn with_wordlist(mut self, wordlist: Vec) -> Self { + self.wordlist = Some(wordlist); + self + } + pub fn with_source(mut self, source: String) -> Self { + self.source = source; + self + } + pub fn files(mut self, enable: bool) -> Self { + self.files = enable; + self + } + pub fn dirs(mut self, enable: bool) -> Self { + self.dirs = enable; + self + } + pub fn commands(mut self, enable: bool) -> Self { + self.commands = enable; + self + } + pub fn users(mut self, enable: bool) -> Self { + self.users = enable; + self + } + pub fn vars(mut self, enable: bool) -> Self { + self.vars = enable; + self + } + pub fn signals(mut self, enable: bool) -> Self { + self.signals = enable; + self + } + pub fn jobs(mut self, enable: bool) -> Self { + self.jobs = enable; + self + } + pub fn from_comp_opts(opts: CompOpts) -> Self { + let CompOpts { + func, + wordlist, + action: _, + flags, + opt_flags, + } = opts; + Self { + function: func, + wordlist, + files: flags.contains(CompFlags::FILES), + dirs: flags.contains(CompFlags::DIRS), + commands: flags.contains(CompFlags::CMDS), + users: flags.contains(CompFlags::USERS), + vars: flags.contains(CompFlags::VARS), + jobs: flags.contains(CompFlags::JOBS), + flags: opt_flags, + signals: false, // TODO: implement signal completion + source: String::new(), + } + } + pub fn exec_comp_func(&self, ctx: &CompContext) -> ShResult> { + let mut vars_to_unset = HashSet::new(); + for var in [ + "COMP_WORDS", + "COMP_CWORD", + "COMP_LINE", + "COMP_POINT", + "COMPREPLY", + ] { + vars_to_unset.insert(var.to_string()); + } + let _guard = VarCtxGuard::new(vars_to_unset); - let CompContext { words, cword, line, cursor_pos } = ctx; + let CompContext { + words, + cword, + line, + cursor_pos, + } = ctx; - let raw_words = words.to_vec().into_iter().map(|tk| tk.to_string()).collect(); - write_vars(|v| v.set_var("COMP_WORDS", VarKind::arr_from_vec(raw_words), VarFlags::NONE))?; - write_vars(|v| v.set_var("COMP_CWORD", VarKind::Str(cword.to_string()), VarFlags::NONE))?; - write_vars(|v| v.set_var("COMP_LINE", VarKind::Str(line.to_string()), VarFlags::NONE))?; - write_vars(|v| v.set_var("COMP_POINT", VarKind::Str(cursor_pos.to_string()), VarFlags::NONE))?; + let raw_words = words.iter().clone().map(|tk| tk.to_string()).collect(); + write_vars(|v| { + v.set_var( + "COMP_WORDS", + VarKind::arr_from_vec(raw_words), + VarFlags::NONE, + ) + })?; + write_vars(|v| { + v.set_var( + "COMP_CWORD", + VarKind::Str(cword.to_string()), + VarFlags::NONE, + ) + })?; + write_vars(|v| v.set_var("COMP_LINE", VarKind::Str(line.to_string()), VarFlags::NONE))?; + write_vars(|v| { + v.set_var( + "COMP_POINT", + VarKind::Str(cursor_pos.to_string()), + VarFlags::NONE, + ) + })?; - let cmd_name = words - .first() - .map(|s| s.to_string()) - .unwrap_or_default(); + let cmd_name = words.first().map(|s| s.to_string()).unwrap_or_default(); - let cword_str = words.get(*cword) - .map(|s| s.to_string()) - .unwrap_or_default(); + let cword_str = words.get(*cword).map(|s| s.to_string()).unwrap_or_default(); - let pword_str = if *cword > 0 { - words.get(cword - 1).map(|s| s.to_string()).unwrap_or_default() - } else { - String::new() - }; + let pword_str = if *cword > 0 { + words + .get(cword - 1) + .map(|s| s.to_string()) + .unwrap_or_default() + } else { + String::new() + }; - let input = format!("{} {cmd_name} {cword_str} {pword_str}", self.function.as_ref().unwrap()); - exec_input(input, None, false)?; + let input = format!( + "{} {cmd_name} {cword_str} {pword_str}", + self.function.as_ref().unwrap() + ); + exec_input(input, None, false)?; - Ok(read_vars(|v| v.get_arr_elems("COMPREPLY")).unwrap_or_default()) - } + Ok(read_vars(|v| v.get_arr_elems("COMPREPLY")).unwrap_or_default()) + } } impl CompSpec for BashCompSpec { - fn complete(&self, ctx: &CompContext) -> ShResult> { - let mut candidates = vec![]; - let prefix = &ctx.words[ctx.cword]; + fn complete(&self, ctx: &CompContext) -> ShResult> { + let mut candidates = vec![]; + let prefix = &ctx.words[ctx.cword]; - let expanded = prefix.clone().expand()?.get_words().join(" "); - if self.files { - candidates.extend(complete_filename(&expanded)); - } - if self.dirs { - candidates.extend(complete_dirs(&expanded)); - } - if self.commands { - candidates.extend(complete_commands(&expanded)); - } - if self.vars { - candidates.extend(complete_vars(&expanded)); - } - if self.users { - candidates.extend(complete_users(&expanded)); - } - if let Some(words) = &self.wordlist { - candidates.extend( - words - .iter() - .filter(|w| w.starts_with(&expanded)) - .cloned(), - ); - } - if self.function.is_some() { - candidates.extend(self.exec_comp_func(ctx)?); - } + let expanded = prefix.clone().expand()?.get_words().join(" "); + if self.files { + candidates.extend(complete_filename(&expanded)); + } + if self.dirs { + candidates.extend(complete_dirs(&expanded)); + } + if self.commands { + candidates.extend(complete_commands(&expanded)); + } + if self.vars { + candidates.extend(complete_vars(&expanded)); + } + if self.users { + candidates.extend(complete_users(&expanded)); + } + if self.jobs { + candidates.extend(complete_jobs(&expanded)); + } + if let Some(words) = &self.wordlist { + candidates.extend(words.iter().filter(|w| w.starts_with(&expanded)).cloned()); + } + if self.function.is_some() { + candidates.extend(self.exec_comp_func(ctx)?); + } - Ok(candidates) - } + Ok(candidates) + } - fn source(&self) -> &str { - &self.source - } + fn source(&self) -> &str { + &self.source + } - fn get_flags(&self) -> CompOptFlags { - self.flags - } + fn get_flags(&self) -> CompOptFlags { + self.flags + } } pub trait CompSpec: Debug + CloneCompSpec { - fn complete(&self, ctx: &CompContext) -> ShResult>; - fn source(&self) -> &str; - fn get_flags(&self) -> CompOptFlags { - CompOptFlags::empty() - } + fn complete(&self, ctx: &CompContext) -> ShResult>; + fn source(&self) -> &str; + fn get_flags(&self) -> CompOptFlags { + CompOptFlags::empty() + } } pub trait CloneCompSpec { - fn clone_box(&self) -> Box; + fn clone_box(&self) -> Box; } impl CloneCompSpec for T { - fn clone_box(&self) -> Box { - Box::new(self.clone()) - } + fn clone_box(&self) -> Box { + Box::new(self.clone()) + } } impl Clone for Box { - fn clone(&self) -> Self { - self.clone_box() - } + fn clone(&self) -> Self { + self.clone_box() + } } pub struct CompContext { - pub words: Vec, - pub cword: usize, - pub line: String, - pub cursor_pos: usize + pub words: Vec, + pub cword: usize, + pub line: String, + pub cursor_pos: usize, } impl CompContext { - pub fn cmd(&self) -> Option<&str> { - self.words.first().map(|s| s.as_str()) - } + pub fn cmd(&self) -> Option<&str> { + self.words.first().map(|s| s.as_str()) + } } pub enum CompResult { @@ -391,20 +469,20 @@ impl CompResult { } } -#[derive(Default,Debug,Clone)] +#[derive(Default, Debug, Clone)] pub struct Completer { pub candidates: Vec, pub selected_idx: usize, pub original_input: String, pub token_span: (usize, usize), pub active: bool, - pub dirs_only: bool, - pub no_space: bool + pub dirs_only: bool, + pub no_space: bool, } impl Completer { pub fn new() -> Self { - Self::default() + Self::default() } pub fn slice_line(line: &str, cursor_pos: usize) -> (&str, &str) { @@ -499,29 +577,31 @@ impl Completer { self.get_completed_line() } - pub fn add_spaces(&mut self) { - if !self.no_space { - self.candidates = std::mem::take(&mut self.candidates) - .into_iter() - .map(|c| { - if !ends_with_unescaped(&c, "/") // directory + pub fn add_spaces(&mut self) { + if !self.no_space { + self.candidates = std::mem::take(&mut self.candidates) + .into_iter() + .map(|c| { + if !ends_with_unescaped(&c, "/") // directory && !ends_with_unescaped(&c, "=") // '='-type arg - && !ends_with_unescaped(&c, " ") { // already has a space - format!("{} ", c) - } else { - c - } - }) - .collect() - } - } + && !ends_with_unescaped(&c, " ") + { + // already has a space + format!("{} ", c) + } else { + c + } + }) + .collect() + } + } pub fn start_completion(&mut self, line: String, cursor_pos: usize) -> ShResult> { let result = self.get_candidates(line.clone(), cursor_pos)?; match result { CompResult::Many { candidates } => { self.candidates = candidates.clone(); - self.add_spaces(); + self.add_spaces(); self.selected_idx = 0; self.original_input = line; self.active = true; @@ -530,7 +610,7 @@ impl Completer { } CompResult::Single { result } => { self.candidates = vec![result.clone()]; - self.add_spaces(); + self.add_spaces(); self.selected_idx = 0; self.original_input = line; self.active = false; @@ -541,7 +621,6 @@ impl Completer { } } - pub fn get_completed_line(&self) -> String { if self.candidates.is_empty() { return self.original_input.clone(); @@ -557,74 +636,85 @@ impl Completer { ) } - pub fn build_comp_ctx(&self, tks: &[Tk], line: &str, cursor_pos: usize) -> ShResult { - let mut ctx = CompContext { - words: vec![], - cword: 0, - line: line.to_string(), - cursor_pos, - }; + pub fn build_comp_ctx(&self, tks: &[Tk], line: &str, cursor_pos: usize) -> ShResult { + let mut ctx = CompContext { + words: vec![], + cword: 0, + line: line.to_string(), + cursor_pos, + }; - let segments = tks - .iter() - .filter(|&tk| !matches!(tk.class, TkRule::SOI | TkRule::EOI)) - .cloned() - .collect::>() - .split_at_separators(); + let segments = tks + .iter() + .filter(|&tk| !matches!(tk.class, TkRule::SOI | TkRule::EOI)) + .cloned() + .collect::>() + .split_at_separators(); - if segments.is_empty() { - return Ok(ctx); - } + if segments.is_empty() { + return Ok(ctx); + } - let relevant_pos = segments - .iter() - .position(|tks| tks.iter().next().is_some_and(|tk| tk.span.start > cursor_pos)) - .map(|i| i.saturating_sub(1)) - .unwrap_or(segments.len().saturating_sub(1)); + let relevant_pos = segments + .iter() + .position(|tks| { + tks + .iter() + .next() + .is_some_and(|tk| tk.span.start > cursor_pos) + }) + .map(|i| i.saturating_sub(1)) + .unwrap_or(segments.len().saturating_sub(1)); - let mut relevant = segments[relevant_pos].to_vec(); + let mut relevant = segments[relevant_pos].to_vec(); - let cword = if let Some(pos) = relevant.iter().position(|tk| { - cursor_pos >= tk.span.start && cursor_pos <= tk.span.end - }) { - pos - } else { - let insert_pos = relevant.iter() - .position(|tk| tk.span.start > cursor_pos) - .unwrap_or(relevant.len()); + let cword = if let Some(pos) = relevant + .iter() + .position(|tk| cursor_pos >= tk.span.start && cursor_pos <= tk.span.end) + { + pos + } else { + let insert_pos = relevant + .iter() + .position(|tk| tk.span.start > cursor_pos) + .unwrap_or(relevant.len()); - let mut new_tk = Tk::default(); - if let Some(tk) = relevant.last() { - let mut span = tk.span.clone(); - span.set_range(cursor_pos..cursor_pos); - new_tk.span = span; - } - relevant.insert(insert_pos, new_tk); - insert_pos - }; + let mut new_tk = Tk::default(); + if let Some(tk) = relevant.last() { + let mut span = tk.span.clone(); + span.set_range(cursor_pos..cursor_pos); + new_tk.span = span; + } + relevant.insert(insert_pos, new_tk); + insert_pos + }; - ctx.words = relevant; - ctx.cword = cword; + ctx.words = relevant; + ctx.cword = cword; - Ok(ctx) - } + Ok(ctx) + } - pub fn try_comp_spec(&self, ctx: &CompContext) -> ShResult { - let Some(cmd) = ctx.cmd() else { - return Ok(CompSpecResult::NoSpec); - }; + pub fn try_comp_spec(&self, ctx: &CompContext) -> ShResult { + let Some(cmd) = ctx.cmd() else { + return Ok(CompSpecResult::NoSpec); + }; - let Some(spec) = read_meta(|m| m.get_comp_spec(cmd)) else { - return Ok(CompSpecResult::NoSpec); - }; + let Some(spec) = read_meta(|m| m.get_comp_spec(cmd)) else { + return Ok(CompSpecResult::NoSpec); + }; - let candidates = spec.complete(ctx)?; - if candidates.is_empty() { - Ok(CompSpecResult::NoMatch { flags: spec.get_flags() }) - } else { - Ok(CompSpecResult::Match(CompResult::from_candidates(candidates))) - } - } + let candidates = spec.complete(ctx)?; + if candidates.is_empty() { + Ok(CompSpecResult::NoMatch { + flags: spec.get_flags(), + }) + } else { + Ok(CompSpecResult::Match(CompResult::from_candidates( + candidates, + ))) + } + } pub fn get_candidates(&mut self, line: String, cursor_pos: usize) -> ShResult { let source = Arc::new(line.clone()); @@ -642,25 +732,25 @@ impl Completer { // Try programmable completion first - match self.try_comp_spec(&ctx)? { - CompSpecResult::NoMatch { flags } => { - if flags.contains(CompOptFlags::DIRNAMES) { - self.dirs_only = true; - } else if flags.contains(CompOptFlags::DEFAULT) { - /* fall through */ - } else { - return Ok(CompResult::NoMatch); - } + match self.try_comp_spec(&ctx)? { + CompSpecResult::NoMatch { flags } => { + if flags.contains(CompOptFlags::DIRNAMES) { + self.dirs_only = true; + } else if flags.contains(CompOptFlags::DEFAULT) { + /* fall through */ + } else { + return Ok(CompResult::NoMatch); + } - if flags.contains(CompOptFlags::NOSPACE) { - self.no_space = true; - } - } - CompSpecResult::Match(comp_result) => { - return Ok(comp_result); - } - CompSpecResult::NoSpec => { /* carry on */ } - } + if flags.contains(CompOptFlags::NOSPACE) { + self.no_space = true; + } + } + CompSpecResult::Match(comp_result) => { + return Ok(comp_result); + } + CompSpecResult::NoSpec => { /* carry on */ } + } // Get the current token from CompContext let Some(mut cur_token) = ctx.words.get(ctx.cword).cloned() else { @@ -672,7 +762,8 @@ impl Completer { self.token_span = (cur_token.span.start, cur_token.span.end); - // Use marker-based context detection for sub-token awareness (e.g. VAR_SUB inside a token) + // Use marker-based context detection for sub-token awareness (e.g. VAR_SUB + // inside a token) let (mut marker_ctx, token_start) = self.get_completion_context(&line, cursor_pos); self.token_span.0 = token_start; cur_token @@ -695,7 +786,7 @@ impl Completer { let last_marker = marker_ctx.last().copied(); let mut candidates = match marker_ctx.pop() { - _ if self.dirs_only => complete_dirs(&expanded), + _ if self.dirs_only => complete_dirs(&expanded), Some(markers::COMMAND) => complete_commands(&expanded), Some(markers::VAR_SUB) => { let var_candidates = complete_vars(&raw_tk); @@ -709,9 +800,11 @@ impl Completer { _ => complete_filename(&expanded), }; - // Graft unexpanded prefix onto candidates to preserve things like $SOME_PATH/file.txt - // Skip for var completions — complete_vars already returns the full $VAR form - let is_var_completion = last_marker == Some(markers::VAR_SUB) && !candidates.is_empty() + // Graft unexpanded prefix onto candidates to preserve things like + // $SOME_PATH/file.txt Skip for var completions — complete_vars already + // returns the full $VAR form + let is_var_completion = last_marker == Some(markers::VAR_SUB) + && !candidates.is_empty() && candidates.iter().any(|c| c.starts_with('$')); if !is_var_completion { candidates = candidates @@ -728,5 +821,4 @@ impl Completer { Ok(CompResult::from_candidates(candidates)) } - } diff --git a/src/readline/highlight.rs b/src/readline/highlight.rs index 7127130..b7be123 100644 --- a/src/readline/highlight.rs +++ b/src/readline/highlight.rs @@ -6,7 +6,10 @@ use std::{ use crate::{ libsh::term::{Style, StyleSet, Styled}, - readline::{annotate_input, markers::{self, is_marker}}, + readline::{ + annotate_input, + markers::{self, is_marker}, + }, state::{read_logic, read_meta, read_shopts}, }; @@ -20,10 +23,10 @@ use crate::{ pub struct Highlighter { input: String, output: String, - linebuf_cursor_pos: usize, + linebuf_cursor_pos: usize, style_stack: Vec, last_was_reset: bool, - in_selection: bool + in_selection: bool, } impl Highlighter { @@ -32,10 +35,10 @@ impl Highlighter { Self { input: String::new(), output: String::new(), - linebuf_cursor_pos: 0, + linebuf_cursor_pos: 0, style_stack: Vec::new(), last_was_reset: true, // start as true so we don't emit a leading reset - in_selection: false + in_selection: false, } } @@ -46,18 +49,18 @@ impl Highlighter { pub fn load_input(&mut self, input: &str, linebuf_cursor_pos: usize) { let input = annotate_input(input); self.input = input; - self.linebuf_cursor_pos = linebuf_cursor_pos; + self.linebuf_cursor_pos = linebuf_cursor_pos; } - pub fn strip_markers(str: &str) -> String { - let mut out = String::new(); - for ch in str.chars() { - if !is_marker(ch) { - out.push(ch); - } - } - out - } + pub fn strip_markers(str: &str) -> String { + let mut out = String::new(); + for ch in str.chars() { + if !is_marker(ch) { + out.push(ch); + } + } + out + } /// Processes the annotated input and generates ANSI-styled output /// @@ -69,14 +72,14 @@ impl Highlighter { let mut input_chars = input.chars().peekable(); while let Some(ch) = input_chars.next() { match ch { - markers::VISUAL_MODE_START => { - self.emit_style(Style::BgWhite | Style::Black); - self.in_selection = true; - } - markers::VISUAL_MODE_END => { - self.reapply_style(); - self.in_selection = false; - } + markers::VISUAL_MODE_START => { + self.emit_style(Style::BgWhite | Style::Black); + self.in_selection = true; + } + markers::VISUAL_MODE_END => { + self.reapply_style(); + self.in_selection = false; + } markers::STRING_DQ_END | markers::STRING_SQ_END | markers::VAR_SUB_END @@ -96,16 +99,16 @@ impl Highlighter { if ch == markers::RESET { break; } - if !is_marker(ch) { - cmd_name.push(ch); - } + if !is_marker(ch) { + cmd_name.push(ch); + } } - match cmd_name.as_str() { - "continue" | "return" | "break" => self.push_style(Style::Magenta), - _ => self.push_style(Style::Green), - } - } + match cmd_name.as_str() { + "continue" | "return" | "break" => self.push_style(Style::Magenta), + _ => self.push_style(Style::Green), + } + } markers::CASE_PAT => self.push_style(Style::Blue), markers::COMMENT => self.push_style(Style::BrightBlack), @@ -114,7 +117,6 @@ impl Highlighter { markers::REDIRECT | markers::OPERATOR => self.push_style(Style::Magenta | Style::Bold), - markers::ASSIGNMENT => { let mut var_name = String::new(); @@ -140,28 +142,30 @@ impl Highlighter { markers::ARG => { let mut arg = String::new(); - let is_last_arg = !input_chars.clone().any(|c| c == markers::ARG || c.is_whitespace()); + let is_last_arg = !input_chars + .clone() + .any(|c| c == markers::ARG || c.is_whitespace()); - if !is_last_arg { - self.push_style(Style::White); - } else { - let mut chars_clone = input_chars.clone(); - while let Some(ch) = chars_clone.next() { - if ch == markers::RESET { - break; - } - arg.push(ch); - } + if !is_last_arg { + self.push_style(Style::White); + } else { + let mut chars_clone = input_chars.clone(); + while let Some(ch) = chars_clone.next() { + if ch == markers::RESET { + break; + } + arg.push(ch); + } - let style = if Self::is_filename(&Self::strip_markers(&arg)) { - Style::White | Style::Underline - } else { - Style::White.into() - }; + let style = if Self::is_filename(&Self::strip_markers(&arg)) { + Style::White | Style::Underline + } else { + Style::White.into() + }; - self.push_style(style); - self.last_was_reset = false; - } + self.push_style(style); + self.last_was_reset = false; + } } markers::COMMAND => { @@ -173,9 +177,12 @@ impl Highlighter { } cmd_name.push(ch); } - let style = if matches!(Self::strip_markers(&cmd_name).as_str(), "break" | "continue" | "return") { - Style::Magenta.into() - } else if Self::is_valid(&Self::strip_markers(&cmd_name)) { + let style = if matches!( + Self::strip_markers(&cmd_name).as_str(), + "break" | "continue" | "return" + ) { + Style::Magenta.into() + } else if Self::is_valid(&Self::strip_markers(&cmd_name)) { Style::Green.into() } else { Style::Red | Style::Bold @@ -292,21 +299,21 @@ impl Highlighter { fn is_valid(command: &str) -> bool { let cmd_path = Path::new(&command); - if cmd_path.is_dir() && read_shopts(|o| o.core.autocd) { - // this is a directory and autocd is enabled - return true; - } + if cmd_path.is_dir() && read_shopts(|o| o.core.autocd) { + // this is a directory and autocd is enabled + return true; + } - if cmd_path.is_absolute() { - // the user has given us an absolute path - let Ok(meta) = cmd_path.metadata() else { - return false; - }; - // this is a file that is executable by someone - meta.permissions().mode() & 0o111 != 0 - } else { - read_meta(|m| m.cached_cmds().get(command).is_some()) - } + if cmd_path.is_absolute() { + // the user has given us an absolute path + let Ok(meta) = cmd_path.metadata() else { + return false; + }; + // this is a file that is executable by someone + meta.permissions().mode() & 0o111 != 0 + } else { + read_meta(|m| m.cached_cmds().get(command).is_some()) + } } fn is_filename(arg: &str) -> bool { @@ -316,9 +323,10 @@ impl Highlighter { return true; } - if path.is_absolute() - && let Some(parent_dir) = path.parent() - && let Ok(entries) = parent_dir.read_dir() { + if path.is_absolute() + && let Some(parent_dir) = path.parent() + && let Ok(entries) = parent_dir.read_dir() + { let files = entries .filter_map(|e| e.ok()) .map(|e| e.file_name().to_string_lossy().to_string()) @@ -334,17 +342,17 @@ impl Highlighter { return true; } } - } + } - read_meta(|m| { - let files = m.cwd_cache(); - for file in files { - if file.starts_with(arg) { - return true; - } - } - false - }) + read_meta(|m| { + let files = m.cwd_cache(); + for file in files { + if file.starts_with(arg) { + return true; + } + } + false + }) } /// Emits a reset ANSI code to the output, with deduplication @@ -363,10 +371,10 @@ impl Highlighter { /// Unconditionally appends the ANSI escape sequence for the given style /// and marks that we're no longer in a reset state. fn emit_style(&mut self, style: StyleSet) { - let mut style = style; - if !style.styles().contains(&Style::BgWhite) { - style = style.add_style(Style::BgBlack); - } + let mut style = style; + if !style.styles().contains(&Style::BgWhite) { + style = style.add_style(Style::BgBlack); + } self.output.push_str(&style.to_string()); self.last_was_reset = false; } @@ -378,9 +386,9 @@ impl Highlighter { pub fn push_style(&mut self, style: impl Into) { let set: StyleSet = style.into(); self.style_stack.push(set.clone()); - if !self.in_selection { - self.emit_style(set.clone()); - } + if !self.in_selection { + self.emit_style(set.clone()); + } } /// Pops a style from the stack and restores the previous style @@ -405,18 +413,18 @@ impl Highlighter { /// the default terminal color between independent commands. pub fn clear_styles(&mut self) { self.style_stack.clear(); - if !self.in_selection { - self.emit_reset(); - } + if !self.in_selection { + self.emit_reset(); + } } - pub fn reapply_style(&mut self) { - if let Some(style) = self.style_stack.last().cloned() { - self.emit_style(style); - } else { - self.emit_reset(); - } - } + pub fn reapply_style(&mut self) { + if let Some(style) = self.style_stack.last().cloned() { + self.emit_style(style); + } else { + self.emit_reset(); + } + } /// Simple marker-to-ANSI replacement (unused in favor of stack-based /// highlighting) diff --git a/src/readline/linebuf.rs b/src/readline/linebuf.rs index d664b5d..e5461ce 100644 --- a/src/readline/linebuf.rs +++ b/src/readline/linebuf.rs @@ -14,7 +14,14 @@ use crate::{ libsh::{ error::ShResult, term::{Style, Styled}, - }, parse::lex::{LexFlags, LexStream, Tk, TkFlags, TkRule}, prelude::*, readline::{markers, register::{write_register, RegisterContent}}, state::read_shopts + }, + parse::lex::{LexFlags, LexStream, Tk, TkFlags, TkRule}, + prelude::*, + readline::{ + markers, + register::{RegisterContent, write_register}, + }, + state::read_shopts, }; const PUNCTUATION: [&str; 3] = ["?", "!", "."]; @@ -326,7 +333,7 @@ pub struct LineBuf { pub insert_mode_start_pos: Option, pub saved_col: Option, - pub auto_indent_level: usize, + pub auto_indent_level: usize, pub undo_stack: Vec, pub redo_stack: Vec, @@ -384,12 +391,12 @@ impl LineBuf { pub fn set_cursor_clamp(&mut self, yn: bool) { self.cursor.exclusive = yn; } - pub fn move_cursor_to_end(&mut self) { - self.move_cursor(MotionKind::To(self.grapheme_indices().len())) - } - pub fn move_cursor_to_start(&mut self) { - self.move_cursor(MotionKind::To(0)) - } + pub fn move_cursor_to_end(&mut self) { + self.move_cursor(MotionKind::To(self.grapheme_indices().len())) + } + pub fn move_cursor_to_start(&mut self) { + self.move_cursor(MotionKind::To(0)) + } pub fn cursor_byte_pos(&mut self) -> usize { self.index_byte_pos(self.cursor.get()) } @@ -496,12 +503,12 @@ impl LineBuf { pub fn grapheme_at_cursor(&mut self) -> Option<&str> { self.grapheme_at(self.cursor.get()) } - pub fn grapheme_before_cursor(&mut self) -> Option<&str> { - if self.cursor.get() == 0 { - return None; - } - self.grapheme_at(self.cursor.ret_sub(1)) - } + pub fn grapheme_before_cursor(&mut self) -> Option<&str> { + if self.cursor.get() == 0 { + return None; + } + self.grapheme_at(self.cursor.ret_sub(1)) + } pub fn mark_insert_mode_start_pos(&mut self) { self.insert_mode_start_pos = Some(self.cursor.get()) } @@ -542,7 +549,7 @@ impl LineBuf { } pub fn slice_to(&mut self, end: usize) -> Option<&str> { self.update_graphemes_lazy(); - self.read_slice_to(end) + self.read_slice_to(end) } pub fn read_slice_to(&self, end: usize) -> Option<&str> { let grapheme_index = self.grapheme_indices().get(end).copied().or_else(|| { @@ -596,9 +603,9 @@ impl LineBuf { self.update_graphemes(); drained } - pub fn drain_inclusive(&mut self, range: RangeInclusive) -> String { - self.drain(*range.start()..range.end().saturating_add(1)) - } + pub fn drain_inclusive(&mut self, range: RangeInclusive) -> String { + self.drain(*range.start()..range.end().saturating_add(1)) + } pub fn push(&mut self, ch: char) { self.buffer.push(ch); self.update_graphemes(); @@ -620,30 +627,31 @@ impl LineBuf { self.update_graphemes(); } pub fn select_range(&self) -> Option<(usize, usize)> { - match self.select_mode? { - SelectMode::Char(_) => { - self.select_range - } - SelectMode::Line(_) => { - let (start, end) = self.select_range?; - let start = self.pos_line_number(start); - let end = self.pos_line_number(end); - let (select_start,_) = self.line_bounds(start); - let (_,select_end) = self.line_bounds(end); - if self.read_grapheme_before(select_end).is_some_and(|gr| gr == "\n") { - Some((select_start, select_end - 1)) - } else { - Some((select_start, select_end)) - } - } - SelectMode::Block(_) => todo!(), - } + match self.select_mode? { + SelectMode::Char(_) => self.select_range, + SelectMode::Line(_) => { + let (start, end) = self.select_range?; + let start = self.pos_line_number(start); + let end = self.pos_line_number(end); + let (select_start, _) = self.line_bounds(start); + let (_, select_end) = self.line_bounds(end); + if self + .read_grapheme_before(select_end) + .is_some_and(|gr| gr == "\n") + { + Some((select_start, select_end - 1)) + } else { + Some((select_start, select_end)) + } + } + SelectMode::Block(_) => todo!(), + } } pub fn start_selecting(&mut self, mode: SelectMode) { - let range_start = self.cursor; - let mut range_end = self.cursor; - range_end.add(1); - self.select_range = Some((range_start.get(), range_end.get())); + let range_start = self.cursor; + let mut range_end = self.cursor; + range_end.add(1); + self.select_range = Some((range_start.get(), range_end.get())); self.select_mode = Some(mode); } pub fn stop_selecting(&mut self) { @@ -656,11 +664,12 @@ impl LineBuf { self.buffer.graphemes(true).filter(|g| *g == "\n").count() } - pub fn pos_line_number(&self, pos: usize) -> usize { - self.read_slice_to(pos) - .map(|slice| slice.graphemes(true).filter(|g| *g == "\n").count()) - .unwrap_or(0) - } + pub fn pos_line_number(&self, pos: usize) -> usize { + self + .read_slice_to(pos) + .map(|slice| slice.graphemes(true).filter(|g| *g == "\n").count()) + .unwrap_or(0) + } pub fn cursor_line_number(&self) -> usize { self .read_slice_to_cursor() @@ -771,14 +780,14 @@ impl LineBuf { } Some(self.line_bounds(line_no)) } - pub fn this_line_exclusive(&mut self) -> (usize, usize) { - let line_no = self.cursor_line_number(); - let (start, mut end) = self.line_bounds(line_no); - if self.read_grapheme_before(end).is_some_and(|gr| gr == "\n") { - end = end.saturating_sub(1); - } - (start, end) - } + pub fn this_line_exclusive(&mut self) -> (usize, usize) { + let line_no = self.cursor_line_number(); + let (start, mut end) = self.line_bounds(line_no); + if self.read_grapheme_before(end).is_some_and(|gr| gr == "\n") { + end = end.saturating_sub(1); + } + (start, end) + } pub fn this_line(&mut self) -> (usize, usize) { let line_no = self.cursor_line_number(); self.line_bounds(line_no) @@ -789,9 +798,9 @@ impl LineBuf { pub fn end_of_line(&mut self) -> usize { self.this_line().1 } - pub fn end_of_line_exclusive(&mut self) -> usize { - self.this_line_exclusive().1 - } + pub fn end_of_line_exclusive(&mut self) -> usize { + self.this_line_exclusive().1 + } pub fn select_lines_up(&mut self, n: usize) -> Option<(usize, usize)> { if self.start_of_line() == 0 { return None; @@ -1929,33 +1938,34 @@ impl LineBuf { let end = start + gr.len(); self.buffer.replace_range(start..end, new); } - pub fn calc_indent_level(&mut self) { - let to_cursor = self - .slice_to_cursor() - .map(|s| s.to_string()) - .unwrap_or(self.buffer.clone()); - let input = Arc::new(to_cursor); - let Ok(tokens) = LexStream::new(input, LexFlags::LEX_UNFINISHED).collect::>>() else { - log::error!("Failed to lex buffer for indent calculation"); - return; - }; - let mut level: usize = 0; - for tk in tokens { - if tk.flags.contains(TkFlags::KEYWORD) { - match tk.as_str() { - "then" | "do" | "in" => level += 1, - "done" | "fi" | "esac" => level = level.saturating_sub(1), - _ => { /* Continue */ } - } - } else if tk.class == TkRule::BraceGrpStart { - level += 1; - } else if tk.class == TkRule::BraceGrpEnd { - level = level.saturating_sub(1); - } - } + pub fn calc_indent_level(&mut self) { + let to_cursor = self + .slice_to_cursor() + .map(|s| s.to_string()) + .unwrap_or(self.buffer.clone()); + let input = Arc::new(to_cursor); + let Ok(tokens) = LexStream::new(input, LexFlags::LEX_UNFINISHED).collect::>>() + else { + log::error!("Failed to lex buffer for indent calculation"); + return; + }; + let mut level: usize = 0; + for tk in tokens { + if tk.flags.contains(TkFlags::KEYWORD) { + match tk.as_str() { + "then" | "do" | "in" => level += 1, + "done" | "fi" | "esac" => level = level.saturating_sub(1), + _ => { /* Continue */ } + } + } else if tk.class == TkRule::BraceGrpStart { + level += 1; + } else if tk.class == TkRule::BraceGrpEnd { + level = level.saturating_sub(1); + } + } - self.auto_indent_level = level; - } + self.auto_indent_level = level; + } pub fn eval_motion(&mut self, verb: Option<&Verb>, motion: MotionCmd) -> MotionKind { let buffer = self.buffer.clone(); if self.has_hint() { @@ -1965,7 +1975,7 @@ impl LineBuf { let eval = match motion { MotionCmd(count, motion @ (Motion::WholeLineInclusive | Motion::WholeLineExclusive)) => { - let exclusive = matches!(motion, Motion::WholeLineExclusive); + let exclusive = matches!(motion, Motion::WholeLineExclusive); let Some((start, mut end)) = (if count == 1 { Some(self.this_line()) @@ -1975,9 +1985,9 @@ impl LineBuf { return MotionKind::Null; }; - if exclusive && self.grapheme_before(end).is_some_and(|gr| gr == "\n") { - end = end.saturating_sub(1); - } + if exclusive && self.grapheme_before(end).is_some_and(|gr| gr == "\n") { + end = end.saturating_sub(1); + } let target_col = if let Some(col) = self.saved_col { col @@ -1994,7 +2004,8 @@ impl LineBuf { if self.cursor.exclusive && line.ends_with("\n") && self.grapheme_at(target_pos) == Some("\n") - && line != "\n" // Allow landing on newline for empty lines + && line != "\n" + // Allow landing on newline for empty lines { target_pos = target_pos.saturating_sub(1); // Don't land on the // newline @@ -2155,7 +2166,7 @@ impl LineBuf { MotionCmd(_, Motion::BeginningOfLine) => MotionKind::On(self.start_of_line()), MotionCmd(count, Motion::EndOfLine) => { let pos = if count == 1 { - self.end_of_line() + self.end_of_line() } else if let Some((_, end)) = self.select_lines_down(count) { end } else { @@ -2228,14 +2239,15 @@ impl LineBuf { }; let Some(line) = self.slice(start..end).map(|s| s.to_string()) else { - log::warn!("Failed to get line slice for motion, start: {start}, end: {end}"); + log::warn!("Failed to get line slice for motion, start: {start}, end: {end}"); return MotionKind::Null; }; let mut target_pos = self.grapheme_index_for_display_col(&line, target_col); if self.cursor.exclusive && line.ends_with("\n") && self.grapheme_at(target_pos) == Some("\n") - && line != "\n" // Allow landing on newline for empty lines + && line != "\n" + // Allow landing on newline for empty lines { target_pos = target_pos.saturating_sub(1); // Don't land on the // newline @@ -2247,7 +2259,6 @@ impl LineBuf { _ => unreachable!(), }; - MotionKind::InclusiveWithTargetCol((start, end), target_pos) } MotionCmd(count, Motion::LineDownCharwise) | MotionCmd(count, Motion::LineUpCharwise) => { @@ -2428,13 +2439,13 @@ impl LineBuf { pub fn range_from_motion(&mut self, motion: &MotionKind) -> Option<(usize, usize)> { let range = match motion { MotionKind::On(pos) => { - let cursor_pos = self.cursor.get(); - if cursor_pos == *pos { - ordered(cursor_pos, pos + 1) // scary - } else { - ordered(cursor_pos, *pos) - } - } + let cursor_pos = self.cursor.get(); + if cursor_pos == *pos { + ordered(cursor_pos, pos + 1) // scary + } else { + ordered(cursor_pos, *pos) + } + } MotionKind::Onto(pos) => { // For motions which include the character at the cursor during operations // but exclude the character during movements @@ -2478,29 +2489,32 @@ impl LineBuf { ) -> ShResult<()> { match verb { Verb::Delete | Verb::Yank | Verb::Change => { - log::debug!("Executing verb: {verb:?} with motion: {motion:?}"); + log::debug!("Executing verb: {verb:?} with motion: {motion:?}"); let Some((mut start, mut end)) = self.range_from_motion(&motion) else { - log::debug!("No range from motion, nothing to do"); + log::debug!("No range from motion, nothing to do"); return Ok(()); }; - log::debug!("Initial range from motion: ({start}, {end})"); - log::debug!("self.grapheme_indices().len(): {}", self.grapheme_indices().len()); + log::debug!("Initial range from motion: ({start}, {end})"); + log::debug!( + "self.grapheme_indices().len(): {}", + self.grapheme_indices().len() + ); - let mut do_indent = false; - if verb == Verb::Change && (start,end) == self.this_line_exclusive() { - do_indent = read_shopts(|o| o.prompt.auto_indent); - } + let mut do_indent = false; + if verb == Verb::Change && (start, end) == self.this_line_exclusive() { + do_indent = read_shopts(|o| o.prompt.auto_indent); + } let mut text = if verb == Verb::Yank { self .slice(start..end) .map(|c| c.to_string()) .unwrap_or_default() - } else if start == self.grapheme_indices().len() && end == self.grapheme_indices().len() { - // user is in normal mode and pressed 'x' on the last char in the buffer - let drained = self.drain(end.saturating_sub(1)..end); - self.update_graphemes(); - drained + } else if start == self.grapheme_indices().len() && end == self.grapheme_indices().len() { + // user is in normal mode and pressed 'x' on the last char in the buffer + let drained = self.drain(end.saturating_sub(1)..end); + self.update_graphemes(); + drained } else { let drained = self.drain(start..end); self.update_graphemes(); @@ -2508,30 +2522,30 @@ impl LineBuf { }; let is_linewise = matches!( motion, - MotionKind::InclusiveWithTargetCol(..) | - MotionKind::ExclusiveWithTargetCol(..) + MotionKind::InclusiveWithTargetCol(..) | MotionKind::ExclusiveWithTargetCol(..) ) || matches!(self.select_mode, Some(SelectMode::Line(_))); let register_content = if is_linewise { - if !text.ends_with('\n') && !text.is_empty() { - text.push('\n'); - } + if !text.ends_with('\n') && !text.is_empty() { + text.push('\n'); + } RegisterContent::Line(text) } else { RegisterContent::Span(text) }; register.write_to_register(register_content); - self.cursor.set(start); - if do_indent { - self.calc_indent_level(); - let tabs = (0..self.auto_indent_level).map(|_| '\t'); - for tab in tabs { - self.insert_at_cursor(tab); - self.cursor.add(1); - } - } else if verb != Verb::Change - && let MotionKind::InclusiveWithTargetCol((_,_), col) = motion { - self.cursor.add(col); - } + self.cursor.set(start); + if do_indent { + self.calc_indent_level(); + let tabs = (0..self.auto_indent_level).map(|_| '\t'); + for tab in tabs { + self.insert_at_cursor(tab); + self.cursor.add(1); + } + } else if verb != Verb::Change + && let MotionKind::InclusiveWithTargetCol((_, _), col) = motion + { + self.cursor.add(col); + } } Verb::Rot13 => { let Some((start, end)) = self.range_from_motion(&motion) else { @@ -2682,7 +2696,7 @@ impl LineBuf { self.buffer.replace_range(pos..pos + new.len(), &old); let new_cursor_pos = self.cursor.get(); - self.cursor.set(cursor_pos); + self.cursor.set(cursor_pos); let new_edit = Edit { pos, cursor_pos: new_cursor_pos, @@ -2701,17 +2715,17 @@ impl LineBuf { if content.is_empty() { return Ok(()); } - if let Some(range) = self.select_range() { - let register_text = self.drain_inclusive(range.0..=range.1); - write_register(None, RegisterContent::Span(register_text)); // swap deleted text into register + if let Some(range) = self.select_range() { + let register_text = self.drain_inclusive(range.0..=range.1); + write_register(None, RegisterContent::Span(register_text)); // swap deleted text into register - let text = content.as_str(); - self.insert_str_at(range.0, text); - self.cursor.set(range.0 + content.char_count()); - self.select_range = None; - self.update_graphemes(); - return Ok(()); - } + let text = content.as_str(); + self.insert_str_at(range.0, text); + self.cursor.set(range.0 + content.char_count()); + self.select_range = None; + self.update_graphemes(); + return Ok(()); + } match content { RegisterContent::Span(ref text) => { let insert_idx = match anchor { @@ -2726,7 +2740,9 @@ impl LineBuf { Anchor::After => self.end_of_line(), Anchor::Before => self.start_of_line(), }; - let needs_newline = self.grapheme_before(insert_idx).is_some_and(|gr| gr != "\n"); + let needs_newline = self + .grapheme_before(insert_idx) + .is_some_and(|gr| gr != "\n"); if needs_newline { let full = format!("\n{}", text); self.insert_str_at(insert_idx, &full); @@ -2788,11 +2804,11 @@ impl LineBuf { let Some((start, end)) = self.range_from_motion(&motion) else { return Ok(()); }; - let move_cursor = self.cursor.get() == start; + let move_cursor = self.cursor.get() == start; self.insert_at(start, '\t'); - if move_cursor { - self.cursor.add(1); - } + if move_cursor { + self.cursor.add(1); + } let mut range_indices = self.grapheme_indices()[start..end].to_vec().into_iter(); while let Some(idx) = range_indices.next() { let gr = self.grapheme_at(idx).unwrap(); @@ -2822,7 +2838,7 @@ impl LineBuf { if self.grapheme_at(start) == Some("\t") { self.remove(start); } - end = end.min(self.grapheme_indices().len().saturating_sub(1)); + end = end.min(self.grapheme_indices().len().saturating_sub(1)); let mut range_indices = self.grapheme_indices()[start..end].to_vec().into_iter(); while let Some(idx) = range_indices.next() { let gr = self.grapheme_at(idx).unwrap(); @@ -2852,29 +2868,29 @@ impl LineBuf { Verb::Equalize => todo!(), Verb::InsertModeLineBreak(anchor) => { let (mut start, end) = self.this_line(); - let auto_indent = read_shopts(|o| o.prompt.auto_indent); + let auto_indent = read_shopts(|o| o.prompt.auto_indent); if start == 0 && end == self.cursor.max { match anchor { Anchor::After => { self.push('\n'); - if auto_indent { - self.calc_indent_level(); - let tabs = (0..self.auto_indent_level).map(|_| '\t'); - for tab in tabs { - self.push(tab); - } - } + if auto_indent { + self.calc_indent_level(); + let tabs = (0..self.auto_indent_level).map(|_| '\t'); + for tab in tabs { + self.push(tab); + } + } self.cursor.set(self.cursor_max()); return Ok(()); } Anchor::Before => { - if auto_indent { - self.calc_indent_level(); - let tabs = (0..self.auto_indent_level).map(|_| '\t'); - for tab in tabs { - self.insert_at(0, tab); - } - } + if auto_indent { + self.calc_indent_level(); + let tabs = (0..self.auto_indent_level).map(|_| '\t'); + for tab in tabs { + self.insert_at(0, tab); + } + } self.insert_at(0, '\n'); self.cursor.set(0); return Ok(()); @@ -2888,52 +2904,52 @@ impl LineBuf { self.cursor.set(end); self.insert_at_cursor('\n'); self.cursor.add(1); - if auto_indent { - self.calc_indent_level(); - let tabs = (0..self.auto_indent_level).map(|_| '\t'); - for tab in tabs { - self.insert_at_cursor(tab); - self.cursor.add(1); - } - } + if auto_indent { + self.calc_indent_level(); + let tabs = (0..self.auto_indent_level).map(|_| '\t'); + for tab in tabs { + self.insert_at_cursor(tab); + self.cursor.add(1); + } + } } Anchor::Before => { self.cursor.set(start); self.insert_at_cursor('\n'); self.cursor.add(1); - if auto_indent { - self.calc_indent_level(); - let tabs = (0..self.auto_indent_level).map(|_| '\t'); - for tab in tabs { - self.insert_at_cursor(tab); - self.cursor.add(1); - } - } + if auto_indent { + self.calc_indent_level(); + let tabs = (0..self.auto_indent_level).map(|_| '\t'); + for tab in tabs { + self.insert_at_cursor(tab); + self.cursor.add(1); + } + } } } } Verb::AcceptLineOrNewline => { - // If this verb has reached this function, it means we have incomplete input - // and therefore must insert a newline instead of accepting the input - if self.cursor.exclusive { - // in this case we are in normal/visual mode, so we don't insert anything - // and just move down a line - let motion = self.eval_motion(None, MotionCmd(1, Motion::LineDownCharwise)); - self.apply_motion(motion); - return Ok(()); - } - let auto_indent = read_shopts(|o| o.prompt.auto_indent); - self.insert_at_cursor('\n'); - self.cursor.add(1); - if auto_indent { - self.calc_indent_level(); - let tabs = (0..self.auto_indent_level).map(|_| '\t'); - for tab in tabs { - self.insert_at_cursor(tab); - self.cursor.add(1); - } - } - } + // If this verb has reached this function, it means we have incomplete input + // and therefore must insert a newline instead of accepting the input + if self.cursor.exclusive { + // in this case we are in normal/visual mode, so we don't insert anything + // and just move down a line + let motion = self.eval_motion(None, MotionCmd(1, Motion::LineDownCharwise)); + self.apply_motion(motion); + return Ok(()); + } + let auto_indent = read_shopts(|o| o.prompt.auto_indent); + self.insert_at_cursor('\n'); + self.cursor.add(1); + if auto_indent { + self.calc_indent_level(); + let tabs = (0..self.auto_indent_level).map(|_| '\t'); + for tab in tabs { + self.insert_at_cursor(tab); + self.cursor.add(1); + } + } + } Verb::Complete | Verb::EndOfFile @@ -2951,7 +2967,11 @@ impl LineBuf { pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> { let clear_redos = !cmd.is_undo_op() || cmd.verb.as_ref().is_some_and(|v| v.1.is_edit()); let is_char_insert = cmd.verb.as_ref().is_some_and(|v| v.1.is_char_insert()); - let is_line_motion = cmd.is_line_motion() || cmd.verb.as_ref().is_some_and(|v| v.1 == Verb::AcceptLineOrNewline); + let is_line_motion = cmd.is_line_motion() + || cmd + .verb + .as_ref() + .is_some_and(|v| v.1 == Verb::AcceptLineOrNewline); let is_undo_op = cmd.is_undo_op(); let edit_is_merging = self.undo_stack.last().is_some_and(|edit| edit.merging); @@ -3024,13 +3044,14 @@ impl LineBuf { self.apply_motion(motion_eval); } - if self.cursor.exclusive - && self.grapheme_at_cursor().is_some_and(|gr| gr == "\n") - && self.grapheme_before_cursor().is_some_and(|gr| gr != "\n") { - // we landed on a newline, and we aren't inbetween two newlines. - self.cursor.sub(1); - self.update_select_range(); - } + if self.cursor.exclusive + && self.grapheme_at_cursor().is_some_and(|gr| gr == "\n") + && self.grapheme_before_cursor().is_some_and(|gr| gr != "\n") + { + // we landed on a newline, and we aren't inbetween two newlines. + self.cursor.sub(1); + self.update_select_range(); + } /* Done executing, do some cleanup */ @@ -3070,10 +3091,10 @@ impl LineBuf { let text = self .hint .clone() - .map(|h| format!("\x1b[90m{h}\x1b[0m")) + .map(|h| format!("\x1b[90m{h}\x1b[0m")) .unwrap_or_default(); - text.replace("\n", "\n\x1b[90m") + text.replace("\n", "\n\x1b[90m") } } @@ -3085,20 +3106,29 @@ impl Display for LineBuf { let start_byte = self.read_idx_byte_pos(start); let end_byte = self.read_idx_byte_pos(end).min(full_buf.len()); - match mode.anchor() { SelectAnchor::Start => { let mut inclusive = start_byte..=end_byte; if *inclusive.end() == full_buf.len() { inclusive = start_byte..=end_byte.saturating_sub(1); } - let selected = format!("{}{}{}", markers::VISUAL_MODE_START, &full_buf[inclusive.clone()], markers::VISUAL_MODE_END) - .replace("\n", format!("\n{}",markers::VISUAL_MODE_START).as_str()); + let selected = format!( + "{}{}{}", + markers::VISUAL_MODE_START, + &full_buf[inclusive.clone()], + markers::VISUAL_MODE_END + ) + .replace("\n", format!("\n{}", markers::VISUAL_MODE_START).as_str()); full_buf.replace_range(inclusive, &selected); } SelectAnchor::End => { - let selected = format!("{}{}{}", markers::VISUAL_MODE_START, &full_buf[start_byte..end_byte], markers::VISUAL_MODE_END) - .replace("\n", format!("\n{}",markers::VISUAL_MODE_START).as_str()); + let selected = format!( + "{}{}{}", + markers::VISUAL_MODE_START, + &full_buf[start_byte..end_byte], + markers::VISUAL_MODE_END + ) + .replace("\n", format!("\n{}", markers::VISUAL_MODE_START).as_str()); full_buf.replace_range(start_byte..end_byte, &selected); } } diff --git a/src/readline/mod.rs b/src/readline/mod.rs index fbf045e..bf2939c 100644 --- a/src/readline/mod.rs +++ b/src/readline/mod.rs @@ -36,12 +36,12 @@ pub mod vimode; pub mod markers { use super::Marker; - /* - * These are invisible Unicode characters used to annotate - * strings with various contextual metadata. - */ + /* + * These are invisible Unicode characters used to annotate + * strings with various contextual metadata. + */ - /* Highlight Markers */ + /* Highlight Markers */ // token-level (derived from token class) pub const COMMAND: Marker = '\u{e100}'; @@ -71,36 +71,36 @@ pub mod markers { pub const ESCAPE: Marker = '\u{e116}'; pub const GLOB: Marker = '\u{e117}'; - // other - pub const VISUAL_MODE_START: Marker = '\u{e118}'; - pub const VISUAL_MODE_END: Marker = '\u{e119}'; + // other + pub const VISUAL_MODE_START: Marker = '\u{e118}'; + pub const VISUAL_MODE_END: Marker = '\u{e119}'; pub const RESET: Marker = '\u{e11a}'; pub const NULL: Marker = '\u{e11b}'; - /* Expansion Markers */ - /// Double quote '"' marker - pub const DUB_QUOTE: Marker = '\u{e001}'; - /// Single quote '\\'' marker - pub const SNG_QUOTE: Marker = '\u{e002}'; - /// Tilde sub marker - pub const TILDE_SUB: Marker = '\u{e003}'; - /// Input process sub marker - pub const PROC_SUB_IN: Marker = '\u{e005}'; - /// Output process sub marker - pub const PROC_SUB_OUT: Marker = '\u{e006}'; - /// Marker for null expansion - /// This is used for when "$@" or "$*" are used in quotes and there are no - /// arguments Without this marker, it would be handled like an empty string, - /// which breaks some commands - pub const NULL_EXPAND: Marker = '\u{e007}'; - /// Explicit marker for argument separation - /// This is used to join the arguments given by "$@", and preserves exact formatting - /// of the original arguments, including quoting - pub const ARG_SEP: Marker = '\u{e008}'; + /* Expansion Markers */ + /// Double quote '"' marker + pub const DUB_QUOTE: Marker = '\u{e001}'; + /// Single quote '\\'' marker + pub const SNG_QUOTE: Marker = '\u{e002}'; + /// Tilde sub marker + pub const TILDE_SUB: Marker = '\u{e003}'; + /// Input process sub marker + pub const PROC_SUB_IN: Marker = '\u{e005}'; + /// Output process sub marker + pub const PROC_SUB_OUT: Marker = '\u{e006}'; + /// Marker for null expansion + /// This is used for when "$@" or "$*" are used in quotes and there are no + /// arguments Without this marker, it would be handled like an empty string, + /// which breaks some commands + pub const NULL_EXPAND: Marker = '\u{e007}'; + /// Explicit marker for argument separation + /// This is used to join the arguments given by "$@", and preserves exact + /// formatting of the original arguments, including quoting + pub const ARG_SEP: Marker = '\u{e008}'; - pub const VI_SEQ_EXP: Marker = '\u{e009}'; + pub const VI_SEQ_EXP: Marker = '\u{e009}'; pub const END_MARKERS: [Marker; 7] = [ VAR_SUB_END, @@ -116,10 +116,10 @@ pub mod markers { ]; pub const SUB_TOKEN: [Marker; 6] = [VAR_SUB, CMD_SUB, PROC_SUB, STRING_DQ, STRING_SQ, GLOB]; - pub const MISC: [Marker; 3] = [ESCAPE, VISUAL_MODE_START, VISUAL_MODE_END]; + pub const MISC: [Marker; 3] = [ESCAPE, VISUAL_MODE_START, VISUAL_MODE_END]; pub fn is_marker(c: Marker) -> bool { - ('\u{e000}'..'\u{efff}').contains(&c) + ('\u{e000}'..'\u{efff}').contains(&c) } } type Marker = char; @@ -135,66 +135,73 @@ pub enum ReadlineEvent { } pub struct Prompt { - ps1_expanded: String, - ps1_raw: String, - psr_expanded: Option, - psr_raw: Option, + ps1_expanded: String, + ps1_raw: String, + psr_expanded: Option, + psr_raw: Option, } impl Prompt { - const DEFAULT_PS1: &str = "\\e[0m\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m "; - pub fn new() -> Self { - let Ok(ps1_raw) = env::var("PS1") else { - return Self::default(); - }; - let Ok(ps1_expanded) = expand_prompt(&ps1_raw) else { - return Self::default(); - }; - let psr_raw = env::var("PSR").ok(); - let psr_expanded = psr_raw.clone().map(|r| expand_prompt(&r)).transpose().ok().flatten(); - Self { - ps1_expanded, - ps1_raw, - psr_expanded, - psr_raw, - } - } + const DEFAULT_PS1: &str = + "\\e[0m\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m "; + pub fn new() -> Self { + let Ok(ps1_raw) = env::var("PS1") else { + return Self::default(); + }; + let Ok(ps1_expanded) = expand_prompt(&ps1_raw) else { + return Self::default(); + }; + let psr_raw = env::var("PSR").ok(); + let psr_expanded = psr_raw + .clone() + .map(|r| expand_prompt(&r)) + .transpose() + .ok() + .flatten(); + Self { + ps1_expanded, + ps1_raw, + psr_expanded, + psr_raw, + } + } - pub fn get_ps1(&self) -> &str { - &self.ps1_expanded - } - pub fn set_ps1(&mut self, ps1_raw: String) -> ShResult<()> { - self.ps1_expanded = expand_prompt(&ps1_raw)?; - self.ps1_raw = ps1_raw; - Ok(()) - } - pub fn set_psr(&mut self, psr_raw: String) -> ShResult<()> { - self.psr_expanded = Some(expand_prompt(&psr_raw)?); - self.psr_raw = Some(psr_raw); - Ok(()) - } - pub fn get_psr(&self) -> Option<&str> { - self.psr_expanded.as_deref() - } + pub fn get_ps1(&self) -> &str { + &self.ps1_expanded + } + pub fn set_ps1(&mut self, ps1_raw: String) -> ShResult<()> { + self.ps1_expanded = expand_prompt(&ps1_raw)?; + self.ps1_raw = ps1_raw; + Ok(()) + } + pub fn set_psr(&mut self, psr_raw: String) -> ShResult<()> { + self.psr_expanded = Some(expand_prompt(&psr_raw)?); + self.psr_raw = Some(psr_raw); + Ok(()) + } + pub fn get_psr(&self) -> Option<&str> { + self.psr_expanded.as_deref() + } - pub fn refresh(&mut self) -> ShResult<()> { - self.ps1_expanded = expand_prompt(&self.ps1_raw)?; - if let Some(psr_raw) = &self.psr_raw { - self.psr_expanded = Some(expand_prompt(psr_raw)?); - } - Ok(()) - } + pub fn refresh(&mut self) -> ShResult<()> { + self.ps1_expanded = expand_prompt(&self.ps1_raw)?; + if let Some(psr_raw) = &self.psr_raw { + self.psr_expanded = Some(expand_prompt(psr_raw)?); + } + Ok(()) + } } impl Default for Prompt { - fn default() -> Self { - Self { - ps1_expanded: expand_prompt(Self::DEFAULT_PS1).unwrap_or_else(|_| Self::DEFAULT_PS1.to_string()), - ps1_raw: Self::DEFAULT_PS1.to_string(), - psr_expanded: None, - psr_raw: None, - } - } + fn default() -> Self { + Self { + ps1_expanded: expand_prompt(Self::DEFAULT_PS1) + .unwrap_or_else(|_| Self::DEFAULT_PS1.to_string()), + ps1_raw: Self::DEFAULT_PS1.to_string(), + psr_expanded: None, + psr_raw: None, + } + } } pub struct ShedVi { @@ -232,7 +239,7 @@ impl ShedVi { history: History::new()?, needs_redraw: true, }; - new.writer.flush_write("\n")?; // ensure we start on a new line, in case the previous command didn't end with a newline + new.writer.flush_write("\n")?; // ensure we start on a new line, in case the previous command didn't end with a newline new.print_line(false)?; Ok(new) } @@ -255,58 +262,55 @@ impl ShedVi { self.needs_redraw = true; } - /// Reset readline state for a new prompt pub fn reset(&mut self, full_redraw: bool) -> ShResult<()> { - // Clear old display before resetting state — old_layout must survive - // so print_line can call clear_rows with the full multi-line layout - self.prompt = Prompt::new(); + // Clear old display before resetting state — old_layout must survive + // so print_line can call clear_rows with the full multi-line layout + self.prompt = Prompt::new(); self.editor = Default::default(); self.mode = Box::new(ViInsert::new()); self.needs_redraw = true; - if full_redraw { - self.old_layout = None; - } + if full_redraw { + self.old_layout = None; + } self.history.pending = None; self.history.reset(); - self.print_line(false) + self.print_line(false) } - pub fn prompt(&self) -> &Prompt { - &self.prompt - } + pub fn prompt(&self) -> &Prompt { + &self.prompt + } - pub fn prompt_mut(&mut self) -> &mut Prompt { - &mut self.prompt - } + pub fn prompt_mut(&mut self) -> &mut Prompt { + &mut self.prompt + } - fn should_submit(&mut self) -> ShResult { - if self.mode.report_mode() == ModeReport::Normal { - return Ok(true); - } - let input = Arc::new(self.editor.buffer.clone()); - self.editor.calc_indent_level(); - let lex_result1 = LexStream::new(Arc::clone(&input), LexFlags::LEX_UNFINISHED).collect::>>(); - let lex_result2 = LexStream::new(Arc::clone(&input), LexFlags::empty()).collect::>>(); - let is_top_level = self.editor.auto_indent_level == 0; + fn should_submit(&mut self) -> ShResult { + if self.mode.report_mode() == ModeReport::Normal { + return Ok(true); + } + let input = Arc::new(self.editor.buffer.clone()); + self.editor.calc_indent_level(); + let lex_result1 = + LexStream::new(Arc::clone(&input), LexFlags::LEX_UNFINISHED).collect::>>(); + let lex_result2 = + LexStream::new(Arc::clone(&input), LexFlags::empty()).collect::>>(); + let is_top_level = self.editor.auto_indent_level == 0; - let is_complete = match (lex_result1.is_err(), lex_result2.is_err()) { - (true, true) => { - return Err(lex_result2.unwrap_err()); - } - (true, false) => { - return Err(lex_result1.unwrap_err()); - } - (false, true) => { - false - } - (false, false) => { - true - } - }; + let is_complete = match (lex_result1.is_err(), lex_result2.is_err()) { + (true, true) => { + return Err(lex_result2.unwrap_err()); + } + (true, false) => { + return Err(lex_result1.unwrap_err()); + } + (false, true) => false, + (false, false) => true, + }; - Ok(is_complete && is_top_level) - } + Ok(is_complete && is_top_level) + } /// Process any available input and return readline event /// This is non-blocking - returns Pending if no complete line yet @@ -362,8 +366,8 @@ impl ShedVi { self.editor.set_hint(hint); } None => { - self.writer.send_bell().ok(); - }, + self.writer.send_bell().ok(); + } } self.needs_redraw = true; @@ -385,9 +389,11 @@ impl ShedVi { continue; } - if cmd.is_submit_action() && (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete)) { + if cmd.is_submit_action() + && (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete)) + { self.editor.set_hint(None); - self.editor.cursor.set(self.editor.cursor_max()); // Move the cursor to the very end + self.editor.cursor.set(self.editor.cursor_max()); // Move the cursor to the very end self.print_line(true)?; // Redraw self.writer.flush_write("\n")?; let buf = self.editor.take_buf(); @@ -407,13 +413,13 @@ impl ShedVi { return Ok(ReadlineEvent::Eof); } else { self.editor = LineBuf::new(); - self.mode = Box::new(ViInsert::new()); + self.mode = Box::new(ViInsert::new()); self.needs_redraw = true; continue; } } - let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit()); + let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit()); let before = self.editor.buffer.clone(); self.exec_cmd(cmd)?; @@ -424,8 +430,8 @@ impl ShedVi { .history .update_pending_cmd((self.editor.as_str(), self.editor.cursor.get())); } else if before == after && has_edit_verb { - self.writer.send_bell().ok(); // bell on no-op commands with a verb (e.g., 'x' on empty line) - } + self.writer.send_bell().ok(); // bell on no-op commands with a verb (e.g., 'x' on empty line) + } let hint = self.history.get_hint(); self.editor.set_hint(hint); @@ -462,21 +468,21 @@ impl ShedVi { }; let entry = self.history.scroll(count); if let Some(entry) = entry { - let editor = std::mem::take(&mut self.editor); + let editor = std::mem::take(&mut self.editor); self.editor.set_buffer(entry.command().to_string()); if self.history.pending.is_none() { self.history.pending = Some(editor); } self.editor.set_hint(None); - self.editor.move_cursor_to_end(); + self.editor.move_cursor_to_end(); } else if let Some(pending) = self.history.pending.take() { self.editor = pending; } else { - // If we are here it should mean we are on our pending command - // And the user tried to scroll history down - // Since there is no "future" history, we should just bell and do nothing - self.writer.send_bell().ok(); - } + // If we are here it should mean we are on our pending command + // And the user tried to scroll history down + // Since there is no "future" history, we should just bell and do nothing + self.writer.send_bell().ok(); + } } pub fn should_accept_hint(&self, event: &KeyEvent) -> bool { if self.editor.cursor_at_max() && self.editor.has_hint() { @@ -512,7 +518,9 @@ impl ShedVi { let line = self.editor.to_string(); let hint = self.editor.get_hint_text(); if crate::state::read_shopts(|s| s.prompt.highlight) { - self.highlighter.load_input(&line,self.editor.cursor_byte_pos()); + self + .highlighter + .load_input(&line, self.editor.cursor_byte_pos()); self.highlighter.highlight(); let highlighted = self.highlighter.take(); format!("{highlighted}{hint}") @@ -524,53 +532,84 @@ impl ShedVi { pub fn print_line(&mut self, final_draw: bool) -> ShResult<()> { let line = self.line_text(); let new_layout = self.get_layout(&line); - let pending_seq = self.mode.pending_seq(); - let mut prompt_string_right = self.prompt.psr_expanded.clone(); + let pending_seq = self.mode.pending_seq(); + let mut prompt_string_right = self.prompt.psr_expanded.clone(); - if prompt_string_right.as_ref().is_some_and(|psr| psr.lines().count() > 1) { - log::warn!("PSR has multiple lines, truncating to one line"); - prompt_string_right = prompt_string_right.map(|psr| psr.lines().next().unwrap_or_default().to_string()); - } - - let row0_used = self.prompt - .get_ps1() - .lines() - .next() - .map(|l| Layout::calc_pos(self.writer.t_cols, l, Pos { col: 0, row: 0 }, 0)) - .map(|p| p.col) - .unwrap_or_default() as usize; - let one_line = new_layout.end.row == 0; + if prompt_string_right + .as_ref() + .is_some_and(|psr| psr.lines().count() > 1) + { + log::warn!("PSR has multiple lines, truncating to one line"); + prompt_string_right = + prompt_string_right.map(|psr| psr.lines().next().unwrap_or_default().to_string()); + } + let row0_used = self + .prompt + .get_ps1() + .lines() + .next() + .map(|l| Layout::calc_pos(self.writer.t_cols, l, Pos { col: 0, row: 0 }, 0)) + .map(|p| p.col) + .unwrap_or_default() as usize; + let one_line = new_layout.end.row == 0; if let Some(layout) = self.old_layout.as_ref() { self.writer.clear_rows(layout)?; } - self.writer.redraw(self.prompt.get_ps1(), &line, &new_layout)?; + self + .writer + .redraw(self.prompt.get_ps1(), &line, &new_layout)?; - let seq_fits = pending_seq.as_ref().is_some_and(|seq| row0_used + 1 < self.writer.t_cols as usize - seq.width()); - let psr_fits = prompt_string_right.as_ref().is_some_and(|psr| new_layout.end.col as usize + 1 < (self.writer.t_cols as usize).saturating_sub(psr.width())); + let seq_fits = pending_seq + .as_ref() + .is_some_and(|seq| row0_used + 1 < self.writer.t_cols as usize - seq.width()); + let psr_fits = prompt_string_right.as_ref().is_some_and(|psr| { + new_layout.end.col as usize + 1 < (self.writer.t_cols as usize).saturating_sub(psr.width()) + }); - if !final_draw && let Some(seq) = pending_seq && !seq.is_empty() && !(prompt_string_right.is_some() && one_line) && seq_fits { - let to_col = self.writer.t_cols - calc_str_width(&seq); - let up = new_layout.cursor.row; // rows to move up from cursor to top line of prompt + if !final_draw + && let Some(seq) = pending_seq + && !seq.is_empty() + && !(prompt_string_right.is_some() && one_line) + && seq_fits + { + let to_col = self.writer.t_cols - calc_str_width(&seq); + let up = new_layout.cursor.row; // rows to move up from cursor to top line of prompt - let move_up = if up > 0 { format!("\x1b[{up}A") } else { String::new() }; + let move_up = if up > 0 { + format!("\x1b[{up}A") + } else { + String::new() + }; - // Save cursor, move up to top row, move right to column, write sequence, restore cursor - self.writer.flush_write(&format!("\x1b7{move_up}\x1b[{to_col}G{seq}\x1b8"))?; - } else if !final_draw && let Some(psr) = prompt_string_right && psr_fits { - let to_col = self.writer.t_cols - calc_str_width(&psr); - let down = new_layout.end.row - new_layout.cursor.row; - let move_down = if down > 0 { format!("\x1b[{down}B") } else { String::new() }; + // Save cursor, move up to top row, move right to column, write sequence, + // restore cursor + self + .writer + .flush_write(&format!("\x1b7{move_up}\x1b[{to_col}G{seq}\x1b8"))?; + } else if !final_draw + && let Some(psr) = prompt_string_right + && psr_fits + { + let to_col = self.writer.t_cols - calc_str_width(&psr); + let down = new_layout.end.row - new_layout.cursor.row; + let move_down = if down > 0 { + format!("\x1b[{down}B") + } else { + String::new() + }; - self.writer.flush_write(&format!("\x1b7{move_down}\x1b[{to_col}G{psr}\x1b8"))?; - } + self + .writer + .flush_write(&format!("\x1b7{move_down}\x1b[{to_col}G{psr}\x1b8"))?; + } self.writer.flush_write(&self.mode.cursor_style())?; self.old_layout = Some(new_layout); - self.needs_redraw = false; + self.needs_redraw = false; Ok(()) } @@ -853,19 +892,22 @@ pub fn get_insertions(input: &str) -> Vec<(usize, Marker)> { /// - Unimplemented features (comments, brace groups) pub fn marker_for(class: &TkRule) -> Option { match class { - TkRule::Pipe | - TkRule::ErrPipe | - TkRule::And | - TkRule::Or | - TkRule::Bg | - TkRule::BraceGrpStart | - TkRule::BraceGrpEnd => { - Some(markers::OPERATOR) - } + TkRule::Pipe + | TkRule::ErrPipe + | TkRule::And + | TkRule::Or + | TkRule::Bg + | TkRule::BraceGrpStart + | TkRule::BraceGrpEnd => Some(markers::OPERATOR), TkRule::Sep => Some(markers::CMD_SEP), TkRule::Redir => Some(markers::REDIRECT), TkRule::Comment => Some(markers::COMMENT), - TkRule::Expanded { exp: _ } | TkRule::EOI | TkRule::SOI | TkRule::Null | TkRule::Str | TkRule::CasePattern => None, + TkRule::Expanded { exp: _ } + | TkRule::EOI + | TkRule::SOI + | TkRule::Null + | TkRule::Str + | TkRule::CasePattern => None, } } @@ -880,9 +922,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> { std::cmp::Ordering::Equal => { let priority = |m: Marker| -> u8 { match m { - markers::VISUAL_MODE_END - | markers::VISUAL_MODE_START - | markers::RESET => 0, + markers::VISUAL_MODE_END | markers::VISUAL_MODE_START | markers::RESET => 0, markers::VAR_SUB | markers::VAR_SUB_END | markers::CMD_SUB @@ -911,9 +951,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> { std::cmp::Ordering::Equal => { let priority = |m: Marker| -> u8 { match m { - markers::VISUAL_MODE_END - | markers::VISUAL_MODE_START - | markers::RESET => 0, + markers::VISUAL_MODE_END | markers::VISUAL_MODE_START | markers::RESET => 0, markers::VAR_SUB | markers::VAR_SUB_END | markers::CMD_SUB @@ -926,7 +964,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> { | markers::STRING_SQ_END | markers::SUBSH_END => 2, - | markers::ARG => 3, // Lowest priority - processed first, overridden by sub-tokens + markers::ARG => 3, // Lowest priority - processed first, overridden by sub-tokens _ => 1, } }; @@ -960,11 +998,11 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> { insertions.push((token.span.start, markers::SUBSH)); return insertions; } else if token.class == TkRule::CasePattern { - insertions.push((token.span.end, markers::RESET)); - insertions.push((token.span.end - 1, markers::CASE_PAT)); - insertions.push((token.span.start, markers::OPERATOR)); - return insertions; - } + insertions.push((token.span.end, markers::RESET)); + insertions.push((token.span.end - 1, markers::CASE_PAT)); + insertions.push((token.span.start, markers::OPERATOR)); + return insertions; + } let token_raw = token.span.as_str(); let mut token_chars = token_raw.char_indices().peekable(); @@ -1144,17 +1182,17 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> { } } '*' | '?' if (!in_dub_qt && !in_sng_qt) => { - let glob_ch = *ch; + let glob_ch = *ch; token_chars.next(); // consume the first glob char if !in_context(markers::COMMAND, &insertions) { - let offset = if glob_ch == '*' && token_chars.peek().is_some_and(|(_, c)| *c == '*') { - // it's one of these probably: ./dir/**/*.txt - token_chars.next(); // consume the second * - 2 - } else { - // just a regular glob char - 1 - }; + let offset = if glob_ch == '*' && token_chars.peek().is_some_and(|(_, c)| *c == '*') { + // it's one of these probably: ./dir/**/*.txt + token_chars.next(); // consume the second * + 2 + } else { + // just a regular glob char + 1 + }; insertions.push((span_start + index + offset, markers::RESET)); insertions.push((span_start + index, markers::GLOB)); } diff --git a/src/readline/term.rs b/src/readline/term.rs index 81c6141..30cd737 100644 --- a/src/readline/term.rs +++ b/src/readline/term.rs @@ -101,35 +101,41 @@ pub fn get_win_size(fd: RawFd) -> (Col, Row) { } fn enumerate_lines(s: &str, left_pad: usize) -> String { - let total_lines = s.lines().count(); - let max_num_len = total_lines.to_string().len(); - s.lines() - .enumerate() - .fold(String::new(), |mut acc, (i, ln)| { - if i == 0 { - acc.push_str(ln); - acc.push('\n'); - } else { - let num = (i + 1).to_string(); - let num_pad = max_num_len - num.len(); - // " 2 | " — num + padding + " | " - let prefix_len = max_num_len + 3; // "N | " - let trail_pad = left_pad.saturating_sub(prefix_len); - if i == total_lines - 1 { - // Don't add a newline to the last line - write!(acc, "\x1b[0m\x1b[90m{}{num} |\x1b[0m {}{ln}", - " ".repeat(num_pad), - " ".repeat(trail_pad), - ).unwrap(); - } else { - writeln!(acc, "\x1b[0m\x1b[90m{}{num} |\x1b[0m {}{ln}", - " ".repeat(num_pad), - " ".repeat(trail_pad), - ).unwrap(); - } - } - acc - }) + let total_lines = s.lines().count(); + let max_num_len = total_lines.to_string().len(); + s.lines() + .enumerate() + .fold(String::new(), |mut acc, (i, ln)| { + if i == 0 { + acc.push_str(ln); + acc.push('\n'); + } else { + let num = (i + 1).to_string(); + let num_pad = max_num_len - num.len(); + // " 2 | " — num + padding + " | " + let prefix_len = max_num_len + 3; // "N | " + let trail_pad = left_pad.saturating_sub(prefix_len); + if i == total_lines - 1 { + // Don't add a newline to the last line + write!( + acc, + "\x1b[0m\x1b[90m{}{num} |\x1b[0m {}{ln}", + " ".repeat(num_pad), + " ".repeat(trail_pad), + ) + .unwrap(); + } else { + writeln!( + acc, + "\x1b[0m\x1b[90m{}{num} |\x1b[0m {}{ln}", + " ".repeat(num_pad), + " ".repeat(trail_pad), + ) + .unwrap(); + } + } + acc + }) } fn write_all(fd: RawFd, buf: &str) -> nix::Result<()> { @@ -171,8 +177,8 @@ fn ends_with_newline(s: &str) -> bool { } pub fn calc_str_width(s: &str) -> u16 { - let mut esc_seq = 0; - s.graphemes(true).map(|g| width(g, &mut esc_seq)).sum() + let mut esc_seq = 0; + s.graphemes(true).map(|g| width(g, &mut esc_seq)).sum() } // Big credit to rustyline for this diff --git a/src/readline/vicmd.rs b/src/readline/vicmd.rs index e92555c..28b54c3 100644 --- a/src/readline/vicmd.rs +++ b/src/readline/vicmd.rs @@ -1,6 +1,6 @@ use bitflags::bitflags; -use super::register::{append_register, read_register, write_register, RegisterContent}; +use super::register::{RegisterContent, append_register, read_register, write_register}; //TODO: write tests that take edit results and cursor positions from actual // neovim edits and test them against the behavior of this editor @@ -383,7 +383,12 @@ impl Motion { pub fn is_linewise(&self) -> bool { matches!( self, - Self::WholeLineInclusive | Self::WholeLineExclusive | Self::LineUp | Self::LineDown | Self::ScreenLineDown | Self::ScreenLineUp + Self::WholeLineInclusive + | Self::WholeLineExclusive + | Self::LineUp + | Self::LineDown + | Self::ScreenLineDown + | Self::ScreenLineUp ) } } diff --git a/src/readline/vimode.rs b/src/readline/vimode.rs index dd1db21..ab94739 100644 --- a/src/readline/vimode.rs +++ b/src/readline/vimode.rs @@ -1020,15 +1020,13 @@ impl ViNormal { impl ViMode for ViNormal { fn handle_key(&mut self, key: E) -> Option { let mut cmd = match key { - E(K::Char('V'), M::NONE) => { - Some(ViCmd { - register: Default::default(), - verb: Some(VerbCmd(1, Verb::VisualModeLine)), - motion: None, - raw_seq: "".into(), - flags: self.flags(), - }) - } + E(K::Char('V'), M::NONE) => Some(ViCmd { + register: Default::default(), + verb: Some(VerbCmd(1, Verb::VisualModeLine)), + motion: None, + raw_seq: "".into(), + flags: self.flags(), + }), E(K::Char(ch), M::NONE) => self.try_parse(ch), E(K::Backspace, M::NONE) => Some(ViCmd { register: Default::default(), @@ -1405,8 +1403,8 @@ impl ViVisual { break 'motion_parse Some(MotionCmd(count, Motion::WholeLineInclusive)); } ('c', Some(VerbCmd(_, Verb::Change))) => { - break 'motion_parse Some(MotionCmd(count, Motion::WholeLineExclusive)); - } + break 'motion_parse Some(MotionCmd(count, Motion::WholeLineExclusive)); + } _ => {} } match ch { diff --git a/src/shopt.rs b/src/shopt.rs index f1e0078..630541e 100644 --- a/src/shopt.rs +++ b/src/shopt.rs @@ -83,14 +83,14 @@ impl ShOpts { } } - pub fn display_opts(&mut self) -> ShResult { - let output = [ - format!("core:\n{}", self.query("core")?.unwrap_or_default()), - format!("prompt:\n{}",self.query("prompt")?.unwrap_or_default()) - ]; + pub fn display_opts(&mut self) -> ShResult { + let output = [ + format!("core:\n{}", self.query("core")?.unwrap_or_default()), + format!("prompt:\n{}", self.query("prompt")?.unwrap_or_default()), + ]; - Ok(output.join("\n")) - } + Ok(output.join("\n")) + } pub fn set(&mut self, opt: &str, val: &str) -> ShResult<()> { let mut query = opt.split('.'); @@ -542,7 +542,10 @@ impl Display for ShOptPrompt { output.push(format!("comp_limit = {}", self.comp_limit)); output.push(format!("highlight = {}", self.highlight)); output.push(format!("auto_indent = {}", self.auto_indent)); - output.push(format!("linebreak_on_incomplete = {}", self.linebreak_on_incomplete)); + output.push(format!( + "linebreak_on_incomplete = {}", + self.linebreak_on_incomplete + )); let final_output = output.join("\n"); diff --git a/src/signal.rs b/src/signal.rs index eed3018..7f5ff40 100644 --- a/src/signal.rs +++ b/src/signal.rs @@ -83,10 +83,10 @@ pub fn check_signals() -> ShResult<()> { run_trap(Signal::SIGCHLD)?; wait_child()?; } - if got_signal(Signal::SIGWINCH) { - GOT_SIGWINCH.store(true, Ordering::SeqCst); - run_trap(Signal::SIGWINCH)?; - } + if got_signal(Signal::SIGWINCH) { + GOT_SIGWINCH.store(true, Ordering::SeqCst); + run_trap(Signal::SIGWINCH)?; + } for sig in MISC_SIGNALS { if got_signal(sig) { @@ -157,7 +157,7 @@ pub fn hang_up(_: libc::c_int) { SHOULD_QUIT.store(true, Ordering::SeqCst); QUIT_CODE.store(1, Ordering::SeqCst); write_jobs(|j| { - j.hang_up(); + j.hang_up(); }); } @@ -286,7 +286,7 @@ pub fn child_exited(pid: Pid, status: WtStat) -> ShResult<()> { if is_fg { take_term()?; } else { - JOB_DONE.store(true, Ordering::SeqCst); + JOB_DONE.store(true, Ordering::SeqCst); let job_order = read_jobs(|j| j.order().to_vec()); let result = read_jobs(|j| j.query(JobID::Pgid(pgid)).cloned()); if let Some(job) = result { diff --git a/src/state.rs b/src/state.rs index 4bab78a..eb63856 100644 --- a/src/state.rs +++ b/src/state.rs @@ -1,14 +1,34 @@ use std::{ - cell::RefCell, cmp::Ordering, collections::{HashMap, HashSet, VecDeque, hash_map::Entry}, fmt::Display, ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref}, os::unix::fs::PermissionsExt, str::FromStr, time::Duration + cell::RefCell, + cmp::Ordering, + collections::{HashMap, HashSet, VecDeque, hash_map::Entry}, + fmt::Display, + ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref}, + os::unix::fs::PermissionsExt, + str::FromStr, + time::Duration, }; use nix::unistd::{User, gethostname, getppid}; use crate::{ - builtin::{BUILTINS, trap::TrapTarget}, exec_input, jobs::JobTab, libsh::{ + builtin::{BUILTINS, trap::TrapTarget}, + exec_input, + jobs::JobTab, + libsh::{ error::{ShErr, ShErrKind, ShResult}, utils::VecDequeExt, - }, parse::{ConjunctNode, NdRule, Node, ParsedSrc, lex::{LexFlags, LexStream, Tk}}, prelude::*, readline::{complete::{BashCompSpec, CompSpec}, markers}, shopt::ShOpts + }, + parse::{ + ConjunctNode, NdRule, Node, ParsedSrc, + lex::{LexFlags, LexStream, Tk}, + }, + prelude::*, + readline::{ + complete::{BashCompSpec, CompSpec}, + markers, + }, + shopt::ShOpts, }; pub struct Shed { @@ -152,10 +172,10 @@ impl ScopeStack { return scope.unset_var(var_name); } } - Err(ShErr::simple( - ShErrKind::ExecFail, - format!("Variable '{}' not found", var_name) - )) + Err(ShErr::simple( + ShErrKind::ExecFail, + format!("Variable '{}' not found", var_name), + )) } pub fn export_var(&mut self, var_name: &str) { for scope in self.scopes.iter_mut().rev() { @@ -183,11 +203,11 @@ impl ScopeStack { flat_vars.insert(var_name.clone(), var.clone()); } } - for var in env::vars() { - if let Entry::Vacant(e) = flat_vars.entry(var.0) { - e.insert(Var::new(VarKind::Str(var.1), VarFlags::EXPORT)); - } - } + for var in env::vars() { + if let Entry::Vacant(e) = flat_vars.entry(var.0) { + e.insert(Var::new(VarKind::Str(var.1), VarFlags::EXPORT)); + } + } flat_vars } @@ -195,128 +215,136 @@ impl ScopeStack { let is_local = self.is_local_var(var_name); if flags.contains(VarFlags::LOCAL) || is_local { self.set_var_local(var_name, val, flags) - } else { - self.set_var_global(var_name, val, flags) + } else { + self.set_var_global(var_name, val, flags) } } - pub fn set_var_indexed(&mut self, var_name: &str, idx: ArrIndex, val: String, flags: VarFlags) -> ShResult<()> { + pub fn set_var_indexed( + &mut self, + var_name: &str, + idx: ArrIndex, + val: String, + flags: VarFlags, + ) -> ShResult<()> { let is_local = self.is_local_var(var_name); if flags.contains(VarFlags::LOCAL) || is_local { - let Some(scope) = self.scopes.last_mut() else { return Ok(()) }; + let Some(scope) = self.scopes.last_mut() else { + return Ok(()); + }; scope.set_index(var_name, idx, val) } else { - let Some(scope) = self.scopes.first_mut() else { return Ok(()) }; + let Some(scope) = self.scopes.first_mut() else { + return Ok(()); + }; scope.set_index(var_name, idx, val) } } fn set_var_global(&mut self, var_name: &str, val: VarKind, flags: VarFlags) -> ShResult<()> { let Some(scope) = self.scopes.first_mut() else { - return Ok(()) + return Ok(()); }; scope.set_var(var_name, val, flags) } fn set_var_local(&mut self, var_name: &str, val: VarKind, flags: VarFlags) -> ShResult<()> { let Some(scope) = self.scopes.last_mut() else { - return Ok(()) + return Ok(()); }; scope.set_var(var_name, val, flags) } - pub fn get_arr_elems(&self, var_name: &str) -> ShResult> { - for scope in self.scopes.iter().rev() { - if scope.var_exists(var_name) - && let Some(var) = scope.vars().get(var_name) { - match var.kind() { - VarKind::Arr(items) => { - let mut item_vec = items.clone() - .into_iter() - .collect::>(); + pub fn get_arr_elems(&self, var_name: &str) -> ShResult> { + for scope in self.scopes.iter().rev() { + if scope.var_exists(var_name) + && let Some(var) = scope.vars().get(var_name) + { + match var.kind() { + VarKind::Arr(items) => { + let mut item_vec = items.clone().into_iter().collect::>(); - item_vec.sort_by_key(|(idx, _)| *idx); // sort by index + item_vec.sort_by_key(|(idx, _)| *idx); // sort by index - return Ok(item_vec.into_iter() - .map(|(_,s)| s) - .collect()) - } - _ => { - return Err(ShErr::simple( - ShErrKind::ExecFail, - format!("Variable '{}' is not an array", var_name) - )); - } - } - } - } - Err(ShErr::simple( - ShErrKind::ExecFail, - format!("Variable '{}' not found", var_name) - )) - } - pub fn index_var(&self, var_name: &str, idx: ArrIndex) -> ShResult { - for scope in self.scopes.iter().rev() { - if scope.var_exists(var_name) - && let Some(var) = scope.vars().get(var_name) { - match var.kind() { - VarKind::Arr(items) => { - let idx = match idx { - ArrIndex::Literal(n) => { - n - } - ArrIndex::FromBack(n) => { - if items.len() >= n { - items.len() - n - } else { - return Err(ShErr::simple( - ShErrKind::ExecFail, - format!("Index {} out of bounds for array '{}'", n, var_name) - )); - } - } - _ => return Err(ShErr::simple( - ShErrKind::ExecFail, - format!("Cannot index all elements of array '{}'", var_name) - )), - }; + return Ok(item_vec.into_iter().map(|(_, s)| s).collect()); + } + _ => { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("Variable '{}' is not an array", var_name), + )); + } + } + } + } + Err(ShErr::simple( + ShErrKind::ExecFail, + format!("Variable '{}' not found", var_name), + )) + } + pub fn index_var(&self, var_name: &str, idx: ArrIndex) -> ShResult { + for scope in self.scopes.iter().rev() { + if scope.var_exists(var_name) + && let Some(var) = scope.vars().get(var_name) + { + match var.kind() { + VarKind::Arr(items) => { + let idx = match idx { + ArrIndex::Literal(n) => n, + ArrIndex::FromBack(n) => { + if items.len() >= n { + items.len() - n + } else { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("Index {} out of bounds for array '{}'", n, var_name), + )); + } + } + _ => { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("Cannot index all elements of array '{}'", var_name), + )); + } + }; - if let Some(item) = items.get(&idx) { - return Ok(item.clone()); - } else { - return Err(ShErr::simple( - ShErrKind::ExecFail, - format!("Index {} out of bounds for array '{}'", idx, var_name) - )); - } - } - _ => { - return Err(ShErr::simple( - ShErrKind::ExecFail, - format!("Variable '{}' is not an array", var_name) - )); - } - } - } - } - Ok("".into()) - } - pub fn try_get_var(&self, var_name: &str) -> Option { - // This version of get_var() is mainly used internally - // so that we have access to Option methods - if let Ok(param) = var_name.parse::() { - let val = self.get_param(param); - if !val.is_empty() { - return Some(val); - } else { - return None; - } - } + if let Some(item) = items.get(&idx) { + return Ok(item.clone()); + } else { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("Index {} out of bounds for array '{}'", idx, var_name), + )); + } + } + _ => { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("Variable '{}' is not an array", var_name), + )); + } + } + } + } + Ok("".into()) + } + pub fn try_get_var(&self, var_name: &str) -> Option { + // This version of get_var() is mainly used internally + // so that we have access to Option methods + if let Ok(param) = var_name.parse::() { + let val = self.get_param(param); + if !val.is_empty() { + return Some(val); + } else { + return None; + } + } - for scope in self.scopes.iter().rev() { - if scope.var_exists(var_name) { - return Some(scope.get_var(var_name)); - } - } + for scope in self.scopes.iter().rev() { + if scope.var_exists(var_name) { + return Some(scope.get_var(var_name)); + } + } - None - } + None + } pub fn get_var(&self, var_name: &str) -> String { if let Ok(param) = var_name.parse::() { return self.get_param(param); @@ -329,21 +357,20 @@ impl ScopeStack { // Fallback to env var std::env::var(var_name).unwrap_or_default() } - pub fn is_local_var(&self, var_name: &str) -> bool { - self.scopes - .last() - .is_some_and(|s| - s.get_var_flags(var_name).is_some_and(|flags| flags.contains(VarFlags::LOCAL)) - ) - } - pub fn get_var_flags(&self, var_name: &str) -> Option { - for scope in self.scopes.iter().rev() { - if scope.var_exists(var_name) { - return scope.get_var_flags(var_name); - } - } - None - } + pub fn is_local_var(&self, var_name: &str) -> bool { + self.scopes.last().is_some_and(|s| { + s.get_var_flags(var_name) + .is_some_and(|flags| flags.contains(VarFlags::LOCAL)) + }) + } + pub fn get_var_flags(&self, var_name: &str) -> Option { + for scope in self.scopes.iter().rev() { + if scope.var_exists(var_name) { + return scope.get_var_flags(var_name); + } + } + None + } pub fn get_param(&self, param: ShellParam) -> String { if param.is_global() && let Some(val) = self.global_params.get(¶m.to_string()) @@ -383,9 +410,9 @@ thread_local! { /// A shell function /// -/// Consists of the BraceGrp Node and the stored ParsedSrc that the node refers to. -/// The Node must be stored with the ParsedSrc because the tokens of the node -/// contain an Arc Which refers to the String held in ParsedSrc +/// Consists of the BraceGrp Node and the stored ParsedSrc that the node refers +/// to. The Node must be stored with the ParsedSrc because the tokens of the +/// node contain an Arc Which refers to the String held in ParsedSrc #[derive(Clone, Debug)] pub struct ShFunc(Node); @@ -404,12 +431,12 @@ impl ShFunc { let ConjunctNode { cmd, operator: _ } = conjunct_node; *cmd } - pub fn body(&self) -> &Node { - &self.0 - } - pub fn body_mut(&mut self) -> &mut Node { - &mut self.0 - } + pub fn body(&self) -> &Node { + &self.0 + } + pub fn body_mut(&mut self) -> &mut Node { + &mut self.0 + } } /// The logic table for the shell @@ -534,86 +561,81 @@ impl VarFlags { #[derive(Clone, Debug)] pub enum ArrIndex { - Literal(usize), - FromBack(usize), - AllJoined, - AllSplit + Literal(usize), + FromBack(usize), + AllJoined, + AllSplit, } impl FromStr for ArrIndex { - type Err = ShErr; - fn from_str(s: &str) -> Result { - match s { - "@" => Ok(Self::AllSplit), - "*" => Ok(Self::AllJoined), - _ if s.starts_with('-') && s[1..].chars().all(|c| c.is_digit(1)) => { - let idx = s[1..].parse::().unwrap(); - Ok(Self::FromBack(idx)) - } - _ if !s.is_empty() && s.chars().all(|c| c.is_ascii_digit()) => { - let idx = s.parse::().unwrap(); - Ok(Self::Literal(idx)) - } - _ => Err(ShErr::simple( - ShErrKind::ParseErr, - format!("Invalid array index: {}", s) - )) - } - } + type Err = ShErr; + fn from_str(s: &str) -> Result { + match s { + "@" => Ok(Self::AllSplit), + "*" => Ok(Self::AllJoined), + _ if s.starts_with('-') && s[1..].chars().all(|c| c.is_digit(1)) => { + let idx = s[1..].parse::().unwrap(); + Ok(Self::FromBack(idx)) + } + _ if !s.is_empty() && s.chars().all(|c| c.is_ascii_digit()) => { + let idx = s.parse::().unwrap(); + Ok(Self::Literal(idx)) + } + _ => Err(ShErr::simple( + ShErrKind::ParseErr, + format!("Invalid array index: {}", s), + )), + } + } } pub fn hashmap_to_vec(map: HashMap) -> Vec { - let mut items = map.into_iter() - .collect::>(); - items.sort_by_key(|(idx, _)| *idx); + let mut items = map.into_iter().collect::>(); + items.sort_by_key(|(idx, _)| *idx); - items.into_iter() - .map(|(_,i)| i) - .collect() + items.into_iter().map(|(_, i)| i).collect() } #[derive(Clone, Debug)] pub enum VarKind { - Str(String), - Int(i32), - Arr(HashMap), - AssocArr(Vec<(String, String)>), + Str(String), + Int(i32), + Arr(HashMap), + AssocArr(Vec<(String, String)>), } impl VarKind { - pub fn arr_from_tk(tk: Tk) -> ShResult { - let raw = tk.as_str(); - if !raw.starts_with('(') || !raw.ends_with(')') { - return Err(ShErr::simple( - ShErrKind::ParseErr, - format!("Invalid array syntax: {}", raw), - )); - } - let raw = raw[1..raw.len() - 1].to_string(); + pub fn arr_from_tk(tk: Tk) -> ShResult { + let raw = tk.as_str(); + if !raw.starts_with('(') || !raw.ends_with(')') { + return Err(ShErr::simple( + ShErrKind::ParseErr, + format!("Invalid array syntax: {}", raw), + )); + } + let raw = raw[1..raw.len() - 1].to_string(); - let tokens: HashMap = LexStream::new(Arc::new(raw), LexFlags::empty()) - .map(|tk| tk.and_then(|tk| tk.expand()).map(|tk| tk.get_words())) - .try_fold(vec![], |mut acc, wrds| { - match wrds { - Ok(wrds) => acc.extend(wrds), - Err(e) => return Err(e), - } - Ok(acc) - })? - .into_iter() - .enumerate() - .collect(); + let tokens: HashMap = LexStream::new(Arc::new(raw), LexFlags::empty()) + .map(|tk| tk.and_then(|tk| tk.expand()).map(|tk| tk.get_words())) + .try_fold(vec![], |mut acc, wrds| { + match wrds { + Ok(wrds) => acc.extend(wrds), + Err(e) => return Err(e), + } + Ok(acc) + })? + .into_iter() + .enumerate() + .collect(); - Ok(Self::Arr(tokens)) - } + Ok(Self::Arr(tokens)) + } - pub fn arr_from_vec(vec: Vec) -> Self { - let tokens: HashMap = vec.into_iter() - .enumerate() - .collect(); + pub fn arr_from_vec(vec: Vec) -> Self { + let tokens: HashMap = vec.into_iter().enumerate().collect(); - Self::Arr(tokens) - } + Self::Arr(tokens) + } } impl Display for VarKind { @@ -622,7 +644,7 @@ impl Display for VarKind { VarKind::Str(s) => write!(f, "{s}"), VarKind::Int(i) => write!(f, "{i}"), VarKind::Arr(items) => { - let items = hashmap_to_vec(items.clone()); + let items = hashmap_to_vec(items.clone()); let mut item_iter = items.iter().peekable(); while let Some(item) = item_iter.next() { write!(f, "{item}")?; @@ -666,9 +688,9 @@ impl Var { pub fn mark_for_export(&mut self) { self.flags.set(VarFlags::EXPORT, true); } - pub fn flags(&self) -> VarFlags { - self.flags - } + pub fn flags(&self) -> VarFlags { + self.flags + } } impl Display for Var { @@ -843,66 +865,69 @@ impl VarTab { std::env::var(var).unwrap_or_default() } } - pub fn get_var_flags(&self, var_name: &str) -> Option { - self.vars.get(var_name).map(|var| var.flags) - } + pub fn get_var_flags(&self, var_name: &str) -> Option { + self.vars.get(var_name).map(|var| var.flags) + } pub fn unset_var(&mut self, var_name: &str) -> ShResult<()> { - if let Some(var) = self.vars.get(var_name) && var.flags.contains(VarFlags::READONLY) { - return Err(ShErr::simple( - ShErrKind::ExecFail, - format!("cannot unset readonly variable '{}'", var_name) - )); - } + if let Some(var) = self.vars.get(var_name) + && var.flags.contains(VarFlags::READONLY) + { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("cannot unset readonly variable '{}'", var_name), + )); + } self.vars.remove(var_name); unsafe { env::remove_var(var_name) }; - Ok(()) + Ok(()) } - pub fn set_index(&mut self, var_name: &str, idx: ArrIndex, val: String) -> ShResult<()> { - if self.var_exists(var_name) - && let Some(var) = self.vars_mut().get_mut(var_name) { - match var.kind_mut() { - VarKind::Arr(items) => { - let idx = match idx { - ArrIndex::Literal(n) => { - n - } - ArrIndex::FromBack(n) => { - if items.len() >= n { - items.len() - n - } else { - return Err(ShErr::simple( - ShErrKind::ExecFail, - format!("Index {} out of bounds for array '{}'", n, var_name) - )); - } - } - _ => return Err(ShErr::simple( - ShErrKind::ExecFail, - format!("Cannot index all elements of array '{}'", var_name) - )), - }; + pub fn set_index(&mut self, var_name: &str, idx: ArrIndex, val: String) -> ShResult<()> { + if self.var_exists(var_name) + && let Some(var) = self.vars_mut().get_mut(var_name) + { + match var.kind_mut() { + VarKind::Arr(items) => { + let idx = match idx { + ArrIndex::Literal(n) => n, + ArrIndex::FromBack(n) => { + if items.len() >= n { + items.len() - n + } else { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("Index {} out of bounds for array '{}'", n, var_name), + )); + } + } + _ => { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("Cannot index all elements of array '{}'", var_name), + )); + } + }; - items.insert(idx, val); - return Ok(()); - } - _ => { - return Err(ShErr::simple( - ShErrKind::ExecFail, - format!("Variable '{}' is not an array", var_name) - )); - } - } - } - Ok(()) - } + items.insert(idx, val); + return Ok(()); + } + _ => { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("Variable '{}' is not an array", var_name), + )); + } + } + } + Ok(()) + } pub fn set_var(&mut self, var_name: &str, val: VarKind, flags: VarFlags) -> ShResult<()> { if let Some(var) = self.vars.get_mut(var_name) { - if var.flags.contains(VarFlags::READONLY) && !flags.contains(VarFlags::READONLY) { - return Err(ShErr::simple( - ShErrKind::ExecFail, - format!("Variable '{}' is readonly", var_name) - )); - } + if var.flags.contains(VarFlags::READONLY) && !flags.contains(VarFlags::READONLY) { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("Variable '{}' is readonly", var_name), + )); + } var.kind = val; var.flags |= flags; if var.flags.contains(VarFlags::EXPORT) || flags.contains(VarFlags::EXPORT) { @@ -919,7 +944,7 @@ impl VarTab { } self.vars.insert(var_name.to_string(), var); } - Ok(()) + Ok(()) } pub fn var_exists(&self, var_name: &str) -> bool { if let Ok(param) = var_name.parse::() { @@ -961,121 +986,170 @@ pub struct MetaTab { // pending system messages system_msg: Vec, - // pushd/popd stack - dir_stack: VecDeque, + // pushd/popd stack + dir_stack: VecDeque, - old_path: Option, - old_pwd: Option, - // valid command cache - path_cache: HashSet, - cwd_cache: HashSet, - // programmable completion specs - comp_specs: HashMap>, + old_path: Option, + old_pwd: Option, + // valid command cache + path_cache: HashSet, + cwd_cache: HashSet, + // programmable completion specs + comp_specs: HashMap>, } impl MetaTab { pub fn new() -> Self { - Self::default() + Self { + comp_specs: Self::get_builtin_comp_specs(), + ..Default::default() + } } - pub fn cached_cmds(&self) -> &HashSet { - &self.path_cache - } - pub fn cwd_cache(&self) -> &HashSet { - &self.cwd_cache - } - pub fn comp_specs(&self) -> &HashMap> { - &self.comp_specs - } - pub fn comp_specs_mut(&mut self) -> &mut HashMap> { - &mut self.comp_specs - } - pub fn get_comp_spec(&self, cmd: &str) -> Option> { - self.comp_specs.get(cmd).map(|spec| spec.clone()) - } - pub fn set_comp_spec(&mut self, cmd: String, spec: Box) { - self.comp_specs.insert(cmd, spec); - } - pub fn remove_comp_spec(&mut self, cmd: &str) -> bool { - self.comp_specs.remove(cmd).is_some() - } - pub fn try_rehash_commands(&mut self) { - let path = env::var("PATH").unwrap_or_default(); - let cwd = env::var("PWD").unwrap_or_default(); - if self.old_path.as_ref().is_some_and(|old| *old == path) - && self.old_pwd.as_ref().is_some_and(|old| *old == cwd) { - log::trace!("PATH and PWD unchanged, skipping rehash"); - return; - } + pub fn get_builtin_comp_specs() -> HashMap> { + let mut map = HashMap::new(); - log::trace!("Rehashing commands for PATH: '{}' and PWD: '{}'", path, cwd); + map.insert( + "cd".into(), + Box::new(BashCompSpec::new().dirs(true)) as Box, + ); + map.insert( + "pushd".into(), + Box::new(BashCompSpec::new().dirs(true)) as Box, + ); + map.insert( + "popd".into(), + Box::new(BashCompSpec::new().dirs(true)) as Box, + ); + map.insert( + "source".into(), + Box::new(BashCompSpec::new().files(true)) as Box, + ); + map.insert( + "bg".into(), + Box::new(BashCompSpec::new().jobs(true)) as Box, + ); + map.insert( + "fg".into(), + Box::new(BashCompSpec::new().jobs(true)) as Box, + ); + map.insert( + "disown".into(), + Box::new(BashCompSpec::new().jobs(true)) as Box, + ); - self.path_cache.clear(); - self.old_path = Some(path.clone()); - self.old_pwd = Some(cwd.clone()); - let paths = path.split(":") - .map(PathBuf::from); + map + } + pub fn cached_cmds(&self) -> &HashSet { + &self.path_cache + } + pub fn cwd_cache(&self) -> &HashSet { + &self.cwd_cache + } + pub fn comp_specs(&self) -> &HashMap> { + &self.comp_specs + } + pub fn comp_specs_mut(&mut self) -> &mut HashMap> { + &mut self.comp_specs + } + pub fn get_comp_spec(&self, cmd: &str) -> Option> { + self.comp_specs.get(cmd).map(|spec| spec.clone()) + } + pub fn set_comp_spec(&mut self, cmd: String, spec: Box) { + self.comp_specs.insert(cmd, spec); + } + pub fn remove_comp_spec(&mut self, cmd: &str) -> bool { + self.comp_specs.remove(cmd).is_some() + } + pub fn try_rehash_commands(&mut self) { + let path = env::var("PATH").unwrap_or_default(); + let cwd = env::var("PWD").unwrap_or_default(); + if self.old_path.as_ref().is_some_and(|old| *old == path) + && self.old_pwd.as_ref().is_some_and(|old| *old == cwd) + { + log::trace!("PATH and PWD unchanged, skipping rehash"); + return; + } - for path in paths { - if let Ok(entries) = path.read_dir() { - for entry in entries.flatten() { - let Ok(meta) = std::fs::metadata(entry.path()) else { continue }; + log::trace!("Rehashing commands for PATH: '{}' and PWD: '{}'", path, cwd); + + self.path_cache.clear(); + self.old_path = Some(path.clone()); + self.old_pwd = Some(cwd.clone()); + let paths = path.split(":").map(PathBuf::from); + + for path in paths { + if let Ok(entries) = path.read_dir() { + for entry in entries.flatten() { + let Ok(meta) = std::fs::metadata(entry.path()) else { + continue; + }; let is_exec = meta.permissions().mode() & 0o111 != 0; - if meta.is_file() && is_exec - && let Some(name) = entry.file_name().to_str() { - self.path_cache.insert(name.to_string()); - } - } - } - } - if let Ok(entries) = Path::new(&cwd).read_dir() { - for entry in entries.flatten() { - let Ok(meta) = std::fs::metadata(entry.path()) else { continue }; - let is_exec = meta.permissions().mode() & 0o111 != 0; + if meta.is_file() + && is_exec + && let Some(name) = entry.file_name().to_str() + { + self.path_cache.insert(name.to_string()); + } + } + } + } + if let Ok(entries) = Path::new(&cwd).read_dir() { + for entry in entries.flatten() { + let Ok(meta) = std::fs::metadata(entry.path()) else { + continue; + }; + let is_exec = meta.permissions().mode() & 0o111 != 0; - if meta.is_file() && is_exec - && let Some(name) = entry.file_name().to_str() { - self.path_cache.insert(format!("./{}", name)); - } - } - } + if meta.is_file() + && is_exec + && let Some(name) = entry.file_name().to_str() + { + self.path_cache.insert(format!("./{}", name)); + } + } + } - read_logic(|l| { - let funcs = l.funcs(); - let aliases = l.aliases(); - for func in funcs.keys() { - self.path_cache.insert(func.clone()); - } - for alias in aliases.keys() { - self.path_cache.insert(alias.clone()); - } - }); + read_logic(|l| { + let funcs = l.funcs(); + let aliases = l.aliases(); + for func in funcs.keys() { + self.path_cache.insert(func.clone()); + } + for alias in aliases.keys() { + self.path_cache.insert(alias.clone()); + } + }); - for cmd in BUILTINS { - self.path_cache.insert(cmd.to_string()); - } - } - pub fn try_rehash_cwd_listing(&mut self) { - let cwd = env::var("PWD").unwrap_or_default(); - if self.old_pwd.as_ref().is_some_and(|old| *old == cwd) { - log::trace!("PWD unchanged, skipping rehash of cwd listing"); - return; - } + for cmd in BUILTINS { + self.path_cache.insert(cmd.to_string()); + } + } + pub fn try_rehash_cwd_listing(&mut self) { + let cwd = env::var("PWD").unwrap_or_default(); + if self.old_pwd.as_ref().is_some_and(|old| *old == cwd) { + log::trace!("PWD unchanged, skipping rehash of cwd listing"); + return; + } - log::debug!("Rehashing cwd listing for PWD: '{}'", cwd); + log::debug!("Rehashing cwd listing for PWD: '{}'", cwd); - if let Ok(entries) = Path::new(&cwd).read_dir() { - for entry in entries.flatten() { - let Ok(meta) = std::fs::metadata(entry.path()) else { continue }; - let is_exec = meta.permissions().mode() & 0o111 != 0; + if let Ok(entries) = Path::new(&cwd).read_dir() { + for entry in entries.flatten() { + let Ok(meta) = std::fs::metadata(entry.path()) else { + continue; + }; + let is_exec = meta.permissions().mode() & 0o111 != 0; - if meta.is_file() && is_exec - && let Some(name) = entry.file_name().to_str() { - self.cwd_cache.insert(name.to_string()); - } - } - } - } + if meta.is_file() + && is_exec + && let Some(name) = entry.file_name().to_str() + { + self.cwd_cache.insert(name.to_string()); + } + } + } + } pub fn start_timer(&mut self) { self.runtime_start = Some(Instant::now()); } @@ -1098,35 +1172,35 @@ impl MetaTab { pub fn system_msg_pending(&self) -> bool { !self.system_msg.is_empty() } - pub fn dir_stack_top(&self) -> Option<&PathBuf> { - self.dir_stack.front() - } - pub fn push_dir(&mut self, path: PathBuf) { - self.dir_stack.push_front(path); - } - pub fn pop_dir(&mut self) -> Option { - self.dir_stack.pop_front() - } - pub fn remove_dir(&mut self, idx: i32) -> Option { - if idx < 0 { - let neg_idx = (self.dir_stack.len() - 1).saturating_sub((-idx) as usize); - self.dir_stack.remove(neg_idx) - } else { - self.dir_stack.remove((idx - 1) as usize) - } - } - pub fn rotate_dirs_fwd(&mut self, steps: usize) { - self.dir_stack.rotate_left(steps); - } - pub fn rotate_dirs_bkwd(&mut self, steps: usize) { - self.dir_stack.rotate_right(steps); - } - pub fn dirs(&self) -> &VecDeque { - &self.dir_stack - } - pub fn dirs_mut(&mut self) -> &mut VecDeque { - &mut self.dir_stack - } + pub fn dir_stack_top(&self) -> Option<&PathBuf> { + self.dir_stack.front() + } + pub fn push_dir(&mut self, path: PathBuf) { + self.dir_stack.push_front(path); + } + pub fn pop_dir(&mut self) -> Option { + self.dir_stack.pop_front() + } + pub fn remove_dir(&mut self, idx: i32) -> Option { + if idx < 0 { + let neg_idx = (self.dir_stack.len() - 1).saturating_sub((-idx) as usize); + self.dir_stack.remove(neg_idx) + } else { + self.dir_stack.remove((idx - 1) as usize) + } + } + pub fn rotate_dirs_fwd(&mut self, steps: usize) { + self.dir_stack.rotate_left(steps); + } + pub fn rotate_dirs_bkwd(&mut self, steps: usize) { + self.dir_stack.rotate_right(steps); + } + pub fn dirs(&self) -> &VecDeque { + &self.dir_stack + } + pub fn dirs_mut(&mut self) -> &mut VecDeque { + &mut self.dir_stack + } } /// Read from the job table @@ -1151,63 +1225,67 @@ pub fn write_vars T>(f: F) -> T { /// Parse `arr[idx]` into (name, raw_index_expr). Pure parsing, no expansion. pub fn parse_arr_bracket(var_name: &str) -> Option<(String, String)> { - let mut chars = var_name.chars(); - let mut name = String::new(); - let mut idx_raw = String::new(); - let mut bracket_depth = 0; + let mut chars = var_name.chars(); + let mut name = String::new(); + let mut idx_raw = String::new(); + let mut bracket_depth = 0; - while let Some(ch) = chars.next() { - match ch { - '\\' => { chars.next(); } - '[' => { - bracket_depth += 1; - if bracket_depth > 1 { - idx_raw.push(ch); - } - } - ']' => { - if bracket_depth > 0 { - bracket_depth -= 1; - if bracket_depth == 0 { - if idx_raw.is_empty() { - return None; - } - break; - } - } - idx_raw.push(ch); - } - _ if bracket_depth > 0 => idx_raw.push(ch), - _ => name.push(ch), - } - } + while let Some(ch) = chars.next() { + match ch { + '\\' => { + chars.next(); + } + '[' => { + bracket_depth += 1; + if bracket_depth > 1 { + idx_raw.push(ch); + } + } + ']' => { + if bracket_depth > 0 { + bracket_depth -= 1; + if bracket_depth == 0 { + if idx_raw.is_empty() { + return None; + } + break; + } + } + idx_raw.push(ch); + } + _ if bracket_depth > 0 => idx_raw.push(ch), + _ => name.push(ch), + } + } - if name.is_empty() || idx_raw.is_empty() { - None - } else { - Some((name, idx_raw)) - } + if name.is_empty() || idx_raw.is_empty() { + None + } else { + Some((name, idx_raw)) + } } /// Expand the raw index expression and parse it into an ArrIndex. pub fn expand_arr_index(idx_raw: &str) -> ShResult { - let expanded = LexStream::new(Arc::new(idx_raw.to_string()), LexFlags::empty()) - .map(|tk| tk.and_then(|tk| tk.expand()).map(|tk| tk.get_words())) - .try_fold(vec![], |mut acc, wrds| { - match wrds { - Ok(wrds) => acc.extend(wrds), - Err(e) => return Err(e), - } - Ok(acc) - })? - .into_iter() - .next() - .ok_or_else(|| ShErr::simple(ShErrKind::ParseErr, "Empty array index"))?; + let expanded = LexStream::new(Arc::new(idx_raw.to_string()), LexFlags::empty()) + .map(|tk| tk.and_then(|tk| tk.expand()).map(|tk| tk.get_words())) + .try_fold(vec![], |mut acc, wrds| { + match wrds { + Ok(wrds) => acc.extend(wrds), + Err(e) => return Err(e), + } + Ok(acc) + })? + .into_iter() + .next() + .ok_or_else(|| ShErr::simple(ShErrKind::ParseErr, "Empty array index"))?; - expanded.parse::().map_err(|_| ShErr::simple( - ShErrKind::ParseErr, - format!("Invalid array index: {}", expanded) - )) + expanded.parse::().map_err(|_| { + ShErr::simple( + ShErrKind::ParseErr, + format!("Invalid array index: {}", expanded), + ) + }) } pub fn read_meta T>(f: F) -> T { diff --git a/src/tests/complete.rs b/src/tests/complete.rs index 30a3ced..965c77e 100644 --- a/src/tests/complete.rs +++ b/src/tests/complete.rs @@ -6,7 +6,7 @@ use tempfile::TempDir; use crate::prompt::readline::complete::Completer; use crate::prompt::readline::markers; -use crate::state::{write_logic, write_vars, VarFlags}; +use crate::state::{VarFlags, write_logic, write_vars}; use super::*; @@ -192,10 +192,12 @@ fn complete_filename_with_slash() { // Should complete files in subdir/ if result.is_some() { - assert!(completer - .candidates - .iter() - .any(|c| c.contains("nested.txt"))); + assert!( + completer + .candidates + .iter() + .any(|c| c.contains("nested.txt")) + ); } } @@ -702,10 +704,12 @@ fn complete_special_characters_in_filename() { if result.is_some() { // Should handle special chars in filenames - assert!(completer - .candidates - .iter() - .any(|c| c.contains("file-with-dash") || c.contains("file_with_underscore"))); + assert!( + completer + .candidates + .iter() + .any(|c| c.contains("file-with-dash") || c.contains("file_with_underscore")) + ); } } diff --git a/src/tests/error.rs b/src/tests/error.rs index 22aa352..577f6e9 100644 --- a/src/tests/error.rs +++ b/src/tests/error.rs @@ -58,8 +58,8 @@ fn unclosed_squote() { #[test] fn unclosed_brc_grp() { let input = "{ foo bar"; - let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty()) - .collect::>>(); + let tokens = + LexStream::new(Arc::new(input.into()), LexFlags::empty()).collect::>>(); let Err(err) = tokens else { panic!("Expected an error, got {:?}", tokens); diff --git a/src/tests/expand.rs b/src/tests/expand.rs index ead07f9..1dbbe9e 100644 --- a/src/tests/expand.rs +++ b/src/tests/expand.rs @@ -9,7 +9,13 @@ use super::*; #[test] fn simple_expansion() { let varsub = "$foo"; - write_vars(|v| v.set_var("foo", VarKind::Str("this is the value of the variable".into()), VarFlags::NONE)); + write_vars(|v| { + v.set_var( + "foo", + VarKind::Str("this is the value of the variable".into()), + VarFlags::NONE, + ) + }); let mut tokens: Vec = LexStream::new(Arc::new(varsub.to_string()), LexFlags::empty()) .map(|tk| tk.unwrap()) @@ -308,7 +314,10 @@ fn dquote_escape_dollar() { fn dquote_escape_backslash() { // "\\" in double quotes should produce a single backslash let result = unescape_str(r#""\\""#); - let inner: String = result.chars().filter(|&c| c != markers::DUB_QUOTE).collect(); + let inner: String = result + .chars() + .filter(|&c| c != markers::DUB_QUOTE) + .collect(); assert_eq!( inner, "\\", "Double backslash should produce single backslash" @@ -319,7 +328,10 @@ fn dquote_escape_backslash() { fn dquote_escape_quote() { // "\"" should produce a literal double quote let result = unescape_str(r#""\"""#); - let inner: String = result.chars().filter(|&c| c != markers::DUB_QUOTE).collect(); + let inner: String = result + .chars() + .filter(|&c| c != markers::DUB_QUOTE) + .collect(); assert!( inner.contains('"'), "Escaped quote should produce literal quote" @@ -330,7 +342,10 @@ fn dquote_escape_quote() { fn dquote_escape_backtick() { // "\`" should strip backslash, produce literal backtick let result = unescape_str(r#""\`""#); - let inner: String = result.chars().filter(|&c| c != markers::DUB_QUOTE).collect(); + let inner: String = result + .chars() + .filter(|&c| c != markers::DUB_QUOTE) + .collect(); assert_eq!( inner, "`", "Escaped backtick should produce literal backtick" @@ -341,7 +356,10 @@ fn dquote_escape_backtick() { fn dquote_escape_nonspecial_preserves_backslash() { // "\a" inside double quotes should preserve the backslash (a is not special) let result = unescape_str(r#""\a""#); - let inner: String = result.chars().filter(|&c| c != markers::DUB_QUOTE).collect(); + let inner: String = result + .chars() + .filter(|&c| c != markers::DUB_QUOTE) + .collect(); assert_eq!( inner, "\\a", "Backslash before non-special char should be preserved" @@ -362,10 +380,16 @@ fn dquote_unescaped_dollar_expands() { fn dquote_mixed_escapes() { // "hello \$world \\end" should have literal $, single backslash let result = unescape_str(r#""hello \$world \\end""#); - assert!(!result.contains(markers::VAR_SUB), "Escaped $ should not expand"); + assert!( + !result.contains(markers::VAR_SUB), + "Escaped $ should not expand" + ); assert!(result.contains('$'), "Literal $ should be in output"); // Should have exactly one backslash (from \\) - let inner: String = result.chars().filter(|&c| c != markers::DUB_QUOTE).collect(); + let inner: String = result + .chars() + .filter(|&c| c != markers::DUB_QUOTE) + .collect(); let backslash_count = inner.chars().filter(|&c| c == '\\').count(); assert_eq!(backslash_count, 1, "\\\\ should produce one backslash"); } diff --git a/src/tests/mod.rs b/src/tests/mod.rs index 5caedde..59408e2 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -4,8 +4,9 @@ use super::*; use crate::expand::{expand_aliases, unescape_str}; use crate::libsh::error::{Note, ShErr, ShErrKind}; use crate::parse::{ + NdRule, Node, ParseStream, lex::{LexFlags, LexStream, Tk, TkRule}, - node_operation, NdRule, Node, ParseStream, + node_operation, }; use crate::state::{write_logic, write_vars}; diff --git a/src/tests/readline.rs b/src/tests/readline.rs index 5294d6b..8ac8f1d 100644 --- a/src/tests/readline.rs +++ b/src/tests/readline.rs @@ -1,12 +1,19 @@ use std::collections::VecDeque; use crate::{ - expand::expand_prompt, libsh::{ + expand::expand_prompt, + libsh::{ error::ShErr, term::{Style, Styled}, - }, prompt::readline::{ - Prompt, ShedVi, history::History, keys::{KeyCode, KeyEvent, ModKeys}, linebuf::LineBuf, term::{KeyReader, LineWriter, raw_mode}, vimode::{ViInsert, ViMode, ViNormal} - } + }, + prompt::readline::{ + Prompt, ShedVi, + history::History, + keys::{KeyCode, KeyEvent, ModKeys}, + linebuf::LineBuf, + term::{KeyReader, LineWriter, raw_mode}, + vimode::{ViInsert, ViMode, ViNormal}, + }, }; use pretty_assertions::assert_eq; @@ -251,9 +258,13 @@ fn linebuf_ascii_content() { #[test] fn expand_default_prompt() { - let prompt = expand_prompt("\\e[0m\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m ".into()).unwrap(); + let prompt = expand_prompt( + "\\e[0m\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m " + .into(), + ) + .unwrap(); - insta::assert_debug_snapshot!(prompt) + insta::assert_debug_snapshot!(prompt) } #[test] diff --git a/src/tests/redir.rs b/src/tests/redir.rs index cdfb18b..a016296 100644 --- a/src/tests/redir.rs +++ b/src/tests/redir.rs @@ -1,8 +1,8 @@ use std::sync::Arc; use crate::parse::{ - lex::{LexFlags, LexStream}, NdRule, Node, ParseStream, Redir, RedirType, + lex::{LexFlags, LexStream}, }; use crate::procio::{IoFrame, IoMode, IoStack}; diff --git a/src/tests/state.rs b/src/tests/state.rs index e87dc26..61d1ed9 100644 --- a/src/tests/state.rs +++ b/src/tests/state.rs @@ -11,8 +11,8 @@ fn scopestack_new() { // Should start with one global scope assert!(stack.var_exists("PATH") || !stack.var_exists("PATH")); // Just check - // it doesn't - // panic + // it doesn't + // panic } #[test]