command arguments are now underlined if they match an existing path -m ran rustfmt on the entire codebase

This commit is contained in:
2026-02-19 21:32:03 -05:00
parent b668dab522
commit a18a0b622f
44 changed files with 5549 additions and 5019 deletions

View File

@@ -3,7 +3,7 @@ use crate::{
libsh::error::{ShErr, ShErrKind, ShResult}, libsh::error::{ShErr, ShErrKind, ShResult},
parse::{NdRule, Node}, parse::{NdRule, Node},
prelude::*, prelude::*,
procio::{borrow_fd, IoStack}, procio::{IoStack, borrow_fd},
state::{self, read_logic, write_logic}, state::{self, read_logic, write_logic},
}; };

View File

@@ -43,15 +43,19 @@ pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> {
} }
if let Err(e) = env::set_current_dir(new_dir) { if let Err(e) = env::set_current_dir(new_dir) {
return Err(ShErr::full( return Err(ShErr::full(
ShErrKind::ExecFail, ShErrKind::ExecFail,
format!("cd: Failed to change directory: {}", e), format!("cd: Failed to change directory: {}", e),
span, span,
)); ));
} }
let new_dir = env::current_dir().map_err( let new_dir = env::current_dir().map_err(|e| {
|e| ShErr::full(ShErrKind::ExecFail, format!("cd: Failed to get current directory: {}", e), span) ShErr::full(
)?; ShErrKind::ExecFail,
format!("cd: Failed to get current directory: {}", e),
span,
)
})?;
unsafe { env::set_var("PWD", new_dir) }; unsafe { env::set_var("PWD", new_dir) };
state::set_status(0); state::set_status(0);

View File

@@ -1,14 +1,34 @@
use std::sync::LazyLock; use std::sync::LazyLock;
use crate::{ use crate::{
builtin::setup_builtin, expand::expand_prompt, getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node}, prelude::*, procio::{IoStack, borrow_fd}, state builtin::setup_builtin,
expand::expand_prompt,
getopt::{Opt, OptSpec, get_opts_from_tokens},
jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
parse::{NdRule, Node},
prelude::*,
procio::{IoStack, borrow_fd},
state,
}; };
pub const ECHO_OPTS: [OptSpec;4] = [ pub const ECHO_OPTS: [OptSpec; 4] = [
OptSpec { opt: Opt::Short('n'), takes_arg: false }, OptSpec {
OptSpec { opt: Opt::Short('E'), takes_arg: false }, opt: Opt::Short('n'),
OptSpec { opt: Opt::Short('e'), takes_arg: false }, takes_arg: false,
OptSpec { opt: Opt::Short('p'), takes_arg: false }, },
OptSpec {
opt: Opt::Short('E'),
takes_arg: false,
},
OptSpec {
opt: Opt::Short('e'),
takes_arg: false,
},
OptSpec {
opt: Opt::Short('p'),
takes_arg: false,
},
]; ];
bitflags! { bitflags! {
@@ -16,7 +36,7 @@ bitflags! {
const NO_NEWLINE = 0b000001; const NO_NEWLINE = 0b000001;
const USE_STDERR = 0b000010; const USE_STDERR = 0b000010;
const USE_ESCAPE = 0b000100; const USE_ESCAPE = 0b000100;
const USE_PROMPT = 0b001000; const USE_PROMPT = 0b001000;
} }
} }
@@ -40,13 +60,15 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
borrow_fd(STDOUT_FILENO) borrow_fd(STDOUT_FILENO)
}; };
let mut echo_output = prepare_echo_args(argv let mut echo_output = prepare_echo_args(
.into_iter() argv
.map(|a| a.0) // Extract the String from the tuple of (String,Span) .into_iter()
.collect::<Vec<_>>(), .map(|a| a.0) // Extract the String from the tuple of (String,Span)
flags.contains(EchoFlags::USE_ESCAPE), .collect::<Vec<_>>(),
flags.contains(EchoFlags::USE_PROMPT) flags.contains(EchoFlags::USE_ESCAPE),
)?.join(" "); flags.contains(EchoFlags::USE_PROMPT),
)?
.join(" ");
if !flags.contains(EchoFlags::NO_NEWLINE) && !echo_output.ends_with('\n') { if !flags.contains(EchoFlags::NO_NEWLINE) && !echo_output.ends_with('\n') {
echo_output.push('\n') echo_output.push('\n')
@@ -58,137 +80,141 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
Ok(()) Ok(())
} }
pub fn prepare_echo_args(argv: Vec<String>, use_escape: bool, use_prompt: bool) -> ShResult<Vec<String>> { pub fn prepare_echo_args(
if !use_escape { argv: Vec<String>,
if use_prompt { use_escape: bool,
let expanded: ShResult<Vec<String>> = argv use_prompt: bool,
.into_iter() ) -> ShResult<Vec<String>> {
.map(|s| expand_prompt(s.as_str())) if !use_escape {
.collect(); if use_prompt {
return expanded let expanded: ShResult<Vec<String>> = argv
} .into_iter()
return Ok(argv); .map(|s| expand_prompt(s.as_str()))
} .collect();
return expanded;
}
return Ok(argv);
}
let mut prepared_args = Vec::with_capacity(argv.len()); let mut prepared_args = Vec::with_capacity(argv.len());
for arg in argv { for arg in argv {
let mut prepared_arg = String::new(); let mut prepared_arg = String::new();
if use_prompt { if use_prompt {
prepared_arg = expand_prompt(&prepared_arg)?; prepared_arg = expand_prompt(&prepared_arg)?;
} }
let mut chars = arg.chars().peekable(); let mut chars = arg.chars().peekable();
while let Some(c) = chars.next() { while let Some(c) = chars.next() {
if c == '\\' { if c == '\\' {
if let Some(&next_char) = chars.peek() { if let Some(&next_char) = chars.peek() {
match next_char { match next_char {
'n' => { 'n' => {
prepared_arg.push('\n'); prepared_arg.push('\n');
chars.next(); chars.next();
} }
't' => { 't' => {
prepared_arg.push('\t'); prepared_arg.push('\t');
chars.next(); chars.next();
} }
'r' => { 'r' => {
prepared_arg.push('\r'); prepared_arg.push('\r');
chars.next(); chars.next();
} }
'a' => { 'a' => {
prepared_arg.push('\x07'); prepared_arg.push('\x07');
chars.next(); chars.next();
} }
'b' => { 'b' => {
prepared_arg.push('\x08'); prepared_arg.push('\x08');
chars.next(); chars.next();
} }
'e' | 'E' => { 'e' | 'E' => {
prepared_arg.push('\x1b'); prepared_arg.push('\x1b');
chars.next(); chars.next();
} }
'x' => { 'x' => {
chars.next(); // consume 'x' chars.next(); // consume 'x'
let mut hex_digits = String::new(); let mut hex_digits = String::new();
for _ in 0..2 { for _ in 0..2 {
if let Some(&hex_char) = chars.peek() { if let Some(&hex_char) = chars.peek() {
if hex_char.is_ascii_hexdigit() { if hex_char.is_ascii_hexdigit() {
hex_digits.push(hex_char); hex_digits.push(hex_char);
chars.next(); chars.next();
} else { } else {
break; break;
} }
} else { } else {
break; break;
} }
} }
if let Ok(value) = u8::from_str_radix(&hex_digits, 16) { if let Ok(value) = u8::from_str_radix(&hex_digits, 16) {
prepared_arg.push(value as char); prepared_arg.push(value as char);
} else { } else {
prepared_arg.push('\\'); prepared_arg.push('\\');
prepared_arg.push('x'); prepared_arg.push('x');
prepared_arg.push_str(&hex_digits); prepared_arg.push_str(&hex_digits);
} }
} }
'0' => { '0' => {
chars.next(); // consume '0' chars.next(); // consume '0'
let mut octal_digits = String::new(); let mut octal_digits = String::new();
for _ in 0..3 { for _ in 0..3 {
if let Some(&octal_char) = chars.peek() { if let Some(&octal_char) = chars.peek() {
if ('0'..='7').contains(&octal_char) { if ('0'..='7').contains(&octal_char) {
octal_digits.push(octal_char); octal_digits.push(octal_char);
chars.next(); chars.next();
} else { } else {
break; break;
} }
} else { } else {
break; break;
} }
} }
if let Ok(value) = u8::from_str_radix(&octal_digits, 8) { if let Ok(value) = u8::from_str_radix(&octal_digits, 8) {
prepared_arg.push(value as char); prepared_arg.push(value as char);
} else { } else {
prepared_arg.push('\\'); prepared_arg.push('\\');
prepared_arg.push('0'); prepared_arg.push('0');
prepared_arg.push_str(&octal_digits); prepared_arg.push_str(&octal_digits);
} }
} }
'\\' => { '\\' => {
prepared_arg.push('\\'); prepared_arg.push('\\');
chars.next(); chars.next();
} }
_ => prepared_arg.push(c), _ => prepared_arg.push(c),
} }
} else { } else {
prepared_arg.push(c); prepared_arg.push(c);
} }
} else { } else {
prepared_arg.push(c); prepared_arg.push(c);
} }
} }
prepared_args.push(prepared_arg); prepared_args.push(prepared_arg);
} }
Ok(prepared_args) Ok(prepared_args)
} }
pub fn get_echo_flags(opts: Vec<Opt>) -> ShResult<EchoFlags> { pub fn get_echo_flags(opts: Vec<Opt>) -> ShResult<EchoFlags> {
let mut flags = EchoFlags::empty(); let mut flags = EchoFlags::empty();
for opt in opts { for opt in opts {
match opt { match opt {
Opt::Short('n') => flags |= EchoFlags::NO_NEWLINE, Opt::Short('n') => flags |= EchoFlags::NO_NEWLINE,
Opt::Short('r') => flags |= EchoFlags::USE_STDERR, Opt::Short('r') => flags |= EchoFlags::USE_STDERR,
Opt::Short('e') => flags |= EchoFlags::USE_ESCAPE, Opt::Short('e') => flags |= EchoFlags::USE_ESCAPE,
Opt::Short('p') => flags |= EchoFlags::USE_PROMPT, Opt::Short('p') => flags |= EchoFlags::USE_PROMPT,
_ => { _ => {
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::ExecFail, ShErrKind::ExecFail,
format!("echo: Unexpected flag '{opt}'"), format!("echo: Unexpected flag '{opt}'"),
)); ));
} }
} }
} }

View File

@@ -35,10 +35,10 @@ pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult
for (arg, _) in argv { for (arg, _) in argv {
if let Some((var, val)) = arg.split_once('=') { if let Some((var, val)) = arg.split_once('=') {
write_vars(|v| v.set_var(var, val, VarFlags::EXPORT)); // Export an assignment like write_vars(|v| v.set_var(var, val, VarFlags::EXPORT)); // Export an assignment like
// 'foo=bar' // 'foo=bar'
} else { } else {
write_vars(|v| v.export_var(&arg)); // Export an existing variable, if write_vars(|v| v.export_var(&arg)); // Export an existing variable, if
// any // any
} }
} }
} }

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
libsh::error::{ShErr, ShErrKind, ShResult}, libsh::error::{ShErr, ShErrKind, ShResult},
parse::{execute::prepare_argv, NdRule, Node}, parse::{NdRule, Node, execute::prepare_argv},
prelude::*, prelude::*,
}; };
@@ -32,7 +32,6 @@ pub fn flowctl(node: Node, kind: ShErrKind) -> ShResult<()> {
code = status; code = status;
} }
let kind = match kind { let kind = match kind {
LoopContinue(_) => LoopContinue(code), LoopContinue(_) => LoopContinue(code),
LoopBreak(_) => LoopBreak(code), LoopBreak(_) => LoopBreak(code),

View File

@@ -1,9 +1,9 @@
use crate::{ use crate::{
jobs::{JobBldr, JobCmdFlags, JobID}, jobs::{JobBldr, JobCmdFlags, JobID},
libsh::error::{ShErr, ShErrKind, ShResult}, libsh::error::{ShErr, ShErrKind, ShResult},
parse::{lex::Span, NdRule, Node}, parse::{NdRule, Node, lex::Span},
prelude::*, prelude::*,
procio::{borrow_fd, IoStack}, procio::{IoStack, borrow_fd},
state::{self, read_jobs, write_jobs}, state::{self, read_jobs, write_jobs},
}; };
@@ -168,7 +168,7 @@ pub fn jobs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
"Invalid flag in jobs call", "Invalid flag in jobs call",
span, span,
)) ));
} }
}; };
flags |= flag flags |= flag

View File

@@ -4,7 +4,9 @@ use crate::{
jobs::{ChildProc, JobBldr}, jobs::{ChildProc, JobBldr},
libsh::error::ShResult, libsh::error::ShResult,
parse::{ parse::{
Redir, execute::prepare_argv, lex::{Span, Tk} Redir,
execute::prepare_argv,
lex::{Span, Tk},
}, },
procio::{IoFrame, IoStack, RedirGuard}, procio::{IoFrame, IoStack, RedirGuard},
}; };
@@ -16,19 +18,17 @@ pub mod export;
pub mod flowctl; pub mod flowctl;
pub mod jobctl; pub mod jobctl;
pub mod pwd; pub mod pwd;
pub mod read;
pub mod shift; pub mod shift;
pub mod shopt; pub mod shopt;
pub mod source; pub mod source;
pub mod test; // [[ ]] thing pub mod test; // [[ ]] thing
pub mod read;
pub mod zoltraak;
pub mod trap; pub mod trap;
pub mod zoltraak;
pub const BUILTINS: [&str; 21] = [ pub const BUILTINS: [&str; 21] = [
"echo", "cd", "read", "export", "pwd", "source", "echo", "cd", "read", "export", "pwd", "source", "shift", "jobs", "fg", "bg", "alias", "unalias",
"shift", "jobs", "fg", "bg", "alias", "unalias", "return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "command", "trap",
"return", "break", "continue", "exit", "zoltraak",
"shopt", "builtin", "command", "trap"
]; ];
/// Sets up a builtin command /// Sets up a builtin command

View File

@@ -3,7 +3,7 @@ use crate::{
libsh::error::ShResult, libsh::error::ShResult,
parse::{NdRule, Node}, parse::{NdRule, Node},
prelude::*, prelude::*,
procio::{borrow_fd, IoStack}, procio::{IoStack, borrow_fd},
state, state,
}; };

View File

@@ -1,188 +1,237 @@
use bitflags::bitflags; use bitflags::bitflags;
use nix::{errno::Errno, libc::{STDIN_FILENO, STDOUT_FILENO}, unistd::{isatty, read, write}}; use nix::{
errno::Errno,
libc::{STDIN_FILENO, STDOUT_FILENO},
unistd::{isatty, read, write},
};
use crate::{builtin::setup_builtin, getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node}, procio::{IoStack, borrow_fd}, prompt::readline::term::RawModeGuard, state::{self, VarFlags, read_vars, write_vars}}; use crate::{
builtin::setup_builtin,
getopt::{Opt, OptSpec, get_opts_from_tokens},
jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
parse::{NdRule, Node},
procio::{IoStack, borrow_fd},
prompt::readline::term::RawModeGuard,
state::{self, VarFlags, read_vars, write_vars},
};
pub const READ_OPTS: [OptSpec;7] = [ pub const READ_OPTS: [OptSpec; 7] = [
OptSpec { opt: Opt::Short('r'), takes_arg: false }, // don't allow backslash escapes OptSpec {
OptSpec { opt: Opt::Short('s'), takes_arg: false }, // don't echo input opt: Opt::Short('r'),
OptSpec { opt: Opt::Short('a'), takes_arg: false }, // read into array takes_arg: false,
OptSpec { opt: Opt::Short('n'), takes_arg: false }, // read only N characters }, // don't allow backslash escapes
OptSpec { opt: Opt::Short('t'), takes_arg: false }, // timeout OptSpec {
OptSpec { opt: Opt::Short('p'), takes_arg: true }, // prompt opt: Opt::Short('s'),
OptSpec { opt: Opt::Short('d'), takes_arg: true }, // read until delimiter takes_arg: false,
}, // don't echo input
OptSpec {
opt: Opt::Short('a'),
takes_arg: false,
}, // read into array
OptSpec {
opt: Opt::Short('n'),
takes_arg: false,
}, // read only N characters
OptSpec {
opt: Opt::Short('t'),
takes_arg: false,
}, // timeout
OptSpec {
opt: Opt::Short('p'),
takes_arg: true,
}, // prompt
OptSpec {
opt: Opt::Short('d'),
takes_arg: true,
}, // read until delimiter
]; ];
bitflags! { bitflags! {
pub struct ReadFlags: u32 { pub struct ReadFlags: u32 {
const NO_ESCAPES = 0b000001; const NO_ESCAPES = 0b000001;
const NO_ECHO = 0b000010; // TODO: unused const NO_ECHO = 0b000010; // TODO: unused
const ARRAY = 0b000100; // TODO: unused const ARRAY = 0b000100; // TODO: unused
const N_CHARS = 0b001000; // TODO: unused const N_CHARS = 0b001000; // TODO: unused
const TIMEOUT = 0b010000; // TODO: unused const TIMEOUT = 0b010000; // TODO: unused
} }
} }
pub struct ReadOpts { pub struct ReadOpts {
prompt: Option<String>, prompt: Option<String>,
delim: u8, // byte representation of the delimiter character delim: u8, // byte representation of the delimiter character
flags: ReadFlags, flags: ReadFlags,
} }
pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn read_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 { let NdRule::Command {
assignments: _, assignments: _,
argv argv,
} = node.class else { } = node.class
unreachable!() else {
}; unreachable!()
};
let (argv, opts) = get_opts_from_tokens(argv, &READ_OPTS); let (argv, opts) = get_opts_from_tokens(argv, &READ_OPTS);
let read_opts = get_read_flags(opts).blame(blame.clone())?; let read_opts = get_read_flags(opts).blame(blame.clone())?;
let (argv, _) = setup_builtin(argv, job, None).blame(blame.clone())?; let (argv, _) = setup_builtin(argv, job, None).blame(blame.clone())?;
if let Some(prompt) = read_opts.prompt { if let Some(prompt) = read_opts.prompt {
write(borrow_fd(STDOUT_FILENO), prompt.as_bytes())?; write(borrow_fd(STDOUT_FILENO), prompt.as_bytes())?;
} }
let input = if isatty(STDIN_FILENO)? { let input = if isatty(STDIN_FILENO)? {
// Restore default terminal settings // Restore default terminal settings
RawModeGuard::with_cooked_mode(|| { RawModeGuard::with_cooked_mode(|| {
let mut input: Vec<u8> = vec![]; let mut input: Vec<u8> = vec![];
let mut escaped = false; let mut escaped = false;
loop { loop {
let mut buf = [0u8;1]; let mut buf = [0u8; 1];
match read(STDIN_FILENO, &mut buf) { match read(STDIN_FILENO, &mut buf) {
Ok(0) => { Ok(0) => {
state::set_status(1); state::set_status(1);
let str_result = String::from_utf8(input.clone()).map_err(|e| ShErr::simple( let str_result = String::from_utf8(input.clone()).map_err(|e| {
ShErrKind::ExecFail, ShErr::simple(
format!("read: Input was not valid UTF-8: {e}"), ShErrKind::ExecFail,
))?; format!("read: Input was not valid UTF-8: {e}"),
return Ok(str_result); // EOF )
} })?;
Ok(_) => { return Ok(str_result); // EOF
if buf[0] == read_opts.delim { }
if read_opts.flags.contains(ReadFlags::NO_ESCAPES) && escaped { Ok(_) => {
input.push(buf[0]); if buf[0] == read_opts.delim {
} else { if read_opts.flags.contains(ReadFlags::NO_ESCAPES) && escaped {
// Delimiter reached, stop reading input.push(buf[0]);
break; } else {
} // Delimiter reached, stop reading
} break;
else if read_opts.flags.contains(ReadFlags::NO_ESCAPES) }
&& buf[0] == b'\\' { } else if read_opts.flags.contains(ReadFlags::NO_ESCAPES) && buf[0] == b'\\' {
escaped = true; escaped = true;
} else { } else {
input.push(buf[0]); input.push(buf[0]);
} }
} }
Err(Errno::EINTR) => continue, Err(Errno::EINTR) => continue,
Err(e) => return Err(ShErr::simple( Err(e) => {
ShErrKind::ExecFail, return Err(ShErr::simple(
format!("read: Failed to read from stdin: {e}"), ShErrKind::ExecFail,
)), format!("read: Failed to read from stdin: {e}"),
} ));
} }
}
}
state::set_status(0); state::set_status(0);
let str_result = String::from_utf8(input.clone()).map_err(|e| ShErr::simple( let str_result = String::from_utf8(input.clone()).map_err(|e| {
ShErrKind::ExecFail, ShErr::simple(
format!("read: Input was not valid UTF-8: {e}"), ShErrKind::ExecFail,
))?; format!("read: Input was not valid UTF-8: {e}"),
Ok(str_result) )
}).blame(blame)? })?;
} else { Ok(str_result)
let mut input: Vec<u8> = vec![]; })
loop { .blame(blame)?
let mut buf = [0u8;1]; } else {
match read(STDIN_FILENO, &mut buf) { let mut input: Vec<u8> = vec![];
Ok(0) => { loop {
state::set_status(1); let mut buf = [0u8; 1];
break; // EOF match read(STDIN_FILENO, &mut buf) {
} Ok(0) => {
Ok(_) => { state::set_status(1);
if buf[0] == read_opts.delim { break; // EOF
break; // Delimiter reached, stop reading }
} Ok(_) => {
input.push(buf[0]); if buf[0] == read_opts.delim {
} break; // Delimiter reached, stop reading
Err(Errno::EINTR) => continue, }
Err(e) => return Err(ShErr::simple( input.push(buf[0]);
ShErrKind::ExecFail, }
format!("read: Failed to read from stdin: {e}"), Err(Errno::EINTR) => continue,
)), Err(e) => {
} return Err(ShErr::simple(
} ShErrKind::ExecFail,
String::from_utf8(input).map_err(|e| ShErr::simple( format!("read: Failed to read from stdin: {e}"),
ShErrKind::ExecFail, ));
format!("read: Input was not valid UTF-8: {e}"), }
))? }
}; }
String::from_utf8(input).map_err(|e| {
ShErr::simple(
ShErrKind::ExecFail,
format!("read: Input was not valid UTF-8: {e}"),
)
})?
};
if argv.is_empty() { if argv.is_empty() {
write_vars(|v| { write_vars(|v| {
v.set_var("REPLY", &input, VarFlags::NONE); v.set_var("REPLY", &input, VarFlags::NONE);
}); });
} else { } else {
// get our field separator // get our field separator
let mut field_sep = read_vars(|v| v.get_var("IFS")); let mut field_sep = read_vars(|v| v.get_var("IFS"));
if field_sep.is_empty() { field_sep = " ".to_string() } if field_sep.is_empty() {
let mut remaining = input; field_sep = " ".to_string()
}
let mut remaining = input;
for (i, arg) in argv.iter().enumerate() { for (i, arg) in argv.iter().enumerate() {
if i == argv.len() - 1 { if i == argv.len() - 1 {
// Last arg, stuff the rest of the input into it // Last arg, stuff the rest of the input into it
write_vars(|v| v.set_var(&arg.0, &remaining, VarFlags::NONE)); write_vars(|v| v.set_var(&arg.0, &remaining, VarFlags::NONE));
break; break;
} }
// trim leading IFS characters // trim leading IFS characters
let trimmed = remaining.trim_start_matches(|c: char| field_sep.contains(c)); let trimmed = remaining.trim_start_matches(|c: char| field_sep.contains(c));
if let Some(idx) = trimmed.find(|c: char| field_sep.contains(c)) { if let Some(idx) = trimmed.find(|c: char| field_sep.contains(c)) {
// We found a field separator, split at the char index // We found a field separator, split at the char index
let (field, rest) = trimmed.split_at(idx); let (field, rest) = trimmed.split_at(idx);
write_vars(|v| v.set_var(&arg.0, field, VarFlags::NONE)); write_vars(|v| v.set_var(&arg.0, field, VarFlags::NONE));
// note that this doesn't account for consecutive IFS characters, which is what that trim above is for // note that this doesn't account for consecutive IFS characters, which is what
remaining = rest.to_string(); // that trim above is for
} else { remaining = rest.to_string();
write_vars(|v| v.set_var(&arg.0, trimmed, VarFlags::NONE)); } else {
remaining.clear(); write_vars(|v| v.set_var(&arg.0, trimmed, VarFlags::NONE));
} remaining.clear();
} }
} }
}
state::set_status(0); state::set_status(0);
Ok(()) Ok(())
} }
pub fn get_read_flags(opts: Vec<Opt>) -> ShResult<ReadOpts> { pub fn get_read_flags(opts: Vec<Opt>) -> ShResult<ReadOpts> {
let mut read_opts = ReadOpts { let mut read_opts = ReadOpts {
prompt: None, prompt: None,
delim: b'\n', delim: b'\n',
flags: ReadFlags::empty(), flags: ReadFlags::empty(),
}; };
for opt in opts { for opt in opts {
match opt { match opt {
Opt::Short('r') => read_opts.flags |= ReadFlags::NO_ESCAPES, Opt::Short('r') => read_opts.flags |= ReadFlags::NO_ESCAPES,
Opt::Short('s') => read_opts.flags |= ReadFlags::NO_ECHO, Opt::Short('s') => read_opts.flags |= ReadFlags::NO_ECHO,
Opt::Short('a') => read_opts.flags |= ReadFlags::ARRAY, Opt::Short('a') => read_opts.flags |= ReadFlags::ARRAY,
Opt::Short('n') => read_opts.flags |= ReadFlags::N_CHARS, Opt::Short('n') => read_opts.flags |= ReadFlags::N_CHARS,
Opt::Short('t') => read_opts.flags |= ReadFlags::TIMEOUT, Opt::Short('t') => read_opts.flags |= ReadFlags::TIMEOUT,
Opt::ShortWithArg('p', prompt) => read_opts.prompt = Some(prompt), Opt::ShortWithArg('p', prompt) => read_opts.prompt = Some(prompt),
Opt::ShortWithArg('d', delim) => read_opts.delim = delim.chars().map(|c| c as u8).next().unwrap_or(b'\n'), Opt::ShortWithArg('d', delim) => {
_ => { read_opts.delim = delim.chars().map(|c| c as u8).next().unwrap_or(b'\n')
return Err(ShErr::simple( }
ShErrKind::ExecFail, _ => {
format!("read: Unexpected flag '{opt}'"), return Err(ShErr::simple(
)); ShErrKind::ExecFail,
} format!("read: Unexpected flag '{opt}'"),
} ));
} }
}
}
Ok(read_opts) Ok(read_opts)
} }

View File

@@ -3,7 +3,7 @@ use crate::{
libsh::error::{ShResult, ShResultExt}, libsh::error::{ShResult, ShResultExt},
parse::{NdRule, Node}, parse::{NdRule, Node},
prelude::*, prelude::*,
procio::{borrow_fd, IoStack}, procio::{IoStack, borrow_fd},
state::write_shopts, state::write_shopts,
}; };

View File

@@ -8,7 +8,7 @@ use regex::Regex;
use crate::{ use crate::{
libsh::error::{ShErr, ShErrKind, ShResult}, libsh::error::{ShErr, ShErrKind, ShResult},
parse::{ConjunctOp, NdRule, Node, TestCase, TEST_UNARY_OPS}, parse::{ConjunctOp, NdRule, Node, TEST_UNARY_OPS, TestCase},
prelude::*, prelude::*,
}; };
@@ -254,7 +254,7 @@ pub fn double_bracket_test(node: Node) -> ShResult<bool> {
msg: "Expected a binary operator in this test call; found a unary operator".into(), msg: "Expected a binary operator in this test call; found a unary operator".into(),
notes: vec![], notes: vec![],
span: err_span, span: err_span,
}) });
} }
TestOp::StringEq => rhs.trim() == lhs.trim(), TestOp::StringEq => rhs.trim() == lhs.trim(),
TestOp::StringNeq => rhs.trim() != lhs.trim(), TestOp::StringNeq => rhs.trim() != lhs.trim(),

View File

@@ -1,162 +1,171 @@
use std::{fmt::Display, str::FromStr}; use std::{fmt::Display, str::FromStr};
use nix::{libc::{STDERR_FILENO, STDOUT_FILENO}, sys::signal::Signal, unistd::write}; use nix::{
libc::{STDERR_FILENO, STDOUT_FILENO},
sys::signal::Signal,
unistd::write,
};
use crate::{builtin::setup_builtin, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node}, procio::{IoStack, borrow_fd}, state::{self, read_logic, write_logic}}; use crate::{
builtin::setup_builtin,
jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult},
parse::{NdRule, Node},
procio::{IoStack, borrow_fd},
state::{self, read_logic, write_logic},
};
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash)] #[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash)]
pub enum TrapTarget { pub enum TrapTarget {
Exit, Exit,
Error, Error,
Signal(Signal) Signal(Signal),
} }
impl FromStr for TrapTarget { impl FromStr for TrapTarget {
type Err = ShErr; type Err = ShErr;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
match s { match s {
"EXIT" => Ok(TrapTarget::Exit), "EXIT" => Ok(TrapTarget::Exit),
"ERR" => Ok(TrapTarget::Error), "ERR" => Ok(TrapTarget::Error),
"INT" => Ok(TrapTarget::Signal(Signal::SIGINT)), "INT" => Ok(TrapTarget::Signal(Signal::SIGINT)),
"QUIT" => Ok(TrapTarget::Signal(Signal::SIGQUIT)), "QUIT" => Ok(TrapTarget::Signal(Signal::SIGQUIT)),
"ILL" => Ok(TrapTarget::Signal(Signal::SIGILL)), "ILL" => Ok(TrapTarget::Signal(Signal::SIGILL)),
"TRAP" => Ok(TrapTarget::Signal(Signal::SIGTRAP)), "TRAP" => Ok(TrapTarget::Signal(Signal::SIGTRAP)),
"ABRT" => Ok(TrapTarget::Signal(Signal::SIGABRT)), "ABRT" => Ok(TrapTarget::Signal(Signal::SIGABRT)),
"BUS" => Ok(TrapTarget::Signal(Signal::SIGBUS)), "BUS" => Ok(TrapTarget::Signal(Signal::SIGBUS)),
"FPE" => Ok(TrapTarget::Signal(Signal::SIGFPE)), "FPE" => Ok(TrapTarget::Signal(Signal::SIGFPE)),
"KILL" => Ok(TrapTarget::Signal(Signal::SIGKILL)), "KILL" => Ok(TrapTarget::Signal(Signal::SIGKILL)),
"USR1" => Ok(TrapTarget::Signal(Signal::SIGUSR1)), "USR1" => Ok(TrapTarget::Signal(Signal::SIGUSR1)),
"SEGV" => Ok(TrapTarget::Signal(Signal::SIGSEGV)), "SEGV" => Ok(TrapTarget::Signal(Signal::SIGSEGV)),
"USR2" => Ok(TrapTarget::Signal(Signal::SIGUSR2)), "USR2" => Ok(TrapTarget::Signal(Signal::SIGUSR2)),
"PIPE" => Ok(TrapTarget::Signal(Signal::SIGPIPE)), "PIPE" => Ok(TrapTarget::Signal(Signal::SIGPIPE)),
"ALRM" => Ok(TrapTarget::Signal(Signal::SIGALRM)), "ALRM" => Ok(TrapTarget::Signal(Signal::SIGALRM)),
"TERM" => Ok(TrapTarget::Signal(Signal::SIGTERM)), "TERM" => Ok(TrapTarget::Signal(Signal::SIGTERM)),
"STKFLT" => Ok(TrapTarget::Signal(Signal::SIGSTKFLT)), "STKFLT" => Ok(TrapTarget::Signal(Signal::SIGSTKFLT)),
"CHLD" => Ok(TrapTarget::Signal(Signal::SIGCHLD)), "CHLD" => Ok(TrapTarget::Signal(Signal::SIGCHLD)),
"CONT" => Ok(TrapTarget::Signal(Signal::SIGCONT)), "CONT" => Ok(TrapTarget::Signal(Signal::SIGCONT)),
"STOP" => Ok(TrapTarget::Signal(Signal::SIGSTOP)), "STOP" => Ok(TrapTarget::Signal(Signal::SIGSTOP)),
"TSTP" => Ok(TrapTarget::Signal(Signal::SIGTSTP)), "TSTP" => Ok(TrapTarget::Signal(Signal::SIGTSTP)),
"TTIN" => Ok(TrapTarget::Signal(Signal::SIGTTIN)), "TTIN" => Ok(TrapTarget::Signal(Signal::SIGTTIN)),
"TTOU" => Ok(TrapTarget::Signal(Signal::SIGTTOU)), "TTOU" => Ok(TrapTarget::Signal(Signal::SIGTTOU)),
"URG" => Ok(TrapTarget::Signal(Signal::SIGURG)), "URG" => Ok(TrapTarget::Signal(Signal::SIGURG)),
"XCPU" => Ok(TrapTarget::Signal(Signal::SIGXCPU)), "XCPU" => Ok(TrapTarget::Signal(Signal::SIGXCPU)),
"XFSZ" => Ok(TrapTarget::Signal(Signal::SIGXFSZ)), "XFSZ" => Ok(TrapTarget::Signal(Signal::SIGXFSZ)),
"VTALRM" => Ok(TrapTarget::Signal(Signal::SIGVTALRM)), "VTALRM" => Ok(TrapTarget::Signal(Signal::SIGVTALRM)),
"PROF" => Ok(TrapTarget::Signal(Signal::SIGPROF)), "PROF" => Ok(TrapTarget::Signal(Signal::SIGPROF)),
"WINCH" => Ok(TrapTarget::Signal(Signal::SIGWINCH)), "WINCH" => Ok(TrapTarget::Signal(Signal::SIGWINCH)),
"IO" => Ok(TrapTarget::Signal(Signal::SIGIO)), "IO" => Ok(TrapTarget::Signal(Signal::SIGIO)),
"PWR" => Ok(TrapTarget::Signal(Signal::SIGPWR)), "PWR" => Ok(TrapTarget::Signal(Signal::SIGPWR)),
"SYS" => Ok(TrapTarget::Signal(Signal::SIGSYS)), "SYS" => Ok(TrapTarget::Signal(Signal::SIGSYS)),
_ => { _ => {
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::ExecFail, ShErrKind::ExecFail,
format!("invalid trap target '{}'", s), format!("invalid trap target '{}'", s),
)) ));
} }
} }
} }
} }
impl Display for TrapTarget { impl Display for TrapTarget {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
TrapTarget::Exit => write!(f, "EXIT"), TrapTarget::Exit => write!(f, "EXIT"),
TrapTarget::Error => write!(f, "ERR"), TrapTarget::Error => write!(f, "ERR"),
TrapTarget::Signal(s) => { TrapTarget::Signal(s) => match s {
match s { Signal::SIGHUP => write!(f, "HUP"),
Signal::SIGHUP => write!(f, "HUP"), Signal::SIGINT => write!(f, "INT"),
Signal::SIGINT => write!(f, "INT"), Signal::SIGQUIT => write!(f, "QUIT"),
Signal::SIGQUIT => write!(f, "QUIT"), Signal::SIGILL => write!(f, "ILL"),
Signal::SIGILL => write!(f, "ILL"), Signal::SIGTRAP => write!(f, "TRAP"),
Signal::SIGTRAP => write!(f, "TRAP"), Signal::SIGABRT => write!(f, "ABRT"),
Signal::SIGABRT => write!(f, "ABRT"), Signal::SIGBUS => write!(f, "BUS"),
Signal::SIGBUS => write!(f, "BUS"), Signal::SIGFPE => write!(f, "FPE"),
Signal::SIGFPE => write!(f, "FPE"), Signal::SIGKILL => write!(f, "KILL"),
Signal::SIGKILL => write!(f, "KILL"), Signal::SIGUSR1 => write!(f, "USR1"),
Signal::SIGUSR1 => write!(f, "USR1"), Signal::SIGSEGV => write!(f, "SEGV"),
Signal::SIGSEGV => write!(f, "SEGV"), Signal::SIGUSR2 => write!(f, "USR2"),
Signal::SIGUSR2 => write!(f, "USR2"), Signal::SIGPIPE => write!(f, "PIPE"),
Signal::SIGPIPE => write!(f, "PIPE"), Signal::SIGALRM => write!(f, "ALRM"),
Signal::SIGALRM => write!(f, "ALRM"), Signal::SIGTERM => write!(f, "TERM"),
Signal::SIGTERM => write!(f, "TERM"), Signal::SIGSTKFLT => write!(f, "STKFLT"),
Signal::SIGSTKFLT => write!(f, "STKFLT"), Signal::SIGCHLD => write!(f, "CHLD"),
Signal::SIGCHLD => write!(f, "CHLD"), Signal::SIGCONT => write!(f, "CONT"),
Signal::SIGCONT => write!(f, "CONT"), Signal::SIGSTOP => write!(f, "STOP"),
Signal::SIGSTOP => write!(f, "STOP"), Signal::SIGTSTP => write!(f, "TSTP"),
Signal::SIGTSTP => write!(f, "TSTP"), Signal::SIGTTIN => write!(f, "TTIN"),
Signal::SIGTTIN => write!(f, "TTIN"), Signal::SIGTTOU => write!(f, "TTOU"),
Signal::SIGTTOU => write!(f, "TTOU"), Signal::SIGURG => write!(f, "URG"),
Signal::SIGURG => write!(f, "URG"), Signal::SIGXCPU => write!(f, "XCPU"),
Signal::SIGXCPU => write!(f, "XCPU"), Signal::SIGXFSZ => write!(f, "XFSZ"),
Signal::SIGXFSZ => write!(f, "XFSZ"), Signal::SIGVTALRM => write!(f, "VTALRM"),
Signal::SIGVTALRM => write!(f, "VTALRM"), Signal::SIGPROF => write!(f, "PROF"),
Signal::SIGPROF => write!(f, "PROF"), Signal::SIGWINCH => write!(f, "WINCH"),
Signal::SIGWINCH => write!(f, "WINCH"), Signal::SIGIO => write!(f, "IO"),
Signal::SIGIO => write!(f, "IO"), Signal::SIGPWR => write!(f, "PWR"),
Signal::SIGPWR => write!(f, "PWR"), Signal::SIGSYS => write!(f, "SYS"),
Signal::SIGSYS => write!(f, "SYS"),
_ => { _ => {
log::warn!("TrapTarget::fmt() : unrecognized signal {}", s); log::warn!("TrapTarget::fmt() : unrecognized signal {}", s);
Err(std::fmt::Error) Err(std::fmt::Error)
} }
} },
} }
} }
}
} }
pub fn trap(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn trap(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let span = node.get_span(); let span = node.get_span();
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
argv, argv,
} = node.class } = node.class
else { else {
unreachable!() 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)))?;
if argv.is_empty() { if argv.is_empty() {
let stdout = borrow_fd(STDOUT_FILENO); let stdout = borrow_fd(STDOUT_FILENO);
return read_logic(|l| -> ShResult<()> { return read_logic(|l| -> ShResult<()> {
for l in l.traps() { for l in l.traps() {
let target = l.0; let target = l.0;
let command = l.1; let command = l.1;
write(stdout, format!("trap -- '{command}' {target}\n").as_bytes())?; write(stdout, format!("trap -- '{command}' {target}\n").as_bytes())?;
} }
Ok(()) Ok(())
}); });
} }
if argv.len() == 1 { if argv.len() == 1 {
let stderr = borrow_fd(STDERR_FILENO); let stderr = borrow_fd(STDERR_FILENO);
write(stderr, b"usage: trap <COMMAND> [SIGNAL...]\n")?; write(stderr, b"usage: trap <COMMAND> [SIGNAL...]\n")?;
state::set_status(1); state::set_status(1);
return Ok(()) return Ok(());
} }
let mut args = argv.into_iter(); let mut args = argv.into_iter();
let command = args.next().unwrap().0; let command = args.next().unwrap().0;
let mut targets = vec![]; let mut targets = vec![];
while let Some((arg, _)) = args.next() { while let Some((arg, _)) = args.next() {
let target = arg.parse::<TrapTarget>()?; let target = arg.parse::<TrapTarget>()?;
targets.push(target); targets.push(target);
} }
for target in targets { for target in targets {
if &command == "-" { if &command == "-" {
write_logic(|l| l.remove_trap(target)) write_logic(|l| l.remove_trap(target))
} else { } else {
write_logic(|l| l.insert_trap(target, command.clone())) write_logic(|l| l.insert_trap(target, command.clone()))
} }
} }
state::set_status(0); state::set_status(0);
Ok(()) Ok(())
} }

View File

@@ -37,14 +37,32 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu
else { else {
unreachable!() unreachable!()
}; };
let zolt_opts = [ let zolt_opts = [
OptSpec { opt: Opt::Long("dry-run".into()), takes_arg: false }, OptSpec {
OptSpec { opt: Opt::Long("confirm".into()), takes_arg: false }, opt: Opt::Long("dry-run".into()),
OptSpec { opt: Opt::Long("no-preserve-root".into()), takes_arg: false }, takes_arg: false,
OptSpec { opt: Opt::Short('r'), takes_arg: false }, },
OptSpec { opt: Opt::Short('f'), takes_arg: false }, OptSpec {
OptSpec { opt: Opt::Short('v'), takes_arg: false } opt: Opt::Long("confirm".into()),
]; takes_arg: false,
},
OptSpec {
opt: Opt::Long("no-preserve-root".into()),
takes_arg: false,
},
OptSpec {
opt: Opt::Short('r'),
takes_arg: false,
},
OptSpec {
opt: Opt::Short('f'),
takes_arg: false,
},
OptSpec {
opt: Opt::Short('v'),
takes_arg: false,
},
];
let mut flags = ZoltFlags::empty(); let mut flags = ZoltFlags::empty();
let (argv, opts) = get_opts_from_tokens(argv, &zolt_opts); let (argv, opts) = get_opts_from_tokens(argv, &zolt_opts);
@@ -56,41 +74,40 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu
"confirm" => flags |= ZoltFlags::CONFIRM, "confirm" => flags |= ZoltFlags::CONFIRM,
"dry-run" => flags |= ZoltFlags::DRY, "dry-run" => flags |= ZoltFlags::DRY,
_ => { _ => {
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
format!("zoltraak: unrecognized option '{flag}'"), format!("zoltraak: unrecognized option '{flag}'"),
)); ));
} }
}, },
Opt::Short(flag) => match flag { Opt::Short(flag) => match flag {
'r' => flags |= ZoltFlags::RECURSIVE, 'r' => flags |= ZoltFlags::RECURSIVE,
'f' => flags |= ZoltFlags::FORCE, 'f' => flags |= ZoltFlags::FORCE,
'v' => flags |= ZoltFlags::VERBOSE, 'v' => flags |= ZoltFlags::VERBOSE,
_ => { _ => {
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
format!("zoltraak: unrecognized option '{flag}'"), format!("zoltraak: unrecognized option '{flag}'"),
)); ));
} }
}, },
Opt::LongWithArg(flag, _) => { Opt::LongWithArg(flag, _) => {
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
format!("zoltraak: unrecognized option '{flag}'"), format!("zoltraak: unrecognized option '{flag}'"),
)); ));
} }
Opt::ShortWithArg(flag, _) => { Opt::ShortWithArg(flag, _) => {
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
format!("zoltraak: unrecognized option '{flag}'"), format!("zoltraak: unrecognized option '{flag}'"),
)); ));
} }
} }
} }
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
for (arg, span) in argv { for (arg, span) in argv {
if &arg == "/" && !flags.contains(ZoltFlags::NO_PRESERVE_ROOT) { if &arg == "/" && !flags.contains(ZoltFlags::NO_PRESERVE_ROOT) {
return Err( return Err(
@@ -109,7 +126,6 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu
} }
} }
Ok(()) Ok(())
} }

File diff suppressed because it is too large Load Diff

View File

@@ -9,14 +9,14 @@ pub type OptSet = Arc<[Opt]>;
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
pub enum Opt { pub enum Opt {
Long(String), Long(String),
LongWithArg(String,String), LongWithArg(String, String),
Short(char), Short(char),
ShortWithArg(char,String), ShortWithArg(char, String),
} }
pub struct OptSpec { pub struct OptSpec {
pub opt: Opt, pub opt: Opt,
pub takes_arg: bool, pub takes_arg: bool,
} }
impl Opt { impl Opt {
@@ -41,8 +41,8 @@ impl Display for Opt {
match self { match self {
Self::Long(opt) => write!(f, "--{}", opt), Self::Long(opt) => write!(f, "--{}", opt),
Self::Short(opt) => write!(f, "-{}", opt), Self::Short(opt) => write!(f, "-{}", opt),
Self::LongWithArg(opt, arg) => write!(f, "--{} {}", opt, arg), Self::LongWithArg(opt, arg) => write!(f, "--{} {}", opt, arg),
Self::ShortWithArg(opt, arg) => write!(f, "-{} {}", opt, arg) Self::ShortWithArg(opt, arg) => write!(f, "-{} {}", opt, arg),
} }
} }
} }
@@ -82,32 +82,33 @@ pub fn get_opts_from_tokens(tokens: Vec<Tk>, opt_specs: &[OptSpec]) -> (Vec<Tk>,
if parsed_opts.is_empty() { if parsed_opts.is_empty() {
non_opts.push(token) non_opts.push(token)
} else { } else {
for opt in parsed_opts { for opt in parsed_opts {
let mut pushed = false; let mut pushed = false;
for opt_spec in opt_specs { for opt_spec in opt_specs {
if opt_spec.opt == opt { if opt_spec.opt == opt {
if opt_spec.takes_arg { if opt_spec.takes_arg {
let arg = tokens_iter.next() let arg = tokens_iter
.map(|t| t.to_string()) .next()
.unwrap_or_default(); .map(|t| t.to_string())
.unwrap_or_default();
let opt = match opt { let opt = match opt {
Opt::Long(ref opt) => Opt::LongWithArg(opt.to_string(), arg), Opt::Long(ref opt) => Opt::LongWithArg(opt.to_string(), arg),
Opt::Short(opt) => Opt::ShortWithArg(opt, arg), Opt::Short(opt) => Opt::ShortWithArg(opt, arg),
_ => unreachable!(), _ => unreachable!(),
}; };
opts.push(opt); opts.push(opt);
pushed = true; pushed = true;
} else { } else {
opts.push(opt.clone()); opts.push(opt.clone());
pushed = true; pushed = true;
} }
} }
} }
if !pushed { if !pushed {
non_opts.push(token.clone()); non_opts.push(token.clone());
} }
} }
} }
} }
(non_opts, opts) (non_opts, opts)

View File

@@ -2,7 +2,11 @@ use crate::{
libsh::{ libsh::{
error::ShResult, error::ShResult,
term::{Style, Styled}, term::{Style, Styled},
}, prelude::*, procio::{IoMode, borrow_fd}, signal::{disable_reaping, enable_reaping}, state::{self, set_status, read_jobs, write_jobs} },
prelude::*,
procio::{IoMode, borrow_fd},
signal::{disable_reaping, enable_reaping},
state::{self, read_jobs, set_status, write_jobs},
}; };
pub const SIG_EXIT_OFFSET: i32 = 128; pub const SIG_EXIT_OFFSET: i32 = 128;
@@ -685,7 +689,9 @@ pub fn wait_fg(job: Job) -> ShResult<()> {
} }
// If job wasn't stopped (moved to bg), clear the fg slot // If job wasn't stopped (moved to bg), clear the fg slot
if !was_stopped { if !was_stopped {
write_jobs(|j| { j.take_fg(); }); write_jobs(|j| {
j.take_fg();
});
} }
take_term()?; take_term()?;
set_status(code); set_status(code);

View File

@@ -408,12 +408,12 @@ pub enum ShErrKind {
ReadlineIntr(String), ReadlineIntr(String),
ReadlineErr, ReadlineErr,
// Not really errors, more like internal signals // Not really errors, more like internal signals
CleanExit(i32), CleanExit(i32),
FuncReturn(i32), FuncReturn(i32),
LoopContinue(i32), LoopContinue(i32),
LoopBreak(i32), LoopBreak(i32),
ClearReadline, ClearReadline,
Null, Null,
} }
@@ -437,7 +437,7 @@ impl Display for ShErrKind {
Self::LoopBreak(_) => "", Self::LoopBreak(_) => "",
Self::ReadlineIntr(_) => "", Self::ReadlineIntr(_) => "",
Self::ReadlineErr => "Readline Error", Self::ReadlineErr => "Readline Error",
Self::ClearReadline => "", Self::ClearReadline => "",
Self::Null => "", Self::Null => "",
}; };
write!(f, "{output}") write!(f, "{output}")

View File

@@ -1,6 +1,6 @@
use termios::{LocalFlags, Termios}; use termios::{LocalFlags, Termios};
use crate::{prelude::*}; use crate::prelude::*;
/// ///
/// The previous state of the terminal options. /// The previous state of the terminal options.
/// ///
@@ -33,44 +33,43 @@ pub(crate) static mut SAVED_TERMIOS: Option<Option<Termios>> = None;
#[derive(Debug)] #[derive(Debug)]
pub struct TermiosGuard { pub struct TermiosGuard {
saved_termios: Option<Termios> saved_termios: Option<Termios>,
} }
impl TermiosGuard { impl TermiosGuard {
pub fn new(new_termios: Termios) -> Self { pub fn new(new_termios: Termios) -> Self {
let mut new = Self { saved_termios: None }; let mut new = Self {
saved_termios: None,
};
if isatty(std::io::stdin().as_raw_fd()).unwrap() { if isatty(std::io::stdin().as_raw_fd()).unwrap() {
let current_termios = termios::tcgetattr(std::io::stdin()).unwrap(); let current_termios = termios::tcgetattr(std::io::stdin()).unwrap();
new.saved_termios = Some(current_termios); new.saved_termios = Some(current_termios);
termios::tcsetattr( termios::tcsetattr(
std::io::stdin(), std::io::stdin(),
nix::sys::termios::SetArg::TCSANOW, nix::sys::termios::SetArg::TCSANOW,
&new_termios, &new_termios,
).unwrap(); )
} .unwrap();
}
new new
} }
} }
impl Default for TermiosGuard { impl Default for TermiosGuard {
fn default() -> Self { fn default() -> Self {
let mut termios = termios::tcgetattr(std::io::stdin()).unwrap(); let mut termios = termios::tcgetattr(std::io::stdin()).unwrap();
termios.local_flags &= !LocalFlags::ECHOCTL; termios.local_flags &= !LocalFlags::ECHOCTL;
Self::new(termios) Self::new(termios)
} }
} }
impl Drop for TermiosGuard { impl Drop for TermiosGuard {
fn drop(&mut self) { fn drop(&mut self) {
if let Some(saved) = &self.saved_termios { if let Some(saved) = &self.saved_termios {
termios::tcsetattr( termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, saved).unwrap();
std::io::stdin(), }
nix::sys::termios::SetArg::TCSANOW, }
saved,
).unwrap();
}
}
} }

View File

@@ -83,8 +83,7 @@ impl TkVecUtils<Tk> for Vec<Tk> {
} }
} }
fn debug_tokens(&self) { fn debug_tokens(&self) {
for token in self { for token in self {}
}
} }
} }

View File

@@ -1,7 +1,7 @@
#![allow( #![allow(
clippy::derivable_impls, clippy::derivable_impls,
clippy::tabs_in_doc_comments, clippy::tabs_in_doc_comments,
clippy::while_let_on_iterator clippy::while_let_on_iterator
)] )]
pub mod builtin; pub mod builtin;
pub mod expand; pub mod expand;
@@ -22,10 +22,10 @@ use std::os::fd::BorrowedFd;
use std::process::ExitCode; use std::process::ExitCode;
use std::sync::atomic::Ordering; use std::sync::atomic::Ordering;
use nix::errno::Errno;
use nix::libc::STDIN_FILENO; use nix::libc::STDIN_FILENO;
use nix::poll::{PollFd, PollFlags, PollTimeout, poll}; use nix::poll::{PollFd, PollFlags, PollTimeout, poll};
use nix::unistd::read; use nix::unistd::read;
use nix::errno::Errno;
use crate::builtin::trap::TrapTarget; use crate::builtin::trap::TrapTarget;
use crate::libsh::error::{ShErr, ShErrKind, ShResult}; use crate::libsh::error::{ShErr, ShErrKind, ShResult};
@@ -41,16 +41,16 @@ use state::{read_vars, write_vars};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
struct FernArgs { struct FernArgs {
script: Option<String>, script: Option<String>,
#[arg(short)] #[arg(short)]
command: Option<String>, command: Option<String>,
#[arg(trailing_var_arg = true)] #[arg(trailing_var_arg = true)]
script_args: Vec<String>, script_args: Vec<String>,
#[arg(long)] #[arg(long)]
version: bool, version: bool,
} }
/// Force evaluation of lazily-initialized values early in shell startup. /// Force evaluation of lazily-initialized values early in shell startup.
@@ -64,178 +64,192 @@ struct FernArgs {
/// closure, which forces access to the variable table and causes its `LazyLock` /// closure, which forces access to the variable table and causes its `LazyLock`
/// constructor to run. /// constructor to run.
fn kickstart_lazy_evals() { fn kickstart_lazy_evals() {
read_vars(|_| {}); read_vars(|_| {});
} }
fn main() -> ExitCode { fn main() -> ExitCode {
env_logger::init(); env_logger::init();
kickstart_lazy_evals(); kickstart_lazy_evals();
let args = FernArgs::parse(); let args = FernArgs::parse();
if args.version { if args.version {
println!("fern {}", env!("CARGO_PKG_VERSION")); println!("fern {}", env!("CARGO_PKG_VERSION"));
return ExitCode::SUCCESS; return ExitCode::SUCCESS;
} }
if let Err(e) = if let Some(path) = args.script { if let Err(e) = if let Some(path) = args.script {
run_script(path, args.script_args) run_script(path, args.script_args)
} else if let Some(cmd) = args.command { } else if let Some(cmd) = args.command {
exec_input(cmd, None, false) exec_input(cmd, None, false)
} else { } else {
fern_interactive() fern_interactive()
} { } {
eprintln!("fern: {e}"); eprintln!("fern: {e}");
}; };
if let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Exit)) if let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Exit))
&& let Err(e) = exec_input(trap, None, false) { && let Err(e) = exec_input(trap, None, false)
eprintln!("fern: error running EXIT trap: {e}"); {
} eprintln!("fern: error running EXIT trap: {e}");
}
ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8) ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8)
} }
fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) -> ShResult<()> { fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) -> ShResult<()> {
let path = path.as_ref(); let path = path.as_ref();
if !path.is_file() { if !path.is_file() {
eprintln!("fern: Failed to open input file: {}", path.display()); eprintln!("fern: Failed to open input file: {}", path.display());
QUIT_CODE.store(1, Ordering::SeqCst); QUIT_CODE.store(1, Ordering::SeqCst);
return Err(ShErr::simple(ShErrKind::CleanExit(1), "input file not found")); return Err(ShErr::simple(
} ShErrKind::CleanExit(1),
let Ok(input) = fs::read_to_string(path) else { "input file not found",
eprintln!("fern: 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 Ok(input) = fs::read_to_string(path) else {
}; eprintln!("fern: 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())); write_vars(|v| {
for arg in args { v.cur_scope_mut()
write_vars(|v| v.cur_scope_mut().bpush_arg(arg)) .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 fern_interactive() -> ShResult<()> { fn fern_interactive() -> ShResult<()> {
let _raw_mode = raw_mode(); // sets raw mode, restores termios on drop let _raw_mode = raw_mode(); // sets raw mode, restores termios on drop
sig_setup(); sig_setup();
if let Err(e) = source_rc() { if let Err(e) = source_rc() {
eprintln!("{e}"); eprintln!("{e}");
} }
// Create readline instance with initial prompt // Create readline instance with initial prompt
let mut readline = match FernVi::new(get_prompt().ok()) { let mut readline = match FernVi::new(get_prompt().ok()) {
Ok(rl) => rl, Ok(rl) => rl,
Err(e) => { Err(e) => {
eprintln!("Failed to initialize readline: {e}"); eprintln!("Failed to initialize readline: {e}");
QUIT_CODE.store(1, Ordering::SeqCst); QUIT_CODE.store(1, Ordering::SeqCst);
return Err(ShErr::simple(ShErrKind::CleanExit(1), "readline initialization failed")); return Err(ShErr::simple(
} ShErrKind::CleanExit(1),
}; "readline initialization failed",
));
}
};
// Main poll loop // Main poll loop
loop { loop {
// Handle any pending signals // Handle any pending signals
while signals_pending() { while signals_pending() {
if let Err(e) = check_signals() { if let Err(e) = check_signals() {
match e.kind() { match e.kind() {
ShErrKind::ClearReadline => { ShErrKind::ClearReadline => {
// Ctrl+C - clear current input and show new prompt // Ctrl+C - clear current input and show new prompt
readline.reset(get_prompt().ok()); readline.reset(get_prompt().ok());
} }
ShErrKind::CleanExit(code) => { ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst); QUIT_CODE.store(*code, Ordering::SeqCst);
return Ok(()); return Ok(());
} }
_ => eprintln!("{e}"), _ => eprintln!("{e}"),
} }
} }
} }
readline.print_line()?; readline.print_line()?;
// Poll for stdin input // Poll for stdin input
let mut fds = [PollFd::new( let mut fds = [PollFd::new(
unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) }, unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) },
PollFlags::POLLIN, PollFlags::POLLIN,
)]; )];
match poll(&mut fds, PollTimeout::MAX) { match poll(&mut fds, PollTimeout::MAX) {
Ok(_) => {} Ok(_) => {}
Err(Errno::EINTR) => { Err(Errno::EINTR) => {
// Interrupted by signal, loop back to handle it // Interrupted by signal, loop back to handle it
continue; continue;
} }
Err(e) => { Err(e) => {
eprintln!("poll error: {e}"); eprintln!("poll error: {e}");
break; break;
} }
} }
// Check if stdin has data // Check if stdin has data
if fds[0].revents().is_some_and(|r| r.contains(PollFlags::POLLIN)) { if fds[0]
let mut buffer = [0u8; 1024]; .revents()
match read(STDIN_FILENO, &mut buffer) { .is_some_and(|r| r.contains(PollFlags::POLLIN))
Ok(0) => { {
// EOF let mut buffer = [0u8; 1024];
break; match read(STDIN_FILENO, &mut buffer) {
} Ok(0) => {
Ok(n) => { // EOF
readline.feed_bytes(&buffer[..n]); break;
} }
Err(Errno::EINTR) => { Ok(n) => {
// Interrupted, continue to handle signals readline.feed_bytes(&buffer[..n]);
continue; }
} Err(Errno::EINTR) => {
Err(e) => { // Interrupted, continue to handle signals
eprintln!("read error: {e}"); continue;
break; }
} Err(e) => {
} eprintln!("read error: {e}");
} break;
}
}
}
// Process any available input // Process any available input
match readline.process_input() { match readline.process_input() {
Ok(ReadlineEvent::Line(input)) => { Ok(ReadlineEvent::Line(input)) => {
let start = Instant::now(); let start = Instant::now();
write_meta(|m| m.start_timer()); write_meta(|m| m.start_timer());
if let Err(e) = exec_input(input, None, true) { if let Err(e) = exec_input(input, None, true) {
match e.kind() { match e.kind() {
ShErrKind::CleanExit(code) => { ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst); QUIT_CODE.store(*code, Ordering::SeqCst);
return Ok(()); return Ok(());
} }
_ => eprintln!("{e}"), _ => eprintln!("{e}"),
} }
} }
let command_run_time = start.elapsed(); let command_run_time = start.elapsed();
log::info!("Command executed in {:.2?}", command_run_time); log::info!("Command executed in {:.2?}", command_run_time);
write_meta(|m| m.stop_timer()); write_meta(|m| m.stop_timer());
// Reset for next command with fresh prompt // Reset for next command with fresh prompt
readline.reset(get_prompt().ok()); readline.reset(get_prompt().ok());
let real_end = start.elapsed(); let real_end = start.elapsed();
log::info!("Total round trip time: {:.2?}", real_end); log::info!("Total round trip time: {:.2?}", real_end);
} }
Ok(ReadlineEvent::Eof) => { Ok(ReadlineEvent::Eof) => {
// Ctrl+D on empty line // Ctrl+D on empty line
QUIT_CODE.store(0, Ordering::SeqCst); QUIT_CODE.store(0, Ordering::SeqCst);
return Ok(()); return Ok(());
} }
Ok(ReadlineEvent::Pending) => { Ok(ReadlineEvent::Pending) => {
// No complete input yet, keep polling // No complete input yet, keep polling
} }
Err(e) => { Err(e) => match e.kind() {
match e.kind() { ShErrKind::CleanExit(code) => {
ShErrKind::CleanExit(code) => { QUIT_CODE.store(*code, Ordering::SeqCst);
QUIT_CODE.store(*code, Ordering::SeqCst); return Ok(());
return Ok(()); }
} _ => eprintln!("{e}"),
_ => eprintln!("{e}"), },
} }
} }
}
}
Ok(()) Ok(())
} }

File diff suppressed because it is too large Load Diff

View File

@@ -47,11 +47,11 @@ impl Span {
pub fn range(&self) -> Range<usize> { pub fn range(&self) -> Range<usize> {
self.range.clone() self.range.clone()
} }
/// With great power comes great responsibility /// With great power comes great responsibility
/// Only use this in the most dire of circumstances /// Only use this in the most dire of circumstances
pub fn set_range(&mut self, range: Range<usize>) { pub fn set_range(&mut self, range: Range<usize>) {
self.range = range; self.range = range;
} }
} }
/// Allows simple access to the underlying range wrapped by the span /// Allows simple access to the underlying range wrapped by the span
@@ -324,13 +324,14 @@ impl LexStream {
let can_be_subshell = chars.peek() == Some(&'('); let can_be_subshell = chars.peek() == Some(&'(');
if self.flags.contains(LexFlags::IN_CASE) if self.flags.contains(LexFlags::IN_CASE)
&& let Some(count) = case_pat_lookahead(chars.clone()) { && let Some(count) = case_pat_lookahead(chars.clone())
pos += count; {
let casepat_tk = self.get_token(self.cursor..pos, TkRule::CasePattern); pos += count;
self.cursor = pos; let casepat_tk = self.get_token(self.cursor..pos, TkRule::CasePattern);
self.set_next_is_cmd(true); self.cursor = pos;
return Ok(casepat_tk); self.set_next_is_cmd(true);
} return Ok(casepat_tk);
}
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
match ch { match ch {
@@ -740,7 +741,10 @@ impl Iterator for LexStream {
} }
self.get_token(ch_idx..self.cursor, TkRule::Sep) self.get_token(ch_idx..self.cursor, TkRule::Sep)
} }
'#' if !self.flags.contains(LexFlags::INTERACTIVE) || crate::state::read_shopts(|s| s.core.interactive_comments) => { '#'
if !self.flags.contains(LexFlags::INTERACTIVE)
|| crate::state::read_shopts(|s| s.core.interactive_comments) =>
{
let ch_idx = self.cursor; let ch_idx = self.cursor;
self.cursor += 1; self.cursor += 1;

View File

@@ -1160,7 +1160,7 @@ impl ParseStream {
let cond_node: CondNode; let cond_node: CondNode;
let mut node_tks = vec![]; let mut node_tks = vec![];
let mut redirs = vec![]; let mut redirs = vec![];
if (!self.check_keyword("while") && !self.check_keyword("until")) || !self.next_tk_is_some() { if (!self.check_keyword("while") && !self.check_keyword("until")) || !self.next_tk_is_some() {
return Ok(None); return Ok(None);
@@ -1238,18 +1238,18 @@ impl ParseStream {
fn parse_pipeln(&mut self) -> ShResult<Option<Node>> { fn parse_pipeln(&mut self) -> ShResult<Option<Node>> {
let mut cmds = vec![]; let mut cmds = vec![];
let mut node_tks = vec![]; let mut node_tks = vec![];
let mut flags = NdFlags::empty(); let mut flags = NdFlags::empty();
while let Some(cmd) = self.parse_block(false)? { while let Some(cmd) = self.parse_block(false)? {
let is_punctuated = node_is_punctuated(&cmd.tokens); let is_punctuated = node_is_punctuated(&cmd.tokens);
node_tks.append(&mut cmd.tokens.clone()); node_tks.append(&mut cmd.tokens.clone());
cmds.push(cmd); cmds.push(cmd);
if *self.next_tk_class() == TkRule::Bg { if *self.next_tk_class() == TkRule::Bg {
let tk = self.next_tk().unwrap(); let tk = self.next_tk().unwrap();
node_tks.push(tk.clone()); node_tks.push(tk.clone());
flags |= NdFlags::BACKGROUND; flags |= NdFlags::BACKGROUND;
break; break;
} else if *self.next_tk_class() != TkRule::Pipe || is_punctuated { } else if *self.next_tk_class() != TkRule::Pipe || is_punctuated {
break; break;
} else if let Some(pipe) = self.next_tk() { } else if let Some(pipe) = self.next_tk() {
node_tks.push(pipe) node_tks.push(pipe)
@@ -1278,7 +1278,7 @@ impl ParseStream {
let mut node_tks = vec![]; let mut node_tks = vec![];
let mut redirs = vec![]; let mut redirs = vec![];
let mut argv = vec![]; let mut argv = vec![];
let mut flags = NdFlags::empty(); let mut flags = NdFlags::empty();
let mut assignments = vec![]; let mut assignments = vec![];
while let Some(prefix_tk) = tk_iter.next() { while let Some(prefix_tk) = tk_iter.next() {
@@ -1315,27 +1315,32 @@ impl ParseStream {
} }
if argv.is_empty() { if argv.is_empty() {
if assignments.is_empty() { if assignments.is_empty() {
return Ok(None); return Ok(None);
} else { } else {
// If we have assignments but no command word, // If we have assignments but no command word,
// return the assignment-only command without parsing more tokens // return the assignment-only command without parsing more tokens
self.commit(node_tks.len()); self.commit(node_tks.len());
return Ok(Some(Node { return Ok(Some(Node {
class: NdRule::Command { assignments, argv }, class: NdRule::Command { assignments, argv },
tokens: node_tks, tokens: node_tks,
flags, flags,
redirs, redirs,
})); }));
} }
} }
while let Some(tk) = tk_iter.next() { while let Some(tk) = tk_iter.next() {
if *self.next_tk_class() == TkRule::Bg { if *self.next_tk_class() == TkRule::Bg {
break; break;
} }
match tk.class { match tk.class {
TkRule::EOI | TkRule::Pipe | TkRule::And | TkRule::BraceGrpEnd | TkRule::Or | TkRule::Bg => break, TkRule::EOI
| TkRule::Pipe
| TkRule::And
| TkRule::BraceGrpEnd
| TkRule::Or
| TkRule::Bg => break,
TkRule::Sep => { TkRule::Sep => {
node_tks.push(tk.clone()); node_tks.push(tk.clone());
break; break;

View File

@@ -19,17 +19,17 @@ pub use std::os::unix::io::{AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd,
pub use bitflags::bitflags; pub use bitflags::bitflags;
pub use nix::{ pub use nix::{
errno::Errno, errno::Errno,
fcntl::{open, OFlag}, fcntl::{OFlag, open},
libc::{self, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO}, libc::{self, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO},
sys::{ 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, stat::Mode,
termios::{self}, termios::{self},
wait::{waitpid, WaitPidFlag as WtFlag, WaitStatus as WtStat}, wait::{WaitPidFlag as WtFlag, WaitStatus as WtStat, waitpid},
}, },
unistd::{ unistd::{
close, dup, dup2, execvpe, fork, getpgid, getpgrp, isatty, pipe, read, setpgid, tcgetpgrp, ForkResult, Pid, close, dup, dup2, execvpe, fork, getpgid, getpgrp, isatty, pipe, read,
tcsetpgrp, write, ForkResult, Pid, setpgid, tcgetpgrp, tcsetpgrp, write,
}, },
}; };

View File

@@ -4,10 +4,13 @@ use std::{
}; };
use crate::{ use crate::{
expand::Expander, libsh::{ expand::Expander,
libsh::{
error::{ShErr, ShErrKind, ShResult}, error::{ShErr, ShErrKind, ShResult},
utils::RedirVecUtils, utils::RedirVecUtils,
}, parse::{Redir, RedirType, get_redir_file}, prelude::* },
parse::{Redir, RedirType, get_redir_file},
prelude::*,
}; };
// Credit to fish-shell for many of the implementation ideas present in this // Credit to fish-shell for many of the implementation ideas present in this
@@ -17,11 +20,11 @@ use crate::{
pub enum IoMode { pub enum IoMode {
Fd { Fd {
tgt_fd: RawFd, tgt_fd: RawFd,
src_fd: RawFd, // Just the fd number - dup2 will handle it at execution time src_fd: RawFd, // Just the fd number - dup2 will handle it at execution time
}, },
OpenedFile { OpenedFile {
tgt_fd: RawFd, tgt_fd: RawFd,
file: Arc<OwnedFd>, // Owns the opened file descriptor file: Arc<OwnedFd>, // Owns the opened file descriptor
}, },
File { File {
tgt_fd: RawFd, tgt_fd: RawFd,
@@ -70,17 +73,12 @@ impl IoMode {
} }
pub fn open_file(mut self) -> ShResult<Self> { pub fn open_file(mut self) -> ShResult<Self> {
if let IoMode::File { tgt_fd, path, mode } = self { if let IoMode::File { tgt_fd, path, mode } = self {
let path_raw = path let path_raw = path.as_os_str().to_str().unwrap_or_default().to_string();
.as_os_str()
.to_str()
.unwrap_or_default()
.to_string();
let expanded_path = Expander::from_raw(&path_raw)? 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
.expand()? // multiple
.join(" "); // should just be one string, will have to find some way to handle a return of multiple
let expanded_pathbuf = PathBuf::from(expanded_path); let expanded_pathbuf = PathBuf::from(expanded_path);
let file = get_redir_file(mode, expanded_pathbuf)?; let file = get_redir_file(mode, expanded_pathbuf)?;
self = IoMode::OpenedFile { self = IoMode::OpenedFile {
@@ -155,9 +153,9 @@ impl<R: Read> IoBuf<R> {
pub struct RedirGuard(IoFrame); pub struct RedirGuard(IoFrame);
impl Drop for RedirGuard { impl Drop for RedirGuard {
fn drop(&mut self) { fn drop(&mut self) {
self.0.restore().ok(); self.0.restore().ok();
} }
} }
/// A struct wrapping three fildescs representing `stdin`, `stdout`, and /// A struct wrapping three fildescs representing `stdin`, `stdout`, and

View File

@@ -1,7 +1,6 @@
pub mod readline; pub mod readline;
pub mod statusline; pub mod statusline;
use crate::{expand::expand_prompt, libsh::error::ShResult, prelude::*}; use crate::{expand::expand_prompt, libsh::error::ShResult, prelude::*};
/// Initialize the line editor /// Initialize the line editor
@@ -16,7 +15,7 @@ pub fn get_prompt() -> ShResult<String> {
"\\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 "; "\\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 ";
return expand_prompt(default); return expand_prompt(default);
}; };
let sanitized = format!("\\e[0m{prompt}"); let sanitized = format!("\\e[0m{prompt}");
expand_prompt(&sanitized) expand_prompt(&sanitized)
} }

View File

@@ -1,445 +1,467 @@
use std::{env, os::unix::fs::PermissionsExt, path::PathBuf, sync::Arc}; use std::{env, os::unix::fs::PermissionsExt, path::PathBuf, sync::Arc};
use crate::{builtin::BUILTINS, libsh::error::{ShErr, ShErrKind, ShResult}, parse::lex::{self, LexFlags, Tk, TkFlags}, prompt::readline::{Marker, annotate_input, annotate_input_recursive, get_insertions, markers::{self, is_marker}}, state::{read_logic, read_vars}}; use crate::{
builtin::BUILTINS,
libsh::error::{ShErr, ShErrKind, ShResult},
parse::lex::{self, LexFlags, Tk, TkFlags},
prompt::readline::{
Marker, annotate_input, annotate_input_recursive, get_insertions,
markers::{self, is_marker},
},
state::{read_logic, read_vars},
};
pub enum CompCtx { pub enum CompCtx {
CmdName, CmdName,
FileName FileName,
} }
pub enum CompResult { pub enum CompResult {
NoMatch, NoMatch,
Single { Single { result: String },
result: String Many { candidates: Vec<String> },
},
Many {
candidates: Vec<String>
}
} }
impl CompResult { impl CompResult {
pub fn from_candidates(candidates: Vec<String>) -> Self { pub fn from_candidates(candidates: Vec<String>) -> Self {
if candidates.is_empty() { if candidates.is_empty() {
Self::NoMatch Self::NoMatch
} else if candidates.len() == 1 { } else if candidates.len() == 1 {
Self::Single { result: candidates[0].clone() } Self::Single {
} else { result: candidates[0].clone(),
Self::Many { candidates } }
} } else {
} Self::Many { candidates }
}
}
} }
pub struct Completer { pub struct Completer {
pub candidates: Vec<String>, pub candidates: Vec<String>,
pub selected_idx: usize, pub selected_idx: usize,
pub original_input: String, pub original_input: String,
pub token_span: (usize, usize), pub token_span: (usize, usize),
pub active: bool, pub active: bool,
} }
impl Completer { impl Completer {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
candidates: vec![], candidates: vec![],
selected_idx: 0, selected_idx: 0,
original_input: String::new(), original_input: String::new(),
token_span: (0, 0), token_span: (0, 0),
active: false, active: false,
} }
} }
pub fn slice_line(line: &str, cursor_pos: usize) -> (&str, &str) { pub fn slice_line(line: &str, cursor_pos: usize) -> (&str, &str) {
let (before_cursor, after_cursor) = line.split_at(cursor_pos); let (before_cursor, after_cursor) = line.split_at(cursor_pos);
(before_cursor, after_cursor) (before_cursor, after_cursor)
} }
pub fn get_completion_context(&self, line: &str, cursor_pos: usize) -> (Vec<Marker>, usize) { pub fn get_completion_context(&self, line: &str, cursor_pos: usize) -> (Vec<Marker>, usize) {
let annotated = annotate_input_recursive(line); let annotated = annotate_input_recursive(line);
log::debug!("Annotated input for completion context: {:?}", annotated); let mut ctx = vec![markers::NULL];
let mut ctx = vec![markers::NULL]; let mut last_priority = 0;
let mut last_priority = 0; let mut ctx_start = 0;
let mut ctx_start = 0; let mut pos = 0;
let mut pos = 0;
for ch in annotated.chars() { for ch in annotated.chars() {
match ch { match ch {
_ if is_marker(ch) => { _ if is_marker(ch) => match ch {
match ch { markers::COMMAND | markers::BUILTIN => {
markers::COMMAND | markers::BUILTIN => { if last_priority < 2 {
log::debug!("Found command marker at position {}", pos); if last_priority > 0 {
if last_priority < 2 { ctx.pop();
if last_priority > 0 { }
ctx.pop(); ctx_start = pos;
} last_priority = 2;
ctx_start = pos; ctx.push(markers::COMMAND);
last_priority = 2; }
ctx.push(markers::COMMAND); }
} markers::VAR_SUB => {
} if last_priority < 3 {
markers::VAR_SUB => { if last_priority > 0 {
log::debug!("Found variable substitution marker at position {}", pos); ctx.pop();
if last_priority < 3 { }
if last_priority > 0 { ctx_start = pos;
ctx.pop(); last_priority = 3;
} ctx.push(markers::VAR_SUB);
ctx_start = pos; }
last_priority = 3; }
ctx.push(markers::VAR_SUB); markers::ARG | markers::ASSIGNMENT => {
} if last_priority < 1 {
} ctx_start = pos;
markers::ARG | markers::ASSIGNMENT => { ctx.push(markers::ARG);
log::debug!("Found argument/assignment marker at position {}", pos); }
if last_priority < 1 { }
ctx_start = pos; _ => {}
ctx.push(markers::ARG); },
} _ => {
} last_priority = 0; // reset priority on normal characters
_ => {} pos += 1; // we hit a normal character, advance our position
} if pos >= cursor_pos {
} break;
_ => { }
last_priority = 0; // reset priority on normal characters }
pos += 1; // we hit a normal character, advance our position }
if pos >= cursor_pos { }
break;
}
}
}
}
(ctx, ctx_start) (ctx, ctx_start)
} }
pub fn reset(&mut self) { pub fn reset(&mut self) {
self.candidates.clear(); self.candidates.clear();
self.selected_idx = 0; self.selected_idx = 0;
self.original_input.clear(); self.original_input.clear();
self.token_span = (0, 0); self.token_span = (0, 0);
self.active = false; self.active = false;
} }
pub fn complete(&mut self, line: String, cursor_pos: usize, direction: i32) -> ShResult<Option<String>> { pub fn complete(
if self.active { &mut self,
Ok(Some(self.cycle_completion(direction))) line: String,
} else { cursor_pos: usize,
self.start_completion(line, cursor_pos) direction: i32,
} ) -> ShResult<Option<String>> {
} if self.active {
Ok(Some(self.cycle_completion(direction)))
} else {
self.start_completion(line, cursor_pos)
}
}
pub fn selected_candidate(&self) -> Option<String> { pub fn selected_candidate(&self) -> Option<String> {
self.candidates.get(self.selected_idx).cloned() self.candidates.get(self.selected_idx).cloned()
} }
pub fn cycle_completion(&mut self, direction: i32) -> String { pub fn cycle_completion(&mut self, direction: i32) -> String {
if self.candidates.is_empty() { if self.candidates.is_empty() {
return self.original_input.clone(); return self.original_input.clone();
} }
let len = self.candidates.len(); let len = self.candidates.len();
self.selected_idx = (self.selected_idx as i32 + direction).rem_euclid(len as i32) as usize; self.selected_idx = (self.selected_idx as i32 + direction).rem_euclid(len as i32) as usize;
self.get_completed_line() self.get_completed_line()
} }
pub fn start_completion(&mut self, line: String, cursor_pos: usize) -> ShResult<Option<String>> { pub fn start_completion(&mut self, line: String, cursor_pos: usize) -> ShResult<Option<String>> {
let result = self.get_candidates(line.clone(), cursor_pos)?; let result = self.get_candidates(line.clone(), cursor_pos)?;
match result { match result {
CompResult::Many { candidates } => { CompResult::Many { candidates } => {
self.candidates = candidates.clone(); self.candidates = candidates.clone();
self.selected_idx = 0; self.selected_idx = 0;
self.original_input = line; self.original_input = line;
self.active = true; self.active = true;
Ok(Some(self.get_completed_line())) Ok(Some(self.get_completed_line()))
} }
CompResult::Single { result } => { CompResult::Single { result } => {
self.candidates = vec![result.clone()]; self.candidates = vec![result.clone()];
self.selected_idx = 0; self.selected_idx = 0;
self.original_input = line; self.original_input = line;
self.active = false; self.active = false;
Ok(Some(self.get_completed_line())) Ok(Some(self.get_completed_line()))
} }
CompResult::NoMatch => Ok(None) CompResult::NoMatch => Ok(None),
}
}
} 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;
pub fn extract_var_name(text: &str) -> Option<(String,usize,usize)> { while let Some(ch) = chars.next() {
let mut chars = text.chars().peekable(); match ch {
let mut name = String::new(); '$' => {
let mut reading_name = false; if chars.peek() == Some(&'{') {
let mut pos = 0; continue;
let mut name_start = 0; }
let mut name_end = 0;
while let Some(ch) = chars.next() { reading_name = true;
match ch { name_start = pos + 1; // Start after the '$'
'$' => { }
if chars.peek() == Some(&'{') { '{' if !reading_name => {
continue; 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; if !reading_name {
name_start = pos + 1; // Start after the '$' return None;
} }
'{' 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 { if name_end == 0 {
return None; name_end = pos;
} }
if name_end == 0 { Some((name, name_start, name_end))
name_end = pos; }
}
Some((name, name_start, name_end)) pub fn get_completed_line(&self) -> String {
} if self.candidates.is_empty() {
return self.original_input.clone();
}
pub fn get_completed_line(&self) -> String { let selected = &self.candidates[self.selected_idx];
if self.candidates.is_empty() { let (start, end) = self.token_span;
return self.original_input.clone(); format!(
} "{}{}{}",
&self.original_input[..start],
selected,
&self.original_input[end..]
)
}
let selected = &self.candidates[self.selected_idx]; pub fn get_candidates(&mut self, line: String, cursor_pos: usize) -> ShResult<CompResult> {
let (start, end) = self.token_span; let source = Arc::new(line.clone());
format!("{}{}{}", &self.original_input[..start], selected, &self.original_input[end..]) let tokens =
} lex::LexStream::new(source, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>()?;
pub fn get_candidates(&mut self, line: String, cursor_pos: usize) -> ShResult<CompResult> { let Some(mut cur_token) = tokens.into_iter().find(|tk| {
let source = Arc::new(line.clone()); let start = tk.span.start;
let tokens = lex::LexStream::new(source, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>()?; let end = tk.span.end;
(start..=end).contains(&cursor_pos)
}) else {
let candidates = Self::complete_filename("./"); // Default to filename completion if no token is found
let end_pos = line.len();
self.token_span = (end_pos, end_pos);
return Ok(CompResult::from_candidates(candidates));
};
let Some(mut cur_token) = tokens.into_iter().find(|tk| { self.token_span = (cur_token.span.start, cur_token.span.end);
let start = tk.span.start;
let end = tk.span.end;
(start..=end).contains(&cursor_pos)
}) else {
log::debug!("No token found at cursor position");
let candidates = Self::complete_filename("./"); // Default to filename completion if no token is found
let end_pos = line.len();
self.token_span = (end_pos, end_pos);
return Ok(CompResult::from_candidates(candidates));
};
self.token_span = (cur_token.span.start, cur_token.span.end); // Look for marker at the START of what we're completing, not at cursor
let (mut ctx, token_start) = self.get_completion_context(&line, cursor_pos);
self.token_span.0 = token_start; // Update start of token span based on context
cur_token
.span
.set_range(self.token_span.0..self.token_span.1); // Update token span to reflect context
// If token contains '=', only complete after the '='
let token_str = cur_token.span.as_str();
if let Some(eq_pos) = token_str.rfind('=') {
// Adjust span to only replace the part after '='
self.token_span.0 = cur_token.span.start + eq_pos + 1;
}
// Look for marker at the START of what we're completing, not at cursor if ctx.last().is_some_and(|m| *m == markers::VAR_SUB) {
let (mut ctx, token_start) = self.get_completion_context(&line, cursor_pos); let var_sub = &cur_token.as_str();
self.token_span.0 = token_start; // Update start of token span based on context if let Some((var_name, start, end)) = Self::extract_var_name(var_sub) {
cur_token.span.set_range(self.token_span.0..self.token_span.1); // Update token span to reflect context if read_vars(|v| v.get_var(&var_name)).is_empty() {
// if we are here, we have a variable substitution that isn't complete
// so let's try to complete it
let ret: ShResult<CompResult> = read_vars(|v| {
let var_matches = v
.flatten_vars()
.keys()
.filter(|k| k.starts_with(&var_name) && *k != &var_name)
.map(|k| k.to_string())
.collect::<Vec<_>>();
// If token contains '=', only complete after the '=' if !var_matches.is_empty() {
let token_str = cur_token.span.as_str(); let name_start = cur_token.span.start + start;
if let Some(eq_pos) = token_str.rfind('=') { let name_end = cur_token.span.start + end;
// Adjust span to only replace the part after '=' self.token_span = (name_start, name_end);
self.token_span.0 = cur_token.span.start + eq_pos + 1; cur_token
} .span
.set_range(self.token_span.0..self.token_span.1);
Ok(CompResult::from_candidates(var_matches))
} else {
Ok(CompResult::NoMatch)
}
});
if ctx.last().is_some_and(|m| *m == markers::VAR_SUB) { if !matches!(ret, Ok(CompResult::NoMatch)) {
let var_sub = &cur_token.as_str(); return ret;
if let Some((var_name,start,end)) = Self::extract_var_name(var_sub) { } else {
log::debug!("Extracted variable name for completion: {}", var_name); ctx.pop();
if read_vars(|v| v.get_var(&var_name)).is_empty() { }
// if we are here, we have a variable substitution that isn't complete } else {
// so let's try to complete it ctx.pop();
let ret: ShResult<CompResult> = read_vars(|v| { }
let var_matches = v.flatten_vars() }
.keys() }
.filter(|k| k.starts_with(&var_name) && *k != &var_name)
.map(|k| k.to_string())
.collect::<Vec<_>>();
if !var_matches.is_empty() { let raw_tk = cur_token.as_str().to_string();
let name_start = cur_token.span.start + start; let expanded_tk = cur_token.expand()?;
let name_end = cur_token.span.start + end; let expanded_words = expanded_tk.get_words().into_iter().collect::<Vec<_>>();
self.token_span = (name_start, name_end); let expanded = expanded_words.join("\\ ");
cur_token.span.set_range(self.token_span.0..self.token_span.1);
Ok(CompResult::from_candidates(var_matches))
} else {
Ok(CompResult::NoMatch)
}
});
if !matches!(ret, Ok(CompResult::NoMatch)) { let mut candidates = match ctx.pop() {
return ret; Some(markers::COMMAND) => Self::complete_command(&expanded)?,
} else { Some(markers::ARG) => Self::complete_filename(&expanded),
ctx.pop(); Some(_) => {
} return Ok(CompResult::NoMatch);
} else { }
ctx.pop(); None => {
} return Ok(CompResult::NoMatch);
} }
} };
let raw_tk = cur_token.as_str().to_string(); // Now we are just going to graft the completed text
let expanded_tk = cur_token.expand()?; // onto the original token. This prevents something like
let expanded_words = expanded_tk.get_words().into_iter().collect::<Vec<_>>(); // $SOME_PATH/
let expanded = expanded_words.join("\\ "); // from being completed into
// /path/to/some_path/file.txt
// and instead returns
// $SOME_PATH/file.txt
candidates = candidates
.into_iter()
.map(|c| match c.strip_prefix(&expanded) {
Some(suffix) => format!("{raw_tk}{suffix}"),
None => c,
})
.collect();
let mut candidates = match ctx.pop() { let limit = crate::state::read_shopts(|s| s.prompt.comp_limit);
Some(markers::COMMAND) => { candidates.truncate(limit);
log::debug!("Completing command: {}", &expanded);
Self::complete_command(&expanded)?
}
Some(markers::ARG) => {
log::debug!("Completing filename: {}", &expanded);
Self::complete_filename(&expanded)
}
Some(m) => {
log::warn!("Unknown marker {:?} in completion context", m);
return Ok(CompResult::NoMatch);
}
None => {
log::warn!("No marker found in completion context");
return Ok(CompResult::NoMatch);
}
};
// Now we are just going to graft the completed text Ok(CompResult::from_candidates(candidates))
// onto the original token. This prevents something like }
// $SOME_PATH/
// from being completed into
// /path/to/some_path/file.txt
// and instead returns
// $SOME_PATH/file.txt
candidates = candidates.into_iter()
.map(|c| match c.strip_prefix(&expanded) {
Some(suffix) => format!("{raw_tk}{suffix}"),
None => c
})
.collect();
let limit = crate::state::read_shopts(|s| s.prompt.comp_limit); fn complete_command(start: &str) -> ShResult<Vec<String>> {
candidates.truncate(limit); let mut candidates = vec![];
Ok(CompResult::from_candidates(candidates)) let path = env::var("PATH").unwrap_or_default();
} let paths = path.split(':').map(PathBuf::from).collect::<Vec<_>>();
for path in paths {
// Skip directories that don't exist (common in PATH)
let Ok(entries) = std::fs::read_dir(path) else {
continue;
};
for entry in entries {
let Ok(entry) = entry else {
continue;
};
let Ok(meta) = entry.metadata() else {
continue;
};
fn complete_command(start: &str) -> ShResult<Vec<String>> { let file_name = entry.file_name().to_string_lossy().to_string();
let mut candidates = vec![];
let path = env::var("PATH").unwrap_or_default(); if meta.is_file()
let paths = path.split(':').map(PathBuf::from).collect::<Vec<_>>(); && (meta.permissions().mode() & 0o111) != 0
for path in paths { && file_name.starts_with(start)
// Skip directories that don't exist (common in PATH) {
let Ok(entries) = std::fs::read_dir(path) else { continue; }; candidates.push(file_name);
for entry in entries { }
let Ok(entry) = entry else { continue; }; }
let Ok(meta) = entry.metadata() else { continue; }; }
let file_name = entry.file_name().to_string_lossy().to_string(); let builtin_candidates = BUILTINS
.iter()
.filter(|b| b.starts_with(start))
.map(|s| s.to_string());
if meta.is_file() candidates.extend(builtin_candidates);
&& (meta.permissions().mode() & 0o111) != 0
&& file_name.starts_with(start) {
candidates.push(file_name);
}
}
}
let builtin_candidates = BUILTINS read_logic(|l| {
.iter() let func_table = l.funcs();
.filter(|b| b.starts_with(start)) let matches = func_table
.map(|s| s.to_string()); .keys()
.filter(|k| k.starts_with(start))
.map(|k| k.to_string());
candidates.extend(builtin_candidates); candidates.extend(matches);
read_logic(|l| { let aliases = l.aliases();
let func_table = l.funcs(); let matches = aliases
let matches = func_table .keys()
.keys() .filter(|k| k.starts_with(start))
.filter(|k| k.starts_with(start)) .map(|k| k.to_string());
.map(|k| k.to_string());
candidates.extend(matches); candidates.extend(matches);
});
let aliases = l.aliases(); // Deduplicate (same command may appear in multiple PATH dirs)
let matches = aliases candidates.sort();
.keys() candidates.dedup();
.filter(|k| k.starts_with(start))
.map(|k| k.to_string());
candidates.extend(matches); Ok(candidates)
}); }
// Deduplicate (same command may appear in multiple PATH dirs) fn complete_filename(start: &str) -> Vec<String> {
candidates.sort(); let mut candidates = vec![];
candidates.dedup(); let has_dotslash = start.starts_with("./");
Ok(candidates) // 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)
};
fn complete_filename(start: &str) -> Vec<String> { let Ok(entries) = std::fs::read_dir(&dir) else {
let mut candidates = vec![]; return candidates;
};
// If completing after '=', only use the part after it for entry in entries.flatten() {
let start = if let Some(eq_pos) = start.rfind('=') { let file_name = entry.file_name();
&start[eq_pos + 1..] let file_str = file_name.to_string_lossy();
} else {
start
};
// Split path into directory and filename parts // Skip hidden files unless explicitly requested
// Use "." if start is empty (e.g., after "foo=") if !prefix.starts_with('.') && file_str.starts_with('.') {
let path = PathBuf::from(if start.is_empty() { "." } else { start }); continue;
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 { if file_str.starts_with(prefix) {
return candidates; // Reconstruct full path
}; let mut full_path = dir.join(&file_name);
for entry in entries.flatten() { // Add trailing slash for directories
let file_name = entry.file_name(); if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
let file_str = file_name.to_string_lossy(); full_path.push(""); // adds trailing /
}
// Skip hidden files unless explicitly requested let mut path_raw = full_path.to_string_lossy().to_string();
if !prefix.starts_with('.') && file_str.starts_with('.') { if path_raw.starts_with("./") && !has_dotslash {
continue; path_raw = path_raw.trim_start_matches("./").to_string();
} }
if file_str.starts_with(prefix) { candidates.push(path_raw);
// Reconstruct full path }
let mut full_path = dir.join(&file_name); }
// Add trailing slash for directories candidates.sort();
if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) { candidates
full_path.push(""); // adds trailing / }
} }
candidates.push(full_path.to_string_lossy().to_string()); impl Default for Completer {
} fn default() -> Self {
} Self::new()
}
candidates.sort();
candidates
}
} }

View File

@@ -1,318 +1,395 @@
use std::{env, os::unix::fs::PermissionsExt, path::{Path, PathBuf}}; use std::{
env,
os::unix::fs::PermissionsExt,
path::{Path, PathBuf},
};
use crate::{libsh::term::{Style, StyleSet, Styled}, prompt::readline::{annotate_input, markers}, state::{read_logic, read_shopts}}; use crate::{
libsh::term::{Style, StyleSet, Styled},
prompt::readline::{annotate_input, markers},
state::{read_logic, read_shopts},
};
/// Syntax highlighter for shell input using Unicode marker-based annotation /// Syntax highlighter for shell input using Unicode marker-based annotation
/// ///
/// The highlighter processes annotated input strings containing invisible Unicode markers /// The highlighter processes annotated input strings containing invisible
/// (U+FDD0-U+FDEF range) that indicate syntax elements. It generates ANSI escape codes /// Unicode markers (U+FDD0-U+FDEF range) that indicate syntax elements. It
/// for terminal display while maintaining a style stack for proper color restoration /// generates ANSI escape codes for terminal display while maintaining a style
/// in nested constructs (e.g., variables inside strings inside command substitutions). /// stack for proper color restoration in nested constructs (e.g., variables
/// inside strings inside command substitutions).
pub struct Highlighter { pub struct Highlighter {
input: String, input: String,
output: String, output: String,
style_stack: Vec<StyleSet>, style_stack: Vec<StyleSet>,
last_was_reset: bool, last_was_reset: bool,
} }
impl Highlighter { impl Highlighter {
/// Creates a new highlighter with empty buffers and reset state /// Creates a new highlighter with empty buffers and reset state
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
input: String::new(), input: String::new(),
output: String::new(), output: String::new(),
style_stack: Vec::new(), style_stack: Vec::new(),
last_was_reset: true, // start as true so we don't emit a leading reset last_was_reset: true, // start as true so we don't emit a leading reset
} }
} }
/// Loads raw input text and annotates it with syntax markers /// Loads raw input text and annotates it with syntax markers
/// ///
/// The input is passed through the annotator which inserts Unicode markers /// The input is passed through the annotator which inserts Unicode markers
/// indicating token types and sub-token constructs (strings, variables, etc.) /// indicating token types and sub-token constructs (strings, variables, etc.)
pub fn load_input(&mut self, input: &str) { pub fn load_input(&mut self, input: &str) {
let input = annotate_input(input); let input = annotate_input(input);
self.input = input; self.input = input;
} }
/// Processes the annotated input and generates ANSI-styled output /// Processes the annotated input and generates ANSI-styled output
/// ///
/// Walks through the input character by character, interpreting markers and /// Walks through the input character by character, interpreting markers and
/// applying appropriate styles. Nested constructs (command substitutions, /// applying appropriate styles. Nested constructs (command substitutions,
/// subshells, strings) are handled recursively with proper style restoration. /// subshells, strings) are handled recursively with proper style restoration.
pub fn highlight(&mut self) { pub fn highlight(&mut self) {
let input = self.input.clone(); let input = self.input.clone();
let mut input_chars = input.chars().peekable(); let mut input_chars = input.chars().peekable();
while let Some(ch) = input_chars.next() { while let Some(ch) = input_chars.next() {
match ch { match ch {
markers::STRING_DQ_END | markers::STRING_DQ_END
markers::STRING_SQ_END | | markers::STRING_SQ_END
markers::VAR_SUB_END | | markers::VAR_SUB_END
markers::CMD_SUB_END | | markers::CMD_SUB_END
markers::PROC_SUB_END | | markers::PROC_SUB_END
markers::SUBSH_END => self.pop_style(), | markers::SUBSH_END => self.pop_style(),
markers::CMD_SEP | markers::CMD_SEP | markers::RESET => self.clear_styles(),
markers::RESET => self.clear_styles(),
markers::STRING_DQ | markers::STRING_SQ | markers::KEYWORD => {
self.push_style(Style::Yellow)
}
markers::BUILTIN => self.push_style(Style::Green),
markers::CASE_PAT => self.push_style(Style::Blue),
markers::STRING_DQ | markers::COMMENT => self.push_style(Style::BrightBlack),
markers::STRING_SQ |
markers::KEYWORD => self.push_style(Style::Yellow),
markers::BUILTIN => self.push_style(Style::Green),
markers::CASE_PAT => self.push_style(Style::Blue),
markers::ARG => self.push_style(Style::White),
markers::COMMENT => self.push_style(Style::BrightBlack),
markers::GLOB => self.push_style(Style::Blue), markers::GLOB => self.push_style(Style::Blue),
markers::REDIRECT | markers::REDIRECT | markers::OPERATOR => self.push_style(Style::Magenta | Style::Bold),
markers::OPERATOR => self.push_style(Style::Magenta | Style::Bold),
markers::ASSIGNMENT => { markers::ASSIGNMENT => {
let mut var_name = String::new(); let mut var_name = String::new();
while let Some(ch) = input_chars.peek() { while let Some(ch) = input_chars.peek() {
if ch == &'=' { if ch == &'=' {
input_chars.next(); // consume the '=' input_chars.next(); // consume the '='
break; break;
} }
match *ch { match *ch {
markers::RESET => break, markers::RESET => break,
_ => { _ => {
var_name.push(*ch); var_name.push(*ch);
input_chars.next(); input_chars.next();
} }
} }
} }
self.output.push_str(&var_name); self.output.push_str(&var_name);
self.push_style(Style::Blue); self.push_style(Style::Blue);
self.output.push('='); self.output.push('=');
self.pop_style(); self.pop_style();
} }
markers::COMMAND => { markers::ARG => {
let mut cmd_name = String::new(); let mut arg = String::new();
while let Some(ch) = input_chars.peek() { let mut chars_clone = input_chars.clone();
if *ch == markers::RESET { while let Some(ch) = chars_clone.next() {
break; if ch == markers::RESET {
} break;
cmd_name.push(*ch); }
input_chars.next(); arg.push(ch);
} }
let style = if Self::is_valid(&cmd_name) {
Style::Green.into()
} else {
Style::Red | Style::Bold
};
self.push_style(style);
self.output.push_str(&cmd_name);
self.last_was_reset = false;
}
markers::CMD_SUB | markers::SUBSH | markers::PROC_SUB => {
let mut inner = String::new();
let mut incomplete = true;
let end_marker = match ch {
markers::CMD_SUB => markers::CMD_SUB_END,
markers::SUBSH => markers::SUBSH_END,
markers::PROC_SUB => markers::PROC_SUB_END,
_ => unreachable!(),
};
while let Some(ch) = input_chars.peek() {
if *ch == end_marker {
incomplete = false;
input_chars.next(); // consume the end marker
break;
}
inner.push(*ch);
input_chars.next();
}
// Determine prefix from content (handles both <( and >( for proc subs) let style = if Self::is_filename(&arg) {
let prefix = match ch { Style::White | Style::Underline
markers::CMD_SUB => "$(", } else {
markers::SUBSH => "(", Style::White.into()
markers::PROC_SUB => { };
if inner.starts_with("<(") { "<(" }
else if inner.starts_with(">(") { ">(" }
else { "<(" } // fallback
}
_ => unreachable!(),
};
let inner_content = if incomplete { self.push_style(style);
inner self.last_was_reset = false;
.strip_prefix(prefix) }
.unwrap_or(&inner)
} else {
inner
.strip_prefix(prefix)
.and_then(|s| s.strip_suffix(")"))
.unwrap_or(&inner)
};
let mut recursive_highlighter = Self::new(); markers::COMMAND => {
recursive_highlighter.load_input(inner_content); let mut cmd_name = String::new();
recursive_highlighter.highlight(); let mut chars_clone = input_chars.clone();
self.push_style(Style::Blue); while let Some(ch) = chars_clone.next() {
self.output.push_str(prefix); if ch == markers::RESET {
self.pop_style(); break;
self.output.push_str(&recursive_highlighter.take()); }
if !incomplete { cmd_name.push(ch);
self.push_style(Style::Blue); }
self.output.push(')'); let style = if Self::is_valid(&cmd_name) {
self.pop_style(); Style::Green.into()
} } else {
self.last_was_reset = false; Style::Red | Style::Bold
} };
markers::VAR_SUB => { self.push_style(style);
let mut var_sub = String::new(); self.last_was_reset = false;
while let Some(ch) = input_chars.peek() { }
if *ch == markers::VAR_SUB_END { markers::CMD_SUB | markers::SUBSH | markers::PROC_SUB => {
input_chars.next(); // consume the end marker let mut inner = String::new();
break; let mut incomplete = true;
} else if markers::is_marker(*ch) { let end_marker = match ch {
input_chars.next(); // skip the marker markers::CMD_SUB => markers::CMD_SUB_END,
continue; markers::SUBSH => markers::SUBSH_END,
} markers::PROC_SUB => markers::PROC_SUB_END,
var_sub.push(*ch); _ => unreachable!(),
input_chars.next(); };
} while let Some(ch) = input_chars.peek() {
let style = Style::Cyan; if *ch == end_marker {
self.push_style(style); incomplete = false;
self.output.push_str(&var_sub); input_chars.next(); // consume the end marker
self.pop_style(); break;
} }
_ => { inner.push(*ch);
if markers::is_marker(ch) { input_chars.next();
} else { }
self.output.push(ch);
self.last_was_reset = false;
}
}
}
}
}
/// Extracts the highlighted output and resets the highlighter state // Determine prefix from content (handles both <( and >( for proc subs)
/// let prefix = match ch {
/// Clears the input buffer, style stack, and returns the generated output markers::CMD_SUB => "$(",
/// containing ANSI escape codes. The highlighter is ready for reuse after this. markers::SUBSH => "(",
pub fn take(&mut self) -> String { markers::PROC_SUB => {
self.input.clear(); if inner.starts_with("<(") {
self.clear_styles(); "<("
std::mem::take(&mut self.output) } else if inner.starts_with(">(") {
} ">("
} else {
"<("
} // fallback
}
_ => unreachable!(),
};
/// Checks if a command name is valid (exists in PATH, is a function, or is an alias) let inner_content = if incomplete {
/// inner.strip_prefix(prefix).unwrap_or(&inner)
/// Searches: } else {
/// 1. Current directory if command is a path inner
/// 2. All directories in PATH environment variable .strip_prefix(prefix)
/// 3. Shell functions and aliases in the current shell state .and_then(|s| s.strip_suffix(")"))
fn is_valid(command: &str) -> bool { .unwrap_or(&inner)
let path = env::var("PATH").unwrap_or_default(); };
let paths = path.split(':');
let cmd_path = PathBuf::from(&command);
if cmd_path.exists() { let mut recursive_highlighter = Self::new();
// the user has given us an absolute path recursive_highlighter.load_input(inner_content);
if cmd_path.is_dir() && read_shopts(|o| o.core.autocd) { recursive_highlighter.highlight();
// this is a directory and autocd is enabled self.push_style(Style::Blue);
return true; self.output.push_str(prefix);
} else { self.pop_style();
let Ok(meta) = cmd_path.metadata() else { return false }; self.output.push_str(&recursive_highlighter.take());
// this is a file that is executable by someone if !incomplete {
return meta.permissions().mode() & 0o111 == 0 self.push_style(Style::Blue);
} self.output.push(')');
} else { self.pop_style();
// they gave us a command name }
// now we must traverse the PATH env var self.last_was_reset = false;
// and see if we find any matches }
for path in paths { markers::VAR_SUB => {
let path = PathBuf::from(path).join(command); let mut var_sub = String::new();
if path.exists() { while let Some(ch) = input_chars.peek() {
let Ok(meta) = path.metadata() else { continue }; if *ch == markers::VAR_SUB_END {
return meta.permissions().mode() & 0o111 != 0; input_chars.next(); // consume the end marker
} break;
} } else if markers::is_marker(*ch) {
input_chars.next(); // skip the marker
continue;
}
var_sub.push(*ch);
input_chars.next();
}
let style = Style::Cyan;
self.push_style(style);
self.output.push_str(&var_sub);
self.pop_style();
}
_ => {
if markers::is_marker(ch) {
} else {
self.output.push(ch);
self.last_was_reset = false;
}
}
}
}
}
// also check shell functions and aliases for any matches /// Extracts the highlighted output and resets the highlighter state
let found = read_logic(|l| l.get_func(command).is_some() || l.get_alias(command).is_some()); ///
if found { /// Clears the input buffer, style stack, and returns the generated output
return true; /// containing ANSI escape codes. The highlighter is ready for reuse after
} /// this.
} pub fn take(&mut self) -> String {
self.input.clear();
self.clear_styles();
std::mem::take(&mut self.output)
}
false /// Checks if a command name is valid (exists in PATH, is a function, or is an
} /// alias)
///
/// Searches:
/// 1. Current directory if command is a path
/// 2. All directories in PATH environment variable
/// 3. Shell functions and aliases in the current shell state
fn is_valid(command: &str) -> bool {
let path = env::var("PATH").unwrap_or_default();
let paths = path.split(':');
let cmd_path = PathBuf::from(&command);
/// Emits a reset ANSI code to the output, with deduplication if cmd_path.exists() {
/// // the user has given us an absolute path
/// Only emits the reset if the last emitted code was not already a reset, if cmd_path.is_dir() && read_shopts(|o| o.core.autocd) {
/// preventing redundant `\x1b[0m` sequences in the output. // this is a directory and autocd is enabled
fn emit_reset(&mut self) { return true;
if !self.last_was_reset { } else {
self.output.push_str(&Style::Reset.to_string()); let Ok(meta) = cmd_path.metadata() else {
self.last_was_reset = true; return false;
} };
} // this is a file that is executable by someone
return meta.permissions().mode() & 0o111 == 0;
}
} else {
// they gave us a command name
// now we must traverse the PATH env var
// and see if we find any matches
for path in paths {
let path = PathBuf::from(path).join(command);
if path.exists() {
let Ok(meta) = path.metadata() else { continue };
return meta.permissions().mode() & 0o111 != 0;
}
}
/// Emits a style ANSI code to the output // also check shell functions and aliases for any matches
/// let found = read_logic(|l| l.get_func(command).is_some() || l.get_alias(command).is_some());
/// Unconditionally appends the ANSI escape sequence for the given style if found {
/// and marks that we're no longer in a reset state. return true;
fn emit_style(&mut self, style: &StyleSet) { }
self.output.push_str(&style.to_string()); }
self.last_was_reset = false;
}
/// Pushes a new style onto the stack and emits its ANSI code false
/// }
/// Used when entering a new syntax context (string, variable, command, etc.).
/// The style stack allows proper restoration when exiting nested constructs.
pub fn push_style(&mut self, style: impl Into<StyleSet>) {
let set: StyleSet = style.into();
self.style_stack.push(set.clone());
self.emit_style(&set);
}
/// Pops a style from the stack and restores the previous style fn is_filename(arg: &str) -> bool {
/// let path = PathBuf::from(arg);
/// Used when exiting a syntax context. If there's a parent style on the stack,
/// it's re-emitted to restore the previous color. Otherwise, emits a reset.
/// This ensures colors are properly restored in nested constructs like
/// `"string with $VAR"` where the string color resumes after the variable.
pub fn pop_style(&mut self) {
self.style_stack.pop();
if let Some(style) = self.style_stack.last().cloned() {
self.emit_style(&style);
} else {
self.emit_reset();
}
}
/// Clears all styles from the stack and emits a reset if path.exists() {
/// return true;
/// Used at command separators and explicit reset markers to return to }
/// the default terminal color between independent commands.
pub fn clear_styles(&mut self) {
self.style_stack.clear();
self.emit_reset();
}
/// Simple marker-to-ANSI replacement (unused in favor of stack-based highlighting) if let Some(parent_dir) = path.parent()
/// && let Ok(entries) = parent_dir.read_dir()
/// Performs direct string replacement of markers with ANSI codes, without {
/// handling nesting or proper color restoration. Kept for reference but not let files = entries
/// used in the current implementation. .filter_map(|e| e.ok())
pub fn trivial_replace(&mut self) { .map(|e| e.file_name().to_string_lossy().to_string())
self.input = self.input .collect::<Vec<_>>();
.replace([markers::RESET, markers::ARG], "\x1b[0m") let Some(arg_filename) = PathBuf::from(arg)
.replace(markers::KEYWORD, "\x1b[33m") .file_name()
.replace(markers::CASE_PAT, "\x1b[34m") .map(|s| s.to_string_lossy().to_string())
.replace(markers::COMMENT, "\x1b[90m") else {
.replace(markers::OPERATOR, "\x1b[35m"); return false;
} };
for file in files {
if file.starts_with(&arg_filename) {
return true;
}
}
};
if let Ok(this_dir) = env::current_dir()
&& let Ok(entries) = this_dir.read_dir()
{
let this_dir_files = entries
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().to_string())
.collect::<Vec<_>>();
for file in this_dir_files {
if file.starts_with(arg) {
return true;
}
}
};
false
}
/// Emits a reset ANSI code to the output, with deduplication
///
/// Only emits the reset if the last emitted code was not already a reset,
/// preventing redundant `\x1b[0m` sequences in the output.
fn emit_reset(&mut self) {
if !self.last_was_reset {
self.output.push_str(&Style::Reset.to_string());
self.last_was_reset = true;
}
}
/// Emits a style ANSI code to the output
///
/// 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) {
self.output.push_str(&style.to_string());
self.last_was_reset = false;
}
/// Pushes a new style onto the stack and emits its ANSI code
///
/// Used when entering a new syntax context (string, variable, command, etc.).
/// The style stack allows proper restoration when exiting nested constructs.
pub fn push_style(&mut self, style: impl Into<StyleSet>) {
let set: StyleSet = style.into();
self.style_stack.push(set.clone());
self.emit_style(&set);
}
/// Pops a style from the stack and restores the previous style
///
/// Used when exiting a syntax context. If there's a parent style on the
/// stack, it's re-emitted to restore the previous color. Otherwise, emits a
/// reset. This ensures colors are properly restored in nested constructs
/// like `"string with $VAR"` where the string color resumes after the
/// variable.
pub fn pop_style(&mut self) {
self.style_stack.pop();
if let Some(style) = self.style_stack.last().cloned() {
self.emit_style(&style);
} else {
self.emit_reset();
}
}
/// Clears all styles from the stack and emits a reset
///
/// Used at command separators and explicit reset markers to return to
/// the default terminal color between independent commands.
pub fn clear_styles(&mut self) {
self.style_stack.clear();
self.emit_reset();
}
/// Simple marker-to-ANSI replacement (unused in favor of stack-based
/// highlighting)
///
/// Performs direct string replacement of markers with ANSI codes, without
/// handling nesting or proper color restoration. Kept for reference but not
/// used in the current implementation.
pub fn trivial_replace(&mut self) {
self.input = self
.input
.replace([markers::RESET, markers::ARG], "\x1b[0m")
.replace(markers::KEYWORD, "\x1b[33m")
.replace(markers::CASE_PAT, "\x1b[34m")
.replace(markers::COMMENT, "\x1b[90m")
.replace(markers::OPERATOR, "\x1b[35m");
}
} }

View File

@@ -189,8 +189,8 @@ fn read_hist_file(path: &Path) -> ShResult<Vec<HistEntry>> {
Ok(raw.parse::<HistEntries>()?.0) Ok(raw.parse::<HistEntries>()?.0)
} }
/// Deduplicate entries, keeping only the most recent occurrence of each command. /// Deduplicate entries, keeping only the most recent occurrence of each
/// Preserves chronological order (oldest to newest). /// command. Preserves chronological order (oldest to newest).
fn dedupe_entries(entries: &[HistEntry]) -> Vec<HistEntry> { fn dedupe_entries(entries: &[HistEntry]) -> Vec<HistEntry> {
let mut seen = HashSet::new(); let mut seen = HashSet::new();
// Iterate backwards (newest first), keeping first occurrence of each command // Iterate backwards (newest first), keeping first occurrence of each command
@@ -207,10 +207,10 @@ fn dedupe_entries(entries: &[HistEntry]) -> Vec<HistEntry> {
pub struct History { pub struct History {
path: PathBuf, path: PathBuf,
pub pending: Option<String>, pub pending: Option<(String, usize)>, // command, cursor_pos
entries: Vec<HistEntry>, entries: Vec<HistEntry>,
search_mask: Vec<HistEntry>, search_mask: Vec<HistEntry>,
no_matches: bool, no_matches: bool,
pub cursor: usize, pub cursor: usize,
search_direction: Direction, search_direction: Direction,
ignore_dups: bool, ignore_dups: bool,
@@ -235,9 +235,9 @@ impl History {
Ok(Self { Ok(Self {
path, path,
entries, entries,
pending: None, pending: None,
search_mask, search_mask,
no_matches: false, no_matches: false,
cursor, cursor,
search_direction: Direction::Backward, search_direction: Direction::Backward,
ignore_dups, ignore_dups,
@@ -245,10 +245,10 @@ impl History {
}) })
} }
pub fn reset(&mut self) { pub fn reset(&mut self) {
self.search_mask = dedupe_entries(&self.entries); self.search_mask = dedupe_entries(&self.entries);
self.cursor = self.search_mask.len(); self.cursor = self.search_mask.len();
} }
pub fn entries(&self) -> &[HistEntry] { pub fn entries(&self) -> &[HistEntry] {
&self.entries &self.entries
@@ -270,14 +270,14 @@ impl History {
self.cursor = self.search_mask.len(); self.cursor = self.search_mask.len();
} }
pub fn update_pending_cmd(&mut self, command: &str) { pub fn update_pending_cmd(&mut self, buf: (&str, usize)) {
let cmd = command.to_string(); let cmd = buf.0.to_string();
let constraint = SearchConstraint { let constraint = SearchConstraint {
kind: SearchKind::Prefix, kind: SearchKind::Prefix,
term: cmd.clone(), term: cmd.clone(),
}; };
self.pending = Some(cmd); self.pending = Some((cmd, buf.1));
self.constrain_entries(constraint); self.constrain_entries(constraint);
} }
@@ -315,11 +315,11 @@ impl History {
.collect(); .collect();
self.search_mask = dedupe_entries(&filtered); self.search_mask = dedupe_entries(&filtered);
self.no_matches = self.search_mask.is_empty(); self.no_matches = self.search_mask.is_empty();
if self.no_matches { if self.no_matches {
// If no matches, reset to full history so user can still scroll through it // If no matches, reset to full history so user can still scroll through it
self.search_mask = dedupe_entries(&self.entries); self.search_mask = dedupe_entries(&self.entries);
} }
} }
self.cursor = self.search_mask.len(); self.cursor = self.search_mask.len();
} }
@@ -328,12 +328,14 @@ impl History {
} }
pub fn hint_entry(&self) -> Option<&HistEntry> { pub fn hint_entry(&self) -> Option<&HistEntry> {
if self.no_matches { return None }; if self.no_matches {
return None;
};
self.search_mask.last() self.search_mask.last()
} }
pub fn get_hint(&self) -> Option<String> { pub fn get_hint(&self) -> Option<String> {
if self.at_pending() && self.pending.as_ref().is_some_and(|p| !p.is_empty()) { if self.at_pending() && self.pending.as_ref().is_some_and(|p| !p.0.is_empty()) {
let entry = self.hint_entry()?; let entry = self.hint_entry()?;
Some(entry.command().to_string()) Some(entry.command().to_string())
} else { } else {
@@ -342,9 +344,15 @@ impl History {
} }
pub fn scroll(&mut self, offset: isize) -> Option<&HistEntry> { pub fn scroll(&mut self, offset: isize) -> Option<&HistEntry> {
self.cursor = self.cursor.saturating_add_signed(offset).clamp(0, self.search_mask.len()); self.cursor = self
.cursor
.saturating_add_signed(offset)
.clamp(0, self.search_mask.len());
log::debug!("Scrolling history by offset {offset} from cursor at index {}", self.cursor); log::debug!(
"Scrolling history by offset {offset} from cursor at index {}",
self.cursor
);
self.search_mask.get(self.cursor) self.search_mask.get(self.cursor)
} }
@@ -378,7 +386,8 @@ impl History {
let last_file_entry = self let last_file_entry = self
.entries .entries
.iter().rfind(|ent| !ent.new) .iter()
.rfind(|ent| !ent.new)
.map(|ent| ent.command.clone()) .map(|ent| ent.command.clone())
.unwrap_or_default(); .unwrap_or_default();
@@ -399,8 +408,8 @@ impl History {
} }
file.write_all(data.as_bytes())?; file.write_all(data.as_bytes())?;
self.pending = None; self.pending = None;
self.reset(); self.reset();
Ok(()) Ok(())
} }

View File

@@ -133,10 +133,10 @@ impl SelectMode {
#[derive(Debug, Clone, PartialEq, Eq)] #[derive(Debug, Clone, PartialEq, Eq)]
pub enum MotionKind { pub enum MotionKind {
To(usize), // Absolute position, exclusive To(usize), // Absolute position, exclusive
On(usize), // Absolute position, inclusive On(usize), // Absolute position, inclusive
Onto(usize), /* Absolute position, operations include the position but motions Onto(usize), /* Absolute position, operations include the position but motions
* exclude it (wtf vim) */ * exclude it (wtf vim) */
Inclusive((usize, usize)), // Range, inclusive Inclusive((usize, usize)), // Range, inclusive
Exclusive((usize, usize)), // Range, exclusive Exclusive((usize, usize)), // Range, exclusive
@@ -360,12 +360,12 @@ impl LineBuf {
pub fn set_hint(&mut self, hint: Option<String>) { pub fn set_hint(&mut self, hint: Option<String>) {
if let Some(hint) = hint { if let Some(hint) = hint {
if let Some(hint) = hint.strip_prefix(&self.buffer) { if let Some(hint) = hint.strip_prefix(&self.buffer) {
if !hint.is_empty() { if !hint.is_empty() {
self.hint = Some(hint.to_string()) self.hint = Some(hint.to_string())
} else { } else {
self.hint = None self.hint = None
} }
} }
} else { } else {
self.hint = None self.hint = None
} }
@@ -563,8 +563,8 @@ impl LineBuf {
self.update_graphemes(); self.update_graphemes();
} }
pub fn drain(&mut self, start: usize, end: usize) -> String { pub fn drain(&mut self, start: usize, end: usize) -> String {
let start = start.max(0); let start = start.max(0);
let end = end.min(self.grapheme_indices().len()); let end = end.min(self.grapheme_indices().len());
let drained = if end == self.grapheme_indices().len() { let drained = if end == self.grapheme_indices().len() {
if start == self.grapheme_indices().len() { if start == self.grapheme_indices().len() {
return String::new(); return String::new();
@@ -628,8 +628,9 @@ impl LineBuf {
self.next_sentence_start_from_punctuation(pos).is_some() self.next_sentence_start_from_punctuation(pos).is_some()
} }
/// If position is at sentence-ending punctuation, returns the position of the next sentence start. /// If position is at sentence-ending punctuation, returns the position of the
/// Handles closing delimiters (`)`, `]`, `"`, `'`) after punctuation. /// next sentence start. Handles closing delimiters (`)`, `]`, `"`, `'`)
/// after punctuation.
#[allow(clippy::collapsible_if)] #[allow(clippy::collapsible_if)]
pub fn next_sentence_start_from_punctuation(&self, pos: usize) -> Option<usize> { pub fn next_sentence_start_from_punctuation(&self, pos: usize) -> Option<usize> {
if let Some(gr) = self.read_grapheme_at(pos) { if let Some(gr) = self.read_grapheme_at(pos) {
@@ -956,9 +957,10 @@ impl LineBuf {
let start = start.unwrap_or(0); let start = start.unwrap_or(0);
if count > 1 if count > 1
&& let Some((_, new_end)) = self.text_obj_sentence(end, count - 1, bound) { && let Some((_, new_end)) = self.text_obj_sentence(end, count - 1, bound)
end = new_end; {
} end = new_end;
}
Some((start, end)) Some((start, end))
} }
@@ -1363,7 +1365,12 @@ impl LineBuf {
} }
/// Find the start of the next word forward /// Find the start of the next word forward
pub fn start_of_word_forward(&mut self, mut pos: usize, word: Word, include_last_char: bool) -> usize { pub fn start_of_word_forward(
&mut self,
mut pos: usize,
word: Word,
include_last_char: bool,
) -> usize {
let default = self.grapheme_indices().len(); let default = self.grapheme_indices().len();
let mut indices_iter = (pos..self.cursor.max).peekable(); let mut indices_iter = (pos..self.cursor.max).peekable();
@@ -1390,8 +1397,7 @@ impl LineBuf {
let on_whitespace = is_whitespace(&cur_char); let on_whitespace = is_whitespace(&cur_char);
if !on_whitespace { if !on_whitespace {
let Some(ws_pos) = let Some(ws_pos) = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
else { else {
return default; return default;
}; };
@@ -1457,7 +1463,12 @@ impl LineBuf {
} }
/// Find the end of the previous word backward /// Find the end of the previous word backward
pub fn end_of_word_backward(&mut self, mut pos: usize, word: Word, include_last_char: bool) -> usize { pub fn end_of_word_backward(
&mut self,
mut pos: usize,
word: Word,
include_last_char: bool,
) -> usize {
let default = self.grapheme_indices().len(); let default = self.grapheme_indices().len();
let mut indices_iter = (0..pos).rev().peekable(); let mut indices_iter = (0..pos).rev().peekable();
@@ -1484,8 +1495,7 @@ impl LineBuf {
let on_whitespace = is_whitespace(&cur_char); let on_whitespace = is_whitespace(&cur_char);
if !on_whitespace { if !on_whitespace {
let Some(ws_pos) = let Some(ws_pos) = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
else { else {
return default; return default;
}; };
@@ -1742,11 +1752,7 @@ impl LineBuf {
}; };
pos = next_ws_pos; pos = next_ws_pos;
if pos == 0 { if pos == 0 { pos } else { pos + 1 }
pos
} else {
pos + 1
}
} }
} }
} }
@@ -1903,7 +1909,7 @@ impl LineBuf {
&& self.grapheme_at(target_pos) == Some("\n") && self.grapheme_at(target_pos) == Some("\n")
{ {
target_pos = target_pos.saturating_sub(1); // Don't land on the target_pos = target_pos.saturating_sub(1); // Don't land on the
// newline // newline
} }
MotionKind::InclusiveWithTargetCol((start, end), target_pos) MotionKind::InclusiveWithTargetCol((start, end), target_pos)
} }
@@ -2141,7 +2147,7 @@ impl LineBuf {
&& self.grapheme_at(target_pos) == Some("\n") && self.grapheme_at(target_pos) == Some("\n")
{ {
target_pos = target_pos.saturating_sub(1); // Don't land on the target_pos = target_pos.saturating_sub(1); // Don't land on the
// newline // newline
} }
let (start, end) = match motion.1 { let (start, end) = match motion.1 {
@@ -2575,15 +2581,16 @@ impl LineBuf {
} }
Verb::SwapVisualAnchor => { Verb::SwapVisualAnchor => {
if let Some((start, end)) = self.select_range() if let Some((start, end)) = self.select_range()
&& let Some(mut mode) = self.select_mode { && let Some(mut mode) = self.select_mode
mode.invert_anchor(); {
let new_cursor_pos = match mode.anchor() { mode.invert_anchor();
SelectAnchor::Start => start, let new_cursor_pos = match mode.anchor() {
SelectAnchor::End => end, SelectAnchor::Start => start,
}; SelectAnchor::End => end,
self.cursor.set(new_cursor_pos); };
self.select_mode = Some(mode) self.cursor.set(new_cursor_pos);
} self.select_mode = Some(mode)
}
} }
Verb::JoinLines => { Verb::JoinLines => {
let start = self.start_of_line(); let start = self.start_of_line();
@@ -2731,10 +2738,12 @@ impl LineBuf {
let edit_is_merging = self.undo_stack.last().is_some_and(|edit| edit.merging); let edit_is_merging = self.undo_stack.last().is_some_and(|edit| edit.merging);
// Merge character inserts into one edit // Merge character inserts into one edit
if edit_is_merging && cmd.verb.as_ref().is_none_or(|v| !v.1.is_char_insert()) if edit_is_merging
&& let Some(edit) = self.undo_stack.last_mut() { && cmd.verb.as_ref().is_none_or(|v| !v.1.is_char_insert())
edit.stop_merge(); && let Some(edit) = self.undo_stack.last_mut()
} {
edit.stop_merge();
}
let ViCmd { let ViCmd {
register, register,
@@ -2821,10 +2830,9 @@ impl LineBuf {
self.saved_col = None; self.saved_col = None;
} }
if is_char_insert if is_char_insert && let Some(edit) = self.undo_stack.last_mut() {
&& let Some(edit) = self.undo_stack.last_mut() { edit.start_merge();
edit.start_merge(); }
}
Ok(()) Ok(())
} }
@@ -2832,9 +2840,13 @@ impl LineBuf {
&self.buffer // FIXME: this will have to be fixed up later &self.buffer // FIXME: this will have to be fixed up later
} }
pub fn get_hint_text(&self) -> String { pub fn get_hint_text(&self) -> String {
self.hint.clone().map(|h| h.styled(Style::BrightBlack)).unwrap_or_default() self
} .hint
.clone()
.map(|h| h.styled(Style::BrightBlack))
.unwrap_or_default()
}
} }
impl Display for LineBuf { impl Display for LineBuf {

File diff suppressed because it is too large Load Diff

View File

@@ -17,11 +17,15 @@ use unicode_segmentation::UnicodeSegmentation;
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use vte::{Parser, Perform}; use vte::{Parser, Perform};
use crate::{prelude::*, procio::borrow_fd, state::{read_meta, write_meta}};
use crate::{ use crate::{
libsh::error::{ShErr, ShErrKind, ShResult}, libsh::error::{ShErr, ShErrKind, ShResult},
prompt::readline::keys::{KeyCode, ModKeys}, prompt::readline::keys::{KeyCode, ModKeys},
}; };
use crate::{
prelude::*,
procio::borrow_fd,
state::{read_meta, write_meta},
};
use super::{keys::KeyEvent, linebuf::LineBuf}; use super::{keys::KeyEvent, linebuf::LineBuf};
@@ -41,7 +45,7 @@ pub fn raw_mode() -> RawModeGuard {
) )
.expect("Failed to set terminal to raw mode"); .expect("Failed to set terminal to raw mode");
let (cols, rows) = get_win_size(STDIN_FILENO); let (cols, rows) = get_win_size(STDIN_FILENO);
RawModeGuard { RawModeGuard {
orig, orig,
@@ -242,9 +246,7 @@ impl Read for TermBuffer {
let result = nix::unistd::read(self.tty, buf); let result = nix::unistd::read(self.tty, buf);
match result { match result {
Ok(n) => Ok(n), Ok(n) => Ok(n),
Err(Errno::EINTR) => { Err(Errno::EINTR) => Err(Errno::EINTR.into()),
Err(Errno::EINTR.into())
}
Err(e) => Err(std::io::Error::from_raw_os_error(e as i32)), Err(e) => Err(std::io::Error::from_raw_os_error(e as i32)),
} }
} }
@@ -280,17 +282,21 @@ impl RawModeGuard {
} }
} }
pub fn with_cooked_mode<F, R>(f: F) -> R pub fn with_cooked_mode<F, R>(f: F) -> R
where F: FnOnce() -> R { where
let raw = tcgetattr(borrow_fd(STDIN_FILENO)).expect("Failed to get terminal attributes"); F: FnOnce() -> R,
let mut cooked = raw.clone(); {
cooked.local_flags |= termios::LocalFlags::ICANON | termios::LocalFlags::ECHO; let raw = tcgetattr(borrow_fd(STDIN_FILENO)).expect("Failed to get terminal attributes");
cooked.input_flags |= termios::InputFlags::ICRNL; let mut cooked = raw.clone();
tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &cooked).expect("Failed to set cooked mode"); cooked.local_flags |= termios::LocalFlags::ICANON | termios::LocalFlags::ECHO;
let res = f(); cooked.input_flags |= termios::InputFlags::ICRNL;
tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &raw).expect("Failed to restore raw mode"); tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &cooked)
res .expect("Failed to set cooked mode");
} let res = f();
tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &raw)
.expect("Failed to restore raw mode");
res
}
} }
impl Drop for RawModeGuard { impl Drop for RawModeGuard {
@@ -333,9 +339,15 @@ impl KeyCollector {
// CSI modifiers: param = 1 + (shift) + (alt*2) + (ctrl*4) + (meta*8) // CSI modifiers: param = 1 + (shift) + (alt*2) + (ctrl*4) + (meta*8)
let bits = param.saturating_sub(1); let bits = param.saturating_sub(1);
let mut mods = ModKeys::empty(); let mut mods = ModKeys::empty();
if bits & 1 != 0 { mods |= ModKeys::SHIFT; } if bits & 1 != 0 {
if bits & 2 != 0 { mods |= ModKeys::ALT; } mods |= ModKeys::SHIFT;
if bits & 4 != 0 { mods |= ModKeys::CTRL; } }
if bits & 2 != 0 {
mods |= ModKeys::ALT;
}
if bits & 4 != 0 {
mods |= ModKeys::CTRL;
}
mods mods
} }
} }
@@ -374,46 +386,72 @@ impl Perform for KeyCollector {
self.push(event); self.push(event);
} }
fn csi_dispatch(&mut self, params: &vte::Params, intermediates: &[u8], _ignore: bool, action: char) { fn csi_dispatch(
let params: Vec<u16> = params.iter() &mut self,
params: &vte::Params,
intermediates: &[u8],
_ignore: bool,
action: char,
) {
let params: Vec<u16> = params
.iter()
.map(|p| p.first().copied().unwrap_or(0)) .map(|p| p.first().copied().unwrap_or(0))
.collect(); .collect();
let event = match (intermediates, action) { let event = match (intermediates, action) {
// Arrow keys: CSI A/B/C/D or CSI 1;mod A/B/C/D // Arrow keys: CSI A/B/C/D or CSI 1;mod A/B/C/D
([], 'A') => { ([], 'A') => {
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty()); let mods = params
.get(1)
.map(|&m| Self::parse_modifiers(m))
.unwrap_or(ModKeys::empty());
KeyEvent(KeyCode::Up, mods) KeyEvent(KeyCode::Up, mods)
} }
([], 'B') => { ([], 'B') => {
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty()); let mods = params
.get(1)
.map(|&m| Self::parse_modifiers(m))
.unwrap_or(ModKeys::empty());
KeyEvent(KeyCode::Down, mods) KeyEvent(KeyCode::Down, mods)
} }
([], 'C') => { ([], 'C') => {
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty()); let mods = params
.get(1)
.map(|&m| Self::parse_modifiers(m))
.unwrap_or(ModKeys::empty());
KeyEvent(KeyCode::Right, mods) KeyEvent(KeyCode::Right, mods)
} }
([], 'D') => { ([], 'D') => {
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty()); let mods = params
.get(1)
.map(|&m| Self::parse_modifiers(m))
.unwrap_or(ModKeys::empty());
KeyEvent(KeyCode::Left, mods) KeyEvent(KeyCode::Left, mods)
} }
// Home/End: CSI H/F or CSI 1;mod H/F // Home/End: CSI H/F or CSI 1;mod H/F
([], 'H') => { ([], 'H') => {
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty()); let mods = params
.get(1)
.map(|&m| Self::parse_modifiers(m))
.unwrap_or(ModKeys::empty());
KeyEvent(KeyCode::Home, mods) KeyEvent(KeyCode::Home, mods)
} }
([], 'F') => { ([], 'F') => {
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty()); let mods = params
.get(1)
.map(|&m| Self::parse_modifiers(m))
.unwrap_or(ModKeys::empty());
KeyEvent(KeyCode::End, mods) KeyEvent(KeyCode::End, mods)
} }
// Shift+Tab: CSI Z // Shift+Tab: CSI Z
([], 'Z') => { ([], 'Z') => KeyEvent(KeyCode::Tab, ModKeys::SHIFT),
KeyEvent(KeyCode::Tab, ModKeys::SHIFT)
}
// Special keys with tilde: CSI num ~ or CSI num;mod ~ // Special keys with tilde: CSI num ~ or CSI num;mod ~
([], '~') => { ([], '~') => {
let key_num = params.first().copied().unwrap_or(0); let key_num = params.first().copied().unwrap_or(0);
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty()); let mods = params
.get(1)
.map(|&m| Self::parse_modifiers(m))
.unwrap_or(ModKeys::empty());
let key = match key_num { let key = match key_num {
1 | 7 => KeyCode::Home, 1 | 7 => KeyCode::Home,
2 => KeyCode::Insert, 2 => KeyCode::Insert,
@@ -473,7 +511,9 @@ impl PollReader {
pub fn feed_bytes(&mut self, bytes: &[u8]) { pub fn feed_bytes(&mut self, bytes: &[u8]) {
if bytes == [b'\x1b'] { if bytes == [b'\x1b'] {
// Single escape byte - user pressed ESC key // Single escape byte - user pressed ESC key
self.collector.push(KeyEvent(KeyCode::Esc, ModKeys::empty())); self
.collector
.push(KeyEvent(KeyCode::Esc, ModKeys::empty()));
return; return;
} }
@@ -914,13 +954,13 @@ impl LineWriter for TermWriter {
let end = new_layout.end; let end = new_layout.end;
let cursor = new_layout.cursor; let cursor = new_layout.cursor;
if read_meta(|m| m.system_msg_pending()) { if read_meta(|m| m.system_msg_pending()) {
let mut system_msg = String::new(); let mut system_msg = String::new();
while let Some(msg) = write_meta(|m| m.pop_system_message()) { while let Some(msg) = write_meta(|m| m.pop_system_message()) {
writeln!(system_msg, "{msg}").map_err(err)?; writeln!(system_msg, "{msg}").map_err(err)?;
} }
self.buffer.push_str(&system_msg); self.buffer.push_str(&system_msg);
} }
self.buffer.push_str(prompt); self.buffer.push_str(prompt);
self.buffer.push_str(line); self.buffer.push_str(line);

View File

@@ -161,14 +161,16 @@ impl ViCmd {
} }
/// If a ViCmd has a linewise motion, but no verb, we change it to charwise /// If a ViCmd has a linewise motion, but no verb, we change it to charwise
pub fn alter_line_motion_if_no_verb(&mut self) { pub fn alter_line_motion_if_no_verb(&mut self) {
if self.is_line_motion() && self.verb.is_none() if self.is_line_motion()
&& let Some(motion) = self.motion.as_mut() { && self.verb.is_none()
match motion.1 { && let Some(motion) = self.motion.as_mut()
Motion::LineUp => motion.1 = Motion::LineUpCharwise, {
Motion::LineDown => motion.1 = Motion::LineDownCharwise, match motion.1 {
_ => unreachable!(), Motion::LineUp => motion.1 = Motion::LineUpCharwise,
} Motion::LineDown => motion.1 = Motion::LineDownCharwise,
_ => unreachable!(),
} }
}
} }
pub fn is_mode_transition(&self) -> bool { pub fn is_mode_transition(&self) -> bool {
self.verb.as_ref().is_some_and(|v| { self.verb.as_ref().is_some_and(|v| {

View File

@@ -315,7 +315,7 @@ impl ViNormal {
return match obj { return match obj {
TextObj::Sentence(_) | TextObj::Paragraph(_) => CmdState::Complete, TextObj::Sentence(_) | TextObj::Paragraph(_) => CmdState::Complete,
_ => CmdState::Invalid, _ => CmdState::Invalid,
} };
} }
Some(_) => return CmdState::Complete, Some(_) => return CmdState::Complete,
None => return CmdState::Pending, None => return CmdState::Pending,
@@ -410,7 +410,7 @@ impl ViNormal {
motion: None, motion: None,
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: self.flags(), flags: self.flags(),
}) });
} }
'~' => { '~' => {
chars_clone.next(); chars_clone.next();
@@ -445,7 +445,7 @@ impl ViNormal {
motion: None, motion: None,
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: self.flags(), flags: self.flags(),
}) });
} }
'x' => { 'x' => {
return Some(ViCmd { return Some(ViCmd {
@@ -454,7 +454,7 @@ impl ViNormal {
motion: Some(MotionCmd(1, Motion::ForwardChar)), motion: Some(MotionCmd(1, Motion::ForwardChar)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: self.flags(), flags: self.flags(),
}) });
} }
'X' => { 'X' => {
return Some(ViCmd { return Some(ViCmd {
@@ -463,7 +463,7 @@ impl ViNormal {
motion: Some(MotionCmd(1, Motion::BackwardChar)), motion: Some(MotionCmd(1, Motion::BackwardChar)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: self.flags(), flags: self.flags(),
}) });
} }
's' => { 's' => {
return Some(ViCmd { return Some(ViCmd {
@@ -472,7 +472,7 @@ impl ViNormal {
motion: Some(MotionCmd(1, Motion::ForwardChar)), motion: Some(MotionCmd(1, Motion::ForwardChar)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: self.flags(), flags: self.flags(),
}) });
} }
'S' => { 'S' => {
return Some(ViCmd { return Some(ViCmd {
@@ -481,7 +481,7 @@ impl ViNormal {
motion: Some(MotionCmd(1, Motion::WholeLine)), motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: self.flags(), flags: self.flags(),
}) });
} }
'p' => { 'p' => {
chars = chars_clone; chars = chars_clone;
@@ -516,7 +516,7 @@ impl ViNormal {
motion: None, motion: None,
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: self.flags(), flags: self.flags(),
}) });
} }
'~' => { '~' => {
return Some(ViCmd { return Some(ViCmd {
@@ -525,7 +525,7 @@ impl ViNormal {
motion: None, motion: None,
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: self.flags(), flags: self.flags(),
}) });
} }
'u' => { 'u' => {
return Some(ViCmd { return Some(ViCmd {
@@ -534,7 +534,7 @@ impl ViNormal {
motion: None, motion: None,
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: self.flags(), flags: self.flags(),
}) });
} }
'v' => { 'v' => {
return Some(ViCmd { return Some(ViCmd {
@@ -543,7 +543,7 @@ impl ViNormal {
motion: None, motion: None,
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: self.flags(), flags: self.flags(),
}) });
} }
'V' => { 'V' => {
return Some(ViCmd { return Some(ViCmd {
@@ -552,7 +552,7 @@ impl ViNormal {
motion: None, motion: None,
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: self.flags(), flags: self.flags(),
}) });
} }
'o' => { 'o' => {
return Some(ViCmd { return Some(ViCmd {
@@ -561,7 +561,7 @@ impl ViNormal {
motion: None, motion: None,
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: self.flags(), flags: self.flags(),
}) });
} }
'O' => { 'O' => {
return Some(ViCmd { return Some(ViCmd {
@@ -570,7 +570,7 @@ impl ViNormal {
motion: None, motion: None,
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: self.flags(), flags: self.flags(),
}) });
} }
'a' => { 'a' => {
return Some(ViCmd { return Some(ViCmd {
@@ -579,7 +579,7 @@ impl ViNormal {
motion: Some(MotionCmd(1, Motion::ForwardChar)), motion: Some(MotionCmd(1, Motion::ForwardChar)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: self.flags(), flags: self.flags(),
}) });
} }
'A' => { 'A' => {
return Some(ViCmd { return Some(ViCmd {
@@ -588,7 +588,7 @@ impl ViNormal {
motion: Some(MotionCmd(1, Motion::EndOfLine)), motion: Some(MotionCmd(1, Motion::EndOfLine)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: self.flags(), flags: self.flags(),
}) });
} }
'i' => { 'i' => {
return Some(ViCmd { return Some(ViCmd {
@@ -597,7 +597,7 @@ impl ViNormal {
motion: None, motion: None,
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: self.flags(), flags: self.flags(),
}) });
} }
'I' => { 'I' => {
return Some(ViCmd { return Some(ViCmd {
@@ -606,7 +606,7 @@ impl ViNormal {
motion: Some(MotionCmd(1, Motion::BeginningOfFirstWord)), motion: Some(MotionCmd(1, Motion::BeginningOfFirstWord)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: self.flags(), flags: self.flags(),
}) });
} }
'J' => { 'J' => {
return Some(ViCmd { return Some(ViCmd {
@@ -615,7 +615,7 @@ impl ViNormal {
motion: None, motion: None,
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: self.flags(), flags: self.flags(),
}) });
} }
'y' => { 'y' => {
chars = chars_clone; chars = chars_clone;
@@ -636,7 +636,7 @@ impl ViNormal {
motion: Some(MotionCmd(1, Motion::EndOfLine)), motion: Some(MotionCmd(1, Motion::EndOfLine)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: self.flags(), flags: self.flags(),
}) });
} }
'D' => { 'D' => {
return Some(ViCmd { return Some(ViCmd {
@@ -645,7 +645,7 @@ impl ViNormal {
motion: Some(MotionCmd(1, Motion::EndOfLine)), motion: Some(MotionCmd(1, Motion::EndOfLine)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: self.flags(), flags: self.flags(),
}) });
} }
'C' => { 'C' => {
return Some(ViCmd { return Some(ViCmd {
@@ -654,7 +654,7 @@ impl ViNormal {
motion: Some(MotionCmd(1, Motion::EndOfLine)), motion: Some(MotionCmd(1, Motion::EndOfLine)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: self.flags(), flags: self.flags(),
}) });
} }
'=' => { '=' => {
chars = chars_clone; chars = chars_clone;
@@ -684,7 +684,7 @@ impl ViNormal {
| ('~', Some(VerbCmd(_, Verb::ToggleCaseRange))) | ('~', Some(VerbCmd(_, Verb::ToggleCaseRange)))
| ('>', Some(VerbCmd(_, Verb::Indent))) | ('>', Some(VerbCmd(_, Verb::Indent)))
| ('<', Some(VerbCmd(_, Verb::Dedent))) => { | ('<', Some(VerbCmd(_, Verb::Dedent))) => {
break 'motion_parse Some(MotionCmd(count, Motion::WholeLine)) break 'motion_parse Some(MotionCmd(count, Motion::WholeLine));
} }
('W', Some(VerbCmd(_, Verb::Change))) => { ('W', Some(VerbCmd(_, Verb::Change))) => {
// Same with 'W' // Same with 'W'
@@ -994,8 +994,7 @@ impl ViNormal {
} }
}; };
if chars.peek().is_some() { if chars.peek().is_some() {}
}
let verb_ref = verb.as_ref().map(|v| &v.1); let verb_ref = verb.as_ref().map(|v| &v.1);
let motion_ref = motion.as_ref().map(|m| &m.1); let motion_ref = motion.as_ref().map(|m| &m.1);
@@ -1185,7 +1184,7 @@ impl ViVisual {
motion: None, motion: None,
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}) });
} }
'?' => { '?' => {
return Some(ViCmd { return Some(ViCmd {
@@ -1194,7 +1193,7 @@ impl ViVisual {
motion: None, motion: None,
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}) });
} }
_ => break 'verb_parse None, _ => break 'verb_parse None,
} }
@@ -1209,7 +1208,7 @@ impl ViVisual {
motion: None, motion: None,
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}) });
} }
'x' => { 'x' => {
chars = chars_clone; chars = chars_clone;
@@ -1222,7 +1221,7 @@ impl ViVisual {
motion: Some(MotionCmd(1, Motion::WholeLine)), motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}) });
} }
'Y' => { 'Y' => {
return Some(ViCmd { return Some(ViCmd {
@@ -1231,7 +1230,7 @@ impl ViVisual {
motion: Some(MotionCmd(1, Motion::WholeLine)), motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}) });
} }
'D' => { 'D' => {
return Some(ViCmd { return Some(ViCmd {
@@ -1240,7 +1239,7 @@ impl ViVisual {
motion: Some(MotionCmd(1, Motion::WholeLine)), motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}) });
} }
'R' | 'C' => { 'R' | 'C' => {
return Some(ViCmd { return Some(ViCmd {
@@ -1249,7 +1248,7 @@ impl ViVisual {
motion: Some(MotionCmd(1, Motion::WholeLine)), motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}) });
} }
'>' => { '>' => {
return Some(ViCmd { return Some(ViCmd {
@@ -1258,7 +1257,7 @@ impl ViVisual {
motion: Some(MotionCmd(1, Motion::WholeLine)), motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}) });
} }
'<' => { '<' => {
return Some(ViCmd { return Some(ViCmd {
@@ -1267,7 +1266,7 @@ impl ViVisual {
motion: Some(MotionCmd(1, Motion::WholeLine)), motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}) });
} }
'=' => { '=' => {
return Some(ViCmd { return Some(ViCmd {
@@ -1276,7 +1275,7 @@ impl ViVisual {
motion: Some(MotionCmd(1, Motion::WholeLine)), motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}) });
} }
'p' | 'P' => { 'p' | 'P' => {
chars = chars_clone; chars = chars_clone;
@@ -1299,7 +1298,7 @@ impl ViVisual {
motion: None, motion: None,
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}) });
} }
'u' => { 'u' => {
return Some(ViCmd { return Some(ViCmd {
@@ -1308,7 +1307,7 @@ impl ViVisual {
motion: None, motion: None,
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}) });
} }
'U' => { 'U' => {
return Some(ViCmd { return Some(ViCmd {
@@ -1317,7 +1316,7 @@ impl ViVisual {
motion: None, motion: None,
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}) });
} }
'O' | 'o' => { 'O' | 'o' => {
return Some(ViCmd { return Some(ViCmd {
@@ -1326,7 +1325,7 @@ impl ViVisual {
motion: None, motion: None,
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}) });
} }
'A' => { 'A' => {
return Some(ViCmd { return Some(ViCmd {
@@ -1335,7 +1334,7 @@ impl ViVisual {
motion: Some(MotionCmd(1, Motion::ForwardChar)), motion: Some(MotionCmd(1, Motion::ForwardChar)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}) });
} }
'I' => { 'I' => {
return Some(ViCmd { return Some(ViCmd {
@@ -1344,7 +1343,7 @@ impl ViVisual {
motion: Some(MotionCmd(1, Motion::BeginningOfLine)), motion: Some(MotionCmd(1, Motion::BeginningOfLine)),
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}) });
} }
'J' => { 'J' => {
return Some(ViCmd { return Some(ViCmd {
@@ -1353,7 +1352,7 @@ impl ViVisual {
motion: None, motion: None,
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}) });
} }
'y' => { 'y' => {
chars = chars_clone; chars = chars_clone;
@@ -1395,7 +1394,7 @@ impl ViVisual {
| ('=', Some(VerbCmd(_, Verb::Equalize))) | ('=', Some(VerbCmd(_, Verb::Equalize)))
| ('>', Some(VerbCmd(_, Verb::Indent))) | ('>', Some(VerbCmd(_, Verb::Indent)))
| ('<', Some(VerbCmd(_, Verb::Dedent))) => { | ('<', Some(VerbCmd(_, Verb::Dedent))) => {
break 'motion_parse Some(MotionCmd(count, Motion::WholeLine)) break 'motion_parse Some(MotionCmd(count, Motion::WholeLine));
} }
_ => {} _ => {}
} }
@@ -1652,8 +1651,7 @@ impl ViVisual {
} }
}; };
if chars.peek().is_some() { if chars.peek().is_some() {}
}
let verb_ref = verb.as_ref().map(|v| &v.1); let verb_ref = verb.as_ref().map(|v| &v.1);
let motion_ref = motion.as_ref().map(|m| &m.1); let motion_ref = motion.as_ref().map(|m| &m.1);

View File

@@ -117,7 +117,7 @@ impl ShOpts {
Note::new("'shopt' takes arguments separated by periods to denote namespaces") Note::new("'shopt' takes arguments separated by periods to denote namespaces")
.with_sub_notes(vec!["Example: 'shopt core.autocd=true'"]), .with_sub_notes(vec!["Example: 'shopt core.autocd=true'"]),
), ),
) );
} }
} }
Ok(()) Ok(())
@@ -263,7 +263,7 @@ impl ShOptCore {
"max_recurse_depth", "max_recurse_depth",
]), ]),
), ),
) );
} }
} }
Ok(()) Ok(())
@@ -445,18 +445,20 @@ impl ShOptPrompt {
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
format!("shopt: Unexpected 'prompt' option '{opt}'"), format!("shopt: Unexpected 'prompt' option '{opt}'"),
) )
.with_note(Note::new("options can be accessed like 'prompt.option_name'")) .with_note(Note::new(
"options can be accessed like 'prompt.option_name'",
))
.with_note( .with_note(
Note::new("'prompt' contains the following options").with_sub_notes(vec![ Note::new("'prompt' contains the following options").with_sub_notes(vec![
"trunc_prompt_path", "trunc_prompt_path",
"edit_mode", "edit_mode",
"comp_limit", "comp_limit",
"highlight", "highlight",
"tab_stop", "tab_stop",
"custom", "custom",
]), ]),
), ),
) );
} }
} }
Ok(()) Ok(())

View File

@@ -3,7 +3,12 @@ use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU64, Ordering};
use nix::sys::signal::{SaFlags, SigAction, sigaction}; use nix::sys::signal::{SaFlags, SigAction, sigaction};
use crate::{ use crate::{
builtin::trap::TrapTarget, jobs::{JobCmdFlags, JobID, take_term}, libsh::error::{ShErr, ShErrKind, ShResult}, parse::execute::exec_input, prelude::*, state::{read_jobs, read_logic, write_jobs, write_meta} builtin::trap::TrapTarget,
jobs::{JobCmdFlags, JobID, take_term},
libsh::error::{ShErr, ShErrKind, ShResult},
parse::execute::exec_input,
prelude::*,
state::{read_jobs, read_logic, write_jobs, write_meta},
}; };
static SIGNALS: AtomicU64 = AtomicU64::new(0); static SIGNALS: AtomicU64 = AtomicU64::new(0);
@@ -12,92 +17,91 @@ pub static REAPING_ENABLED: AtomicBool = AtomicBool::new(true);
pub static SHOULD_QUIT: AtomicBool = AtomicBool::new(false); pub static SHOULD_QUIT: AtomicBool = AtomicBool::new(false);
pub static QUIT_CODE: AtomicI32 = AtomicI32::new(0); pub static QUIT_CODE: AtomicI32 = AtomicI32::new(0);
const MISC_SIGNALS: [Signal;22] = [ const MISC_SIGNALS: [Signal; 22] = [
Signal::SIGILL, Signal::SIGILL,
Signal::SIGTRAP, Signal::SIGTRAP,
Signal::SIGABRT, Signal::SIGABRT,
Signal::SIGBUS, Signal::SIGBUS,
Signal::SIGFPE, Signal::SIGFPE,
Signal::SIGUSR1, Signal::SIGUSR1,
Signal::SIGSEGV, Signal::SIGSEGV,
Signal::SIGUSR2, Signal::SIGUSR2,
Signal::SIGPIPE, Signal::SIGPIPE,
Signal::SIGALRM, Signal::SIGALRM,
Signal::SIGTERM, Signal::SIGTERM,
Signal::SIGSTKFLT, Signal::SIGSTKFLT,
Signal::SIGCONT, Signal::SIGCONT,
Signal::SIGURG, Signal::SIGURG,
Signal::SIGXCPU, Signal::SIGXCPU,
Signal::SIGXFSZ, Signal::SIGXFSZ,
Signal::SIGVTALRM, Signal::SIGVTALRM,
Signal::SIGPROF, Signal::SIGPROF,
Signal::SIGWINCH, Signal::SIGWINCH,
Signal::SIGIO, Signal::SIGIO,
Signal::SIGPWR, Signal::SIGPWR,
Signal::SIGSYS, Signal::SIGSYS,
]; ];
pub fn signals_pending() -> bool { pub fn signals_pending() -> bool {
SIGNALS.load(Ordering::SeqCst) != 0 || SHOULD_QUIT.load(Ordering::SeqCst) SIGNALS.load(Ordering::SeqCst) != 0 || SHOULD_QUIT.load(Ordering::SeqCst)
} }
pub fn check_signals() -> ShResult<()> { pub fn check_signals() -> ShResult<()> {
let pending = SIGNALS.swap(0, Ordering::SeqCst); let pending = SIGNALS.swap(0, Ordering::SeqCst);
let got_signal = |sig: Signal| -> bool { pending & (1 << sig as u64) != 0 }; let got_signal = |sig: Signal| -> bool { pending & (1 << sig as u64) != 0 };
let run_trap = |sig: Signal| -> ShResult<()> { let run_trap = |sig: Signal| -> ShResult<()> {
if let Some(command) = read_logic(|l| l.get_trap(TrapTarget::Signal(sig))) { if let Some(command) = read_logic(|l| l.get_trap(TrapTarget::Signal(sig))) {
exec_input(command, None, false)?; exec_input(command, None, false)?;
} }
Ok(()) Ok(())
}; };
if got_signal(Signal::SIGINT) { if got_signal(Signal::SIGINT) {
interrupt()?; interrupt()?;
run_trap(Signal::SIGINT)?; run_trap(Signal::SIGINT)?;
return Err(ShErr::simple(ShErrKind::ClearReadline, "")); return Err(ShErr::simple(ShErrKind::ClearReadline, ""));
} }
if got_signal(Signal::SIGHUP) { if got_signal(Signal::SIGHUP) {
run_trap(Signal::SIGHUP)?; run_trap(Signal::SIGHUP)?;
hang_up(0); hang_up(0);
} }
if got_signal(Signal::SIGQUIT) { if got_signal(Signal::SIGQUIT) {
run_trap(Signal::SIGQUIT)?; run_trap(Signal::SIGQUIT)?;
hang_up(0); hang_up(0);
} }
if got_signal(Signal::SIGTSTP) { if got_signal(Signal::SIGTSTP) {
run_trap(Signal::SIGTSTP)?; run_trap(Signal::SIGTSTP)?;
terminal_stop()?; terminal_stop()?;
} }
if got_signal(Signal::SIGCHLD) && REAPING_ENABLED.load(Ordering::SeqCst) { if got_signal(Signal::SIGCHLD) && REAPING_ENABLED.load(Ordering::SeqCst) {
run_trap(Signal::SIGCHLD)?; run_trap(Signal::SIGCHLD)?;
wait_child()?; wait_child()?;
} }
for sig in MISC_SIGNALS { for sig in MISC_SIGNALS {
if got_signal(sig) { if got_signal(sig) {
run_trap(sig)?; run_trap(sig)?;
} }
} }
if SHOULD_QUIT.load(Ordering::SeqCst) { if SHOULD_QUIT.load(Ordering::SeqCst) {
let code = QUIT_CODE.load(Ordering::SeqCst); let code = QUIT_CODE.load(Ordering::SeqCst);
return Err(ShErr::simple(ShErrKind::CleanExit(code), "exit")); return Err(ShErr::simple(ShErrKind::CleanExit(code), "exit"));
} }
Ok(()) Ok(())
} }
pub fn disable_reaping() { pub fn disable_reaping() {
REAPING_ENABLED.store(false, Ordering::SeqCst); REAPING_ENABLED.store(false, Ordering::SeqCst);
} }
pub fn enable_reaping() { pub fn enable_reaping() {
REAPING_ENABLED.store(true, Ordering::SeqCst); REAPING_ENABLED.store(true, Ordering::SeqCst);
} }
pub fn sig_setup() { pub fn sig_setup() {
let flags = SaFlags::empty(); let flags = SaFlags::empty();
let action = SigAction::new(SigHandler::Handler(handle_signal), flags, SigSet::empty());
let action = SigAction::new(SigHandler::Handler(handle_signal), flags, SigSet::empty());
let ignore = SigAction::new(SigHandler::SigIgn, flags, SigSet::empty()); let ignore = SigAction::new(SigHandler::SigIgn, flags, SigSet::empty());
@@ -136,12 +140,12 @@ pub fn sig_setup() {
} }
extern "C" fn handle_signal(sig: libc::c_int) { extern "C" fn handle_signal(sig: libc::c_int) {
SIGNALS.fetch_or(1 << sig, Ordering::SeqCst); SIGNALS.fetch_or(1 << sig, Ordering::SeqCst);
} }
pub fn hang_up(_: libc::c_int) { pub fn hang_up(_: libc::c_int) {
SHOULD_QUIT.store(true, Ordering::SeqCst); SHOULD_QUIT.store(true, Ordering::SeqCst);
QUIT_CODE.store(1, Ordering::SeqCst); QUIT_CODE.store(1, Ordering::SeqCst);
write_jobs(|j| { write_jobs(|j| {
for job in j.jobs_mut().iter_mut().flatten() { for job in j.jobs_mut().iter_mut().flatten() {
job.killpg(Signal::SIGTERM).ok(); job.killpg(Signal::SIGTERM).ok();
@@ -154,10 +158,10 @@ pub fn terminal_stop() -> ShResult<()> {
if let Some(job) = j.get_fg_mut() { if let Some(job) = j.get_fg_mut() {
job.killpg(Signal::SIGTSTP) job.killpg(Signal::SIGTSTP)
} else { } else {
Ok(()) Ok(())
} }
}) })
// TODO: It seems like there is supposed to be a take_term() call here // TODO: It seems like there is supposed to be a take_term() call here
} }
pub fn interrupt() -> ShResult<()> { pub fn interrupt() -> ShResult<()> {
@@ -269,19 +273,19 @@ pub fn child_exited(pid: Pid, status: WtStat) -> ShResult<()> {
} else { } else {
None None
} }
}) }) && is_finished
&& is_finished { {
if is_fg { if is_fg {
take_term()?; take_term()?;
} else { } else {
println!(); println!();
let job_order = read_jobs(|j| j.order().to_vec()); let job_order = read_jobs(|j| j.order().to_vec());
let result = read_jobs(|j| j.query(JobID::Pgid(pgid)).cloned()); let result = read_jobs(|j| j.query(JobID::Pgid(pgid)).cloned());
if let Some(job) = result { if let Some(job) = result {
let job_complete_msg = job.display(&job_order, JobCmdFlags::PIDS).to_string(); let job_complete_msg = job.display(&job_order, JobCmdFlags::PIDS).to_string();
write_meta(|m| m.post_system_message(job_complete_msg)) write_meta(|m| m.post_system_message(job_complete_msg))
}
} }
} }
}
Ok(()) Ok(())
} }

View File

@@ -1,249 +1,262 @@
use std::{ use std::{
cell::RefCell, collections::{HashMap, VecDeque}, fmt::Display, ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref}, str::FromStr, time::Duration cell::RefCell,
collections::{HashMap, VecDeque},
fmt::Display,
ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref},
str::FromStr,
time::Duration,
}; };
use nix::unistd::{gethostname, getppid, User}; use nix::unistd::{User, gethostname, getppid};
use crate::{ use crate::{
builtin::trap::TrapTarget, exec_input, jobs::JobTab, libsh::{ builtin::trap::TrapTarget,
exec_input,
jobs::JobTab,
libsh::{
error::{ShErr, ShErrKind, ShResult}, error::{ShErr, ShErrKind, ShResult},
utils::VecDequeExt, utils::VecDequeExt,
}, parse::{ConjunctNode, NdRule, Node, ParsedSrc}, prelude::*, shopt::ShOpts },
parse::{ConjunctNode, NdRule, Node, ParsedSrc},
prelude::*,
shopt::ShOpts,
}; };
pub struct Fern { pub struct Fern {
pub jobs: RefCell<JobTab>, pub jobs: RefCell<JobTab>,
pub var_scopes: RefCell<ScopeStack>, pub var_scopes: RefCell<ScopeStack>,
pub meta: RefCell<MetaTab>, pub meta: RefCell<MetaTab>,
pub logic: RefCell<LogTab>, pub logic: RefCell<LogTab>,
pub shopts: RefCell<ShOpts>, pub shopts: RefCell<ShOpts>,
} }
impl Fern { impl Fern {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
jobs: RefCell::new(JobTab::new()), jobs: RefCell::new(JobTab::new()),
var_scopes: RefCell::new(ScopeStack::new()), var_scopes: RefCell::new(ScopeStack::new()),
meta: RefCell::new(MetaTab::new()), meta: RefCell::new(MetaTab::new()),
logic: RefCell::new(LogTab::new()), logic: RefCell::new(LogTab::new()),
shopts: RefCell::new(ShOpts::default()), shopts: RefCell::new(ShOpts::default()),
} }
} }
} }
impl Default for Fern { impl Default for Fern {
fn default() -> Self { fn default() -> Self {
Self::new() Self::new()
} }
} }
#[derive(Hash, Eq, PartialEq, Debug, Clone, Copy)] #[derive(Hash, Eq, PartialEq, Debug, Clone, Copy)]
pub enum ShellParam { pub enum ShellParam {
// Global // Global
Status, Status,
ShPid, ShPid,
LastJob, LastJob,
ShellName, ShellName,
// Local // Local
Pos(usize), Pos(usize),
AllArgs, AllArgs,
AllArgsStr, AllArgsStr,
ArgCount ArgCount,
} }
impl ShellParam { impl ShellParam {
pub fn is_global(&self) -> bool { pub fn is_global(&self) -> bool {
matches!( matches!(
self, self,
Self::Status | Self::ShPid | Self::LastJob | Self::ShellName Self::Status | Self::ShPid | Self::LastJob | Self::ShellName
) )
} }
} }
impl Display for ShellParam { impl Display for ShellParam {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Self::Status => write!(f, "?"), Self::Status => write!(f, "?"),
Self::ShPid => write!(f, "$"), Self::ShPid => write!(f, "$"),
Self::LastJob => write!(f, "!"), Self::LastJob => write!(f, "!"),
Self::ShellName => write!(f, "0"), Self::ShellName => write!(f, "0"),
Self::Pos(n) => write!(f, "{}", n), Self::Pos(n) => write!(f, "{}", n),
Self::AllArgs => write!(f, "@"), Self::AllArgs => write!(f, "@"),
Self::AllArgsStr => write!(f, "*"), Self::AllArgsStr => write!(f, "*"),
Self::ArgCount => write!(f, "#"), Self::ArgCount => write!(f, "#"),
} }
} }
} }
impl FromStr for ShellParam { impl FromStr for ShellParam {
type Err = ShErr; type Err = ShErr;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
match s { match s {
"?" => Ok(Self::Status), "?" => Ok(Self::Status),
"$" => Ok(Self::ShPid), "$" => Ok(Self::ShPid),
"!" => Ok(Self::LastJob), "!" => Ok(Self::LastJob),
"0" => Ok(Self::ShellName), "0" => Ok(Self::ShellName),
"@" => Ok(Self::AllArgs), "@" => Ok(Self::AllArgs),
"*" => Ok(Self::AllArgsStr), "*" => Ok(Self::AllArgsStr),
"#" => Ok(Self::ArgCount), "#" => Ok(Self::ArgCount),
n if n.parse::<usize>().is_ok() => { n if n.parse::<usize>().is_ok() => {
let idx = n.parse::<usize>().unwrap(); let idx = n.parse::<usize>().unwrap();
Ok(Self::Pos(idx)) Ok(Self::Pos(idx))
} }
_ => Err(ShErr::simple( _ => Err(ShErr::simple(
ShErrKind::InternalErr, ShErrKind::InternalErr,
format!("Invalid shell parameter: {}", s), format!("Invalid shell parameter: {}", s),
)), )),
} }
} }
} }
#[derive(Clone, Default, Debug)] #[derive(Clone, Default, Debug)]
pub struct ScopeStack { pub struct ScopeStack {
// ALWAYS keep one scope. // ALWAYS keep one scope.
// The bottom scope is the global variable space. // The bottom scope is the global variable space.
// Scopes that come after that are pushed in functions, // Scopes that come after that are pushed in functions,
// and only contain variables that are defined using `local`. // and only contain variables that are defined using `local`.
scopes: Vec<VarTab>, scopes: Vec<VarTab>,
depth: u32, depth: u32,
// Global parameters such as $?, $!, $$, etc // Global parameters such as $?, $!, $$, etc
global_params: HashMap<String, String>, global_params: HashMap<String, String>,
} }
impl ScopeStack { impl ScopeStack {
pub fn new() -> Self { pub fn new() -> Self {
let mut new = Self::default(); let mut new = Self::default();
new.scopes.push(VarTab::new()); new.scopes.push(VarTab::new());
let shell_name = std::env::args().next().unwrap_or_else(|| "fern".to_string()); let shell_name = std::env::args()
new.global_params.insert(ShellParam::ShellName.to_string(), shell_name); .next()
new .unwrap_or_else(|| "fern".to_string());
} new
pub fn descend(&mut self, argv: Option<Vec<String>>) { .global_params
let mut new_vars = VarTab::new(); .insert(ShellParam::ShellName.to_string(), shell_name);
if let Some(argv) = argv { new
for arg in argv { }
new_vars.bpush_arg(arg); pub fn descend(&mut self, argv: Option<Vec<String>>) {
} let mut new_vars = VarTab::new();
} if let Some(argv) = argv {
self.scopes.push(new_vars); for arg in argv {
self.depth += 1; new_vars.bpush_arg(arg);
} }
pub fn ascend(&mut self) { }
if self.depth >= 1 { self.scopes.push(new_vars);
self.scopes.pop(); self.depth += 1;
self.depth -= 1; }
} pub fn ascend(&mut self) {
} if self.depth >= 1 {
pub fn cur_scope(&self) -> &VarTab { self.scopes.pop();
self.scopes.last().unwrap() self.depth -= 1;
} }
pub fn cur_scope_mut(&mut self) -> &mut VarTab { }
self.scopes.last_mut().unwrap() pub fn cur_scope(&self) -> &VarTab {
} self.scopes.last().unwrap()
pub fn unset_var(&mut self, var_name: &str) { }
for scope in self.scopes.iter_mut().rev() { pub fn cur_scope_mut(&mut self) -> &mut VarTab {
if scope.var_exists(var_name) { self.scopes.last_mut().unwrap()
scope.unset_var(var_name); }
return; pub fn unset_var(&mut self, var_name: &str) {
} for scope in self.scopes.iter_mut().rev() {
} if scope.var_exists(var_name) {
} scope.unset_var(var_name);
pub fn export_var(&mut self, var_name: &str) { return;
for scope in self.scopes.iter_mut().rev() { }
if scope.var_exists(var_name) { }
scope.export_var(var_name); }
return; pub fn export_var(&mut self, var_name: &str) {
} for scope in self.scopes.iter_mut().rev() {
} if scope.var_exists(var_name) {
} scope.export_var(var_name);
pub fn var_exists(&self, var_name: &str) -> bool { return;
for scope in self.scopes.iter().rev() { }
if scope.var_exists(var_name) { }
return true; }
} pub fn var_exists(&self, var_name: &str) -> bool {
} for scope in self.scopes.iter().rev() {
if let Ok(param) = var_name.parse::<ShellParam>() { if scope.var_exists(var_name) {
return self.global_params.contains_key(&param.to_string()); return true;
} }
false }
} if let Ok(param) = var_name.parse::<ShellParam>() {
pub fn flatten_vars(&self) -> HashMap<String, Var> { return self.global_params.contains_key(&param.to_string());
let mut flat_vars = HashMap::new(); }
for scope in self.scopes.iter() { false
for (var_name, var) in scope.vars() { }
flat_vars.insert(var_name.clone(), var.clone()); pub fn flatten_vars(&self) -> HashMap<String, Var> {
} let mut flat_vars = HashMap::new();
} for scope in self.scopes.iter() {
flat_vars for (var_name, var) in scope.vars() {
} flat_vars.insert(var_name.clone(), var.clone());
pub fn set_var(&mut self, var_name: &str, val: &str, flags: VarFlags) { }
if flags.contains(VarFlags::LOCAL) { }
self.set_var_local(var_name, val, flags); flat_vars
} else { }
self.set_var_global(var_name, val, flags); pub fn set_var(&mut self, var_name: &str, val: &str, flags: VarFlags) {
} if flags.contains(VarFlags::LOCAL) {
} self.set_var_local(var_name, val, flags);
fn set_var_global(&mut self, var_name: &str, val: &str, flags: VarFlags) { } else {
if let Some(scope) = self.scopes.first_mut() { self.set_var_global(var_name, val, flags);
scope.set_var(var_name, val, flags); }
} }
} fn set_var_global(&mut self, var_name: &str, val: &str, flags: VarFlags) {
fn set_var_local(&mut self, var_name: &str, val: &str, flags: VarFlags) { if let Some(scope) = self.scopes.first_mut() {
if let Some(scope) = self.scopes.last_mut() { scope.set_var(var_name, val, flags);
scope.set_var(var_name, val, flags); }
} }
} fn set_var_local(&mut self, var_name: &str, val: &str, flags: VarFlags) {
pub fn get_var(&self, var_name: &str) -> String { if let Some(scope) = self.scopes.last_mut() {
if let Ok(param) = var_name.parse::<ShellParam>() { scope.set_var(var_name, val, flags);
return self.get_param(param); }
} }
for scope in self.scopes.iter().rev() { pub fn get_var(&self, var_name: &str) -> String {
if scope.var_exists(var_name) { if let Ok(param) = var_name.parse::<ShellParam>() {
return scope.get_var(var_name); return self.get_param(param);
} }
} for scope in self.scopes.iter().rev() {
// Fallback to env var if scope.var_exists(var_name) {
std::env::var(var_name).unwrap_or_default() return scope.get_var(var_name);
} }
pub fn get_param(&self, param: ShellParam) -> String { }
if param.is_global() && let Some(val) = self.global_params.get(&param.to_string()) { // Fallback to env var
return val.clone(); std::env::var(var_name).unwrap_or_default()
} }
for scope in self.scopes.iter().rev() { pub fn get_param(&self, param: ShellParam) -> String {
let val = scope.get_param(param); if param.is_global()
if !val.is_empty() { && let Some(val) = self.global_params.get(&param.to_string())
return val; {
} return val.clone();
} }
// Fallback to empty string for scope in self.scopes.iter().rev() {
"".into() let val = scope.get_param(param);
} if !val.is_empty() {
/// Set a shell parameter return val;
/// Therefore, these are global state and we use the global scope }
pub fn set_param(&mut self, param: ShellParam, val: &str) { }
match param { // Fallback to empty string
ShellParam::ShPid | "".into()
ShellParam::Status | }
ShellParam::LastJob | /// Set a shell parameter
ShellParam::ShellName => { /// Therefore, these are global state and we use the global scope
self.global_params.insert(param.to_string(), val.to_string()); pub fn set_param(&mut self, param: ShellParam, val: &str) {
} match param {
ShellParam::Pos(_) | ShellParam::ShPid | ShellParam::Status | ShellParam::LastJob | ShellParam::ShellName => {
ShellParam::AllArgs | self
ShellParam::AllArgsStr | .global_params
ShellParam::ArgCount => { .insert(param.to_string(), val.to_string());
if let Some(scope) = self.scopes.first_mut() { }
scope.set_param(param, val); ShellParam::Pos(_) | ShellParam::AllArgs | ShellParam::AllArgsStr | ShellParam::ArgCount => {
} if let Some(scope) = self.scopes.first_mut() {
} scope.set_param(param, val);
} }
} }
}
}
} }
thread_local! { thread_local! {
pub static FERN: Fern = Fern::new(); pub static FERN: Fern = Fern::new();
} }
/// A shell function /// A shell function
@@ -287,7 +300,7 @@ impl Deref for ShFunc {
pub struct LogTab { pub struct LogTab {
functions: HashMap<String, ShFunc>, functions: HashMap<String, ShFunc>,
aliases: HashMap<String, String>, aliases: HashMap<String, String>,
traps: HashMap<TrapTarget, String>, traps: HashMap<TrapTarget, String>,
} }
impl LogTab { impl LogTab {
@@ -297,18 +310,18 @@ impl LogTab {
pub fn insert_func(&mut self, name: &str, src: ShFunc) { pub fn insert_func(&mut self, name: &str, src: ShFunc) {
self.functions.insert(name.into(), src); self.functions.insert(name.into(), src);
} }
pub fn insert_trap(&mut self, target: TrapTarget, command: String) { pub fn insert_trap(&mut self, target: TrapTarget, command: String) {
self.traps.insert(target, command); self.traps.insert(target, command);
} }
pub fn get_trap(&self, target: TrapTarget) -> Option<String> { pub fn get_trap(&self, target: TrapTarget) -> Option<String> {
self.traps.get(&target).cloned() self.traps.get(&target).cloned()
} }
pub fn remove_trap(&mut self, target: TrapTarget) { pub fn remove_trap(&mut self, target: TrapTarget) {
self.traps.remove(&target); self.traps.remove(&target);
} }
pub fn traps(&self) -> &HashMap<TrapTarget, String> { pub fn traps(&self) -> &HashMap<TrapTarget, String> {
&self.traps &self.traps
} }
pub fn get_func(&self, name: &str) -> Option<ShFunc> { pub fn get_func(&self, name: &str) -> Option<ShFunc> {
self.functions.get(name).cloned() self.functions.get(name).cloned()
} }
@@ -339,103 +352,103 @@ impl LogTab {
pub struct VarFlags(u8); pub struct VarFlags(u8);
impl VarFlags { impl VarFlags {
pub const NONE : Self = Self(0); pub const NONE: Self = Self(0);
pub const EXPORT : Self = Self(1 << 0); pub const EXPORT: Self = Self(1 << 0);
pub const LOCAL : Self = Self(1 << 1); pub const LOCAL: Self = Self(1 << 1);
pub const READONLY : Self = Self(1 << 2); pub const READONLY: Self = Self(1 << 2);
} }
impl BitOr for VarFlags { impl BitOr for VarFlags {
type Output = Self; type Output = Self;
fn bitor(self, rhs: Self) -> Self::Output { fn bitor(self, rhs: Self) -> Self::Output {
Self(self.0 | rhs.0) Self(self.0 | rhs.0)
} }
} }
impl BitOrAssign for VarFlags { impl BitOrAssign for VarFlags {
fn bitor_assign(&mut self, rhs: Self) { fn bitor_assign(&mut self, rhs: Self) {
self.0 |= rhs.0; self.0 |= rhs.0;
} }
} }
impl BitAnd for VarFlags { impl BitAnd for VarFlags {
type Output = Self; type Output = Self;
fn bitand(self, rhs: Self) -> Self::Output { fn bitand(self, rhs: Self) -> Self::Output {
Self(self.0 & rhs.0) Self(self.0 & rhs.0)
} }
} }
impl BitAndAssign for VarFlags { impl BitAndAssign for VarFlags {
fn bitand_assign(&mut self, rhs: Self) { fn bitand_assign(&mut self, rhs: Self) {
self.0 &= rhs.0; self.0 &= rhs.0;
} }
} }
impl VarFlags { impl VarFlags {
pub fn contains(&self, flag: Self) -> bool { pub fn contains(&self, flag: Self) -> bool {
(self.0 & flag.0) == flag.0 (self.0 & flag.0) == flag.0
} }
pub fn intersects(&self, flag: Self) -> bool { pub fn intersects(&self, flag: Self) -> bool {
(self.0 & flag.0) != 0 (self.0 & flag.0) != 0
} }
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.0 == 0 self.0 == 0
} }
pub fn insert(&mut self, flag: Self) { pub fn insert(&mut self, flag: Self) {
self.0 |= flag.0; self.0 |= flag.0;
} }
pub fn remove(&mut self, flag: Self) { pub fn remove(&mut self, flag: Self) {
self.0 &= !flag.0; self.0 &= !flag.0;
} }
pub fn toggle(&mut self, flag: Self) { pub fn toggle(&mut self, flag: Self) {
self.0 ^= flag.0; self.0 ^= flag.0;
} }
pub fn set(&mut self, flag: Self, value: bool) { pub fn set(&mut self, flag: Self, value: bool) {
if value { if value {
self.insert(flag); self.insert(flag);
} else { } else {
self.remove(flag); self.remove(flag);
} }
} }
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub enum VarKind { pub enum VarKind {
Str(String), Str(String),
Int(i32), Int(i32),
Arr(Vec<String>), Arr(Vec<String>),
AssocArr(Vec<(String, String)>), AssocArr(Vec<(String, String)>),
} }
impl Display for VarKind { impl Display for VarKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
VarKind::Str(s) => write!(f, "{s}"), VarKind::Str(s) => write!(f, "{s}"),
VarKind::Int(i) => write!(f, "{i}"), VarKind::Int(i) => write!(f, "{i}"),
VarKind::Arr(items) => { VarKind::Arr(items) => {
let mut item_iter = items.iter().peekable(); let mut item_iter = items.iter().peekable();
while let Some(item) = item_iter.next() { while let Some(item) = item_iter.next() {
write!(f, "{item}")?; write!(f, "{item}")?;
if item_iter.peek().is_some() { if item_iter.peek().is_some() {
write!(f, " ")?; write!(f, " ")?;
} }
} }
Ok(()) Ok(())
} }
VarKind::AssocArr(items) => { VarKind::AssocArr(items) => {
let mut item_iter = items.iter().peekable(); let mut item_iter = items.iter().peekable();
while let Some(item) = item_iter.next() { while let Some(item) = item_iter.next() {
let (k,v) = item; let (k, v) = item;
write!(f, "{k}={v}")?; write!(f, "{k}={v}")?;
if item_iter.peek().is_some() { if item_iter.peek().is_some() {
write!(f, " ")?; write!(f, " ")?;
} }
} }
Ok(()) Ok(())
} }
} }
} }
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@@ -446,26 +459,23 @@ pub struct Var {
impl Var { impl Var {
pub fn new(kind: VarKind, flags: VarFlags) -> Self { pub fn new(kind: VarKind, flags: VarFlags) -> Self {
Self { Self { flags, kind }
flags, }
kind pub fn kind(&self) -> &VarKind {
} &self.kind
}
pub fn kind_mut(&mut self) -> &mut VarKind {
&mut self.kind
} }
pub fn kind(&self) -> &VarKind {
&self.kind
}
pub fn kind_mut(&mut self) -> &mut VarKind {
&mut self.kind
}
pub fn mark_for_export(&mut self) { pub fn mark_for_export(&mut self) {
self.flags.set(VarFlags::EXPORT, true); self.flags.set(VarFlags::EXPORT, true);
} }
} }
impl Display for Var { impl Display for Var {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.kind.fmt(f) self.kind.fmt(f)
} }
} }
#[derive(Default, Clone, Debug)] #[derive(Default, Clone, Debug)]
@@ -528,23 +538,23 @@ impl VarTab {
.map(|hname| hname.to_string_lossy().to_string()) .map(|hname| hname.to_string_lossy().to_string())
.unwrap_or_default(); .unwrap_or_default();
unsafe { unsafe {
env::set_var("IFS", " \t\n"); env::set_var("IFS", " \t\n");
env::set_var("HOST", hostname.clone()); env::set_var("HOST", hostname.clone());
env::set_var("UID", uid.to_string()); env::set_var("UID", uid.to_string());
env::set_var("PPID", getppid().to_string()); env::set_var("PPID", getppid().to_string());
env::set_var("TMPDIR", "/tmp"); env::set_var("TMPDIR", "/tmp");
env::set_var("TERM", term); env::set_var("TERM", term);
env::set_var("LANG", "en_US.UTF-8"); env::set_var("LANG", "en_US.UTF-8");
env::set_var("USER", username.clone()); env::set_var("USER", username.clone());
env::set_var("LOGNAME", username); env::set_var("LOGNAME", username);
env::set_var("PWD", pathbuf_to_string(std::env::current_dir())); env::set_var("PWD", pathbuf_to_string(std::env::current_dir()));
env::set_var("OLDPWD", pathbuf_to_string(std::env::current_dir())); env::set_var("OLDPWD", pathbuf_to_string(std::env::current_dir()));
env::set_var("HOME", home.clone()); env::set_var("HOME", home.clone());
env::set_var("SHELL", pathbuf_to_string(std::env::current_exe())); env::set_var("SHELL", pathbuf_to_string(std::env::current_exe()));
env::set_var("FERN_HIST", format!("{}/.fernhist", home)); env::set_var("FERN_HIST", format!("{}/.fernhist", home));
env::set_var("FERN_RC", format!("{}/.fernrc", home)); env::set_var("FERN_RC", format!("{}/.fernrc", home));
} }
} }
pub fn init_sh_argv(&mut self) { pub fn init_sh_argv(&mut self) {
for arg in env::args() { for arg in env::args() {
@@ -575,7 +585,10 @@ impl VarTab {
self.bpush_arg(env::current_exe().unwrap().to_str().unwrap().to_string()); self.bpush_arg(env::current_exe().unwrap().to_str().unwrap().to_string());
} }
fn update_arg_params(&mut self) { fn update_arg_params(&mut self) {
self.set_param(ShellParam::AllArgs, &self.sh_argv.clone().to_vec()[1..].join(" ")); self.set_param(
ShellParam::AllArgs,
&self.sh_argv.clone().to_vec()[1..].join(" "),
);
self.set_param(ShellParam::ArgCount, &(self.sh_argv.len() - 1).to_string()); self.set_param(ShellParam::ArgCount, &(self.sh_argv.len() - 1).to_string());
} }
/// Push an arg to the front of the arg deque /// Push an arg to the front of the arg deque
@@ -619,29 +632,29 @@ impl VarTab {
} }
} }
pub fn get_var(&self, var: &str) -> String { pub fn get_var(&self, var: &str) -> String {
if let Ok(param) = var.parse::<ShellParam>() { if let Ok(param) = var.parse::<ShellParam>() {
let param = self.get_param(param); let param = self.get_param(param);
if !param.is_empty() { if !param.is_empty() {
return param; return param;
} }
} }
if let Some(var) = self.vars.get(var).map(|s| s.to_string()) { if let Some(var) = self.vars.get(var).map(|s| s.to_string()) {
var var
} else { } else {
std::env::var(var).unwrap_or_default() std::env::var(var).unwrap_or_default()
} }
} }
pub fn unset_var(&mut self, var_name: &str) { pub fn unset_var(&mut self, var_name: &str) {
self.vars.remove(var_name); self.vars.remove(var_name);
unsafe { env::remove_var(var_name) }; unsafe { env::remove_var(var_name) };
} }
pub fn set_var(&mut self, var_name: &str, val: &str, flags: VarFlags) { pub fn set_var(&mut self, var_name: &str, val: &str, flags: VarFlags) {
if let Some(var) = self.vars.get_mut(var_name) { if let Some(var) = self.vars.get_mut(var_name) {
var.kind = VarKind::Str(val.to_string()); var.kind = VarKind::Str(val.to_string());
if var.flags.contains(VarFlags::EXPORT) || flags.contains(VarFlags::EXPORT) { if var.flags.contains(VarFlags::EXPORT) || flags.contains(VarFlags::EXPORT) {
if flags.contains(VarFlags::EXPORT) && !var.flags.contains(VarFlags::EXPORT) { if flags.contains(VarFlags::EXPORT) && !var.flags.contains(VarFlags::EXPORT) {
var.mark_for_export(); var.mark_for_export();
} }
unsafe { env::set_var(var_name, val) }; unsafe { env::set_var(var_name, val) };
} }
} else { } else {
@@ -663,39 +676,35 @@ impl VarTab {
self.params.insert(param, val.to_string()); self.params.insert(param, val.to_string());
} }
pub fn get_param(&self, param: ShellParam) -> String { pub fn get_param(&self, param: ShellParam) -> String {
match param { match param {
ShellParam::Pos(n) => { ShellParam::Pos(n) => self
self .sh_argv()
.sh_argv() .get(n)
.get(n) .map(|s| s.to_string())
.map(|s| s.to_string()) .unwrap_or_default(),
.unwrap_or_default() ShellParam::Status => self
} .params
ShellParam::Status => { .get(&ShellParam::Status)
self .map(|s| s.to_string())
.params .unwrap_or("0".into()),
.get(&ShellParam::Status) _ => self
.map(|s| s.to_string()) .params
.unwrap_or("0".into()) .get(&param)
} .map(|s| s.to_string())
_ => self .unwrap_or_default(),
.params }
.get(&param)
.map(|s| s.to_string())
.unwrap_or_default(),
}
} }
} }
/// A table of metadata for the shell /// A table of metadata for the shell
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct MetaTab { pub struct MetaTab {
// command running duration // command running duration
runtime_start: Option<Instant>, runtime_start: Option<Instant>,
runtime_stop: Option<Instant>, runtime_stop: Option<Instant>,
// pending system messages // pending system messages
system_msg: Vec<String> system_msg: Vec<String>,
} }
impl MetaTab { impl MetaTab {
@@ -708,76 +717,76 @@ impl MetaTab {
pub fn stop_timer(&mut self) { pub fn stop_timer(&mut self) {
self.runtime_stop = Some(Instant::now()); self.runtime_stop = Some(Instant::now());
} }
pub fn get_time(&self) -> Option<Duration> { pub fn get_time(&self) -> Option<Duration> {
if let (Some(start), Some(stop)) = (self.runtime_start, self.runtime_stop) { if let (Some(start), Some(stop)) = (self.runtime_start, self.runtime_stop) {
Some(stop.duration_since(start)) Some(stop.duration_since(start))
} else { } else {
None None
} }
} }
pub fn post_system_message(&mut self, message: String) { pub fn post_system_message(&mut self, message: String) {
self.system_msg.push(message); self.system_msg.push(message);
} }
pub fn pop_system_message(&mut self) -> Option<String> { pub fn pop_system_message(&mut self) -> Option<String> {
self.system_msg.pop() self.system_msg.pop()
} }
pub fn system_msg_pending(&self) -> bool { pub fn system_msg_pending(&self) -> bool {
!self.system_msg.is_empty() !self.system_msg.is_empty()
} }
} }
/// Read from the job table /// Read from the job table
pub fn read_jobs<T, F: FnOnce(&JobTab) -> T>(f: F) -> T { pub fn read_jobs<T, F: FnOnce(&JobTab) -> T>(f: F) -> T {
FERN.with(|fern| f(&fern.jobs.borrow())) FERN.with(|fern| f(&fern.jobs.borrow()))
} }
/// Write to the job table /// Write to the job table
pub fn write_jobs<T, F: FnOnce(&mut JobTab) -> T>(f: F) -> T { pub fn write_jobs<T, F: FnOnce(&mut JobTab) -> T>(f: F) -> T {
FERN.with(|fern| f(&mut fern.jobs.borrow_mut())) FERN.with(|fern| f(&mut fern.jobs.borrow_mut()))
} }
/// Read from the var scope stack /// Read from the var scope stack
pub fn read_vars<T, F: FnOnce(&ScopeStack) -> T>(f: F) -> T { pub fn read_vars<T, F: FnOnce(&ScopeStack) -> T>(f: F) -> T {
FERN.with(|fern| f(&fern.var_scopes.borrow())) FERN.with(|fern| f(&fern.var_scopes.borrow()))
} }
/// Write to the variable table /// Write to the variable table
pub fn write_vars<T, F: FnOnce(&mut ScopeStack) -> T>(f: F) -> T { pub fn write_vars<T, F: FnOnce(&mut ScopeStack) -> T>(f: F) -> T {
FERN.with(|fern| f(&mut fern.var_scopes.borrow_mut())) FERN.with(|fern| f(&mut fern.var_scopes.borrow_mut()))
} }
pub fn read_meta<T, F: FnOnce(&MetaTab) -> T>(f: F) -> T { pub fn read_meta<T, F: FnOnce(&MetaTab) -> T>(f: F) -> T {
FERN.with(|fern| f(&fern.meta.borrow())) FERN.with(|fern| f(&fern.meta.borrow()))
} }
/// Write to the meta table /// Write to the meta table
pub fn write_meta<T, F: FnOnce(&mut MetaTab) -> T>(f: F) -> T { pub fn write_meta<T, F: FnOnce(&mut MetaTab) -> T>(f: F) -> T {
FERN.with(|fern| f(&mut fern.meta.borrow_mut())) FERN.with(|fern| f(&mut fern.meta.borrow_mut()))
} }
/// Read from the logic table /// Read from the logic table
pub fn read_logic<T, F: FnOnce(&LogTab) -> T>(f: F) -> T { pub fn read_logic<T, F: FnOnce(&LogTab) -> T>(f: F) -> T {
FERN.with(|fern| f(&fern.logic.borrow())) FERN.with(|fern| f(&fern.logic.borrow()))
} }
/// Write to the logic table /// Write to the logic table
pub fn write_logic<T, F: FnOnce(&mut LogTab) -> T>(f: F) -> T { pub fn write_logic<T, F: FnOnce(&mut LogTab) -> T>(f: F) -> T {
FERN.with(|fern| f(&mut fern.logic.borrow_mut())) FERN.with(|fern| f(&mut fern.logic.borrow_mut()))
} }
pub fn read_shopts<T, F: FnOnce(&ShOpts) -> T>(f: F) -> T { pub fn read_shopts<T, F: FnOnce(&ShOpts) -> T>(f: F) -> T {
FERN.with(|fern| f(&fern.shopts.borrow())) FERN.with(|fern| f(&fern.shopts.borrow()))
} }
pub fn write_shopts<T, F: FnOnce(&mut ShOpts) -> T>(f: F) -> T { pub fn write_shopts<T, F: FnOnce(&mut ShOpts) -> T>(f: F) -> T {
FERN.with(|fern| f(&mut fern.shopts.borrow_mut())) FERN.with(|fern| f(&mut fern.shopts.borrow_mut()))
} }
pub fn descend_scope(argv: Option<Vec<String>>) { pub fn descend_scope(argv: Option<Vec<String>>) {
write_vars(|v| v.descend(argv)); write_vars(|v| v.descend(argv));
} }
pub fn ascend_scope() { pub fn ascend_scope() {
write_vars(|v| v.ascend()); write_vars(|v| v.ascend());
} }
/// This function is used internally and ideally never sees user input /// This function is used internally and ideally never sees user input
@@ -788,7 +797,9 @@ pub fn get_shopt(path: &str) -> String {
} }
pub fn get_status() -> i32 { pub fn get_status() -> i32 {
read_vars(|v| v.get_param(ShellParam::Status)).parse::<i32>().unwrap() read_vars(|v| v.get_param(ShellParam::Status))
.parse::<i32>()
.unwrap()
} }
#[track_caller] #[track_caller]
pub fn set_status(code: i32) { pub fn set_status(code: i32) {

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
use std::collections::HashSet; use std::collections::HashSet;
use crate::expand::{perform_param_expansion, DUB_QUOTE, VAR_SUB}; use crate::expand::{DUB_QUOTE, VAR_SUB, perform_param_expansion};
use crate::state::VarFlags; use crate::state::VarFlags;
use super::*; use super::*;
@@ -293,70 +293,78 @@ fn param_expansion_replacesuffix() {
#[test] #[test]
fn dquote_escape_dollar() { fn dquote_escape_dollar() {
// "\$foo" should strip backslash, produce literal $foo (no expansion) // "\$foo" should strip backslash, produce literal $foo (no expansion)
let result = unescape_str(r#""\$foo""#); let result = unescape_str(r#""\$foo""#);
assert!(!result.contains(VAR_SUB), "Escaped $ should not become VAR_SUB"); assert!(
assert!(result.contains('$'), "Literal $ should be preserved"); !result.contains(VAR_SUB),
assert!(!result.contains('\\'), "Backslash should be stripped"); "Escaped $ should not become VAR_SUB"
);
assert!(result.contains('$'), "Literal $ should be preserved");
assert!(!result.contains('\\'), "Backslash should be stripped");
} }
#[test] #[test]
fn dquote_escape_backslash() { fn dquote_escape_backslash() {
// "\\" in double quotes should produce a single backslash // "\\" in double quotes should produce a single backslash
let result = unescape_str(r#""\\""#); let result = unescape_str(r#""\\""#);
let inner: String = result.chars() let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
.filter(|&c| c != DUB_QUOTE) assert_eq!(
.collect(); inner, "\\",
assert_eq!(inner, "\\", "Double backslash should produce single backslash"); "Double backslash should produce single backslash"
);
} }
#[test] #[test]
fn dquote_escape_quote() { fn dquote_escape_quote() {
// "\"" should produce a literal double quote // "\"" should produce a literal double quote
let result = unescape_str(r#""\"""#); let result = unescape_str(r#""\"""#);
let inner: String = result.chars() let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
.filter(|&c| c != DUB_QUOTE) assert!(
.collect(); inner.contains('"'),
assert!(inner.contains('"'), "Escaped quote should produce literal quote"); "Escaped quote should produce literal quote"
);
} }
#[test] #[test]
fn dquote_escape_backtick() { fn dquote_escape_backtick() {
// "\`" should strip backslash, produce literal backtick // "\`" should strip backslash, produce literal backtick
let result = unescape_str(r#""\`""#); let result = unescape_str(r#""\`""#);
let inner: String = result.chars() let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
.filter(|&c| c != DUB_QUOTE) assert_eq!(
.collect(); inner, "`",
assert_eq!(inner, "`", "Escaped backtick should produce literal backtick"); "Escaped backtick should produce literal backtick"
);
} }
#[test] #[test]
fn dquote_escape_nonspecial_preserves_backslash() { fn dquote_escape_nonspecial_preserves_backslash() {
// "\a" inside double quotes should preserve the backslash (a is not special) // "\a" inside double quotes should preserve the backslash (a is not special)
let result = unescape_str(r#""\a""#); let result = unescape_str(r#""\a""#);
let inner: String = result.chars() let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
.filter(|&c| c != DUB_QUOTE) assert_eq!(
.collect(); inner, "\\a",
assert_eq!(inner, "\\a", "Backslash before non-special char should be preserved"); "Backslash before non-special char should be preserved"
);
} }
#[test] #[test]
fn dquote_unescaped_dollar_expands() { fn dquote_unescaped_dollar_expands() {
// "$foo" inside double quotes should produce VAR_SUB (expansion marker) // "$foo" inside double quotes should produce VAR_SUB (expansion marker)
let result = unescape_str(r#""$foo""#); let result = unescape_str(r#""$foo""#);
assert!(result.contains(VAR_SUB), "Unescaped $ should become VAR_SUB"); assert!(
result.contains(VAR_SUB),
"Unescaped $ should become VAR_SUB"
);
} }
#[test] #[test]
fn dquote_mixed_escapes() { fn dquote_mixed_escapes() {
// "hello \$world \\end" should have literal $, single backslash // "hello \$world \\end" should have literal $, single backslash
let result = unescape_str(r#""hello \$world \\end""#); let result = unescape_str(r#""hello \$world \\end""#);
assert!(!result.contains(VAR_SUB), "Escaped $ should not expand"); assert!(!result.contains(VAR_SUB), "Escaped $ should not expand");
assert!(result.contains('$'), "Literal $ should be in output"); assert!(result.contains('$'), "Literal $ should be in output");
// Should have exactly one backslash (from \\) // Should have exactly one backslash (from \\)
let inner: String = result.chars() let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
.filter(|&c| c != DUB_QUOTE) let backslash_count = inner.chars().filter(|&c| c == '\\').count();
.collect(); assert_eq!(backslash_count, 1, "\\\\ should produce one backslash");
let backslash_count = inner.chars().filter(|&c| c == '\\').count();
assert_eq!(backslash_count, 1, "\\\\ should produce one backslash");
} }

View File

@@ -1,27 +1,29 @@
use crate::prompt::readline::{ use crate::prompt::readline::{
annotate_input, annotate_input_recursive, markers, annotate_input, annotate_input_recursive, highlight::Highlighter, markers,
highlight::Highlighter,
}; };
use super::*; use super::*;
/// Helper to check if a marker exists at any position in the annotated string /// Helper to check if a marker exists at any position in the annotated string
fn has_marker(annotated: &str, marker: char) -> bool { fn has_marker(annotated: &str, marker: char) -> bool {
annotated.contains(marker) annotated.contains(marker)
} }
/// Helper to find the position of a marker in the annotated string /// Helper to find the position of a marker in the annotated string
fn find_marker(annotated: &str, marker: char) -> Option<usize> { fn find_marker(annotated: &str, marker: char) -> Option<usize> {
annotated.find(marker) annotated.find(marker)
} }
/// Helper to check if markers appear in the correct order /// Helper to check if markers appear in the correct order
fn marker_before(annotated: &str, first: char, second: char) -> bool { fn marker_before(annotated: &str, first: char, second: char) -> bool {
if let (Some(pos1), Some(pos2)) = (find_marker(annotated, first), find_marker(annotated, second)) { if let (Some(pos1), Some(pos2)) = (
pos1 < pos2 find_marker(annotated, first),
} else { find_marker(annotated, second),
false ) {
} pos1 < pos2
} else {
false
}
} }
// ============================================================================ // ============================================================================
@@ -30,69 +32,70 @@ fn marker_before(annotated: &str, first: char, second: char) -> bool {
#[test] #[test]
fn annotate_simple_command() { fn annotate_simple_command() {
let input = "/bin/ls -la"; let input = "/bin/ls -la";
let annotated = annotate_input(input); let annotated = annotate_input(input);
// Should have COMMAND marker for "/bin/ls" (external command) // Should have COMMAND marker for "/bin/ls" (external command)
assert!(has_marker(&annotated, markers::COMMAND)); assert!(has_marker(&annotated, markers::COMMAND));
// Should have ARG marker for "-la" // Should have ARG marker for "-la"
assert!(has_marker(&annotated, markers::ARG)); assert!(has_marker(&annotated, markers::ARG));
// Should have RESET markers // Should have RESET markers
assert!(has_marker(&annotated, markers::RESET)); assert!(has_marker(&annotated, markers::RESET));
} }
#[test] #[test]
fn annotate_builtin_command() { fn annotate_builtin_command() {
let input = "export FOO=bar"; let input = "export FOO=bar";
let annotated = annotate_input(input); let annotated = annotate_input(input);
// Should mark "export" as BUILTIN // Should mark "export" as BUILTIN
assert!(has_marker(&annotated, markers::BUILTIN)); assert!(has_marker(&annotated, markers::BUILTIN));
// Should mark assignment (or ARG if assignment isn't specifically marked separately) // Should mark assignment (or ARG if assignment isn't specifically marked
assert!(has_marker(&annotated, markers::ASSIGNMENT) || has_marker(&annotated, markers::ARG)); // separately)
assert!(has_marker(&annotated, markers::ASSIGNMENT) || has_marker(&annotated, markers::ARG));
} }
#[test] #[test]
fn annotate_operator() { fn annotate_operator() {
let input = "ls | grep foo"; let input = "ls | grep foo";
let annotated = annotate_input(input); let annotated = annotate_input(input);
// Should have OPERATOR marker for pipe // Should have OPERATOR marker for pipe
assert!(has_marker(&annotated, markers::OPERATOR)); assert!(has_marker(&annotated, markers::OPERATOR));
// Should have COMMAND markers for both commands // Should have COMMAND markers for both commands
let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count(); let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count();
assert_eq!(command_count, 2); assert_eq!(command_count, 2);
} }
#[test] #[test]
fn annotate_redirect() { fn annotate_redirect() {
let input = "echo hello > output.txt"; let input = "echo hello > output.txt";
let annotated = annotate_input(input); let annotated = annotate_input(input);
// Should have REDIRECT marker // Should have REDIRECT marker
assert!(has_marker(&annotated, markers::REDIRECT)); assert!(has_marker(&annotated, markers::REDIRECT));
} }
#[test] #[test]
fn annotate_keyword() { fn annotate_keyword() {
let input = "if true; then echo yes; fi"; let input = "if true; then echo yes; fi";
let annotated = annotate_input(input); let annotated = annotate_input(input);
// Should have KEYWORD markers for if/then/fi // Should have KEYWORD markers for if/then/fi
assert!(has_marker(&annotated, markers::KEYWORD)); assert!(has_marker(&annotated, markers::KEYWORD));
} }
#[test] #[test]
fn annotate_command_separator() { fn annotate_command_separator() {
let input = "echo foo; echo bar"; let input = "echo foo; echo bar";
let annotated = annotate_input(input); let annotated = annotate_input(input);
// Should have CMD_SEP marker for semicolon // Should have CMD_SEP marker for semicolon
assert!(has_marker(&annotated, markers::CMD_SEP)); assert!(has_marker(&annotated, markers::CMD_SEP));
} }
// ============================================================================ // ============================================================================
@@ -101,83 +104,87 @@ fn annotate_command_separator() {
#[test] #[test]
fn annotate_variable_simple() { fn annotate_variable_simple() {
let input = "echo $foo"; let input = "echo $foo";
let annotated = annotate_input(input); let annotated = annotate_input(input);
// Should have VAR_SUB markers // Should have VAR_SUB markers
assert!(has_marker(&annotated, markers::VAR_SUB)); assert!(has_marker(&annotated, markers::VAR_SUB));
assert!(has_marker(&annotated, markers::VAR_SUB_END)); assert!(has_marker(&annotated, markers::VAR_SUB_END));
} }
#[test] #[test]
fn annotate_variable_braces() { fn annotate_variable_braces() {
let input = "echo ${foo}"; let input = "echo ${foo}";
let annotated = annotate_input(input); let annotated = annotate_input(input);
// Should have VAR_SUB markers for ${foo} // Should have VAR_SUB markers for ${foo}
assert!(has_marker(&annotated, markers::VAR_SUB)); assert!(has_marker(&annotated, markers::VAR_SUB));
assert!(has_marker(&annotated, markers::VAR_SUB_END)); assert!(has_marker(&annotated, markers::VAR_SUB_END));
} }
#[test] #[test]
fn annotate_double_quoted_string() { fn annotate_double_quoted_string() {
let input = r#"echo "hello world""#; let input = r#"echo "hello world""#;
let annotated = annotate_input(input); let annotated = annotate_input(input);
// Should have STRING_DQ markers // Should have STRING_DQ markers
assert!(has_marker(&annotated, markers::STRING_DQ)); assert!(has_marker(&annotated, markers::STRING_DQ));
assert!(has_marker(&annotated, markers::STRING_DQ_END)); assert!(has_marker(&annotated, markers::STRING_DQ_END));
} }
#[test] #[test]
fn annotate_single_quoted_string() { fn annotate_single_quoted_string() {
let input = "echo 'hello world'"; let input = "echo 'hello world'";
let annotated = annotate_input(input); let annotated = annotate_input(input);
// Should have STRING_SQ markers // Should have STRING_SQ markers
assert!(has_marker(&annotated, markers::STRING_SQ)); assert!(has_marker(&annotated, markers::STRING_SQ));
assert!(has_marker(&annotated, markers::STRING_SQ_END)); assert!(has_marker(&annotated, markers::STRING_SQ_END));
} }
#[test] #[test]
fn annotate_variable_in_string() { fn annotate_variable_in_string() {
let input = r#"echo "hello $USER""#; let input = r#"echo "hello $USER""#;
let annotated = annotate_input(input); let annotated = annotate_input(input);
// Should have both STRING_DQ and VAR_SUB markers // Should have both STRING_DQ and VAR_SUB markers
assert!(has_marker(&annotated, markers::STRING_DQ)); assert!(has_marker(&annotated, markers::STRING_DQ));
assert!(has_marker(&annotated, markers::VAR_SUB)); assert!(has_marker(&annotated, markers::VAR_SUB));
// VAR_SUB should be inside STRING_DQ // VAR_SUB should be inside STRING_DQ
assert!(marker_before(&annotated, markers::STRING_DQ, markers::VAR_SUB)); assert!(marker_before(
&annotated,
markers::STRING_DQ,
markers::VAR_SUB
));
} }
#[test] #[test]
fn annotate_glob_asterisk() { fn annotate_glob_asterisk() {
let input = "ls *.txt"; let input = "ls *.txt";
let annotated = annotate_input(input); let annotated = annotate_input(input);
// Should have GLOB marker for * // Should have GLOB marker for *
assert!(has_marker(&annotated, markers::GLOB)); assert!(has_marker(&annotated, markers::GLOB));
} }
#[test] #[test]
fn annotate_glob_question() { fn annotate_glob_question() {
let input = "ls file?.txt"; let input = "ls file?.txt";
let annotated = annotate_input(input); let annotated = annotate_input(input);
// Should have GLOB marker for ? // Should have GLOB marker for ?
assert!(has_marker(&annotated, markers::GLOB)); assert!(has_marker(&annotated, markers::GLOB));
} }
#[test] #[test]
fn annotate_glob_bracket() { fn annotate_glob_bracket() {
let input = "ls file[abc].txt"; let input = "ls file[abc].txt";
let annotated = annotate_input(input); let annotated = annotate_input(input);
// Should have GLOB markers for bracket expression // Should have GLOB markers for bracket expression
let glob_count = annotated.chars().filter(|&c| c == markers::GLOB).count(); let glob_count = annotated.chars().filter(|&c| c == markers::GLOB).count();
assert!(glob_count >= 2); // Opening and closing assert!(glob_count >= 2); // Opening and closing
} }
// ============================================================================ // ============================================================================
@@ -186,32 +193,32 @@ fn annotate_glob_bracket() {
#[test] #[test]
fn annotate_command_sub_basic() { fn annotate_command_sub_basic() {
let input = "echo $(whoami)"; let input = "echo $(whoami)";
let annotated = annotate_input(input); let annotated = annotate_input(input);
// Should have CMD_SUB markers (but not recursively annotated yet) // Should have CMD_SUB markers (but not recursively annotated yet)
assert!(has_marker(&annotated, markers::CMD_SUB)); assert!(has_marker(&annotated, markers::CMD_SUB));
assert!(has_marker(&annotated, markers::CMD_SUB_END)); assert!(has_marker(&annotated, markers::CMD_SUB_END));
} }
#[test] #[test]
fn annotate_subshell_basic() { fn annotate_subshell_basic() {
let input = "(cd /tmp && ls)"; let input = "(cd /tmp && ls)";
let annotated = annotate_input(input); let annotated = annotate_input(input);
// Should have SUBSH markers // Should have SUBSH markers
assert!(has_marker(&annotated, markers::SUBSH)); assert!(has_marker(&annotated, markers::SUBSH));
assert!(has_marker(&annotated, markers::SUBSH_END)); assert!(has_marker(&annotated, markers::SUBSH_END));
} }
#[test] #[test]
fn annotate_process_sub_output() { fn annotate_process_sub_output() {
let input = "diff <(ls dir1) <(ls dir2)"; let input = "diff <(ls dir1) <(ls dir2)";
let annotated = annotate_input(input); let annotated = annotate_input(input);
// Should have PROC_SUB markers // Should have PROC_SUB markers
assert!(has_marker(&annotated, markers::PROC_SUB)); assert!(has_marker(&annotated, markers::PROC_SUB));
assert!(has_marker(&annotated, markers::PROC_SUB_END)); assert!(has_marker(&annotated, markers::PROC_SUB_END));
} }
// ============================================================================ // ============================================================================
@@ -220,88 +227,97 @@ fn annotate_process_sub_output() {
#[test] #[test]
fn annotate_recursive_command_sub() { fn annotate_recursive_command_sub() {
let input = "echo $(whoami)"; let input = "echo $(whoami)";
let annotated = annotate_input_recursive(input); let annotated = annotate_input_recursive(input);
// Should have CMD_SUB markers // Should have CMD_SUB markers
assert!(has_marker(&annotated, markers::CMD_SUB)); assert!(has_marker(&annotated, markers::CMD_SUB));
assert!(has_marker(&annotated, markers::CMD_SUB_END)); assert!(has_marker(&annotated, markers::CMD_SUB_END));
// Inside the command sub, "whoami" should be marked as COMMAND // Inside the command sub, "whoami" should be marked as COMMAND
// The recursive annotator should have processed the inside // The recursive annotator should have processed the inside
assert!(has_marker(&annotated, markers::COMMAND)); assert!(has_marker(&annotated, markers::COMMAND));
} }
#[test] #[test]
fn annotate_recursive_nested_command_sub() { fn annotate_recursive_nested_command_sub() {
let input = "echo $(echo $(whoami))"; let input = "echo $(echo $(whoami))";
let annotated = annotate_input_recursive(input); let annotated = annotate_input_recursive(input);
// Should have multiple CMD_SUB markers (nested) // Should have multiple CMD_SUB markers (nested)
let cmd_sub_count = annotated.chars().filter(|&c| c == markers::CMD_SUB).count(); let cmd_sub_count = annotated.chars().filter(|&c| c == markers::CMD_SUB).count();
assert!(cmd_sub_count >= 2, "Should have at least 2 CMD_SUB markers for nested substitutions"); assert!(
cmd_sub_count >= 2,
"Should have at least 2 CMD_SUB markers for nested substitutions"
);
} }
#[test] #[test]
fn annotate_recursive_command_sub_with_args() { fn annotate_recursive_command_sub_with_args() {
let input = "echo $(grep foo file.txt)"; let input = "echo $(grep foo file.txt)";
let annotated = annotate_input_recursive(input); let annotated = annotate_input_recursive(input);
// Should have BUILTIN for echo and possibly COMMAND for grep (if in PATH) // Should have BUILTIN for echo and possibly COMMAND for grep (if in PATH)
// Just check that we have command-type markers // Just check that we have command-type markers
let builtin_count = annotated.chars().filter(|&c| c == markers::BUILTIN).count(); let builtin_count = annotated.chars().filter(|&c| c == markers::BUILTIN).count();
let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count(); let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count();
assert!(builtin_count + command_count >= 2, "Expected at least 2 command markers (BUILTIN or COMMAND)"); assert!(
builtin_count + command_count >= 2,
"Expected at least 2 command markers (BUILTIN or COMMAND)"
);
} }
#[test] #[test]
fn annotate_recursive_subshell() { fn annotate_recursive_subshell() {
let input = "(echo hello; echo world)"; let input = "(echo hello; echo world)";
let annotated = annotate_input_recursive(input); let annotated = annotate_input_recursive(input);
// Should have SUBSH markers // Should have SUBSH markers
assert!(has_marker(&annotated, markers::SUBSH)); assert!(has_marker(&annotated, markers::SUBSH));
assert!(has_marker(&annotated, markers::SUBSH_END)); assert!(has_marker(&annotated, markers::SUBSH_END));
// Inside should be annotated with BUILTIN (echo is a builtin) and CMD_SEP // Inside should be annotated with BUILTIN (echo is a builtin) and CMD_SEP
assert!(has_marker(&annotated, markers::BUILTIN)); assert!(has_marker(&annotated, markers::BUILTIN));
assert!(has_marker(&annotated, markers::CMD_SEP)); assert!(has_marker(&annotated, markers::CMD_SEP));
} }
#[test] #[test]
fn annotate_recursive_process_sub() { fn annotate_recursive_process_sub() {
let input = "diff <(ls -la)"; let input = "diff <(ls -la)";
let annotated = annotate_input_recursive(input); let annotated = annotate_input_recursive(input);
// Should have PROC_SUB markers // Should have PROC_SUB markers
assert!(has_marker(&annotated, markers::PROC_SUB)); assert!(has_marker(&annotated, markers::PROC_SUB));
// ls should be marked as COMMAND inside the process sub // ls should be marked as COMMAND inside the process sub
assert!(has_marker(&annotated, markers::COMMAND)); assert!(has_marker(&annotated, markers::COMMAND));
} }
#[test] #[test]
fn annotate_recursive_command_sub_in_string() { fn annotate_recursive_command_sub_in_string() {
let input = r#"echo "current user: $(whoami)""#; let input = r#"echo "current user: $(whoami)""#;
let annotated = annotate_input_recursive(input); let annotated = annotate_input_recursive(input);
// Should have STRING_DQ, CMD_SUB, and COMMAND markers // Should have STRING_DQ, CMD_SUB, and COMMAND markers
assert!(has_marker(&annotated, markers::STRING_DQ)); assert!(has_marker(&annotated, markers::STRING_DQ));
assert!(has_marker(&annotated, markers::CMD_SUB)); assert!(has_marker(&annotated, markers::CMD_SUB));
assert!(has_marker(&annotated, markers::COMMAND)); assert!(has_marker(&annotated, markers::COMMAND));
} }
#[test] #[test]
fn annotate_recursive_deeply_nested() { fn annotate_recursive_deeply_nested() {
let input = r#"echo "outer: $(echo "inner: $(whoami)")""#; let input = r#"echo "outer: $(echo "inner: $(whoami)")""#;
let annotated = annotate_input_recursive(input); let annotated = annotate_input_recursive(input);
// Should have multiple STRING_DQ and CMD_SUB markers // Should have multiple STRING_DQ and CMD_SUB markers
let string_count = annotated.chars().filter(|&c| c == markers::STRING_DQ).count(); let string_count = annotated
let cmd_sub_count = annotated.chars().filter(|&c| c == markers::CMD_SUB).count(); .chars()
.filter(|&c| c == markers::STRING_DQ)
.count();
let cmd_sub_count = annotated.chars().filter(|&c| c == markers::CMD_SUB).count();
assert!(string_count >= 2, "Should have multiple STRING_DQ markers"); assert!(string_count >= 2, "Should have multiple STRING_DQ markers");
assert!(cmd_sub_count >= 2, "Should have multiple CMD_SUB markers"); assert!(cmd_sub_count >= 2, "Should have multiple CMD_SUB markers");
} }
// ============================================================================ // ============================================================================
@@ -310,33 +326,37 @@ fn annotate_recursive_deeply_nested() {
#[test] #[test]
fn marker_priority_var_in_string() { fn marker_priority_var_in_string() {
let input = r#""$foo""#; let input = r#""$foo""#;
let annotated = annotate_input(input); let annotated = annotate_input(input);
// STRING_DQ should come before VAR_SUB (outer before inner) // STRING_DQ should come before VAR_SUB (outer before inner)
assert!(marker_before(&annotated, markers::STRING_DQ, markers::VAR_SUB)); assert!(marker_before(
&annotated,
markers::STRING_DQ,
markers::VAR_SUB
));
} }
#[test] #[test]
fn marker_priority_arg_vs_string() { fn marker_priority_arg_vs_string() {
let input = r#"echo "hello""#; let input = r#"echo "hello""#;
let annotated = annotate_input(input); let annotated = annotate_input(input);
// Both ARG and STRING_DQ should be present // Both ARG and STRING_DQ should be present
// STRING_DQ should be inside the ARG token's span // STRING_DQ should be inside the ARG token's span
assert!(has_marker(&annotated, markers::ARG)); assert!(has_marker(&annotated, markers::ARG));
assert!(has_marker(&annotated, markers::STRING_DQ)); assert!(has_marker(&annotated, markers::STRING_DQ));
} }
#[test] #[test]
fn marker_priority_reset_placement() { fn marker_priority_reset_placement() {
let input = "echo hello"; let input = "echo hello";
let annotated = annotate_input(input); let annotated = annotate_input(input);
// RESET markers should appear after each token // RESET markers should appear after each token
// There should be multiple RESET markers // There should be multiple RESET markers
let reset_count = annotated.chars().filter(|&c| c == markers::RESET).count(); let reset_count = annotated.chars().filter(|&c| c == markers::RESET).count();
assert!(reset_count >= 2); assert!(reset_count >= 2);
} }
// ============================================================================ // ============================================================================
@@ -345,127 +365,131 @@ fn marker_priority_reset_placement() {
#[test] #[test]
fn highlighter_produces_ansi_codes() { fn highlighter_produces_ansi_codes() {
let mut highlighter = Highlighter::new(); let mut highlighter = Highlighter::new();
highlighter.load_input("echo hello"); highlighter.load_input("echo hello");
highlighter.highlight(); highlighter.highlight();
let output = highlighter.take(); let output = highlighter.take();
// Should contain ANSI escape codes // Should contain ANSI escape codes
assert!(output.contains("\x1b["), "Output should contain ANSI escape sequences"); assert!(
output.contains("\x1b["),
"Output should contain ANSI escape sequences"
);
// Should still contain the original text // Should still contain the original text
assert!(output.contains("echo")); assert!(output.contains("echo"));
assert!(output.contains("hello")); assert!(output.contains("hello"));
} }
#[test] #[test]
fn highlighter_handles_empty_input() { fn highlighter_handles_empty_input() {
let mut highlighter = Highlighter::new(); let mut highlighter = Highlighter::new();
highlighter.load_input(""); highlighter.load_input("");
highlighter.highlight(); highlighter.highlight();
let output = highlighter.take(); let output = highlighter.take();
// Should not crash and should return empty or minimal output // Should not crash and should return empty or minimal output
assert!(output.len() < 10); // Just escape codes or empty assert!(output.len() < 10); // Just escape codes or empty
} }
#[test] #[test]
fn highlighter_command_validation() { fn highlighter_command_validation() {
let mut highlighter = Highlighter::new(); let mut highlighter = Highlighter::new();
// Valid command (echo exists) // Valid command (echo exists)
highlighter.load_input("echo test"); highlighter.load_input("echo test");
highlighter.highlight(); highlighter.highlight();
let valid_output = highlighter.take(); let valid_output = highlighter.take();
// Invalid command (definitely doesn't exist) // Invalid command (definitely doesn't exist)
highlighter.load_input("xyznotacommand123 test"); highlighter.load_input("xyznotacommand123 test");
highlighter.highlight(); highlighter.highlight();
let invalid_output = highlighter.take(); let invalid_output = highlighter.take();
// Both should have ANSI codes // Both should have ANSI codes
assert!(valid_output.contains("\x1b[")); assert!(valid_output.contains("\x1b["));
assert!(invalid_output.contains("\x1b[")); assert!(invalid_output.contains("\x1b["));
// The color codes should be different (green vs red) // The color codes should be different (green vs red)
// Valid commands should have \x1b[32m (green) // Valid commands should have \x1b[32m (green)
// Invalid commands should have \x1b[31m (red) or \x1b[1;31m (bold red) // Invalid commands should have \x1b[31m (red) or \x1b[1;31m (bold red)
} }
#[test] #[test]
fn highlighter_preserves_text_content() { fn highlighter_preserves_text_content() {
let input = "echo hello world"; let input = "echo hello world";
let mut highlighter = Highlighter::new(); let mut highlighter = Highlighter::new();
highlighter.load_input(input); highlighter.load_input(input);
highlighter.highlight(); highlighter.highlight();
let output = highlighter.take(); let output = highlighter.take();
// Remove ANSI codes to check text content // Remove ANSI codes to check text content
let text_only: String = output.chars() let text_only: String = output
.filter(|c| !c.is_control() && *c != '\x1b') .chars()
.collect(); .filter(|c| !c.is_control() && *c != '\x1b')
.collect();
// Should still contain the words (might have escape sequence fragments) // Should still contain the words (might have escape sequence fragments)
assert!(output.contains("echo")); assert!(output.contains("echo"));
assert!(output.contains("hello")); assert!(output.contains("hello"));
assert!(output.contains("world")); assert!(output.contains("world"));
} }
#[test] #[test]
fn highlighter_multiple_tokens() { fn highlighter_multiple_tokens() {
let mut highlighter = Highlighter::new(); let mut highlighter = Highlighter::new();
highlighter.load_input("ls -la | grep foo"); highlighter.load_input("ls -la | grep foo");
highlighter.highlight(); highlighter.highlight();
let output = highlighter.take(); let output = highlighter.take();
// Should contain all tokens // Should contain all tokens
assert!(output.contains("ls")); assert!(output.contains("ls"));
assert!(output.contains("-la")); assert!(output.contains("-la"));
assert!(output.contains("|")); assert!(output.contains("|"));
assert!(output.contains("grep")); assert!(output.contains("grep"));
assert!(output.contains("foo")); assert!(output.contains("foo"));
// Should have ANSI codes // Should have ANSI codes
assert!(output.contains("\x1b[")); assert!(output.contains("\x1b["));
} }
#[test] #[test]
fn highlighter_string_with_variable() { fn highlighter_string_with_variable() {
let mut highlighter = Highlighter::new(); let mut highlighter = Highlighter::new();
highlighter.load_input(r#"echo "hello $USER""#); highlighter.load_input(r#"echo "hello $USER""#);
highlighter.highlight(); highlighter.highlight();
let output = highlighter.take(); let output = highlighter.take();
// Should contain the text // Should contain the text
assert!(output.contains("echo")); assert!(output.contains("echo"));
assert!(output.contains("hello")); assert!(output.contains("hello"));
assert!(output.contains("USER")); assert!(output.contains("USER"));
// Should have ANSI codes for different elements // Should have ANSI codes for different elements
assert!(output.contains("\x1b[")); assert!(output.contains("\x1b["));
} }
#[test] #[test]
fn highlighter_reusable() { fn highlighter_reusable() {
let mut highlighter = Highlighter::new(); let mut highlighter = Highlighter::new();
// First input // First input
highlighter.load_input("echo first"); highlighter.load_input("echo first");
highlighter.highlight(); highlighter.highlight();
let output1 = highlighter.take(); let output1 = highlighter.take();
// Second input (reusing same highlighter) // Second input (reusing same highlighter)
highlighter.load_input("echo second"); highlighter.load_input("echo second");
highlighter.highlight(); highlighter.highlight();
let output2 = highlighter.take(); let output2 = highlighter.take();
// Both should work // Both should work
assert!(output1.contains("first")); assert!(output1.contains("first"));
assert!(output2.contains("second")); assert!(output2.contains("second"));
// Should not contain each other's text // Should not contain each other's text
assert!(!output1.contains("second")); assert!(!output1.contains("second"));
assert!(!output2.contains("first")); assert!(!output2.contains("first"));
} }
// ============================================================================ // ============================================================================
@@ -474,133 +498,143 @@ fn highlighter_reusable() {
#[test] #[test]
fn annotate_unclosed_string() { fn annotate_unclosed_string() {
let input = r#"echo "hello"#; let input = r#"echo "hello"#;
let annotated = annotate_input(input); let annotated = annotate_input(input);
// Should handle unclosed string gracefully // Should handle unclosed string gracefully
assert!(has_marker(&annotated, markers::STRING_DQ)); assert!(has_marker(&annotated, markers::STRING_DQ));
// May or may not have STRING_DQ_END depending on implementation // May or may not have STRING_DQ_END depending on implementation
} }
#[test] #[test]
fn annotate_unclosed_command_sub() { fn annotate_unclosed_command_sub() {
let input = "echo $(whoami"; let input = "echo $(whoami";
let annotated = annotate_input(input); let annotated = annotate_input(input);
// Should handle unclosed command sub gracefully // Should handle unclosed command sub gracefully
assert!(has_marker(&annotated, markers::CMD_SUB)); assert!(has_marker(&annotated, markers::CMD_SUB));
} }
#[test] #[test]
fn annotate_empty_command_sub() { fn annotate_empty_command_sub() {
let input = "echo $()"; let input = "echo $()";
let annotated = annotate_input_recursive(input); let annotated = annotate_input_recursive(input);
// Should handle empty command sub // Should handle empty command sub
assert!(has_marker(&annotated, markers::CMD_SUB)); assert!(has_marker(&annotated, markers::CMD_SUB));
assert!(has_marker(&annotated, markers::CMD_SUB_END)); assert!(has_marker(&annotated, markers::CMD_SUB_END));
} }
#[test] #[test]
fn annotate_escaped_characters() { fn annotate_escaped_characters() {
let input = r#"echo \$foo \`bar\` \"test\""#; let input = r#"echo \$foo \`bar\` \"test\""#;
let annotated = annotate_input(input); let annotated = annotate_input(input);
// Should not mark escaped $ as variable // Should not mark escaped $ as variable
// This is tricky - the behavior depends on implementation // This is tricky - the behavior depends on implementation
// At minimum, should not crash // At minimum, should not crash
} }
#[test] #[test]
fn annotate_special_variables() { fn annotate_special_variables() {
let input = "echo $0 $1 $2 $3 $4"; let input = "echo $0 $1 $2 $3 $4";
let annotated = annotate_input(input); let annotated = annotate_input(input);
// Should mark positional parameters // Should mark positional parameters
let var_count = annotated.chars().filter(|&c| c == markers::VAR_SUB).count(); let var_count = annotated.chars().filter(|&c| c == markers::VAR_SUB).count();
assert!(var_count >= 5, "Expected at least 5 VAR_SUB markers, found {}", var_count); assert!(
var_count >= 5,
"Expected at least 5 VAR_SUB markers, found {}",
var_count
);
} }
#[test] #[test]
fn annotate_variable_no_expansion_in_single_quotes() { fn annotate_variable_no_expansion_in_single_quotes() {
let input = "echo '$foo'"; let input = "echo '$foo'";
let annotated = annotate_input(input); let annotated = annotate_input(input);
// Should have STRING_SQ markers // Should have STRING_SQ markers
assert!(has_marker(&annotated, markers::STRING_SQ)); assert!(has_marker(&annotated, markers::STRING_SQ));
// Should NOT have VAR_SUB markers (variables don't expand in single quotes) // Should NOT have VAR_SUB markers (variables don't expand in single quotes)
// Note: The annotator might still mark it - depends on implementation // Note: The annotator might still mark it - depends on implementation
} }
#[test] #[test]
fn annotate_complex_pipeline() { fn annotate_complex_pipeline() {
let input = "cat file.txt | grep pattern | sed 's/foo/bar/' | sort | uniq"; let input = "cat file.txt | grep pattern | sed 's/foo/bar/' | sort | uniq";
let annotated = annotate_input(input); let annotated = annotate_input(input);
// Should have multiple OPERATOR markers for pipes // Should have multiple OPERATOR markers for pipes
let operator_count = annotated.chars().filter(|&c| c == markers::OPERATOR).count(); let operator_count = annotated
assert!(operator_count >= 4); .chars()
.filter(|&c| c == markers::OPERATOR)
.count();
assert!(operator_count >= 4);
// Should have multiple COMMAND markers // Should have multiple COMMAND markers
let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count(); let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count();
assert!(command_count >= 5); assert!(command_count >= 5);
} }
#[test] #[test]
fn annotate_assignment_with_command_sub() { fn annotate_assignment_with_command_sub() {
let input = "FOO=$(whoami)"; let input = "FOO=$(whoami)";
let annotated = annotate_input_recursive(input); let annotated = annotate_input_recursive(input);
// Should have ASSIGNMENT marker // Should have ASSIGNMENT marker
assert!(has_marker(&annotated, markers::ASSIGNMENT)); assert!(has_marker(&annotated, markers::ASSIGNMENT));
// Should have CMD_SUB marker // Should have CMD_SUB marker
assert!(has_marker(&annotated, markers::CMD_SUB)); assert!(has_marker(&annotated, markers::CMD_SUB));
// Inside command sub should have COMMAND marker // Inside command sub should have COMMAND marker
assert!(has_marker(&annotated, markers::COMMAND)); assert!(has_marker(&annotated, markers::COMMAND));
} }
#[test] #[test]
fn annotate_redirect_with_fd() { fn annotate_redirect_with_fd() {
let input = "command 2>&1"; let input = "command 2>&1";
let annotated = annotate_input(input); let annotated = annotate_input(input);
// Should have REDIRECT marker for the redirect operator // Should have REDIRECT marker for the redirect operator
assert!(has_marker(&annotated, markers::REDIRECT)); assert!(has_marker(&annotated, markers::REDIRECT));
} }
#[test] #[test]
fn annotate_multiple_redirects() { fn annotate_multiple_redirects() {
let input = "command > out.txt 2>&1"; let input = "command > out.txt 2>&1";
let annotated = annotate_input(input); let annotated = annotate_input(input);
// Should have multiple REDIRECT markers // Should have multiple REDIRECT markers
let redirect_count = annotated.chars().filter(|&c| c == markers::REDIRECT).count(); let redirect_count = annotated
assert!(redirect_count >= 2); .chars()
.filter(|&c| c == markers::REDIRECT)
.count();
assert!(redirect_count >= 2);
} }
#[test] #[test]
fn annotate_here_string() { fn annotate_here_string() {
let input = "cat <<< 'hello world'"; let input = "cat <<< 'hello world'";
let annotated = annotate_input(input); let annotated = annotate_input(input);
// Should have REDIRECT marker for <<< // Should have REDIRECT marker for <<<
assert!(has_marker(&annotated, markers::REDIRECT)); assert!(has_marker(&annotated, markers::REDIRECT));
// Should have STRING_SQ markers // Should have STRING_SQ markers
assert!(has_marker(&annotated, markers::STRING_SQ)); assert!(has_marker(&annotated, markers::STRING_SQ));
} }
#[test] #[test]
fn annotate_unicode_content() { fn annotate_unicode_content() {
let input = "echo 'hello 世界 🌍'"; let input = "echo 'hello 世界 🌍'";
let annotated = annotate_input(input); let annotated = annotate_input(input);
// Should handle unicode gracefully // Should handle unicode gracefully
assert!(has_marker(&annotated, markers::BUILTIN)); assert!(has_marker(&annotated, markers::BUILTIN));
assert!(has_marker(&annotated, markers::STRING_SQ)); assert!(has_marker(&annotated, markers::STRING_SQ));
} }
// ============================================================================ // ============================================================================
@@ -609,26 +643,26 @@ fn annotate_unicode_content() {
#[test] #[test]
fn regression_arg_marker_at_position_zero() { fn regression_arg_marker_at_position_zero() {
// Regression test: ARG marker was appearing at position 3 for input "ech" // Regression test: ARG marker was appearing at position 3 for input "ech"
// This was caused by SOI/EOI tokens falling through to ARG annotation // This was caused by SOI/EOI tokens falling through to ARG annotation
let input = "ech"; let input = "ech";
let annotated = annotate_input(input); let annotated = annotate_input(input);
// Should only have COMMAND marker, not ARG // Should only have COMMAND marker, not ARG
// (incomplete command should still be marked as command attempt) // (incomplete command should still be marked as command attempt)
assert!(has_marker(&annotated, markers::COMMAND)); assert!(has_marker(&annotated, markers::COMMAND));
} }
#[test] #[test]
fn regression_string_color_in_annotated_strings() { fn regression_string_color_in_annotated_strings() {
// Regression test: ARG marker was overriding STRING_DQ color // Regression test: ARG marker was overriding STRING_DQ color
let input = r#"echo "test""#; let input = r#"echo "test""#;
let annotated = annotate_input(input); let annotated = annotate_input(input);
// STRING_DQ should be present and properly positioned // STRING_DQ should be present and properly positioned
assert!(has_marker(&annotated, markers::STRING_DQ)); assert!(has_marker(&annotated, markers::STRING_DQ));
assert!(has_marker(&annotated, markers::STRING_DQ_END)); assert!(has_marker(&annotated, markers::STRING_DQ_END));
// The string markers should come after the ARG marker // The string markers should come after the ARG marker
// (so they override it in the highlighting) // (so they override it in the highlighting)
} }

View File

@@ -4,8 +4,9 @@ use super::*;
use crate::expand::{expand_aliases, unescape_str}; use crate::expand::{expand_aliases, unescape_str};
use crate::libsh::error::{Note, ShErr, ShErrKind}; use crate::libsh::error::{Note, ShErr, ShErrKind};
use crate::parse::{ use crate::parse::{
NdRule, Node, ParseStream,
lex::{LexFlags, LexStream, Tk, TkRule}, lex::{LexFlags, LexStream, Tk, TkRule},
node_operation, NdRule, Node, ParseStream, node_operation,
}; };
use crate::state::{write_logic, write_vars}; use crate::state::{write_logic, write_vars};

View File

@@ -1,14 +1,17 @@
use std::collections::VecDeque; use std::collections::VecDeque;
use crate::{ use crate::{
libsh::{error::ShErr, term::{Style, Styled}}, libsh::{
error::ShErr,
term::{Style, Styled},
},
prompt::readline::{ prompt::readline::{
FernVi,
history::History, history::History,
keys::{KeyCode, KeyEvent, ModKeys}, keys::{KeyCode, KeyEvent, ModKeys},
linebuf::LineBuf, linebuf::LineBuf,
term::{raw_mode, KeyReader, LineWriter}, term::{KeyReader, LineWriter, raw_mode},
vimode::{ViInsert, ViMode, ViNormal}, vimode::{ViInsert, ViMode, ViNormal},
FernVi,
}, },
}; };
@@ -173,8 +176,9 @@ impl LineWriter for TestWriter {
} }
} }
// NOTE: FernVi structure has changed significantly and readline() method no longer exists // NOTE: FernVi structure has changed significantly and readline() method no
// These test helpers are disabled until they can be properly updated // longer exists These test helpers are disabled until they can be properly
// updated
/* /*
impl FernVi { impl FernVi {
pub fn new_test(prompt: Option<String>, input: &str, initial: &str) -> Self { pub fn new_test(prompt: Option<String>, input: &str, initial: &str) -> Self {
@@ -612,10 +616,10 @@ fn fernvi_test_mode_change() {
#[test] #[test]
fn fernvi_test_lorem_ipsum_1() { fn fernvi_test_lorem_ipsum_1() {
assert_eq!(fernvi_test( assert_eq!(fernvi_test(
"\x1bwwwwwwww5dWdBdBjjdwjdwbbbcwasdasdasdasd\x1b\r", "\x1bwwwwwwww5dWdBdBjjdwjdwbbbcwasdasdasdasd\x1b\r",
LOREM_IPSUM), LOREM_IPSUM),
"Lorem ipsum dolor sit amet, incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in repin voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur asdasdasdasd occaecat cupinon proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra." "Lorem ipsum dolor sit amet, incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in repin voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur asdasdasdasd occaecat cupinon proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra."
) )
} }
#[test] #[test]
@@ -632,9 +636,9 @@ fn fernvi_test_lorem_ipsum_undo() {
#[test] #[test]
fn fernvi_test_lorem_ipsum_ctrl_w() { fn fernvi_test_lorem_ipsum_ctrl_w() {
assert_eq!(fernvi_test( assert_eq!(fernvi_test(
"\x1bj5wiasdasdkjhaksjdhkajshd\x17wordswordswords\x17somemorewords\x17\x1b[D\x1b[D\x17\x1b\r", "\x1bj5wiasdasdkjhaksjdhkajshd\x17wordswordswords\x17somemorewords\x17\x1b[D\x1b[D\x17\x1b\r",
LOREM_IPSUM), LOREM_IPSUM),
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim am, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra." "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim am, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra."
) )
} }
*/ */

View File

@@ -1,8 +1,8 @@
use std::sync::Arc; use std::sync::Arc;
use crate::parse::{ use crate::parse::{
lex::{LexFlags, LexStream}, NdRule, Node, ParseStream, Redir, RedirType,
Node, NdRule, ParseStream, RedirType, Redir, lex::{LexFlags, LexStream},
}; };
use crate::procio::{IoFrame, IoMode, IoStack}; use crate::procio::{IoFrame, IoMode, IoStack};
@@ -11,187 +11,238 @@ use crate::procio::{IoFrame, IoMode, IoStack};
// ============================================================================ // ============================================================================
fn parse_command(input: &str) -> Node { fn parse_command(input: &str) -> Node {
let source = Arc::new(input.to_string()); let source = Arc::new(input.to_string());
let tokens = LexStream::new(source, LexFlags::empty()) let tokens = LexStream::new(source, LexFlags::empty())
.flatten() .flatten()
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let mut nodes = ParseStream::new(tokens) let mut nodes = ParseStream::new(tokens).flatten().collect::<Vec<_>>();
.flatten()
.collect::<Vec<_>>();
assert_eq!(nodes.len(), 1, "Expected exactly one node"); assert_eq!(nodes.len(), 1, "Expected exactly one node");
let top_node = nodes.remove(0); let top_node = nodes.remove(0);
// Navigate to the actual Command node within the AST structure // Navigate to the actual Command node within the AST structure
// Structure is typically: Conjunction -> Pipeline -> Command // Structure is typically: Conjunction -> Pipeline -> Command
match top_node.class { match top_node.class {
NdRule::Conjunction { elements } => { NdRule::Conjunction { elements } => {
let first_element = elements.into_iter().next().expect("Expected at least one conjunction element"); let first_element = elements
match first_element.cmd.class { .into_iter()
NdRule::Pipeline { cmds, .. } => { .next()
let mut commands = cmds; .expect("Expected at least one conjunction element");
assert_eq!(commands.len(), 1, "Expected exactly one command in pipeline"); match first_element.cmd.class {
commands.remove(0) NdRule::Pipeline { cmds, .. } => {
} let mut commands = cmds;
NdRule::Command { .. } => *first_element.cmd, assert_eq!(
_ => panic!("Expected Command or Pipeline node, got {:?}", first_element.cmd.class), commands.len(),
} 1,
} "Expected exactly one command in pipeline"
NdRule::Pipeline { cmds, .. } => { );
let mut commands = cmds; commands.remove(0)
assert_eq!(commands.len(), 1, "Expected exactly one command in pipeline"); }
commands.remove(0) NdRule::Command { .. } => *first_element.cmd,
} _ => panic!(
NdRule::Command { .. } => top_node, "Expected Command or Pipeline node, got {:?}",
_ => panic!("Expected Conjunction, Pipeline, or Command node, got {:?}", top_node.class), first_element.cmd.class
} ),
}
}
NdRule::Pipeline { cmds, .. } => {
let mut commands = cmds;
assert_eq!(
commands.len(),
1,
"Expected exactly one command in pipeline"
);
commands.remove(0)
}
NdRule::Command { .. } => top_node,
_ => panic!(
"Expected Conjunction, Pipeline, or Command node, got {:?}",
top_node.class
),
}
} }
#[test] #[test]
fn parse_output_redirect() { fn parse_output_redirect() {
let node = parse_command("echo hello > output.txt"); let node = parse_command("echo hello > output.txt");
assert_eq!(node.redirs.len(), 1); assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0]; let redir = &node.redirs[0];
assert!(matches!(redir.class, RedirType::Output)); assert!(matches!(redir.class, RedirType::Output));
assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 1, .. })); assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 1, .. }));
} }
#[test] #[test]
fn parse_append_redirect() { fn parse_append_redirect() {
let node = parse_command("echo hello >> output.txt"); let node = parse_command("echo hello >> output.txt");
assert_eq!(node.redirs.len(), 1); assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0]; let redir = &node.redirs[0];
assert!(matches!(redir.class, RedirType::Append)); assert!(matches!(redir.class, RedirType::Append));
assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 1, .. })); assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 1, .. }));
} }
#[test] #[test]
fn parse_input_redirect() { fn parse_input_redirect() {
let node = parse_command("cat < input.txt"); let node = parse_command("cat < input.txt");
assert_eq!(node.redirs.len(), 1); assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0]; let redir = &node.redirs[0];
assert!(matches!(redir.class, RedirType::Input)); assert!(matches!(redir.class, RedirType::Input));
assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 0, .. })); assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 0, .. }));
} }
#[test] #[test]
fn parse_stderr_redirect() { fn parse_stderr_redirect() {
let node = parse_command("ls 2> errors.txt"); let node = parse_command("ls 2> errors.txt");
assert_eq!(node.redirs.len(), 1); assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0]; let redir = &node.redirs[0];
assert!(matches!(redir.class, RedirType::Output)); assert!(matches!(redir.class, RedirType::Output));
assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 2, .. })); assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 2, .. }));
} }
#[test] #[test]
fn parse_stderr_to_stdout() { fn parse_stderr_to_stdout() {
let node = parse_command("ls 2>&1"); let node = parse_command("ls 2>&1");
assert_eq!(node.redirs.len(), 1); assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0]; let redir = &node.redirs[0];
assert!(matches!(redir.io_mode, IoMode::Fd { tgt_fd: 2, src_fd: 1 })); assert!(matches!(
redir.io_mode,
IoMode::Fd {
tgt_fd: 2,
src_fd: 1
}
));
} }
#[test] #[test]
fn parse_stdout_to_stderr() { fn parse_stdout_to_stderr() {
let node = parse_command("echo test 1>&2"); let node = parse_command("echo test 1>&2");
assert_eq!(node.redirs.len(), 1); assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0]; let redir = &node.redirs[0];
assert!(matches!(redir.io_mode, IoMode::Fd { tgt_fd: 1, src_fd: 2 })); assert!(matches!(
redir.io_mode,
IoMode::Fd {
tgt_fd: 1,
src_fd: 2
}
));
} }
#[test] #[test]
fn parse_multiple_redirects() { fn parse_multiple_redirects() {
let node = parse_command("cmd < input.txt > output.txt 2> errors.txt"); let node = parse_command("cmd < input.txt > output.txt 2> errors.txt");
assert_eq!(node.redirs.len(), 3); assert_eq!(node.redirs.len(), 3);
// Input redirect // Input redirect
assert!(matches!(node.redirs[0].class, RedirType::Input)); assert!(matches!(node.redirs[0].class, RedirType::Input));
assert!(matches!(node.redirs[0].io_mode, IoMode::File { tgt_fd: 0, .. })); assert!(matches!(
node.redirs[0].io_mode,
IoMode::File { tgt_fd: 0, .. }
));
// Stdout redirect // Stdout redirect
assert!(matches!(node.redirs[1].class, RedirType::Output)); assert!(matches!(node.redirs[1].class, RedirType::Output));
assert!(matches!(node.redirs[1].io_mode, IoMode::File { tgt_fd: 1, .. })); assert!(matches!(
node.redirs[1].io_mode,
IoMode::File { tgt_fd: 1, .. }
));
// Stderr redirect // Stderr redirect
assert!(matches!(node.redirs[2].class, RedirType::Output)); assert!(matches!(node.redirs[2].class, RedirType::Output));
assert!(matches!(node.redirs[2].io_mode, IoMode::File { tgt_fd: 2, .. })); assert!(matches!(
node.redirs[2].io_mode,
IoMode::File { tgt_fd: 2, .. }
));
} }
#[test] #[test]
fn parse_custom_fd_redirect() { fn parse_custom_fd_redirect() {
let node = parse_command("echo test 3> fd3.txt"); let node = parse_command("echo test 3> fd3.txt");
assert_eq!(node.redirs.len(), 1); assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0]; let redir = &node.redirs[0];
assert!(matches!(redir.class, RedirType::Output)); assert!(matches!(redir.class, RedirType::Output));
assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 3, .. })); assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 3, .. }));
} }
#[test] #[test]
fn parse_custom_fd_dup() { fn parse_custom_fd_dup() {
let node = parse_command("cmd 3>&4"); let node = parse_command("cmd 3>&4");
assert_eq!(node.redirs.len(), 1); assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0]; let redir = &node.redirs[0];
assert!(matches!(redir.io_mode, IoMode::Fd { tgt_fd: 3, src_fd: 4 })); assert!(matches!(
redir.io_mode,
IoMode::Fd {
tgt_fd: 3,
src_fd: 4
}
));
} }
#[test] #[test]
fn parse_heredoc() { fn parse_heredoc() {
let node = parse_command("cat << EOF"); let node = parse_command("cat << EOF");
assert_eq!(node.redirs.len(), 1); assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0]; let redir = &node.redirs[0];
assert!(matches!(redir.class, RedirType::HereDoc)); assert!(matches!(redir.class, RedirType::HereDoc));
} }
#[test] #[test]
fn parse_herestring() { fn parse_herestring() {
let node = parse_command("cat <<< 'hello world'"); let node = parse_command("cat <<< 'hello world'");
assert_eq!(node.redirs.len(), 1); assert_eq!(node.redirs.len(), 1);
let redir = &node.redirs[0]; let redir = &node.redirs[0];
assert!(matches!(redir.class, RedirType::HereString)); assert!(matches!(redir.class, RedirType::HereString));
} }
#[test] #[test]
fn parse_redirect_with_no_space() { fn parse_redirect_with_no_space() {
let node = parse_command("echo hello >output.txt"); let node = parse_command("echo hello >output.txt");
assert_eq!(node.redirs.len(), 1); assert_eq!(node.redirs.len(), 1);
assert!(matches!(node.redirs[0].class, RedirType::Output)); assert!(matches!(node.redirs[0].class, RedirType::Output));
} }
#[test] #[test]
fn parse_redirect_order_preserved() { fn parse_redirect_order_preserved() {
let node = parse_command("cmd 2>&1 > file.txt"); let node = parse_command("cmd 2>&1 > file.txt");
assert_eq!(node.redirs.len(), 2); assert_eq!(node.redirs.len(), 2);
// First redirect: 2>&1 // First redirect: 2>&1
assert!(matches!(node.redirs[0].io_mode, IoMode::Fd { tgt_fd: 2, src_fd: 1 })); assert!(matches!(
node.redirs[0].io_mode,
IoMode::Fd {
tgt_fd: 2,
src_fd: 1
}
));
// Second redirect: > file.txt // Second redirect: > file.txt
assert!(matches!(node.redirs[1].class, RedirType::Output)); assert!(matches!(node.redirs[1].class, RedirType::Output));
assert!(matches!(node.redirs[1].io_mode, IoMode::File { tgt_fd: 1, .. })); assert!(matches!(
node.redirs[1].io_mode,
IoMode::File { tgt_fd: 1, .. }
));
} }
// ============================================================================ // ============================================================================
@@ -200,148 +251,148 @@ fn parse_redirect_order_preserved() {
#[test] #[test]
fn iostack_new() { fn iostack_new() {
let stack = IoStack::new(); let stack = IoStack::new();
assert_eq!(stack.len(), 1, "IoStack should start with one frame"); assert_eq!(stack.len(), 1, "IoStack should start with one frame");
assert_eq!(stack.curr_frame().len(), 0, "Initial frame should be empty"); assert_eq!(stack.curr_frame().len(), 0, "Initial frame should be empty");
} }
#[test] #[test]
fn iostack_push_pop_frame() { fn iostack_push_pop_frame() {
let mut stack = IoStack::new(); let mut stack = IoStack::new();
// Push a new frame // Push a new frame
stack.push_frame(IoFrame::new()); stack.push_frame(IoFrame::new());
assert_eq!(stack.len(), 2); assert_eq!(stack.len(), 2);
// Pop it back // Pop it back
let frame = stack.pop_frame(); let frame = stack.pop_frame();
assert_eq!(frame.len(), 0); assert_eq!(frame.len(), 0);
assert_eq!(stack.len(), 1); assert_eq!(stack.len(), 1);
} }
#[test] #[test]
fn iostack_never_empties() { fn iostack_never_empties() {
let mut stack = IoStack::new(); let mut stack = IoStack::new();
// Try to pop the last frame // Try to pop the last frame
let frame = stack.pop_frame(); let frame = stack.pop_frame();
assert_eq!(frame.len(), 0); assert_eq!(frame.len(), 0);
// Stack should still have one frame // Stack should still have one frame
assert_eq!(stack.len(), 1); assert_eq!(stack.len(), 1);
// Pop again - should still have one frame // Pop again - should still have one frame
let frame = stack.pop_frame(); let frame = stack.pop_frame();
assert_eq!(frame.len(), 0); assert_eq!(frame.len(), 0);
assert_eq!(stack.len(), 1); assert_eq!(stack.len(), 1);
} }
#[test] #[test]
fn iostack_push_to_frame() { fn iostack_push_to_frame() {
let mut stack = IoStack::new(); let mut stack = IoStack::new();
let redir = crate::parse::Redir::new( let redir = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output);
IoMode::fd(1, 2),
RedirType::Output,
);
stack.push_to_frame(redir); stack.push_to_frame(redir);
assert_eq!(stack.curr_frame().len(), 1); assert_eq!(stack.curr_frame().len(), 1);
} }
#[test] #[test]
fn iostack_append_to_frame() { fn iostack_append_to_frame() {
let mut stack = IoStack::new(); let mut stack = IoStack::new();
let redirs = vec![ let redirs = vec![
crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output), crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output),
crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output), crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output),
]; ];
stack.append_to_frame(redirs); stack.append_to_frame(redirs);
assert_eq!(stack.curr_frame().len(), 2); assert_eq!(stack.curr_frame().len(), 2);
} }
#[test] #[test]
fn iostack_frame_isolation() { fn iostack_frame_isolation() {
let mut stack = IoStack::new(); let mut stack = IoStack::new();
// Add redir to first frame // Add redir to first frame
let redir1 = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output); let redir1 = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output);
stack.push_to_frame(redir1); stack.push_to_frame(redir1);
assert_eq!(stack.curr_frame().len(), 1); assert_eq!(stack.curr_frame().len(), 1);
// Push new frame // Push new frame
stack.push_frame(IoFrame::new()); stack.push_frame(IoFrame::new());
assert_eq!(stack.curr_frame().len(), 0, "New frame should be empty"); assert_eq!(stack.curr_frame().len(), 0, "New frame should be empty");
// Add redir to second frame // Add redir to second frame
let redir2 = crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output); let redir2 = crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output);
stack.push_to_frame(redir2); stack.push_to_frame(redir2);
assert_eq!(stack.curr_frame().len(), 1); assert_eq!(stack.curr_frame().len(), 1);
// Pop second frame // Pop second frame
let frame2 = stack.pop_frame(); let frame2 = stack.pop_frame();
assert_eq!(frame2.len(), 1); assert_eq!(frame2.len(), 1);
// First frame should still have its redir // First frame should still have its redir
assert_eq!(stack.curr_frame().len(), 1); assert_eq!(stack.curr_frame().len(), 1);
} }
#[test] #[test]
fn iostack_flatten() { fn iostack_flatten() {
let mut stack = IoStack::new(); let mut stack = IoStack::new();
// Add redir to first frame // Add redir to first frame
let redir1 = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output); let redir1 = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output);
stack.push_to_frame(redir1); stack.push_to_frame(redir1);
// Push new frame with redir // Push new frame with redir
let mut frame2 = IoFrame::new(); let mut frame2 = IoFrame::new();
frame2.push(crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output)); frame2.push(crate::parse::Redir::new(
stack.push_frame(frame2); IoMode::fd(2, 1),
RedirType::Output,
));
stack.push_frame(frame2);
// Push third frame with redir // Push third frame with redir
let mut frame3 = IoFrame::new(); let mut frame3 = IoFrame::new();
frame3.push(crate::parse::Redir::new(IoMode::fd(0, 3), RedirType::Input)); frame3.push(crate::parse::Redir::new(IoMode::fd(0, 3), RedirType::Input));
stack.push_frame(frame3); stack.push_frame(frame3);
assert_eq!(stack.len(), 3); assert_eq!(stack.len(), 3);
// Flatten // Flatten
stack.flatten(); stack.flatten();
// Should have one frame with all redirects // Should have one frame with all redirects
assert_eq!(stack.len(), 1); assert_eq!(stack.len(), 1);
assert_eq!(stack.curr_frame().len(), 3); assert_eq!(stack.curr_frame().len(), 3);
} }
#[test] #[test]
fn ioframe_new() { fn ioframe_new() {
let frame = IoFrame::new(); let frame = IoFrame::new();
assert_eq!(frame.len(), 0); assert_eq!(frame.len(), 0);
} }
#[test] #[test]
fn ioframe_from_redirs() { fn ioframe_from_redirs() {
let redirs = vec![ let redirs = vec![
crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output), crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output),
crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output), crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output),
]; ];
let frame = IoFrame::from_redirs(redirs); let frame = IoFrame::from_redirs(redirs);
assert_eq!(frame.len(), 2); assert_eq!(frame.len(), 2);
} }
#[test] #[test]
fn ioframe_push() { fn ioframe_push() {
let mut frame = IoFrame::new(); let mut frame = IoFrame::new();
let redir = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output); let redir = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output);
frame.push(redir); frame.push(redir);
assert_eq!(frame.len(), 1); assert_eq!(frame.len(), 1);
} }
// ============================================================================ // ============================================================================
@@ -350,28 +401,28 @@ fn ioframe_push() {
#[test] #[test]
fn iomode_fd_construction() { fn iomode_fd_construction() {
let io_mode = IoMode::fd(2, 1); let io_mode = IoMode::fd(2, 1);
match io_mode { match io_mode {
IoMode::Fd { tgt_fd, src_fd } => { IoMode::Fd { tgt_fd, src_fd } => {
assert_eq!(tgt_fd, 2); assert_eq!(tgt_fd, 2);
assert_eq!(src_fd, 1); assert_eq!(src_fd, 1);
} }
_ => panic!("Expected IoMode::Fd"), _ => panic!("Expected IoMode::Fd"),
} }
} }
#[test] #[test]
fn iomode_tgt_fd() { fn iomode_tgt_fd() {
let fd_mode = IoMode::fd(2, 1); let fd_mode = IoMode::fd(2, 1);
assert_eq!(fd_mode.tgt_fd(), 2); assert_eq!(fd_mode.tgt_fd(), 2);
let file_mode = IoMode::file(1, std::path::PathBuf::from("test.txt"), RedirType::Output); let file_mode = IoMode::file(1, std::path::PathBuf::from("test.txt"), RedirType::Output);
assert_eq!(file_mode.tgt_fd(), 1); assert_eq!(file_mode.tgt_fd(), 1);
} }
#[test] #[test]
fn iomode_src_fd() { fn iomode_src_fd() {
let fd_mode = IoMode::fd(2, 1); let fd_mode = IoMode::fd(2, 1);
assert_eq!(fd_mode.src_fd(), 1); assert_eq!(fd_mode.src_fd(), 1);
} }

View File

@@ -6,264 +6,280 @@ use crate::state::{LogTab, ScopeStack, ShellParam, VarFlags, VarTab};
#[test] #[test]
fn scopestack_new() { fn scopestack_new() {
let stack = ScopeStack::new(); let stack = ScopeStack::new();
// Should start with one global scope // Should start with one global scope
assert!(stack.var_exists("PATH") || !stack.var_exists("PATH")); // Just check it doesn't panic assert!(stack.var_exists("PATH") || !stack.var_exists("PATH")); // Just check
// it doesn't
// panic
} }
#[test] #[test]
fn scopestack_descend_ascend() { fn scopestack_descend_ascend() {
let mut stack = ScopeStack::new(); let mut stack = ScopeStack::new();
// Set a global variable // Set a global variable
stack.set_var("GLOBAL", "value1", VarFlags::NONE); stack.set_var("GLOBAL", "value1", VarFlags::NONE);
assert_eq!(stack.get_var("GLOBAL"), "value1"); assert_eq!(stack.get_var("GLOBAL"), "value1");
// Descend into a new scope // Descend into a new scope
stack.descend(None); stack.descend(None);
// Global should still be visible // Global should still be visible
assert_eq!(stack.get_var("GLOBAL"), "value1"); assert_eq!(stack.get_var("GLOBAL"), "value1");
// Set a local variable // Set a local variable
stack.set_var("LOCAL", "value2", VarFlags::LOCAL); stack.set_var("LOCAL", "value2", VarFlags::LOCAL);
assert_eq!(stack.get_var("LOCAL"), "value2"); assert_eq!(stack.get_var("LOCAL"), "value2");
// Ascend back to global scope // Ascend back to global scope
stack.ascend(); stack.ascend();
// Global should still exist // Global should still exist
assert_eq!(stack.get_var("GLOBAL"), "value1"); assert_eq!(stack.get_var("GLOBAL"), "value1");
// Local should no longer be visible // Local should no longer be visible
assert_eq!(stack.get_var("LOCAL"), ""); assert_eq!(stack.get_var("LOCAL"), "");
} }
#[test] #[test]
fn scopestack_variable_shadowing() { fn scopestack_variable_shadowing() {
let mut stack = ScopeStack::new(); let mut stack = ScopeStack::new();
// Set global variable // Set global variable
stack.set_var("VAR", "global", VarFlags::NONE); stack.set_var("VAR", "global", VarFlags::NONE);
assert_eq!(stack.get_var("VAR"), "global"); assert_eq!(stack.get_var("VAR"), "global");
// Descend into local scope // Descend into local scope
stack.descend(None); stack.descend(None);
// Set local variable with same name // Set local variable with same name
stack.set_var("VAR", "local", VarFlags::LOCAL); stack.set_var("VAR", "local", VarFlags::LOCAL);
assert_eq!(stack.get_var("VAR"), "local", "Local should shadow global"); assert_eq!(stack.get_var("VAR"), "local", "Local should shadow global");
// Ascend back // Ascend back
stack.ascend(); stack.ascend();
// Global should be restored // Global should be restored
assert_eq!(stack.get_var("VAR"), "global", "Global should be unchanged after ascend"); assert_eq!(
stack.get_var("VAR"),
"global",
"Global should be unchanged after ascend"
);
} }
#[test] #[test]
fn scopestack_local_vs_global_flag() { fn scopestack_local_vs_global_flag() {
let mut stack = ScopeStack::new(); let mut stack = ScopeStack::new();
// Descend into a local scope // Descend into a local scope
stack.descend(None); stack.descend(None);
// Set with LOCAL flag - should go in current scope // Set with LOCAL flag - should go in current scope
stack.set_var("LOCAL_VAR", "local", VarFlags::LOCAL); stack.set_var("LOCAL_VAR", "local", VarFlags::LOCAL);
// Set without LOCAL flag - should go in global scope // Set without LOCAL flag - should go in global scope
stack.set_var("GLOBAL_VAR", "global", VarFlags::NONE); stack.set_var("GLOBAL_VAR", "global", VarFlags::NONE);
// Both visible from local scope // Both visible from local scope
assert_eq!(stack.get_var("LOCAL_VAR"), "local"); assert_eq!(stack.get_var("LOCAL_VAR"), "local");
assert_eq!(stack.get_var("GLOBAL_VAR"), "global"); assert_eq!(stack.get_var("GLOBAL_VAR"), "global");
// Ascend to global // Ascend to global
stack.ascend(); stack.ascend();
// Only global var should be visible // Only global var should be visible
assert_eq!(stack.get_var("GLOBAL_VAR"), "global"); assert_eq!(stack.get_var("GLOBAL_VAR"), "global");
assert_eq!(stack.get_var("LOCAL_VAR"), ""); assert_eq!(stack.get_var("LOCAL_VAR"), "");
} }
#[test] #[test]
fn scopestack_multiple_levels() { fn scopestack_multiple_levels() {
let mut stack = ScopeStack::new(); let mut stack = ScopeStack::new();
stack.set_var("LEVEL0", "global", VarFlags::NONE); stack.set_var("LEVEL0", "global", VarFlags::NONE);
// Level 1 // Level 1
stack.descend(None); stack.descend(None);
stack.set_var("LEVEL1", "first", VarFlags::LOCAL); stack.set_var("LEVEL1", "first", VarFlags::LOCAL);
// Level 2 // Level 2
stack.descend(None); stack.descend(None);
stack.set_var("LEVEL2", "second", VarFlags::LOCAL); stack.set_var("LEVEL2", "second", VarFlags::LOCAL);
// All variables visible from deepest scope // All variables visible from deepest scope
assert_eq!(stack.get_var("LEVEL0"), "global"); assert_eq!(stack.get_var("LEVEL0"), "global");
assert_eq!(stack.get_var("LEVEL1"), "first"); assert_eq!(stack.get_var("LEVEL1"), "first");
assert_eq!(stack.get_var("LEVEL2"), "second"); assert_eq!(stack.get_var("LEVEL2"), "second");
// Ascend to level 1 // Ascend to level 1
stack.ascend(); stack.ascend();
assert_eq!(stack.get_var("LEVEL0"), "global"); assert_eq!(stack.get_var("LEVEL0"), "global");
assert_eq!(stack.get_var("LEVEL1"), "first"); assert_eq!(stack.get_var("LEVEL1"), "first");
assert_eq!(stack.get_var("LEVEL2"), ""); assert_eq!(stack.get_var("LEVEL2"), "");
// Ascend to global // Ascend to global
stack.ascend(); stack.ascend();
assert_eq!(stack.get_var("LEVEL0"), "global"); assert_eq!(stack.get_var("LEVEL0"), "global");
assert_eq!(stack.get_var("LEVEL1"), ""); assert_eq!(stack.get_var("LEVEL1"), "");
assert_eq!(stack.get_var("LEVEL2"), ""); assert_eq!(stack.get_var("LEVEL2"), "");
} }
#[test] #[test]
fn scopestack_cannot_ascend_past_global() { fn scopestack_cannot_ascend_past_global() {
let mut stack = ScopeStack::new(); let mut stack = ScopeStack::new();
stack.set_var("VAR", "value", VarFlags::NONE); stack.set_var("VAR", "value", VarFlags::NONE);
// Try to ascend from global scope (should be no-op) // Try to ascend from global scope (should be no-op)
stack.ascend(); stack.ascend();
stack.ascend(); stack.ascend();
stack.ascend(); stack.ascend();
// Variable should still exist // Variable should still exist
assert_eq!(stack.get_var("VAR"), "value"); assert_eq!(stack.get_var("VAR"), "value");
} }
#[test] #[test]
fn scopestack_descend_with_args() { fn scopestack_descend_with_args() {
let mut stack = ScopeStack::new(); let mut stack = ScopeStack::new();
// Get initial param values from global scope (test process args) // Get initial param values from global scope (test process args)
let global_param_1 = stack.get_param(ShellParam::Pos(1)); let global_param_1 = stack.get_param(ShellParam::Pos(1));
// Descend with positional parameters // Descend with positional parameters
let args = vec!["local_arg1".to_string(), "local_arg2".to_string()]; let args = vec!["local_arg1".to_string(), "local_arg2".to_string()];
stack.descend(Some(args)); stack.descend(Some(args));
// In local scope, positional params come from the VarTab created during descend // In local scope, positional params come from the VarTab created during descend
// VarTab::new() initializes with process args, then our args are appended // VarTab::new() initializes with process args, then our args are appended
// So we check that SOME positional parameter exists (implementation detail may vary) // So we check that SOME positional parameter exists (implementation detail may
let local_param = stack.get_param(ShellParam::Pos(1)); // vary)
assert!(!local_param.is_empty(), "Should have positional parameters in local scope"); let local_param = stack.get_param(ShellParam::Pos(1));
assert!(
!local_param.is_empty(),
"Should have positional parameters in local scope"
);
// Ascend back // Ascend back
stack.ascend(); stack.ascend();
// Should be back to global scope parameters // Should be back to global scope parameters
assert_eq!(stack.get_param(ShellParam::Pos(1)), global_param_1); assert_eq!(stack.get_param(ShellParam::Pos(1)), global_param_1);
} }
#[test] #[test]
fn scopestack_global_parameters() { fn scopestack_global_parameters() {
let mut stack = ScopeStack::new(); let mut stack = ScopeStack::new();
// Set global parameters // Set global parameters
stack.set_param(ShellParam::Status, "0"); stack.set_param(ShellParam::Status, "0");
stack.set_param(ShellParam::LastJob, "1234"); stack.set_param(ShellParam::LastJob, "1234");
assert_eq!(stack.get_param(ShellParam::Status), "0"); assert_eq!(stack.get_param(ShellParam::Status), "0");
assert_eq!(stack.get_param(ShellParam::LastJob), "1234"); assert_eq!(stack.get_param(ShellParam::LastJob), "1234");
// Descend into local scope // Descend into local scope
stack.descend(None); stack.descend(None);
// Global parameters should still be visible // Global parameters should still be visible
assert_eq!(stack.get_param(ShellParam::Status), "0"); assert_eq!(stack.get_param(ShellParam::Status), "0");
assert_eq!(stack.get_param(ShellParam::LastJob), "1234"); assert_eq!(stack.get_param(ShellParam::LastJob), "1234");
// Modify global parameter from local scope // Modify global parameter from local scope
stack.set_param(ShellParam::Status, "1"); stack.set_param(ShellParam::Status, "1");
assert_eq!(stack.get_param(ShellParam::Status), "1"); assert_eq!(stack.get_param(ShellParam::Status), "1");
// Ascend // Ascend
stack.ascend(); stack.ascend();
// Global parameter should retain modified value // Global parameter should retain modified value
assert_eq!(stack.get_param(ShellParam::Status), "1"); assert_eq!(stack.get_param(ShellParam::Status), "1");
} }
#[test] #[test]
fn scopestack_unset_var() { fn scopestack_unset_var() {
let mut stack = ScopeStack::new(); let mut stack = ScopeStack::new();
stack.set_var("VAR", "value", VarFlags::NONE); stack.set_var("VAR", "value", VarFlags::NONE);
assert_eq!(stack.get_var("VAR"), "value"); assert_eq!(stack.get_var("VAR"), "value");
stack.unset_var("VAR"); stack.unset_var("VAR");
assert_eq!(stack.get_var("VAR"), ""); assert_eq!(stack.get_var("VAR"), "");
assert!(!stack.var_exists("VAR")); assert!(!stack.var_exists("VAR"));
} }
#[test] #[test]
fn scopestack_unset_finds_innermost() { fn scopestack_unset_finds_innermost() {
let mut stack = ScopeStack::new(); let mut stack = ScopeStack::new();
// Set global // Set global
stack.set_var("VAR", "global", VarFlags::NONE); stack.set_var("VAR", "global", VarFlags::NONE);
// Descend and shadow // Descend and shadow
stack.descend(None); stack.descend(None);
stack.set_var("VAR", "local", VarFlags::LOCAL); stack.set_var("VAR", "local", VarFlags::LOCAL);
assert_eq!(stack.get_var("VAR"), "local"); assert_eq!(stack.get_var("VAR"), "local");
// Unset should remove local, revealing global // Unset should remove local, revealing global
stack.unset_var("VAR"); stack.unset_var("VAR");
assert_eq!(stack.get_var("VAR"), "global"); assert_eq!(stack.get_var("VAR"), "global");
} }
#[test] #[test]
fn scopestack_export_var() { fn scopestack_export_var() {
let mut stack = ScopeStack::new(); let mut stack = ScopeStack::new();
stack.set_var("VAR", "value", VarFlags::NONE); stack.set_var("VAR", "value", VarFlags::NONE);
// Export the variable // Export the variable
stack.export_var("VAR"); stack.export_var("VAR");
// Variable should still be accessible (flag is internal detail) // Variable should still be accessible (flag is internal detail)
assert_eq!(stack.get_var("VAR"), "value"); assert_eq!(stack.get_var("VAR"), "value");
} }
#[test] #[test]
fn scopestack_var_exists() { fn scopestack_var_exists() {
let mut stack = ScopeStack::new(); let mut stack = ScopeStack::new();
assert!(!stack.var_exists("NONEXISTENT")); assert!(!stack.var_exists("NONEXISTENT"));
stack.set_var("EXISTS", "yes", VarFlags::NONE); stack.set_var("EXISTS", "yes", VarFlags::NONE);
assert!(stack.var_exists("EXISTS")); assert!(stack.var_exists("EXISTS"));
stack.descend(None); stack.descend(None);
assert!(stack.var_exists("EXISTS"), "Global var should be visible in local scope"); assert!(
stack.var_exists("EXISTS"),
"Global var should be visible in local scope"
);
stack.set_var("LOCAL", "yes", VarFlags::LOCAL); stack.set_var("LOCAL", "yes", VarFlags::LOCAL);
assert!(stack.var_exists("LOCAL")); assert!(stack.var_exists("LOCAL"));
stack.ascend(); stack.ascend();
assert!(!stack.var_exists("LOCAL"), "Local var should not exist after ascend"); assert!(
!stack.var_exists("LOCAL"),
"Local var should not exist after ascend"
);
} }
#[test] #[test]
fn scopestack_flatten_vars() { fn scopestack_flatten_vars() {
let mut stack = ScopeStack::new(); let mut stack = ScopeStack::new();
stack.set_var("GLOBAL1", "g1", VarFlags::NONE); stack.set_var("GLOBAL1", "g1", VarFlags::NONE);
stack.set_var("GLOBAL2", "g2", VarFlags::NONE); stack.set_var("GLOBAL2", "g2", VarFlags::NONE);
stack.descend(None); stack.descend(None);
stack.set_var("LOCAL1", "l1", VarFlags::LOCAL); stack.set_var("LOCAL1", "l1", VarFlags::LOCAL);
let flattened = stack.flatten_vars(); let flattened = stack.flatten_vars();
// Should contain variables from all scopes // Should contain variables from all scopes
assert!(flattened.contains_key("GLOBAL1")); assert!(flattened.contains_key("GLOBAL1"));
assert!(flattened.contains_key("GLOBAL2")); assert!(flattened.contains_key("GLOBAL2"));
assert!(flattened.contains_key("LOCAL1")); assert!(flattened.contains_key("LOCAL1"));
} }
// ============================================================================ // ============================================================================
@@ -272,78 +288,81 @@ fn scopestack_flatten_vars() {
#[test] #[test]
fn logtab_new() { fn logtab_new() {
let logtab = LogTab::new(); let logtab = LogTab::new();
assert_eq!(logtab.funcs().len(), 0); assert_eq!(logtab.funcs().len(), 0);
assert_eq!(logtab.aliases().len(), 0); assert_eq!(logtab.aliases().len(), 0);
} }
#[test] #[test]
fn logtab_insert_get_alias() { fn logtab_insert_get_alias() {
let mut logtab = LogTab::new(); let mut logtab = LogTab::new();
logtab.insert_alias("ll", "ls -la"); logtab.insert_alias("ll", "ls -la");
assert_eq!(logtab.get_alias("ll"), Some("ls -la".to_string())); assert_eq!(logtab.get_alias("ll"), Some("ls -la".to_string()));
assert_eq!(logtab.get_alias("nonexistent"), None); assert_eq!(logtab.get_alias("nonexistent"), None);
} }
#[test] #[test]
fn logtab_overwrite_alias() { fn logtab_overwrite_alias() {
let mut logtab = LogTab::new(); let mut logtab = LogTab::new();
logtab.insert_alias("ll", "ls -la"); logtab.insert_alias("ll", "ls -la");
assert_eq!(logtab.get_alias("ll"), Some("ls -la".to_string())); assert_eq!(logtab.get_alias("ll"), Some("ls -la".to_string()));
logtab.insert_alias("ll", "ls -lah"); logtab.insert_alias("ll", "ls -lah");
assert_eq!(logtab.get_alias("ll"), Some("ls -lah".to_string())); assert_eq!(logtab.get_alias("ll"), Some("ls -lah".to_string()));
} }
#[test] #[test]
fn logtab_remove_alias() { fn logtab_remove_alias() {
let mut logtab = LogTab::new(); let mut logtab = LogTab::new();
logtab.insert_alias("ll", "ls -la"); logtab.insert_alias("ll", "ls -la");
assert!(logtab.get_alias("ll").is_some()); assert!(logtab.get_alias("ll").is_some());
logtab.remove_alias("ll"); logtab.remove_alias("ll");
assert!(logtab.get_alias("ll").is_none()); assert!(logtab.get_alias("ll").is_none());
} }
#[test] #[test]
fn logtab_clear_aliases() { fn logtab_clear_aliases() {
let mut logtab = LogTab::new(); let mut logtab = LogTab::new();
logtab.insert_alias("ll", "ls -la"); logtab.insert_alias("ll", "ls -la");
logtab.insert_alias("la", "ls -A"); logtab.insert_alias("la", "ls -A");
logtab.insert_alias("l", "ls -CF"); logtab.insert_alias("l", "ls -CF");
assert_eq!(logtab.aliases().len(), 3); assert_eq!(logtab.aliases().len(), 3);
logtab.clear_aliases(); logtab.clear_aliases();
assert_eq!(logtab.aliases().len(), 0); assert_eq!(logtab.aliases().len(), 0);
} }
#[test] #[test]
fn logtab_multiple_aliases() { fn logtab_multiple_aliases() {
let mut logtab = LogTab::new(); let mut logtab = LogTab::new();
logtab.insert_alias("ll", "ls -la"); logtab.insert_alias("ll", "ls -la");
logtab.insert_alias("la", "ls -A"); logtab.insert_alias("la", "ls -A");
logtab.insert_alias("grep", "grep --color=auto"); logtab.insert_alias("grep", "grep --color=auto");
assert_eq!(logtab.aliases().len(), 3); assert_eq!(logtab.aliases().len(), 3);
assert_eq!(logtab.get_alias("ll"), Some("ls -la".to_string())); assert_eq!(logtab.get_alias("ll"), Some("ls -la".to_string()));
assert_eq!(logtab.get_alias("la"), Some("ls -A".to_string())); assert_eq!(logtab.get_alias("la"), Some("ls -A".to_string()));
assert_eq!(logtab.get_alias("grep"), Some("grep --color=auto".to_string())); assert_eq!(
logtab.get_alias("grep"),
Some("grep --color=auto".to_string())
);
} }
// Note: Function tests are limited because ShFunc requires complex setup (parsed AST) // Note: Function tests are limited because ShFunc requires complex setup
// We'll test the basic storage/retrieval mechanics // (parsed AST) We'll test the basic storage/retrieval mechanics
#[test] #[test]
fn logtab_funcs_empty_initially() { fn logtab_funcs_empty_initially() {
let logtab = LogTab::new(); let logtab = LogTab::new();
assert_eq!(logtab.funcs().len(), 0); assert_eq!(logtab.funcs().len(), 0);
assert!(logtab.get_func("nonexistent").is_none()); assert!(logtab.get_func("nonexistent").is_none());
} }
// ============================================================================ // ============================================================================
@@ -352,109 +371,112 @@ fn logtab_funcs_empty_initially() {
#[test] #[test]
fn vartab_new() { fn vartab_new() {
let vartab = VarTab::new(); let vartab = VarTab::new();
// VarTab initializes with some default params, just check it doesn't panic // VarTab initializes with some default params, just check it doesn't panic
assert!(vartab.get_var("NONEXISTENT").is_empty()); assert!(vartab.get_var("NONEXISTENT").is_empty());
} }
#[test] #[test]
fn vartab_set_get_var() { fn vartab_set_get_var() {
let mut vartab = VarTab::new(); let mut vartab = VarTab::new();
vartab.set_var("TEST", "value", VarFlags::NONE); vartab.set_var("TEST", "value", VarFlags::NONE);
assert_eq!(vartab.get_var("TEST"), "value"); assert_eq!(vartab.get_var("TEST"), "value");
} }
#[test] #[test]
fn vartab_overwrite_var() { fn vartab_overwrite_var() {
let mut vartab = VarTab::new(); let mut vartab = VarTab::new();
vartab.set_var("VAR", "value1", VarFlags::NONE); vartab.set_var("VAR", "value1", VarFlags::NONE);
assert_eq!(vartab.get_var("VAR"), "value1"); assert_eq!(vartab.get_var("VAR"), "value1");
vartab.set_var("VAR", "value2", VarFlags::NONE); vartab.set_var("VAR", "value2", VarFlags::NONE);
assert_eq!(vartab.get_var("VAR"), "value2"); assert_eq!(vartab.get_var("VAR"), "value2");
} }
#[test] #[test]
fn vartab_var_exists() { fn vartab_var_exists() {
let mut vartab = VarTab::new(); let mut vartab = VarTab::new();
assert!(!vartab.var_exists("TEST")); assert!(!vartab.var_exists("TEST"));
vartab.set_var("TEST", "value", VarFlags::NONE); vartab.set_var("TEST", "value", VarFlags::NONE);
assert!(vartab.var_exists("TEST")); assert!(vartab.var_exists("TEST"));
} }
#[test] #[test]
fn vartab_unset_var() { fn vartab_unset_var() {
let mut vartab = VarTab::new(); let mut vartab = VarTab::new();
vartab.set_var("VAR", "value", VarFlags::NONE); vartab.set_var("VAR", "value", VarFlags::NONE);
assert!(vartab.var_exists("VAR")); assert!(vartab.var_exists("VAR"));
vartab.unset_var("VAR"); vartab.unset_var("VAR");
assert!(!vartab.var_exists("VAR")); assert!(!vartab.var_exists("VAR"));
assert_eq!(vartab.get_var("VAR"), ""); assert_eq!(vartab.get_var("VAR"), "");
} }
#[test] #[test]
fn vartab_export_var() { fn vartab_export_var() {
let mut vartab = VarTab::new(); let mut vartab = VarTab::new();
vartab.set_var("VAR", "value", VarFlags::NONE); vartab.set_var("VAR", "value", VarFlags::NONE);
vartab.export_var("VAR"); vartab.export_var("VAR");
// Variable should still be accessible // Variable should still be accessible
assert_eq!(vartab.get_var("VAR"), "value"); assert_eq!(vartab.get_var("VAR"), "value");
} }
#[test] #[test]
fn vartab_positional_params() { fn vartab_positional_params() {
let mut vartab = VarTab::new(); let mut vartab = VarTab::new();
// Get the current argv length // Get the current argv length
let initial_len = vartab.sh_argv().len(); let initial_len = vartab.sh_argv().len();
// Clear and reinitialize with known args // Clear and reinitialize with known args
vartab.clear_args(); // This keeps $0 as current exe vartab.clear_args(); // This keeps $0 as current exe
// After clear_args, should have just $0 // After clear_args, should have just $0
// Push additional args // Push additional args
vartab.bpush_arg("test_arg1".to_string()); vartab.bpush_arg("test_arg1".to_string());
vartab.bpush_arg("test_arg2".to_string()); vartab.bpush_arg("test_arg2".to_string());
// Now sh_argv should be: [exe_path, test_arg1, test_arg2] // Now sh_argv should be: [exe_path, test_arg1, test_arg2]
// Pos(0) = exe_path, Pos(1) = test_arg1, Pos(2) = test_arg2 // Pos(0) = exe_path, Pos(1) = test_arg1, Pos(2) = test_arg2
let final_len = vartab.sh_argv().len(); let final_len = vartab.sh_argv().len();
assert!(final_len > initial_len || final_len >= 1, "Should have arguments"); assert!(
final_len > initial_len || final_len >= 1,
"Should have arguments"
);
// Just verify we can retrieve the last args we pushed // Just verify we can retrieve the last args we pushed
let last_idx = final_len - 1; let last_idx = final_len - 1;
assert_eq!(vartab.get_param(ShellParam::Pos(last_idx)), "test_arg2"); assert_eq!(vartab.get_param(ShellParam::Pos(last_idx)), "test_arg2");
} }
#[test] #[test]
fn vartab_shell_argv_operations() { fn vartab_shell_argv_operations() {
let mut vartab = VarTab::new(); let mut vartab = VarTab::new();
// Clear initial args and set fresh ones // Clear initial args and set fresh ones
vartab.clear_args(); vartab.clear_args();
// Push args (clear_args leaves $0, so these become $1, $2, $3) // Push args (clear_args leaves $0, so these become $1, $2, $3)
vartab.bpush_arg("arg1".to_string()); vartab.bpush_arg("arg1".to_string());
vartab.bpush_arg("arg2".to_string()); vartab.bpush_arg("arg2".to_string());
vartab.bpush_arg("arg3".to_string()); vartab.bpush_arg("arg3".to_string());
// Get initial arg count // Get initial arg count
let initial_len = vartab.sh_argv().len(); let initial_len = vartab.sh_argv().len();
// Pop first arg (removes $0) // Pop first arg (removes $0)
let popped = vartab.fpop_arg(); let popped = vartab.fpop_arg();
assert!(popped.is_some()); assert!(popped.is_some());
// Should have one fewer arg // Should have one fewer arg
assert_eq!(vartab.sh_argv().len(), initial_len - 1); assert_eq!(vartab.sh_argv().len(), initial_len - 1);
} }
// ============================================================================ // ============================================================================
@@ -463,39 +485,39 @@ fn vartab_shell_argv_operations() {
#[test] #[test]
fn varflags_none() { fn varflags_none() {
let flags = VarFlags::NONE; let flags = VarFlags::NONE;
assert!(!flags.contains(VarFlags::EXPORT)); assert!(!flags.contains(VarFlags::EXPORT));
assert!(!flags.contains(VarFlags::LOCAL)); assert!(!flags.contains(VarFlags::LOCAL));
assert!(!flags.contains(VarFlags::READONLY)); assert!(!flags.contains(VarFlags::READONLY));
} }
#[test] #[test]
fn varflags_export() { fn varflags_export() {
let flags = VarFlags::EXPORT; let flags = VarFlags::EXPORT;
assert!(flags.contains(VarFlags::EXPORT)); assert!(flags.contains(VarFlags::EXPORT));
assert!(!flags.contains(VarFlags::LOCAL)); assert!(!flags.contains(VarFlags::LOCAL));
} }
#[test] #[test]
fn varflags_local() { fn varflags_local() {
let flags = VarFlags::LOCAL; let flags = VarFlags::LOCAL;
assert!(!flags.contains(VarFlags::EXPORT)); assert!(!flags.contains(VarFlags::EXPORT));
assert!(flags.contains(VarFlags::LOCAL)); assert!(flags.contains(VarFlags::LOCAL));
} }
#[test] #[test]
fn varflags_combine() { fn varflags_combine() {
let flags = VarFlags::EXPORT | VarFlags::LOCAL; let flags = VarFlags::EXPORT | VarFlags::LOCAL;
assert!(flags.contains(VarFlags::EXPORT)); assert!(flags.contains(VarFlags::EXPORT));
assert!(flags.contains(VarFlags::LOCAL)); assert!(flags.contains(VarFlags::LOCAL));
assert!(!flags.contains(VarFlags::READONLY)); assert!(!flags.contains(VarFlags::READONLY));
} }
#[test] #[test]
fn varflags_readonly() { fn varflags_readonly() {
let flags = VarFlags::READONLY; let flags = VarFlags::READONLY;
assert!(flags.contains(VarFlags::READONLY)); assert!(flags.contains(VarFlags::READONLY));
assert!(!flags.contains(VarFlags::EXPORT)); assert!(!flags.contains(VarFlags::EXPORT));
} }
// ============================================================================ // ============================================================================
@@ -504,49 +526,70 @@ fn varflags_readonly() {
#[test] #[test]
fn shellparam_is_global() { fn shellparam_is_global() {
assert!(ShellParam::Status.is_global()); assert!(ShellParam::Status.is_global());
assert!(ShellParam::ShPid.is_global()); assert!(ShellParam::ShPid.is_global());
assert!(ShellParam::LastJob.is_global()); assert!(ShellParam::LastJob.is_global());
assert!(ShellParam::ShellName.is_global()); assert!(ShellParam::ShellName.is_global());
assert!(!ShellParam::Pos(1).is_global()); assert!(!ShellParam::Pos(1).is_global());
assert!(!ShellParam::AllArgs.is_global()); assert!(!ShellParam::AllArgs.is_global());
assert!(!ShellParam::AllArgsStr.is_global()); assert!(!ShellParam::AllArgsStr.is_global());
assert!(!ShellParam::ArgCount.is_global()); assert!(!ShellParam::ArgCount.is_global());
} }
#[test] #[test]
fn shellparam_from_str() { fn shellparam_from_str() {
assert!(matches!("?".parse::<ShellParam>().unwrap(), ShellParam::Status)); assert!(matches!(
assert!(matches!("$".parse::<ShellParam>().unwrap(), ShellParam::ShPid)); "?".parse::<ShellParam>().unwrap(),
assert!(matches!("!".parse::<ShellParam>().unwrap(), ShellParam::LastJob)); ShellParam::Status
assert!(matches!("0".parse::<ShellParam>().unwrap(), ShellParam::ShellName)); ));
assert!(matches!("@".parse::<ShellParam>().unwrap(), ShellParam::AllArgs)); assert!(matches!(
assert!(matches!("*".parse::<ShellParam>().unwrap(), ShellParam::AllArgsStr)); "$".parse::<ShellParam>().unwrap(),
assert!(matches!("#".parse::<ShellParam>().unwrap(), ShellParam::ArgCount)); ShellParam::ShPid
));
assert!(matches!(
"!".parse::<ShellParam>().unwrap(),
ShellParam::LastJob
));
assert!(matches!(
"0".parse::<ShellParam>().unwrap(),
ShellParam::ShellName
));
assert!(matches!(
"@".parse::<ShellParam>().unwrap(),
ShellParam::AllArgs
));
assert!(matches!(
"*".parse::<ShellParam>().unwrap(),
ShellParam::AllArgsStr
));
assert!(matches!(
"#".parse::<ShellParam>().unwrap(),
ShellParam::ArgCount
));
match "1".parse::<ShellParam>().unwrap() { match "1".parse::<ShellParam>().unwrap() {
ShellParam::Pos(n) => assert_eq!(n, 1), ShellParam::Pos(n) => assert_eq!(n, 1),
_ => panic!("Expected Pos(1)"), _ => panic!("Expected Pos(1)"),
} }
match "42".parse::<ShellParam>().unwrap() { match "42".parse::<ShellParam>().unwrap() {
ShellParam::Pos(n) => assert_eq!(n, 42), ShellParam::Pos(n) => assert_eq!(n, 42),
_ => panic!("Expected Pos(42)"), _ => panic!("Expected Pos(42)"),
} }
assert!("invalid".parse::<ShellParam>().is_err()); assert!("invalid".parse::<ShellParam>().is_err());
} }
#[test] #[test]
fn shellparam_display() { fn shellparam_display() {
assert_eq!(ShellParam::Status.to_string(), "?"); assert_eq!(ShellParam::Status.to_string(), "?");
assert_eq!(ShellParam::ShPid.to_string(), "$"); assert_eq!(ShellParam::ShPid.to_string(), "$");
assert_eq!(ShellParam::LastJob.to_string(), "!"); assert_eq!(ShellParam::LastJob.to_string(), "!");
assert_eq!(ShellParam::ShellName.to_string(), "0"); assert_eq!(ShellParam::ShellName.to_string(), "0");
assert_eq!(ShellParam::AllArgs.to_string(), "@"); assert_eq!(ShellParam::AllArgs.to_string(), "@");
assert_eq!(ShellParam::AllArgsStr.to_string(), "*"); assert_eq!(ShellParam::AllArgsStr.to_string(), "*");
assert_eq!(ShellParam::ArgCount.to_string(), "#"); assert_eq!(ShellParam::ArgCount.to_string(), "#");
assert_eq!(ShellParam::Pos(1).to_string(), "1"); assert_eq!(ShellParam::Pos(1).to_string(), "1");
assert_eq!(ShellParam::Pos(99).to_string(), "99"); assert_eq!(ShellParam::Pos(99).to_string(), "99");
} }