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 959ea9346a
commit 74988166f0
44 changed files with 5549 additions and 5019 deletions

View File

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

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) {
return Err(ShErr::full(
ShErrKind::ExecFail,
format!("cd: Failed to change directory: {}", e),
span,
));
}
let new_dir = env::current_dir().map_err(
|e| ShErr::full(ShErrKind::ExecFail, format!("cd: Failed to get current directory: {}", e), span)
)?;
return Err(ShErr::full(
ShErrKind::ExecFail,
format!("cd: Failed to change directory: {}", e),
span,
));
}
let new_dir = env::current_dir().map_err(|e| {
ShErr::full(
ShErrKind::ExecFail,
format!("cd: Failed to get current directory: {}", e),
span,
)
})?;
unsafe { env::set_var("PWD", new_dir) };
state::set_status(0);

View File

@@ -1,14 +1,34 @@
use std::sync::LazyLock;
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] = [
OptSpec { opt: Opt::Short('n'), 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 },
pub const ECHO_OPTS: [OptSpec; 4] = [
OptSpec {
opt: Opt::Short('n'),
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! {
@@ -16,7 +36,7 @@ bitflags! {
const NO_NEWLINE = 0b000001;
const USE_STDERR = 0b000010;
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)
};
let mut echo_output = prepare_echo_args(argv
.into_iter()
.map(|a| a.0) // Extract the String from the tuple of (String,Span)
.collect::<Vec<_>>(),
flags.contains(EchoFlags::USE_ESCAPE),
flags.contains(EchoFlags::USE_PROMPT)
)?.join(" ");
let mut echo_output = prepare_echo_args(
argv
.into_iter()
.map(|a| a.0) // Extract the String from the tuple of (String,Span)
.collect::<Vec<_>>(),
flags.contains(EchoFlags::USE_ESCAPE),
flags.contains(EchoFlags::USE_PROMPT),
)?
.join(" ");
if !flags.contains(EchoFlags::NO_NEWLINE) && !echo_output.ends_with('\n') {
echo_output.push('\n')
@@ -58,137 +80,141 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
Ok(())
}
pub fn prepare_echo_args(argv: Vec<String>, use_escape: bool, use_prompt: bool) -> ShResult<Vec<String>> {
if !use_escape {
if use_prompt {
let expanded: ShResult<Vec<String>> = argv
.into_iter()
.map(|s| expand_prompt(s.as_str()))
.collect();
return expanded
}
return Ok(argv);
}
pub fn prepare_echo_args(
argv: Vec<String>,
use_escape: bool,
use_prompt: bool,
) -> ShResult<Vec<String>> {
if !use_escape {
if use_prompt {
let expanded: ShResult<Vec<String>> = argv
.into_iter()
.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 {
let mut prepared_arg = String::new();
if use_prompt {
prepared_arg = expand_prompt(&prepared_arg)?;
}
for arg in argv {
let mut prepared_arg = String::new();
if use_prompt {
prepared_arg = expand_prompt(&prepared_arg)?;
}
let mut chars = arg.chars().peekable();
let mut chars = arg.chars().peekable();
while let Some(c) = chars.next() {
if c == '\\' {
if let Some(&next_char) = chars.peek() {
match next_char {
'n' => {
prepared_arg.push('\n');
chars.next();
}
't' => {
prepared_arg.push('\t');
chars.next();
}
'r' => {
prepared_arg.push('\r');
chars.next();
}
'a' => {
prepared_arg.push('\x07');
chars.next();
}
'b' => {
prepared_arg.push('\x08');
chars.next();
}
'e' | 'E' => {
prepared_arg.push('\x1b');
chars.next();
}
'x' => {
chars.next(); // consume 'x'
let mut hex_digits = String::new();
for _ in 0..2 {
if let Some(&hex_char) = chars.peek() {
if hex_char.is_ascii_hexdigit() {
hex_digits.push(hex_char);
chars.next();
} else {
break;
}
} else {
break;
}
}
if let Ok(value) = u8::from_str_radix(&hex_digits, 16) {
prepared_arg.push(value as char);
} else {
prepared_arg.push('\\');
prepared_arg.push('x');
prepared_arg.push_str(&hex_digits);
}
}
'0' => {
chars.next(); // consume '0'
let mut octal_digits = String::new();
for _ in 0..3 {
if let Some(&octal_char) = chars.peek() {
if ('0'..='7').contains(&octal_char) {
octal_digits.push(octal_char);
chars.next();
} else {
break;
}
} else {
break;
}
}
if let Ok(value) = u8::from_str_radix(&octal_digits, 8) {
prepared_arg.push(value as char);
} else {
prepared_arg.push('\\');
prepared_arg.push('0');
prepared_arg.push_str(&octal_digits);
}
}
'\\' => {
prepared_arg.push('\\');
chars.next();
}
_ => prepared_arg.push(c),
}
} else {
prepared_arg.push(c);
}
} else {
prepared_arg.push(c);
}
}
while let Some(c) = chars.next() {
if c == '\\' {
if let Some(&next_char) = chars.peek() {
match next_char {
'n' => {
prepared_arg.push('\n');
chars.next();
}
't' => {
prepared_arg.push('\t');
chars.next();
}
'r' => {
prepared_arg.push('\r');
chars.next();
}
'a' => {
prepared_arg.push('\x07');
chars.next();
}
'b' => {
prepared_arg.push('\x08');
chars.next();
}
'e' | 'E' => {
prepared_arg.push('\x1b');
chars.next();
}
'x' => {
chars.next(); // consume 'x'
let mut hex_digits = String::new();
for _ in 0..2 {
if let Some(&hex_char) = chars.peek() {
if hex_char.is_ascii_hexdigit() {
hex_digits.push(hex_char);
chars.next();
} else {
break;
}
} else {
break;
}
}
if let Ok(value) = u8::from_str_radix(&hex_digits, 16) {
prepared_arg.push(value as char);
} else {
prepared_arg.push('\\');
prepared_arg.push('x');
prepared_arg.push_str(&hex_digits);
}
}
'0' => {
chars.next(); // consume '0'
let mut octal_digits = String::new();
for _ in 0..3 {
if let Some(&octal_char) = chars.peek() {
if ('0'..='7').contains(&octal_char) {
octal_digits.push(octal_char);
chars.next();
} else {
break;
}
} else {
break;
}
}
if let Ok(value) = u8::from_str_radix(&octal_digits, 8) {
prepared_arg.push(value as char);
} else {
prepared_arg.push('\\');
prepared_arg.push('0');
prepared_arg.push_str(&octal_digits);
}
}
'\\' => {
prepared_arg.push('\\');
chars.next();
}
_ => prepared_arg.push(c),
}
} else {
prepared_arg.push(c);
}
} else {
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> {
let mut flags = EchoFlags::empty();
for opt in opts {
for opt in opts {
match opt {
Opt::Short('n') => flags |= EchoFlags::NO_NEWLINE,
Opt::Short('r') => flags |= EchoFlags::USE_STDERR,
Opt::Short('e') => flags |= EchoFlags::USE_ESCAPE,
Opt::Short('p') => flags |= EchoFlags::USE_PROMPT,
_ => {
return Err(ShErr::simple(
ShErrKind::ExecFail,
format!("echo: Unexpected flag '{opt}'"),
));
}
Opt::Short('p') => flags |= EchoFlags::USE_PROMPT,
_ => {
return Err(ShErr::simple(
ShErrKind::ExecFail,
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 {
if let Some((var, val)) = arg.split_once('=') {
write_vars(|v| v.set_var(var, val, VarFlags::EXPORT)); // Export an assignment like
// 'foo=bar'
// 'foo=bar'
} else {
write_vars(|v| v.export_var(&arg)); // Export an existing variable, if
// any
// any
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,7 +8,7 @@ use regex::Regex;
use crate::{
libsh::error::{ShErr, ShErrKind, ShResult},
parse::{ConjunctOp, NdRule, Node, TestCase, TEST_UNARY_OPS},
parse::{ConjunctOp, NdRule, Node, TEST_UNARY_OPS, TestCase},
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(),
notes: vec![],
span: err_span,
})
});
}
TestOp::StringEq => rhs.trim() == lhs.trim(),
TestOp::StringNeq => rhs.trim() != lhs.trim(),

View File

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

View File

@@ -37,14 +37,32 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu
else {
unreachable!()
};
let zolt_opts = [
OptSpec { opt: Opt::Long("dry-run".into()), takes_arg: false },
OptSpec { 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 zolt_opts = [
OptSpec {
opt: Opt::Long("dry-run".into()),
takes_arg: false,
},
OptSpec {
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 (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,
"dry-run" => flags |= ZoltFlags::DRY,
_ => {
return Err(ShErr::simple(
ShErrKind::SyntaxErr,
format!("zoltraak: unrecognized option '{flag}'"),
));
}
return Err(ShErr::simple(
ShErrKind::SyntaxErr,
format!("zoltraak: unrecognized option '{flag}'"),
));
}
},
Opt::Short(flag) => match flag {
'r' => flags |= ZoltFlags::RECURSIVE,
'f' => flags |= ZoltFlags::FORCE,
'v' => flags |= ZoltFlags::VERBOSE,
_ => {
return Err(ShErr::simple(
ShErrKind::SyntaxErr,
format!("zoltraak: unrecognized option '{flag}'"),
));
}
return Err(ShErr::simple(
ShErrKind::SyntaxErr,
format!("zoltraak: unrecognized option '{flag}'"),
));
}
},
Opt::LongWithArg(flag, _) => {
return Err(ShErr::simple(
ShErrKind::SyntaxErr,
format!("zoltraak: unrecognized option '{flag}'"),
));
}
Opt::ShortWithArg(flag, _) => {
return Err(ShErr::simple(
ShErrKind::SyntaxErr,
format!("zoltraak: unrecognized option '{flag}'"),
));
}
Opt::LongWithArg(flag, _) => {
return Err(ShErr::simple(
ShErrKind::SyntaxErr,
format!("zoltraak: unrecognized option '{flag}'"),
));
}
Opt::ShortWithArg(flag, _) => {
return Err(ShErr::simple(
ShErrKind::SyntaxErr,
format!("zoltraak: unrecognized option '{flag}'"),
));
}
}
}
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
for (arg, span) in argv {
if &arg == "/" && !flags.contains(ZoltFlags::NO_PRESERVE_ROOT) {
return Err(
@@ -109,7 +126,6 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu
}
}
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)]
pub enum Opt {
Long(String),
LongWithArg(String,String),
LongWithArg(String, String),
Short(char),
ShortWithArg(char,String),
ShortWithArg(char, String),
}
pub struct OptSpec {
pub opt: Opt,
pub takes_arg: bool,
pub opt: Opt,
pub takes_arg: bool,
}
impl Opt {
@@ -41,8 +41,8 @@ impl Display for Opt {
match self {
Self::Long(opt) => write!(f, "--{}", opt),
Self::Short(opt) => write!(f, "-{}", opt),
Self::LongWithArg(opt, arg) => write!(f, "--{} {}", opt, arg),
Self::ShortWithArg(opt, arg) => write!(f, "-{} {}", opt, arg)
Self::LongWithArg(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() {
non_opts.push(token)
} else {
for opt in parsed_opts {
let mut pushed = false;
for opt_spec in opt_specs {
if opt_spec.opt == opt {
if opt_spec.takes_arg {
let arg = tokens_iter.next()
.map(|t| t.to_string())
.unwrap_or_default();
for opt in parsed_opts {
let mut pushed = false;
for opt_spec in opt_specs {
if opt_spec.opt == opt {
if opt_spec.takes_arg {
let arg = tokens_iter
.next()
.map(|t| t.to_string())
.unwrap_or_default();
let opt = match opt {
Opt::Long(ref opt) => Opt::LongWithArg(opt.to_string(), arg),
Opt::Short(opt) => Opt::ShortWithArg(opt, arg),
_ => unreachable!(),
};
opts.push(opt);
pushed = true;
} else {
opts.push(opt.clone());
pushed = true;
}
}
}
if !pushed {
non_opts.push(token.clone());
}
}
let opt = match opt {
Opt::Long(ref opt) => Opt::LongWithArg(opt.to_string(), arg),
Opt::Short(opt) => Opt::ShortWithArg(opt, arg),
_ => unreachable!(),
};
opts.push(opt);
pushed = true;
} else {
opts.push(opt.clone());
pushed = true;
}
}
}
if !pushed {
non_opts.push(token.clone());
}
}
}
}
(non_opts, opts)

View File

@@ -2,7 +2,11 @@ use crate::{
libsh::{
error::ShResult,
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;
@@ -685,7 +689,9 @@ pub fn wait_fg(job: Job) -> ShResult<()> {
}
// If job wasn't stopped (moved to bg), clear the fg slot
if !was_stopped {
write_jobs(|j| { j.take_fg(); });
write_jobs(|j| {
j.take_fg();
});
}
take_term()?;
set_status(code);

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,445 +1,467 @@
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 {
CmdName,
FileName
CmdName,
FileName,
}
pub enum CompResult {
NoMatch,
Single {
result: String
},
Many {
candidates: Vec<String>
}
NoMatch,
Single { result: String },
Many { candidates: Vec<String> },
}
impl CompResult {
pub fn from_candidates(candidates: Vec<String>) -> Self {
if candidates.is_empty() {
Self::NoMatch
} else if candidates.len() == 1 {
Self::Single { result: candidates[0].clone() }
} else {
Self::Many { candidates }
}
}
pub fn from_candidates(candidates: Vec<String>) -> Self {
if candidates.is_empty() {
Self::NoMatch
} else if candidates.len() == 1 {
Self::Single {
result: candidates[0].clone(),
}
} else {
Self::Many { candidates }
}
}
}
pub struct Completer {
pub candidates: Vec<String>,
pub selected_idx: usize,
pub original_input: String,
pub token_span: (usize, usize),
pub active: bool,
pub candidates: Vec<String>,
pub selected_idx: usize,
pub original_input: String,
pub token_span: (usize, usize),
pub active: bool,
}
impl Completer {
pub fn new() -> Self {
Self {
candidates: vec![],
selected_idx: 0,
original_input: String::new(),
token_span: (0, 0),
active: false,
}
}
pub fn new() -> Self {
Self {
candidates: vec![],
selected_idx: 0,
original_input: String::new(),
token_span: (0, 0),
active: false,
}
}
pub fn slice_line(line: &str, cursor_pos: usize) -> (&str, &str) {
let (before_cursor, after_cursor) = line.split_at(cursor_pos);
(before_cursor, after_cursor)
}
pub fn slice_line(line: &str, cursor_pos: usize) -> (&str, &str) {
let (before_cursor, after_cursor) = line.split_at(cursor_pos);
(before_cursor, after_cursor)
}
pub fn get_completion_context(&self, line: &str, cursor_pos: usize) -> (Vec<Marker>, usize) {
let annotated = annotate_input_recursive(line);
log::debug!("Annotated input for completion context: {:?}", annotated);
let mut ctx = vec![markers::NULL];
let mut last_priority = 0;
let mut ctx_start = 0;
let mut pos = 0;
pub fn get_completion_context(&self, line: &str, cursor_pos: usize) -> (Vec<Marker>, usize) {
let annotated = annotate_input_recursive(line);
let mut ctx = vec![markers::NULL];
let mut last_priority = 0;
let mut ctx_start = 0;
let mut pos = 0;
for ch in annotated.chars() {
match ch {
_ if is_marker(ch) => {
match ch {
markers::COMMAND | markers::BUILTIN => {
log::debug!("Found command marker at position {}", pos);
if last_priority < 2 {
if last_priority > 0 {
ctx.pop();
}
ctx_start = pos;
last_priority = 2;
ctx.push(markers::COMMAND);
}
}
markers::VAR_SUB => {
log::debug!("Found variable substitution marker at position {}", pos);
if last_priority < 3 {
if last_priority > 0 {
ctx.pop();
}
ctx_start = pos;
last_priority = 3;
ctx.push(markers::VAR_SUB);
}
}
markers::ARG | markers::ASSIGNMENT => {
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;
}
}
}
}
for ch in annotated.chars() {
match ch {
_ if is_marker(ch) => match ch {
markers::COMMAND | markers::BUILTIN => {
if last_priority < 2 {
if last_priority > 0 {
ctx.pop();
}
ctx_start = pos;
last_priority = 2;
ctx.push(markers::COMMAND);
}
}
markers::VAR_SUB => {
if last_priority < 3 {
if last_priority > 0 {
ctx.pop();
}
ctx_start = pos;
last_priority = 3;
ctx.push(markers::VAR_SUB);
}
}
markers::ARG | markers::ASSIGNMENT => {
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;
}
}
}
}
(ctx, ctx_start)
}
(ctx, ctx_start)
}
pub fn reset(&mut self) {
self.candidates.clear();
self.selected_idx = 0;
self.original_input.clear();
self.token_span = (0, 0);
self.active = false;
}
pub fn reset(&mut self) {
self.candidates.clear();
self.selected_idx = 0;
self.original_input.clear();
self.token_span = (0, 0);
self.active = false;
}
pub fn complete(&mut self, line: String, cursor_pos: usize, direction: i32) -> ShResult<Option<String>> {
if self.active {
Ok(Some(self.cycle_completion(direction)))
} else {
self.start_completion(line, cursor_pos)
}
}
pub fn complete(
&mut self,
line: String,
cursor_pos: usize,
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> {
self.candidates.get(self.selected_idx).cloned()
}
pub fn selected_candidate(&self) -> Option<String> {
self.candidates.get(self.selected_idx).cloned()
}
pub fn cycle_completion(&mut self, direction: i32) -> String {
if self.candidates.is_empty() {
return self.original_input.clone();
}
pub fn cycle_completion(&mut self, direction: i32) -> String {
if self.candidates.is_empty() {
return self.original_input.clone();
}
let len = self.candidates.len();
self.selected_idx = (self.selected_idx as i32 + direction).rem_euclid(len as i32) as usize;
let len = self.candidates.len();
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>> {
let result = self.get_candidates(line.clone(), cursor_pos)?;
match result {
CompResult::Many { candidates } => {
self.candidates = candidates.clone();
self.selected_idx = 0;
self.original_input = line;
self.active = true;
pub fn start_completion(&mut self, line: String, cursor_pos: usize) -> ShResult<Option<String>> {
let result = self.get_candidates(line.clone(), cursor_pos)?;
match result {
CompResult::Many { candidates } => {
self.candidates = candidates.clone();
self.selected_idx = 0;
self.original_input = line;
self.active = true;
Ok(Some(self.get_completed_line()))
}
CompResult::Single { result } => {
self.candidates = vec![result.clone()];
self.selected_idx = 0;
self.original_input = line;
self.active = false;
Ok(Some(self.get_completed_line()))
}
CompResult::Single { result } => {
self.candidates = vec![result.clone()];
self.selected_idx = 0;
self.original_input = line;
self.active = false;
Ok(Some(self.get_completed_line()))
}
CompResult::NoMatch => Ok(None)
Ok(Some(self.get_completed_line()))
}
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)> {
let mut chars = text.chars().peekable();
let mut name = String::new();
let mut reading_name = false;
let mut pos = 0;
let mut name_start = 0;
let mut name_end = 0;
while let Some(ch) = chars.next() {
match ch {
'$' => {
if chars.peek() == Some(&'{') {
continue;
}
while let Some(ch) = chars.next() {
match ch {
'$' => {
if chars.peek() == Some(&'{') {
continue;
}
reading_name = true;
name_start = pos + 1; // Start after the '$'
}
'{' if !reading_name => {
reading_name = true;
name_start = pos + 1;
}
ch if ch.is_alphanumeric() || ch == '_' => {
if reading_name {
name.push(ch);
}
}
_ => {
if reading_name {
name_end = pos; // End before the non-alphanumeric character
break;
}
}
}
pos += 1;
}
reading_name = true;
name_start = pos + 1; // Start after the '$'
}
'{' if !reading_name => {
reading_name = true;
name_start = pos + 1;
}
ch if ch.is_alphanumeric() || ch == '_' => {
if reading_name {
name.push(ch);
}
}
_ => {
if reading_name {
name_end = pos; // End before the non-alphanumeric character
break;
}
}
}
pos += 1;
}
if !reading_name {
return None;
}
if !reading_name {
return None;
}
if name_end == 0 {
name_end = pos;
}
if name_end == 0 {
name_end = pos;
}
Some((name, name_start, name_end))
}
Some((name, name_start, name_end))
}
pub fn get_completed_line(&self) -> String {
if self.candidates.is_empty() {
return self.original_input.clone();
}
pub fn get_completed_line(&self) -> String {
if self.candidates.is_empty() {
return self.original_input.clone();
}
let selected = &self.candidates[self.selected_idx];
let (start, end) = self.token_span;
format!(
"{}{}{}",
&self.original_input[..start],
selected,
&self.original_input[end..]
)
}
let selected = &self.candidates[self.selected_idx];
let (start, end) = self.token_span;
format!("{}{}{}", &self.original_input[..start], selected, &self.original_input[end..])
}
pub fn get_candidates(&mut self, line: String, cursor_pos: usize) -> ShResult<CompResult> {
let source = Arc::new(line.clone());
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 source = Arc::new(line.clone());
let tokens = lex::LexStream::new(source, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>()?;
let Some(mut cur_token) = tokens.into_iter().find(|tk| {
let start = tk.span.start;
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| {
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);
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
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 ctx.last().is_some_and(|m| *m == markers::VAR_SUB) {
let var_sub = &cur_token.as_str();
if let Some((var_name, start, end)) = Self::extract_var_name(var_sub) {
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 '='
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;
}
if !var_matches.is_empty() {
let name_start = cur_token.span.start + start;
let name_end = cur_token.span.start + end;
self.token_span = (name_start, name_end);
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) {
let var_sub = &cur_token.as_str();
if let Some((var_name,start,end)) = Self::extract_var_name(var_sub) {
log::debug!("Extracted variable name for completion: {}", var_name);
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 !matches!(ret, Ok(CompResult::NoMatch)) {
return ret;
} else {
ctx.pop();
}
} else {
ctx.pop();
}
}
}
if !var_matches.is_empty() {
let name_start = cur_token.span.start + start;
let name_end = cur_token.span.start + end;
self.token_span = (name_start, name_end);
cur_token.span.set_range(self.token_span.0..self.token_span.1);
Ok(CompResult::from_candidates(var_matches))
} else {
Ok(CompResult::NoMatch)
}
});
let raw_tk = cur_token.as_str().to_string();
let expanded_tk = cur_token.expand()?;
let expanded_words = expanded_tk.get_words().into_iter().collect::<Vec<_>>();
let expanded = expanded_words.join("\\ ");
if !matches!(ret, Ok(CompResult::NoMatch)) {
return ret;
} else {
ctx.pop();
}
} else {
ctx.pop();
}
}
}
let mut candidates = match ctx.pop() {
Some(markers::COMMAND) => Self::complete_command(&expanded)?,
Some(markers::ARG) => Self::complete_filename(&expanded),
Some(_) => {
return Ok(CompResult::NoMatch);
}
None => {
return Ok(CompResult::NoMatch);
}
};
let raw_tk = cur_token.as_str().to_string();
let expanded_tk = cur_token.expand()?;
let expanded_words = expanded_tk.get_words().into_iter().collect::<Vec<_>>();
let expanded = expanded_words.join("\\ ");
// Now we are just going to graft the completed text
// 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 mut candidates = match ctx.pop() {
Some(markers::COMMAND) => {
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);
}
};
let limit = crate::state::read_shopts(|s| s.prompt.comp_limit);
candidates.truncate(limit);
// Now we are just going to graft the completed text
// 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();
Ok(CompResult::from_candidates(candidates))
}
let limit = crate::state::read_shopts(|s| s.prompt.comp_limit);
candidates.truncate(limit);
fn complete_command(start: &str) -> ShResult<Vec<String>> {
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 mut candidates = vec![];
let file_name = entry.file_name().to_string_lossy().to_string();
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; };
if meta.is_file()
&& (meta.permissions().mode() & 0o111) != 0
&& file_name.starts_with(start)
{
candidates.push(file_name);
}
}
}
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()
&& (meta.permissions().mode() & 0o111) != 0
&& file_name.starts_with(start) {
candidates.push(file_name);
}
}
}
candidates.extend(builtin_candidates);
let builtin_candidates = BUILTINS
.iter()
.filter(|b| b.starts_with(start))
.map(|s| s.to_string());
read_logic(|l| {
let func_table = l.funcs();
let matches = func_table
.keys()
.filter(|k| k.starts_with(start))
.map(|k| k.to_string());
candidates.extend(builtin_candidates);
candidates.extend(matches);
read_logic(|l| {
let func_table = l.funcs();
let matches = func_table
.keys()
.filter(|k| k.starts_with(start))
.map(|k| k.to_string());
let aliases = l.aliases();
let matches = aliases
.keys()
.filter(|k| k.starts_with(start))
.map(|k| k.to_string());
candidates.extend(matches);
candidates.extend(matches);
});
let aliases = l.aliases();
let matches = aliases
.keys()
.filter(|k| k.starts_with(start))
.map(|k| k.to_string());
// Deduplicate (same command may appear in multiple PATH dirs)
candidates.sort();
candidates.dedup();
candidates.extend(matches);
});
Ok(candidates)
}
// Deduplicate (same command may appear in multiple PATH dirs)
candidates.sort();
candidates.dedup();
fn complete_filename(start: &str) -> Vec<String> {
let mut candidates = vec![];
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 mut candidates = vec![];
let Ok(entries) = std::fs::read_dir(&dir) else {
return candidates;
};
// If completing after '=', only use the part after it
let start = if let Some(eq_pos) = start.rfind('=') {
&start[eq_pos + 1..]
} else {
start
};
for entry in entries.flatten() {
let file_name = entry.file_name();
let file_str = file_name.to_string_lossy();
// 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)
};
// Skip hidden files unless explicitly requested
if !prefix.starts_with('.') && file_str.starts_with('.') {
continue;
}
let Ok(entries) = std::fs::read_dir(&dir) else {
return candidates;
};
if file_str.starts_with(prefix) {
// Reconstruct full path
let mut full_path = dir.join(&file_name);
for entry in entries.flatten() {
let file_name = entry.file_name();
let file_str = file_name.to_string_lossy();
// Add trailing slash for directories
if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
full_path.push(""); // adds trailing /
}
// Skip hidden files unless explicitly requested
if !prefix.starts_with('.') && file_str.starts_with('.') {
continue;
}
let mut path_raw = full_path.to_string_lossy().to_string();
if path_raw.starts_with("./") && !has_dotslash {
path_raw = path_raw.trim_start_matches("./").to_string();
}
if file_str.starts_with(prefix) {
// Reconstruct full path
let mut full_path = dir.join(&file_name);
candidates.push(path_raw);
}
}
// Add trailing slash for directories
if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
full_path.push(""); // adds trailing /
}
candidates.push(full_path.to_string_lossy().to_string());
}
}
candidates.sort();
candidates
}
candidates.sort();
candidates
}
}
impl Default for Completer {
fn default() -> Self {
Self::new()
}
}

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
///
/// The highlighter processes annotated input strings containing invisible Unicode markers
/// (U+FDD0-U+FDEF range) that indicate syntax elements. It generates ANSI escape codes
/// for terminal display while maintaining a style stack for proper color restoration
/// in nested constructs (e.g., variables inside strings inside command substitutions).
/// The highlighter processes annotated input strings containing invisible
/// Unicode markers (U+FDD0-U+FDEF range) that indicate syntax elements. It
/// generates ANSI escape codes for terminal display while maintaining a style
/// stack for proper color restoration in nested constructs (e.g., variables
/// inside strings inside command substitutions).
pub struct Highlighter {
input: String,
output: String,
style_stack: Vec<StyleSet>,
last_was_reset: bool,
input: String,
output: String,
style_stack: Vec<StyleSet>,
last_was_reset: bool,
}
impl Highlighter {
/// Creates a new highlighter with empty buffers and reset state
pub fn new() -> Self {
Self {
input: String::new(),
output: String::new(),
style_stack: Vec::new(),
last_was_reset: true, // start as true so we don't emit a leading reset
}
}
/// Creates a new highlighter with empty buffers and reset state
pub fn new() -> Self {
Self {
input: String::new(),
output: String::new(),
style_stack: Vec::new(),
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
///
/// The input is passed through the annotator which inserts Unicode markers
/// indicating token types and sub-token constructs (strings, variables, etc.)
pub fn load_input(&mut self, input: &str) {
let input = annotate_input(input);
self.input = input;
}
/// Loads raw input text and annotates it with syntax markers
///
/// The input is passed through the annotator which inserts Unicode markers
/// indicating token types and sub-token constructs (strings, variables, etc.)
pub fn load_input(&mut self, input: &str) {
let input = annotate_input(input);
self.input = input;
}
/// Processes the annotated input and generates ANSI-styled output
///
/// Walks through the input character by character, interpreting markers and
/// applying appropriate styles. Nested constructs (command substitutions,
/// subshells, strings) are handled recursively with proper style restoration.
pub fn highlight(&mut self) {
let input = self.input.clone();
let mut input_chars = input.chars().peekable();
while let Some(ch) = input_chars.next() {
match ch {
markers::STRING_DQ_END |
markers::STRING_SQ_END |
markers::VAR_SUB_END |
markers::CMD_SUB_END |
markers::PROC_SUB_END |
markers::SUBSH_END => self.pop_style(),
/// Processes the annotated input and generates ANSI-styled output
///
/// Walks through the input character by character, interpreting markers and
/// applying appropriate styles. Nested constructs (command substitutions,
/// subshells, strings) are handled recursively with proper style restoration.
pub fn highlight(&mut self) {
let input = self.input.clone();
let mut input_chars = input.chars().peekable();
while let Some(ch) = input_chars.next() {
match ch {
markers::STRING_DQ_END
| markers::STRING_SQ_END
| markers::VAR_SUB_END
| markers::CMD_SUB_END
| markers::PROC_SUB_END
| markers::SUBSH_END => self.pop_style(),
markers::CMD_SEP |
markers::RESET => self.clear_styles(),
markers::CMD_SEP | 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::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::COMMENT => self.push_style(Style::BrightBlack),
markers::GLOB => self.push_style(Style::Blue),
markers::GLOB => self.push_style(Style::Blue),
markers::REDIRECT |
markers::OPERATOR => self.push_style(Style::Magenta | Style::Bold),
markers::REDIRECT | markers::OPERATOR => self.push_style(Style::Magenta | Style::Bold),
markers::ASSIGNMENT => {
let mut var_name = String::new();
markers::ASSIGNMENT => {
let mut var_name = String::new();
while let Some(ch) = input_chars.peek() {
if ch == &'=' {
input_chars.next(); // consume the '='
break;
}
match *ch {
markers::RESET => break,
_ => {
var_name.push(*ch);
input_chars.next();
}
}
}
while let Some(ch) = input_chars.peek() {
if ch == &'=' {
input_chars.next(); // consume the '='
break;
}
match *ch {
markers::RESET => break,
_ => {
var_name.push(*ch);
input_chars.next();
}
}
}
self.output.push_str(&var_name);
self.push_style(Style::Blue);
self.output.push('=');
self.pop_style();
}
self.output.push_str(&var_name);
self.push_style(Style::Blue);
self.output.push('=');
self.pop_style();
}
markers::COMMAND => {
let mut cmd_name = String::new();
while let Some(ch) = input_chars.peek() {
if *ch == markers::RESET {
break;
}
cmd_name.push(*ch);
input_chars.next();
}
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();
}
markers::ARG => {
let mut arg = String::new();
let mut chars_clone = input_chars.clone();
while let Some(ch) = chars_clone.next() {
if ch == markers::RESET {
break;
}
arg.push(ch);
}
// Determine prefix from content (handles both <( and >( for proc subs)
let prefix = match ch {
markers::CMD_SUB => "$(",
markers::SUBSH => "(",
markers::PROC_SUB => {
if inner.starts_with("<(") { "<(" }
else if inner.starts_with(">(") { ">(" }
else { "<(" } // fallback
}
_ => unreachable!(),
};
let style = if Self::is_filename(&arg) {
Style::White | Style::Underline
} else {
Style::White.into()
};
let inner_content = if incomplete {
inner
.strip_prefix(prefix)
.unwrap_or(&inner)
} else {
inner
.strip_prefix(prefix)
.and_then(|s| s.strip_suffix(")"))
.unwrap_or(&inner)
};
self.push_style(style);
self.last_was_reset = false;
}
let mut recursive_highlighter = Self::new();
recursive_highlighter.load_input(inner_content);
recursive_highlighter.highlight();
self.push_style(Style::Blue);
self.output.push_str(prefix);
self.pop_style();
self.output.push_str(&recursive_highlighter.take());
if !incomplete {
self.push_style(Style::Blue);
self.output.push(')');
self.pop_style();
}
self.last_was_reset = false;
}
markers::VAR_SUB => {
let mut var_sub = String::new();
while let Some(ch) = input_chars.peek() {
if *ch == markers::VAR_SUB_END {
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;
}
}
}
}
}
markers::COMMAND => {
let mut cmd_name = String::new();
let mut chars_clone = input_chars.clone();
while let Some(ch) = chars_clone.next() {
if ch == markers::RESET {
break;
}
cmd_name.push(ch);
}
let style = if Self::is_valid(&cmd_name) {
Style::Green.into()
} else {
Style::Red | Style::Bold
};
self.push_style(style);
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();
}
/// Extracts the highlighted output and resets the highlighter state
///
/// Clears the input buffer, style stack, and returns the generated output
/// 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)
}
// Determine prefix from content (handles both <( and >( for proc subs)
let prefix = match ch {
markers::CMD_SUB => "$(",
markers::SUBSH => "(",
markers::PROC_SUB => {
if inner.starts_with("<(") {
"<("
} 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)
///
/// 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);
let inner_content = if incomplete {
inner.strip_prefix(prefix).unwrap_or(&inner)
} else {
inner
.strip_prefix(prefix)
.and_then(|s| s.strip_suffix(")"))
.unwrap_or(&inner)
};
if cmd_path.exists() {
// the user has given us an absolute path
if cmd_path.is_dir() && read_shopts(|o| o.core.autocd) {
// this is a directory and autocd is enabled
return true;
} else {
let Ok(meta) = cmd_path.metadata() else { 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;
}
}
let mut recursive_highlighter = Self::new();
recursive_highlighter.load_input(inner_content);
recursive_highlighter.highlight();
self.push_style(Style::Blue);
self.output.push_str(prefix);
self.pop_style();
self.output.push_str(&recursive_highlighter.take());
if !incomplete {
self.push_style(Style::Blue);
self.output.push(')');
self.pop_style();
}
self.last_was_reset = false;
}
markers::VAR_SUB => {
let mut var_sub = String::new();
while let Some(ch) = input_chars.peek() {
if *ch == markers::VAR_SUB_END {
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
let found = read_logic(|l| l.get_func(command).is_some() || l.get_alias(command).is_some());
if found {
return true;
}
}
/// Extracts the highlighted output and resets the highlighter state
///
/// Clears the input buffer, style stack, and returns the generated output
/// 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
///
/// 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;
}
}
if cmd_path.exists() {
// the user has given us an absolute path
if cmd_path.is_dir() && read_shopts(|o| o.core.autocd) {
// this is a directory and autocd is enabled
return true;
} else {
let Ok(meta) = cmd_path.metadata() else {
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
///
/// 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;
}
// 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());
if found {
return true;
}
}
/// 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);
}
false
}
/// 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();
}
}
fn is_filename(arg: &str) -> bool {
let path = PathBuf::from(arg);
/// 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();
}
if path.exists() {
return true;
}
/// 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");
}
if let Some(parent_dir) = path.parent()
&& let Ok(entries) = parent_dir.read_dir()
{
let files = entries
.filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().to_string())
.collect::<Vec<_>>();
let Some(arg_filename) = PathBuf::from(arg)
.file_name()
.map(|s| s.to_string_lossy().to_string())
else {
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)
}
/// Deduplicate entries, keeping only the most recent occurrence of each command.
/// Preserves chronological order (oldest to newest).
/// Deduplicate entries, keeping only the most recent occurrence of each
/// command. Preserves chronological order (oldest to newest).
fn dedupe_entries(entries: &[HistEntry]) -> Vec<HistEntry> {
let mut seen = HashSet::new();
// Iterate backwards (newest first), keeping first occurrence of each command
@@ -207,10 +207,10 @@ fn dedupe_entries(entries: &[HistEntry]) -> Vec<HistEntry> {
pub struct History {
path: PathBuf,
pub pending: Option<String>,
pub pending: Option<(String, usize)>, // command, cursor_pos
entries: Vec<HistEntry>,
search_mask: Vec<HistEntry>,
no_matches: bool,
no_matches: bool,
pub cursor: usize,
search_direction: Direction,
ignore_dups: bool,
@@ -235,9 +235,9 @@ impl History {
Ok(Self {
path,
entries,
pending: None,
pending: None,
search_mask,
no_matches: false,
no_matches: false,
cursor,
search_direction: Direction::Backward,
ignore_dups,
@@ -245,10 +245,10 @@ impl History {
})
}
pub fn reset(&mut self) {
self.search_mask = dedupe_entries(&self.entries);
self.cursor = self.search_mask.len();
}
pub fn reset(&mut self) {
self.search_mask = dedupe_entries(&self.entries);
self.cursor = self.search_mask.len();
}
pub fn entries(&self) -> &[HistEntry] {
&self.entries
@@ -270,14 +270,14 @@ impl History {
self.cursor = self.search_mask.len();
}
pub fn update_pending_cmd(&mut self, command: &str) {
let cmd = command.to_string();
pub fn update_pending_cmd(&mut self, buf: (&str, usize)) {
let cmd = buf.0.to_string();
let constraint = SearchConstraint {
kind: SearchKind::Prefix,
term: cmd.clone(),
};
self.pending = Some(cmd);
self.pending = Some((cmd, buf.1));
self.constrain_entries(constraint);
}
@@ -315,11 +315,11 @@ impl History {
.collect();
self.search_mask = dedupe_entries(&filtered);
self.no_matches = self.search_mask.is_empty();
if self.no_matches {
// If no matches, reset to full history so user can still scroll through it
self.search_mask = dedupe_entries(&self.entries);
}
self.no_matches = self.search_mask.is_empty();
if self.no_matches {
// If no matches, reset to full history so user can still scroll through it
self.search_mask = dedupe_entries(&self.entries);
}
}
self.cursor = self.search_mask.len();
}
@@ -328,12 +328,14 @@ impl History {
}
pub fn hint_entry(&self) -> Option<&HistEntry> {
if self.no_matches { return None };
if self.no_matches {
return None;
};
self.search_mask.last()
}
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()?;
Some(entry.command().to_string())
} else {
@@ -342,9 +344,15 @@ impl History {
}
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)
}
@@ -378,7 +386,8 @@ impl History {
let last_file_entry = self
.entries
.iter().rfind(|ent| !ent.new)
.iter()
.rfind(|ent| !ent.new)
.map(|ent| ent.command.clone())
.unwrap_or_default();
@@ -399,8 +408,8 @@ impl History {
}
file.write_all(data.as_bytes())?;
self.pending = None;
self.reset();
self.pending = None;
self.reset();
Ok(())
}

View File

@@ -133,10 +133,10 @@ impl SelectMode {
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MotionKind {
To(usize), // Absolute position, exclusive
On(usize), // Absolute position, inclusive
Onto(usize), /* Absolute position, operations include the position but motions
* exclude it (wtf vim) */
To(usize), // Absolute position, exclusive
On(usize), // Absolute position, inclusive
Onto(usize), /* Absolute position, operations include the position but motions
* exclude it (wtf vim) */
Inclusive((usize, usize)), // Range, inclusive
Exclusive((usize, usize)), // Range, exclusive
@@ -360,12 +360,12 @@ impl LineBuf {
pub fn set_hint(&mut self, hint: Option<String>) {
if let Some(hint) = hint {
if let Some(hint) = hint.strip_prefix(&self.buffer) {
if !hint.is_empty() {
self.hint = Some(hint.to_string())
} else {
self.hint = None
}
}
if !hint.is_empty() {
self.hint = Some(hint.to_string())
} else {
self.hint = None
}
}
} else {
self.hint = None
}
@@ -563,8 +563,8 @@ impl LineBuf {
self.update_graphemes();
}
pub fn drain(&mut self, start: usize, end: usize) -> String {
let start = start.max(0);
let end = end.min(self.grapheme_indices().len());
let start = start.max(0);
let end = end.min(self.grapheme_indices().len());
let drained = if end == self.grapheme_indices().len() {
if start == self.grapheme_indices().len() {
return String::new();
@@ -628,8 +628,9 @@ impl LineBuf {
self.next_sentence_start_from_punctuation(pos).is_some()
}
/// If position is at sentence-ending punctuation, returns the position of the next sentence start.
/// Handles closing delimiters (`)`, `]`, `"`, `'`) after punctuation.
/// If position is at sentence-ending punctuation, returns the position of the
/// next sentence start. Handles closing delimiters (`)`, `]`, `"`, `'`)
/// after punctuation.
#[allow(clippy::collapsible_if)]
pub fn next_sentence_start_from_punctuation(&self, pos: usize) -> Option<usize> {
if let Some(gr) = self.read_grapheme_at(pos) {
@@ -956,9 +957,10 @@ impl LineBuf {
let start = start.unwrap_or(0);
if count > 1
&& let Some((_, new_end)) = self.text_obj_sentence(end, count - 1, bound) {
end = new_end;
}
&& let Some((_, new_end)) = self.text_obj_sentence(end, count - 1, bound)
{
end = new_end;
}
Some((start, end))
}
@@ -1363,7 +1365,12 @@ impl LineBuf {
}
/// 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 mut indices_iter = (pos..self.cursor.max).peekable();
@@ -1390,8 +1397,7 @@ impl LineBuf {
let on_whitespace = is_whitespace(&cur_char);
if !on_whitespace {
let Some(ws_pos) =
indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
let Some(ws_pos) = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
else {
return default;
};
@@ -1457,7 +1463,12 @@ impl LineBuf {
}
/// 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 mut indices_iter = (0..pos).rev().peekable();
@@ -1484,8 +1495,7 @@ impl LineBuf {
let on_whitespace = is_whitespace(&cur_char);
if !on_whitespace {
let Some(ws_pos) =
indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
let Some(ws_pos) = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
else {
return default;
};
@@ -1742,11 +1752,7 @@ impl LineBuf {
};
pos = next_ws_pos;
if pos == 0 {
pos
} else {
pos + 1
}
if pos == 0 { pos } else { pos + 1 }
}
}
}
@@ -1903,7 +1909,7 @@ impl LineBuf {
&& self.grapheme_at(target_pos) == Some("\n")
{
target_pos = target_pos.saturating_sub(1); // Don't land on the
// newline
// newline
}
MotionKind::InclusiveWithTargetCol((start, end), target_pos)
}
@@ -2141,7 +2147,7 @@ impl LineBuf {
&& self.grapheme_at(target_pos) == Some("\n")
{
target_pos = target_pos.saturating_sub(1); // Don't land on the
// newline
// newline
}
let (start, end) = match motion.1 {
@@ -2575,15 +2581,16 @@ impl LineBuf {
}
Verb::SwapVisualAnchor => {
if let Some((start, end)) = self.select_range()
&& let Some(mut mode) = self.select_mode {
mode.invert_anchor();
let new_cursor_pos = match mode.anchor() {
SelectAnchor::Start => start,
SelectAnchor::End => end,
};
self.cursor.set(new_cursor_pos);
self.select_mode = Some(mode)
}
&& let Some(mut mode) = self.select_mode
{
mode.invert_anchor();
let new_cursor_pos = match mode.anchor() {
SelectAnchor::Start => start,
SelectAnchor::End => end,
};
self.cursor.set(new_cursor_pos);
self.select_mode = Some(mode)
}
}
Verb::JoinLines => {
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);
// Merge character inserts into one edit
if edit_is_merging && cmd.verb.as_ref().is_none_or(|v| !v.1.is_char_insert())
&& let Some(edit) = self.undo_stack.last_mut() {
edit.stop_merge();
}
if edit_is_merging
&& cmd.verb.as_ref().is_none_or(|v| !v.1.is_char_insert())
&& let Some(edit) = self.undo_stack.last_mut()
{
edit.stop_merge();
}
let ViCmd {
register,
@@ -2821,10 +2830,9 @@ impl LineBuf {
self.saved_col = None;
}
if is_char_insert
&& let Some(edit) = self.undo_stack.last_mut() {
edit.start_merge();
}
if is_char_insert && let Some(edit) = self.undo_stack.last_mut() {
edit.start_merge();
}
Ok(())
}
@@ -2832,9 +2840,13 @@ impl LineBuf {
&self.buffer // FIXME: this will have to be fixed up later
}
pub fn get_hint_text(&self) -> String {
self.hint.clone().map(|h| h.styled(Style::BrightBlack)).unwrap_or_default()
}
pub fn get_hint_text(&self) -> String {
self
.hint
.clone()
.map(|h| h.styled(Style::BrightBlack))
.unwrap_or_default()
}
}
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 vte::{Parser, Perform};
use crate::{prelude::*, procio::borrow_fd, state::{read_meta, write_meta}};
use crate::{
libsh::error::{ShErr, ShErrKind, ShResult},
prompt::readline::keys::{KeyCode, ModKeys},
};
use crate::{
prelude::*,
procio::borrow_fd,
state::{read_meta, write_meta},
};
use super::{keys::KeyEvent, linebuf::LineBuf};
@@ -41,7 +45,7 @@ pub fn raw_mode() -> RawModeGuard {
)
.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 {
orig,
@@ -242,9 +246,7 @@ impl Read for TermBuffer {
let result = nix::unistd::read(self.tty, buf);
match result {
Ok(n) => Ok(n),
Err(Errno::EINTR) => {
Err(Errno::EINTR.into())
}
Err(Errno::EINTR) => Err(Errno::EINTR.into()),
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
where F: FnOnce() -> R {
let raw = tcgetattr(borrow_fd(STDIN_FILENO)).expect("Failed to get terminal attributes");
let mut cooked = raw.clone();
cooked.local_flags |= termios::LocalFlags::ICANON | termios::LocalFlags::ECHO;
cooked.input_flags |= termios::InputFlags::ICRNL;
tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &cooked).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
}
pub fn with_cooked_mode<F, R>(f: F) -> R
where
F: FnOnce() -> R,
{
let raw = tcgetattr(borrow_fd(STDIN_FILENO)).expect("Failed to get terminal attributes");
let mut cooked = raw.clone();
cooked.local_flags |= termios::LocalFlags::ICANON | termios::LocalFlags::ECHO;
cooked.input_flags |= termios::InputFlags::ICRNL;
tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &cooked)
.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 {
@@ -333,9 +339,15 @@ impl KeyCollector {
// CSI modifiers: param = 1 + (shift) + (alt*2) + (ctrl*4) + (meta*8)
let bits = param.saturating_sub(1);
let mut mods = ModKeys::empty();
if bits & 1 != 0 { mods |= ModKeys::SHIFT; }
if bits & 2 != 0 { mods |= ModKeys::ALT; }
if bits & 4 != 0 { mods |= ModKeys::CTRL; }
if bits & 1 != 0 {
mods |= ModKeys::SHIFT;
}
if bits & 2 != 0 {
mods |= ModKeys::ALT;
}
if bits & 4 != 0 {
mods |= ModKeys::CTRL;
}
mods
}
}
@@ -374,46 +386,72 @@ impl Perform for KeyCollector {
self.push(event);
}
fn csi_dispatch(&mut self, params: &vte::Params, intermediates: &[u8], _ignore: bool, action: char) {
let params: Vec<u16> = params.iter()
fn csi_dispatch(
&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))
.collect();
let event = match (intermediates, action) {
// Arrow keys: CSI A/B/C/D or CSI 1;mod A/B/C/D
([], '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)
}
([], '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)
}
([], '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)
}
([], '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)
}
// Home/End: CSI H/F or CSI 1;mod H/F
([], '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)
}
([], '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)
}
// Shift+Tab: CSI Z
([], 'Z') => {
KeyEvent(KeyCode::Tab, ModKeys::SHIFT)
}
([], 'Z') => KeyEvent(KeyCode::Tab, ModKeys::SHIFT),
// Special keys with tilde: CSI num ~ or CSI num;mod ~
([], '~') => {
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 {
1 | 7 => KeyCode::Home,
2 => KeyCode::Insert,
@@ -473,7 +511,9 @@ impl PollReader {
pub fn feed_bytes(&mut self, bytes: &[u8]) {
if bytes == [b'\x1b'] {
// Single escape byte - user pressed ESC key
self.collector.push(KeyEvent(KeyCode::Esc, ModKeys::empty()));
self
.collector
.push(KeyEvent(KeyCode::Esc, ModKeys::empty()));
return;
}
@@ -914,13 +954,13 @@ impl LineWriter for TermWriter {
let end = new_layout.end;
let cursor = new_layout.cursor;
if read_meta(|m| m.system_msg_pending()) {
let mut system_msg = String::new();
while let Some(msg) = write_meta(|m| m.pop_system_message()) {
writeln!(system_msg, "{msg}").map_err(err)?;
}
self.buffer.push_str(&system_msg);
}
if read_meta(|m| m.system_msg_pending()) {
let mut system_msg = String::new();
while let Some(msg) = write_meta(|m| m.pop_system_message()) {
writeln!(system_msg, "{msg}").map_err(err)?;
}
self.buffer.push_str(&system_msg);
}
self.buffer.push_str(prompt);
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
pub fn alter_line_motion_if_no_verb(&mut self) {
if self.is_line_motion() && self.verb.is_none()
&& let Some(motion) = self.motion.as_mut() {
match motion.1 {
Motion::LineUp => motion.1 = Motion::LineUpCharwise,
Motion::LineDown => motion.1 = Motion::LineDownCharwise,
_ => unreachable!(),
}
if self.is_line_motion()
&& self.verb.is_none()
&& let Some(motion) = self.motion.as_mut()
{
match motion.1 {
Motion::LineUp => motion.1 = Motion::LineUpCharwise,
Motion::LineDown => motion.1 = Motion::LineDownCharwise,
_ => unreachable!(),
}
}
}
pub fn is_mode_transition(&self) -> bool {
self.verb.as_ref().is_some_and(|v| {

View File

@@ -315,7 +315,7 @@ impl ViNormal {
return match obj {
TextObj::Sentence(_) | TextObj::Paragraph(_) => CmdState::Complete,
_ => CmdState::Invalid,
}
};
}
Some(_) => return CmdState::Complete,
None => return CmdState::Pending,
@@ -410,7 +410,7 @@ impl ViNormal {
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags(),
})
});
}
'~' => {
chars_clone.next();
@@ -445,7 +445,7 @@ impl ViNormal {
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags(),
})
});
}
'x' => {
return Some(ViCmd {
@@ -454,7 +454,7 @@ impl ViNormal {
motion: Some(MotionCmd(1, Motion::ForwardChar)),
raw_seq: self.take_cmd(),
flags: self.flags(),
})
});
}
'X' => {
return Some(ViCmd {
@@ -463,7 +463,7 @@ impl ViNormal {
motion: Some(MotionCmd(1, Motion::BackwardChar)),
raw_seq: self.take_cmd(),
flags: self.flags(),
})
});
}
's' => {
return Some(ViCmd {
@@ -472,7 +472,7 @@ impl ViNormal {
motion: Some(MotionCmd(1, Motion::ForwardChar)),
raw_seq: self.take_cmd(),
flags: self.flags(),
})
});
}
'S' => {
return Some(ViCmd {
@@ -481,7 +481,7 @@ impl ViNormal {
motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(),
flags: self.flags(),
})
});
}
'p' => {
chars = chars_clone;
@@ -516,7 +516,7 @@ impl ViNormal {
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags(),
})
});
}
'~' => {
return Some(ViCmd {
@@ -525,7 +525,7 @@ impl ViNormal {
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags(),
})
});
}
'u' => {
return Some(ViCmd {
@@ -534,7 +534,7 @@ impl ViNormal {
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags(),
})
});
}
'v' => {
return Some(ViCmd {
@@ -543,7 +543,7 @@ impl ViNormal {
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags(),
})
});
}
'V' => {
return Some(ViCmd {
@@ -552,7 +552,7 @@ impl ViNormal {
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags(),
})
});
}
'o' => {
return Some(ViCmd {
@@ -561,7 +561,7 @@ impl ViNormal {
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags(),
})
});
}
'O' => {
return Some(ViCmd {
@@ -570,7 +570,7 @@ impl ViNormal {
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags(),
})
});
}
'a' => {
return Some(ViCmd {
@@ -579,7 +579,7 @@ impl ViNormal {
motion: Some(MotionCmd(1, Motion::ForwardChar)),
raw_seq: self.take_cmd(),
flags: self.flags(),
})
});
}
'A' => {
return Some(ViCmd {
@@ -588,7 +588,7 @@ impl ViNormal {
motion: Some(MotionCmd(1, Motion::EndOfLine)),
raw_seq: self.take_cmd(),
flags: self.flags(),
})
});
}
'i' => {
return Some(ViCmd {
@@ -597,7 +597,7 @@ impl ViNormal {
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags(),
})
});
}
'I' => {
return Some(ViCmd {
@@ -606,7 +606,7 @@ impl ViNormal {
motion: Some(MotionCmd(1, Motion::BeginningOfFirstWord)),
raw_seq: self.take_cmd(),
flags: self.flags(),
})
});
}
'J' => {
return Some(ViCmd {
@@ -615,7 +615,7 @@ impl ViNormal {
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags(),
})
});
}
'y' => {
chars = chars_clone;
@@ -636,7 +636,7 @@ impl ViNormal {
motion: Some(MotionCmd(1, Motion::EndOfLine)),
raw_seq: self.take_cmd(),
flags: self.flags(),
})
});
}
'D' => {
return Some(ViCmd {
@@ -645,7 +645,7 @@ impl ViNormal {
motion: Some(MotionCmd(1, Motion::EndOfLine)),
raw_seq: self.take_cmd(),
flags: self.flags(),
})
});
}
'C' => {
return Some(ViCmd {
@@ -654,7 +654,7 @@ impl ViNormal {
motion: Some(MotionCmd(1, Motion::EndOfLine)),
raw_seq: self.take_cmd(),
flags: self.flags(),
})
});
}
'=' => {
chars = chars_clone;
@@ -684,7 +684,7 @@ impl ViNormal {
| ('~', Some(VerbCmd(_, Verb::ToggleCaseRange)))
| ('>', Some(VerbCmd(_, Verb::Indent)))
| ('<', 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))) => {
// 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 motion_ref = motion.as_ref().map(|m| &m.1);
@@ -1185,7 +1184,7 @@ impl ViVisual {
motion: None,
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
})
});
}
'?' => {
return Some(ViCmd {
@@ -1194,7 +1193,7 @@ impl ViVisual {
motion: None,
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
})
});
}
_ => break 'verb_parse None,
}
@@ -1209,7 +1208,7 @@ impl ViVisual {
motion: None,
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
})
});
}
'x' => {
chars = chars_clone;
@@ -1222,7 +1221,7 @@ impl ViVisual {
motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
})
});
}
'Y' => {
return Some(ViCmd {
@@ -1231,7 +1230,7 @@ impl ViVisual {
motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
})
});
}
'D' => {
return Some(ViCmd {
@@ -1240,7 +1239,7 @@ impl ViVisual {
motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
})
});
}
'R' | 'C' => {
return Some(ViCmd {
@@ -1249,7 +1248,7 @@ impl ViVisual {
motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
})
});
}
'>' => {
return Some(ViCmd {
@@ -1258,7 +1257,7 @@ impl ViVisual {
motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
})
});
}
'<' => {
return Some(ViCmd {
@@ -1267,7 +1266,7 @@ impl ViVisual {
motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
})
});
}
'=' => {
return Some(ViCmd {
@@ -1276,7 +1275,7 @@ impl ViVisual {
motion: Some(MotionCmd(1, Motion::WholeLine)),
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
})
});
}
'p' | 'P' => {
chars = chars_clone;
@@ -1299,7 +1298,7 @@ impl ViVisual {
motion: None,
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
})
});
}
'u' => {
return Some(ViCmd {
@@ -1308,7 +1307,7 @@ impl ViVisual {
motion: None,
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
})
});
}
'U' => {
return Some(ViCmd {
@@ -1317,7 +1316,7 @@ impl ViVisual {
motion: None,
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
})
});
}
'O' | 'o' => {
return Some(ViCmd {
@@ -1326,7 +1325,7 @@ impl ViVisual {
motion: None,
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
})
});
}
'A' => {
return Some(ViCmd {
@@ -1335,7 +1334,7 @@ impl ViVisual {
motion: Some(MotionCmd(1, Motion::ForwardChar)),
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
})
});
}
'I' => {
return Some(ViCmd {
@@ -1344,7 +1343,7 @@ impl ViVisual {
motion: Some(MotionCmd(1, Motion::BeginningOfLine)),
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
})
});
}
'J' => {
return Some(ViCmd {
@@ -1353,7 +1352,7 @@ impl ViVisual {
motion: None,
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
})
});
}
'y' => {
chars = chars_clone;
@@ -1395,7 +1394,7 @@ impl ViVisual {
| ('=', Some(VerbCmd(_, Verb::Equalize)))
| ('>', Some(VerbCmd(_, Verb::Indent)))
| ('<', 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 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")
.with_sub_notes(vec!["Example: 'shopt core.autocd=true'"]),
),
)
);
}
}
Ok(())
@@ -263,7 +263,7 @@ impl ShOptCore {
"max_recurse_depth",
]),
),
)
);
}
}
Ok(())
@@ -445,18 +445,20 @@ impl ShOptPrompt {
ShErrKind::SyntaxErr,
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(
Note::new("'prompt' contains the following options").with_sub_notes(vec![
"trunc_prompt_path",
"edit_mode",
"comp_limit",
"highlight",
"tab_stop",
"custom",
"trunc_prompt_path",
"edit_mode",
"comp_limit",
"highlight",
"tab_stop",
"custom",
]),
),
)
);
}
}
Ok(())

View File

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

View File

@@ -1,249 +1,262 @@
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::{
builtin::trap::TrapTarget, exec_input, jobs::JobTab, libsh::{
builtin::trap::TrapTarget,
exec_input,
jobs::JobTab,
libsh::{
error::{ShErr, ShErrKind, ShResult},
utils::VecDequeExt,
}, parse::{ConjunctNode, NdRule, Node, ParsedSrc}, prelude::*, shopt::ShOpts
},
parse::{ConjunctNode, NdRule, Node, ParsedSrc},
prelude::*,
shopt::ShOpts,
};
pub struct Fern {
pub jobs: RefCell<JobTab>,
pub var_scopes: RefCell<ScopeStack>,
pub meta: RefCell<MetaTab>,
pub logic: RefCell<LogTab>,
pub shopts: RefCell<ShOpts>,
pub jobs: RefCell<JobTab>,
pub var_scopes: RefCell<ScopeStack>,
pub meta: RefCell<MetaTab>,
pub logic: RefCell<LogTab>,
pub shopts: RefCell<ShOpts>,
}
impl Fern {
pub fn new() -> Self {
Self {
jobs: RefCell::new(JobTab::new()),
var_scopes: RefCell::new(ScopeStack::new()),
meta: RefCell::new(MetaTab::new()),
logic: RefCell::new(LogTab::new()),
shopts: RefCell::new(ShOpts::default()),
}
}
pub fn new() -> Self {
Self {
jobs: RefCell::new(JobTab::new()),
var_scopes: RefCell::new(ScopeStack::new()),
meta: RefCell::new(MetaTab::new()),
logic: RefCell::new(LogTab::new()),
shopts: RefCell::new(ShOpts::default()),
}
}
}
impl Default for Fern {
fn default() -> Self {
Self::new()
}
fn default() -> Self {
Self::new()
}
}
#[derive(Hash, Eq, PartialEq, Debug, Clone, Copy)]
pub enum ShellParam {
// Global
Status,
ShPid,
LastJob,
ShellName,
// Global
Status,
ShPid,
LastJob,
ShellName,
// Local
Pos(usize),
AllArgs,
AllArgsStr,
ArgCount
// Local
Pos(usize),
AllArgs,
AllArgsStr,
ArgCount,
}
impl ShellParam {
pub fn is_global(&self) -> bool {
matches!(
self,
Self::Status | Self::ShPid | Self::LastJob | Self::ShellName
)
}
pub fn is_global(&self) -> bool {
matches!(
self,
Self::Status | Self::ShPid | Self::LastJob | Self::ShellName
)
}
}
impl Display for ShellParam {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Status => write!(f, "?"),
Self::ShPid => write!(f, "$"),
Self::LastJob => write!(f, "!"),
Self::ShellName => write!(f, "0"),
Self::Pos(n) => write!(f, "{}", n),
Self::AllArgs => write!(f, "@"),
Self::AllArgsStr => write!(f, "*"),
Self::ArgCount => write!(f, "#"),
}
}
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Status => write!(f, "?"),
Self::ShPid => write!(f, "$"),
Self::LastJob => write!(f, "!"),
Self::ShellName => write!(f, "0"),
Self::Pos(n) => write!(f, "{}", n),
Self::AllArgs => write!(f, "@"),
Self::AllArgsStr => write!(f, "*"),
Self::ArgCount => write!(f, "#"),
}
}
}
impl FromStr for ShellParam {
type Err = ShErr;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"?" => Ok(Self::Status),
"$" => Ok(Self::ShPid),
"!" => Ok(Self::LastJob),
"0" => Ok(Self::ShellName),
"@" => Ok(Self::AllArgs),
"*" => Ok(Self::AllArgsStr),
"#" => Ok(Self::ArgCount),
n if n.parse::<usize>().is_ok() => {
let idx = n.parse::<usize>().unwrap();
Ok(Self::Pos(idx))
}
_ => Err(ShErr::simple(
ShErrKind::InternalErr,
format!("Invalid shell parameter: {}", s),
)),
}
}
type Err = ShErr;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"?" => Ok(Self::Status),
"$" => Ok(Self::ShPid),
"!" => Ok(Self::LastJob),
"0" => Ok(Self::ShellName),
"@" => Ok(Self::AllArgs),
"*" => Ok(Self::AllArgsStr),
"#" => Ok(Self::ArgCount),
n if n.parse::<usize>().is_ok() => {
let idx = n.parse::<usize>().unwrap();
Ok(Self::Pos(idx))
}
_ => Err(ShErr::simple(
ShErrKind::InternalErr,
format!("Invalid shell parameter: {}", s),
)),
}
}
}
#[derive(Clone, Default, Debug)]
pub struct ScopeStack {
// ALWAYS keep one scope.
// The bottom scope is the global variable space.
// Scopes that come after that are pushed in functions,
// and only contain variables that are defined using `local`.
scopes: Vec<VarTab>,
depth: u32,
// ALWAYS keep one scope.
// The bottom scope is the global variable space.
// Scopes that come after that are pushed in functions,
// and only contain variables that are defined using `local`.
scopes: Vec<VarTab>,
depth: u32,
// Global parameters such as $?, $!, $$, etc
global_params: HashMap<String, String>,
// Global parameters such as $?, $!, $$, etc
global_params: HashMap<String, String>,
}
impl ScopeStack {
pub fn new() -> Self {
let mut new = Self::default();
new.scopes.push(VarTab::new());
let shell_name = std::env::args().next().unwrap_or_else(|| "fern".to_string());
new.global_params.insert(ShellParam::ShellName.to_string(), shell_name);
new
}
pub fn descend(&mut self, argv: Option<Vec<String>>) {
let mut new_vars = VarTab::new();
if let Some(argv) = argv {
for arg in argv {
new_vars.bpush_arg(arg);
}
}
self.scopes.push(new_vars);
self.depth += 1;
}
pub fn ascend(&mut self) {
if self.depth >= 1 {
self.scopes.pop();
self.depth -= 1;
}
}
pub fn cur_scope(&self) -> &VarTab {
self.scopes.last().unwrap()
}
pub fn cur_scope_mut(&mut self) -> &mut VarTab {
self.scopes.last_mut().unwrap()
}
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);
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);
return;
}
}
}
pub fn var_exists(&self, var_name: &str) -> bool {
for scope in self.scopes.iter().rev() {
if scope.var_exists(var_name) {
return true;
}
}
if let Ok(param) = var_name.parse::<ShellParam>() {
return self.global_params.contains_key(&param.to_string());
}
false
}
pub fn flatten_vars(&self) -> HashMap<String, Var> {
let mut flat_vars = HashMap::new();
for scope in self.scopes.iter() {
for (var_name, var) in scope.vars() {
flat_vars.insert(var_name.clone(), var.clone());
}
}
flat_vars
}
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);
} else {
self.set_var_global(var_name, val, flags);
}
}
fn set_var_global(&mut self, var_name: &str, val: &str, flags: VarFlags) {
if let Some(scope) = self.scopes.first_mut() {
scope.set_var(var_name, val, flags);
}
}
fn set_var_local(&mut self, var_name: &str, val: &str, flags: VarFlags) {
if let Some(scope) = self.scopes.last_mut() {
scope.set_var(var_name, val, flags);
}
}
pub fn get_var(&self, var_name: &str) -> String {
if let Ok(param) = var_name.parse::<ShellParam>() {
return self.get_param(param);
}
for scope in self.scopes.iter().rev() {
if scope.var_exists(var_name) {
return scope.get_var(var_name);
}
}
// Fallback to env var
std::env::var(var_name).unwrap_or_default()
}
pub fn get_param(&self, param: ShellParam) -> String {
if param.is_global() && let Some(val) = self.global_params.get(&param.to_string()) {
return val.clone();
}
for scope in self.scopes.iter().rev() {
let val = scope.get_param(param);
if !val.is_empty() {
return val;
}
}
// Fallback to empty string
"".into()
}
/// Set a shell parameter
/// Therefore, these are global state and we use the global scope
pub fn set_param(&mut self, param: ShellParam, val: &str) {
match param {
ShellParam::ShPid |
ShellParam::Status |
ShellParam::LastJob |
ShellParam::ShellName => {
self.global_params.insert(param.to_string(), val.to_string());
}
ShellParam::Pos(_) |
ShellParam::AllArgs |
ShellParam::AllArgsStr |
ShellParam::ArgCount => {
if let Some(scope) = self.scopes.first_mut() {
scope.set_param(param, val);
}
}
}
}
pub fn new() -> Self {
let mut new = Self::default();
new.scopes.push(VarTab::new());
let shell_name = std::env::args()
.next()
.unwrap_or_else(|| "fern".to_string());
new
.global_params
.insert(ShellParam::ShellName.to_string(), shell_name);
new
}
pub fn descend(&mut self, argv: Option<Vec<String>>) {
let mut new_vars = VarTab::new();
if let Some(argv) = argv {
for arg in argv {
new_vars.bpush_arg(arg);
}
}
self.scopes.push(new_vars);
self.depth += 1;
}
pub fn ascend(&mut self) {
if self.depth >= 1 {
self.scopes.pop();
self.depth -= 1;
}
}
pub fn cur_scope(&self) -> &VarTab {
self.scopes.last().unwrap()
}
pub fn cur_scope_mut(&mut self) -> &mut VarTab {
self.scopes.last_mut().unwrap()
}
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);
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);
return;
}
}
}
pub fn var_exists(&self, var_name: &str) -> bool {
for scope in self.scopes.iter().rev() {
if scope.var_exists(var_name) {
return true;
}
}
if let Ok(param) = var_name.parse::<ShellParam>() {
return self.global_params.contains_key(&param.to_string());
}
false
}
pub fn flatten_vars(&self) -> HashMap<String, Var> {
let mut flat_vars = HashMap::new();
for scope in self.scopes.iter() {
for (var_name, var) in scope.vars() {
flat_vars.insert(var_name.clone(), var.clone());
}
}
flat_vars
}
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);
} else {
self.set_var_global(var_name, val, flags);
}
}
fn set_var_global(&mut self, var_name: &str, val: &str, flags: VarFlags) {
if let Some(scope) = self.scopes.first_mut() {
scope.set_var(var_name, val, flags);
}
}
fn set_var_local(&mut self, var_name: &str, val: &str, flags: VarFlags) {
if let Some(scope) = self.scopes.last_mut() {
scope.set_var(var_name, val, flags);
}
}
pub fn get_var(&self, var_name: &str) -> String {
if let Ok(param) = var_name.parse::<ShellParam>() {
return self.get_param(param);
}
for scope in self.scopes.iter().rev() {
if scope.var_exists(var_name) {
return scope.get_var(var_name);
}
}
// Fallback to env var
std::env::var(var_name).unwrap_or_default()
}
pub fn get_param(&self, param: ShellParam) -> String {
if param.is_global()
&& let Some(val) = self.global_params.get(&param.to_string())
{
return val.clone();
}
for scope in self.scopes.iter().rev() {
let val = scope.get_param(param);
if !val.is_empty() {
return val;
}
}
// Fallback to empty string
"".into()
}
/// Set a shell parameter
/// Therefore, these are global state and we use the global scope
pub fn set_param(&mut self, param: ShellParam, val: &str) {
match param {
ShellParam::ShPid | ShellParam::Status | ShellParam::LastJob | ShellParam::ShellName => {
self
.global_params
.insert(param.to_string(), val.to_string());
}
ShellParam::Pos(_) | ShellParam::AllArgs | ShellParam::AllArgsStr | ShellParam::ArgCount => {
if let Some(scope) = self.scopes.first_mut() {
scope.set_param(param, val);
}
}
}
}
}
thread_local! {
pub static FERN: Fern = Fern::new();
pub static FERN: Fern = Fern::new();
}
/// A shell function
@@ -287,7 +300,7 @@ impl Deref for ShFunc {
pub struct LogTab {
functions: HashMap<String, ShFunc>,
aliases: HashMap<String, String>,
traps: HashMap<TrapTarget, String>,
traps: HashMap<TrapTarget, String>,
}
impl LogTab {
@@ -297,18 +310,18 @@ impl LogTab {
pub fn insert_func(&mut self, name: &str, src: ShFunc) {
self.functions.insert(name.into(), src);
}
pub fn insert_trap(&mut self, target: TrapTarget, command: String) {
self.traps.insert(target, command);
}
pub fn get_trap(&self, target: TrapTarget) -> Option<String> {
self.traps.get(&target).cloned()
}
pub fn remove_trap(&mut self, target: TrapTarget) {
self.traps.remove(&target);
}
pub fn traps(&self) -> &HashMap<TrapTarget, String> {
&self.traps
}
pub fn insert_trap(&mut self, target: TrapTarget, command: String) {
self.traps.insert(target, command);
}
pub fn get_trap(&self, target: TrapTarget) -> Option<String> {
self.traps.get(&target).cloned()
}
pub fn remove_trap(&mut self, target: TrapTarget) {
self.traps.remove(&target);
}
pub fn traps(&self) -> &HashMap<TrapTarget, String> {
&self.traps
}
pub fn get_func(&self, name: &str) -> Option<ShFunc> {
self.functions.get(name).cloned()
}
@@ -339,103 +352,103 @@ impl LogTab {
pub struct VarFlags(u8);
impl VarFlags {
pub const NONE : Self = Self(0);
pub const EXPORT : Self = Self(1 << 0);
pub const LOCAL : Self = Self(1 << 1);
pub const READONLY : Self = Self(1 << 2);
pub const NONE: Self = Self(0);
pub const EXPORT: Self = Self(1 << 0);
pub const LOCAL: Self = Self(1 << 1);
pub const READONLY: Self = Self(1 << 2);
}
impl BitOr for VarFlags {
type Output = Self;
fn bitor(self, rhs: Self) -> Self::Output {
Self(self.0 | rhs.0)
}
type Output = Self;
fn bitor(self, rhs: Self) -> Self::Output {
Self(self.0 | rhs.0)
}
}
impl BitOrAssign for VarFlags {
fn bitor_assign(&mut self, rhs: Self) {
self.0 |= rhs.0;
}
fn bitor_assign(&mut self, rhs: Self) {
self.0 |= rhs.0;
}
}
impl BitAnd for VarFlags {
type Output = Self;
fn bitand(self, rhs: Self) -> Self::Output {
Self(self.0 & rhs.0)
}
type Output = Self;
fn bitand(self, rhs: Self) -> Self::Output {
Self(self.0 & rhs.0)
}
}
impl BitAndAssign for VarFlags {
fn bitand_assign(&mut self, rhs: Self) {
self.0 &= rhs.0;
}
fn bitand_assign(&mut self, rhs: Self) {
self.0 &= rhs.0;
}
}
impl VarFlags {
pub fn contains(&self, flag: Self) -> bool {
(self.0 & flag.0) == flag.0
}
pub fn intersects(&self, flag: Self) -> bool {
(self.0 & flag.0) != 0
}
pub fn is_empty(&self) -> bool {
self.0 == 0
}
pub fn contains(&self, flag: Self) -> bool {
(self.0 & flag.0) == flag.0
}
pub fn intersects(&self, flag: Self) -> bool {
(self.0 & flag.0) != 0
}
pub fn is_empty(&self) -> bool {
self.0 == 0
}
pub fn insert(&mut self, flag: Self) {
self.0 |= flag.0;
}
pub fn remove(&mut self, flag: Self) {
self.0 &= !flag.0;
}
pub fn toggle(&mut self, flag: Self) {
self.0 ^= flag.0;
}
pub fn set(&mut self, flag: Self, value: bool) {
if value {
self.insert(flag);
} else {
self.remove(flag);
}
}
pub fn insert(&mut self, flag: Self) {
self.0 |= flag.0;
}
pub fn remove(&mut self, flag: Self) {
self.0 &= !flag.0;
}
pub fn toggle(&mut self, flag: Self) {
self.0 ^= flag.0;
}
pub fn set(&mut self, flag: Self, value: bool) {
if value {
self.insert(flag);
} else {
self.remove(flag);
}
}
}
#[derive(Clone, Debug)]
pub enum VarKind {
Str(String),
Int(i32),
Arr(Vec<String>),
AssocArr(Vec<(String, String)>),
Str(String),
Int(i32),
Arr(Vec<String>),
AssocArr(Vec<(String, String)>),
}
impl Display for VarKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
VarKind::Str(s) => write!(f, "{s}"),
VarKind::Int(i) => write!(f, "{i}"),
VarKind::Arr(items) => {
let mut item_iter = items.iter().peekable();
while let Some(item) = item_iter.next() {
write!(f, "{item}")?;
if item_iter.peek().is_some() {
write!(f, " ")?;
}
}
Ok(())
}
VarKind::AssocArr(items) => {
let mut item_iter = items.iter().peekable();
while let Some(item) = item_iter.next() {
let (k,v) = item;
write!(f, "{k}={v}")?;
if item_iter.peek().is_some() {
write!(f, " ")?;
}
}
Ok(())
}
}
}
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
VarKind::Str(s) => write!(f, "{s}"),
VarKind::Int(i) => write!(f, "{i}"),
VarKind::Arr(items) => {
let mut item_iter = items.iter().peekable();
while let Some(item) = item_iter.next() {
write!(f, "{item}")?;
if item_iter.peek().is_some() {
write!(f, " ")?;
}
}
Ok(())
}
VarKind::AssocArr(items) => {
let mut item_iter = items.iter().peekable();
while let Some(item) = item_iter.next() {
let (k, v) = item;
write!(f, "{k}={v}")?;
if item_iter.peek().is_some() {
write!(f, " ")?;
}
}
Ok(())
}
}
}
}
#[derive(Clone, Debug)]
@@ -446,26 +459,23 @@ pub struct Var {
impl Var {
pub fn new(kind: VarKind, flags: VarFlags) -> Self {
Self {
flags,
kind
}
Self { 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) {
self.flags.set(VarFlags::EXPORT, true);
}
}
impl Display for Var {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.kind.fmt(f)
}
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.kind.fmt(f)
}
}
#[derive(Default, Clone, Debug)]
@@ -528,23 +538,23 @@ impl VarTab {
.map(|hname| hname.to_string_lossy().to_string())
.unwrap_or_default();
unsafe {
env::set_var("IFS", " \t\n");
env::set_var("HOST", hostname.clone());
env::set_var("UID", uid.to_string());
env::set_var("PPID", getppid().to_string());
env::set_var("TMPDIR", "/tmp");
env::set_var("TERM", term);
env::set_var("LANG", "en_US.UTF-8");
env::set_var("USER", username.clone());
env::set_var("LOGNAME", username);
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("HOME", home.clone());
env::set_var("SHELL", pathbuf_to_string(std::env::current_exe()));
env::set_var("FERN_HIST", format!("{}/.fernhist", home));
env::set_var("FERN_RC", format!("{}/.fernrc", home));
}
unsafe {
env::set_var("IFS", " \t\n");
env::set_var("HOST", hostname.clone());
env::set_var("UID", uid.to_string());
env::set_var("PPID", getppid().to_string());
env::set_var("TMPDIR", "/tmp");
env::set_var("TERM", term);
env::set_var("LANG", "en_US.UTF-8");
env::set_var("USER", username.clone());
env::set_var("LOGNAME", username);
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("HOME", home.clone());
env::set_var("SHELL", pathbuf_to_string(std::env::current_exe()));
env::set_var("FERN_HIST", format!("{}/.fernhist", home));
env::set_var("FERN_RC", format!("{}/.fernrc", home));
}
}
pub fn init_sh_argv(&mut self) {
for arg in env::args() {
@@ -575,7 +585,10 @@ impl VarTab {
self.bpush_arg(env::current_exe().unwrap().to_str().unwrap().to_string());
}
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());
}
/// Push an arg to the front of the arg deque
@@ -619,29 +632,29 @@ impl VarTab {
}
}
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);
if !param.is_empty() {
return param;
}
}
}
if let Some(var) = self.vars.get(var).map(|s| s.to_string()) {
var
} else {
std::env::var(var).unwrap_or_default()
}
}
pub fn unset_var(&mut self, var_name: &str) {
self.vars.remove(var_name);
unsafe { env::remove_var(var_name) };
}
pub fn unset_var(&mut self, var_name: &str) {
self.vars.remove(var_name);
unsafe { env::remove_var(var_name) };
}
pub fn set_var(&mut self, var_name: &str, val: &str, flags: VarFlags) {
if let Some(var) = self.vars.get_mut(var_name) {
var.kind = VarKind::Str(val.to_string());
if var.flags.contains(VarFlags::EXPORT) || flags.contains(VarFlags::EXPORT) {
if flags.contains(VarFlags::EXPORT) && !var.flags.contains(VarFlags::EXPORT) {
var.mark_for_export();
}
if flags.contains(VarFlags::EXPORT) && !var.flags.contains(VarFlags::EXPORT) {
var.mark_for_export();
}
unsafe { env::set_var(var_name, val) };
}
} else {
@@ -663,39 +676,35 @@ impl VarTab {
self.params.insert(param, val.to_string());
}
pub fn get_param(&self, param: ShellParam) -> String {
match param {
ShellParam::Pos(n) => {
self
.sh_argv()
.get(n)
.map(|s| s.to_string())
.unwrap_or_default()
}
ShellParam::Status => {
self
.params
.get(&ShellParam::Status)
.map(|s| s.to_string())
.unwrap_or("0".into())
}
_ => self
.params
.get(&param)
.map(|s| s.to_string())
.unwrap_or_default(),
}
match param {
ShellParam::Pos(n) => self
.sh_argv()
.get(n)
.map(|s| s.to_string())
.unwrap_or_default(),
ShellParam::Status => self
.params
.get(&ShellParam::Status)
.map(|s| s.to_string())
.unwrap_or("0".into()),
_ => self
.params
.get(&param)
.map(|s| s.to_string())
.unwrap_or_default(),
}
}
}
/// A table of metadata for the shell
#[derive(Default, Debug)]
pub struct MetaTab {
// command running duration
// command running duration
runtime_start: Option<Instant>,
runtime_stop: Option<Instant>,
runtime_stop: Option<Instant>,
// pending system messages
system_msg: Vec<String>
// pending system messages
system_msg: Vec<String>,
}
impl MetaTab {
@@ -708,76 +717,76 @@ impl MetaTab {
pub fn stop_timer(&mut self) {
self.runtime_stop = Some(Instant::now());
}
pub fn get_time(&self) -> Option<Duration> {
if let (Some(start), Some(stop)) = (self.runtime_start, self.runtime_stop) {
Some(stop.duration_since(start))
} else {
None
}
}
pub fn post_system_message(&mut self, message: String) {
self.system_msg.push(message);
}
pub fn pop_system_message(&mut self) -> Option<String> {
self.system_msg.pop()
}
pub fn system_msg_pending(&self) -> bool {
!self.system_msg.is_empty()
}
pub fn get_time(&self) -> Option<Duration> {
if let (Some(start), Some(stop)) = (self.runtime_start, self.runtime_stop) {
Some(stop.duration_since(start))
} else {
None
}
}
pub fn post_system_message(&mut self, message: String) {
self.system_msg.push(message);
}
pub fn pop_system_message(&mut self) -> Option<String> {
self.system_msg.pop()
}
pub fn system_msg_pending(&self) -> bool {
!self.system_msg.is_empty()
}
}
/// Read from the job table
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
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
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
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 {
FERN.with(|fern| f(&fern.meta.borrow()))
FERN.with(|fern| f(&fern.meta.borrow()))
}
/// Write to the meta table
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
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
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 {
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 {
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>>) {
write_vars(|v| v.descend(argv));
write_vars(|v| v.descend(argv));
}
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
@@ -788,7 +797,9 @@ pub fn get_shopt(path: &str) -> String {
}
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]
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 crate::expand::{perform_param_expansion, DUB_QUOTE, VAR_SUB};
use crate::expand::{DUB_QUOTE, VAR_SUB, perform_param_expansion};
use crate::state::VarFlags;
use super::*;
@@ -293,70 +293,78 @@ fn param_expansion_replacesuffix() {
#[test]
fn dquote_escape_dollar() {
// "\$foo" should strip backslash, produce literal $foo (no expansion)
let result = unescape_str(r#""\$foo""#);
assert!(!result.contains(VAR_SUB), "Escaped $ should not become VAR_SUB");
assert!(result.contains('$'), "Literal $ should be preserved");
assert!(!result.contains('\\'), "Backslash should be stripped");
// "\$foo" should strip backslash, produce literal $foo (no expansion)
let result = unescape_str(r#""\$foo""#);
assert!(
!result.contains(VAR_SUB),
"Escaped $ should not become VAR_SUB"
);
assert!(result.contains('$'), "Literal $ should be preserved");
assert!(!result.contains('\\'), "Backslash should be stripped");
}
#[test]
fn dquote_escape_backslash() {
// "\\" in double quotes should produce a single backslash
let result = unescape_str(r#""\\""#);
let inner: String = result.chars()
.filter(|&c| c != DUB_QUOTE)
.collect();
assert_eq!(inner, "\\", "Double backslash should produce single backslash");
// "\\" in double quotes should produce a single backslash
let result = unescape_str(r#""\\""#);
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
assert_eq!(
inner, "\\",
"Double backslash should produce single backslash"
);
}
#[test]
fn dquote_escape_quote() {
// "\"" should produce a literal double quote
let result = unescape_str(r#""\"""#);
let inner: String = result.chars()
.filter(|&c| c != DUB_QUOTE)
.collect();
assert!(inner.contains('"'), "Escaped quote should produce literal quote");
// "\"" should produce a literal double quote
let result = unescape_str(r#""\"""#);
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
assert!(
inner.contains('"'),
"Escaped quote should produce literal quote"
);
}
#[test]
fn dquote_escape_backtick() {
// "\`" should strip backslash, produce literal backtick
let result = unescape_str(r#""\`""#);
let inner: String = result.chars()
.filter(|&c| c != DUB_QUOTE)
.collect();
assert_eq!(inner, "`", "Escaped backtick should produce literal backtick");
// "\`" should strip backslash, produce literal backtick
let result = unescape_str(r#""\`""#);
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
assert_eq!(
inner, "`",
"Escaped backtick should produce literal backtick"
);
}
#[test]
fn dquote_escape_nonspecial_preserves_backslash() {
// "\a" inside double quotes should preserve the backslash (a is not special)
let result = unescape_str(r#""\a""#);
let inner: String = result.chars()
.filter(|&c| c != DUB_QUOTE)
.collect();
assert_eq!(inner, "\\a", "Backslash before non-special char should be preserved");
// "\a" inside double quotes should preserve the backslash (a is not special)
let result = unescape_str(r#""\a""#);
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
assert_eq!(
inner, "\\a",
"Backslash before non-special char should be preserved"
);
}
#[test]
fn dquote_unescaped_dollar_expands() {
// "$foo" inside double quotes should produce VAR_SUB (expansion marker)
let result = unescape_str(r#""$foo""#);
assert!(result.contains(VAR_SUB), "Unescaped $ should become VAR_SUB");
// "$foo" inside double quotes should produce VAR_SUB (expansion marker)
let result = unescape_str(r#""$foo""#);
assert!(
result.contains(VAR_SUB),
"Unescaped $ should become VAR_SUB"
);
}
#[test]
fn dquote_mixed_escapes() {
// "hello \$world \\end" should have literal $, single backslash
let result = unescape_str(r#""hello \$world \\end""#);
assert!(!result.contains(VAR_SUB), "Escaped $ should not expand");
assert!(result.contains('$'), "Literal $ should be in output");
// Should have exactly one backslash (from \\)
let inner: String = result.chars()
.filter(|&c| c != DUB_QUOTE)
.collect();
let backslash_count = inner.chars().filter(|&c| c == '\\').count();
assert_eq!(backslash_count, 1, "\\\\ should produce one backslash");
// "hello \$world \\end" should have literal $, single backslash
let result = unescape_str(r#""hello \$world \\end""#);
assert!(!result.contains(VAR_SUB), "Escaped $ should not expand");
assert!(result.contains('$'), "Literal $ should be in output");
// Should have exactly one backslash (from \\)
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
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::{
annotate_input, annotate_input_recursive, markers,
highlight::Highlighter,
annotate_input, annotate_input_recursive, highlight::Highlighter, markers,
};
use super::*;
/// Helper to check if a marker exists at any position in the annotated string
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
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
fn marker_before(annotated: &str, first: char, second: char) -> bool {
if let (Some(pos1), Some(pos2)) = (find_marker(annotated, first), find_marker(annotated, second)) {
pos1 < pos2
} else {
false
}
if let (Some(pos1), Some(pos2)) = (
find_marker(annotated, first),
find_marker(annotated, second),
) {
pos1 < pos2
} else {
false
}
}
// ============================================================================
@@ -30,69 +32,70 @@ fn marker_before(annotated: &str, first: char, second: char) -> bool {
#[test]
fn annotate_simple_command() {
let input = "/bin/ls -la";
let annotated = annotate_input(input);
let input = "/bin/ls -la";
let annotated = annotate_input(input);
// Should have COMMAND marker for "/bin/ls" (external command)
assert!(has_marker(&annotated, markers::COMMAND));
// Should have COMMAND marker for "/bin/ls" (external command)
assert!(has_marker(&annotated, markers::COMMAND));
// Should have ARG marker for "-la"
assert!(has_marker(&annotated, markers::ARG));
// Should have ARG marker for "-la"
assert!(has_marker(&annotated, markers::ARG));
// Should have RESET markers
assert!(has_marker(&annotated, markers::RESET));
// Should have RESET markers
assert!(has_marker(&annotated, markers::RESET));
}
#[test]
fn annotate_builtin_command() {
let input = "export FOO=bar";
let annotated = annotate_input(input);
let input = "export FOO=bar";
let annotated = annotate_input(input);
// Should mark "export" as BUILTIN
assert!(has_marker(&annotated, markers::BUILTIN));
// Should mark "export" as BUILTIN
assert!(has_marker(&annotated, markers::BUILTIN));
// Should mark assignment (or ARG if assignment isn't specifically marked separately)
assert!(has_marker(&annotated, markers::ASSIGNMENT) || has_marker(&annotated, markers::ARG));
// Should mark assignment (or ARG if assignment isn't specifically marked
// separately)
assert!(has_marker(&annotated, markers::ASSIGNMENT) || has_marker(&annotated, markers::ARG));
}
#[test]
fn annotate_operator() {
let input = "ls | grep foo";
let annotated = annotate_input(input);
let input = "ls | grep foo";
let annotated = annotate_input(input);
// Should have OPERATOR marker for pipe
assert!(has_marker(&annotated, markers::OPERATOR));
// Should have OPERATOR marker for pipe
assert!(has_marker(&annotated, markers::OPERATOR));
// Should have COMMAND markers for both commands
let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count();
assert_eq!(command_count, 2);
// Should have COMMAND markers for both commands
let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count();
assert_eq!(command_count, 2);
}
#[test]
fn annotate_redirect() {
let input = "echo hello > output.txt";
let annotated = annotate_input(input);
let input = "echo hello > output.txt";
let annotated = annotate_input(input);
// Should have REDIRECT marker
assert!(has_marker(&annotated, markers::REDIRECT));
// Should have REDIRECT marker
assert!(has_marker(&annotated, markers::REDIRECT));
}
#[test]
fn annotate_keyword() {
let input = "if true; then echo yes; fi";
let annotated = annotate_input(input);
let input = "if true; then echo yes; fi";
let annotated = annotate_input(input);
// Should have KEYWORD markers for if/then/fi
assert!(has_marker(&annotated, markers::KEYWORD));
// Should have KEYWORD markers for if/then/fi
assert!(has_marker(&annotated, markers::KEYWORD));
}
#[test]
fn annotate_command_separator() {
let input = "echo foo; echo bar";
let annotated = annotate_input(input);
let input = "echo foo; echo bar";
let annotated = annotate_input(input);
// Should have CMD_SEP marker for semicolon
assert!(has_marker(&annotated, markers::CMD_SEP));
// Should have CMD_SEP marker for semicolon
assert!(has_marker(&annotated, markers::CMD_SEP));
}
// ============================================================================
@@ -101,83 +104,87 @@ fn annotate_command_separator() {
#[test]
fn annotate_variable_simple() {
let input = "echo $foo";
let annotated = annotate_input(input);
let input = "echo $foo";
let annotated = annotate_input(input);
// Should have VAR_SUB markers
assert!(has_marker(&annotated, markers::VAR_SUB));
assert!(has_marker(&annotated, markers::VAR_SUB_END));
// Should have VAR_SUB markers
assert!(has_marker(&annotated, markers::VAR_SUB));
assert!(has_marker(&annotated, markers::VAR_SUB_END));
}
#[test]
fn annotate_variable_braces() {
let input = "echo ${foo}";
let annotated = annotate_input(input);
let input = "echo ${foo}";
let annotated = annotate_input(input);
// Should have VAR_SUB markers for ${foo}
assert!(has_marker(&annotated, markers::VAR_SUB));
assert!(has_marker(&annotated, markers::VAR_SUB_END));
// Should have VAR_SUB markers for ${foo}
assert!(has_marker(&annotated, markers::VAR_SUB));
assert!(has_marker(&annotated, markers::VAR_SUB_END));
}
#[test]
fn annotate_double_quoted_string() {
let input = r#"echo "hello world""#;
let annotated = annotate_input(input);
let input = r#"echo "hello world""#;
let annotated = annotate_input(input);
// Should have STRING_DQ markers
assert!(has_marker(&annotated, markers::STRING_DQ));
assert!(has_marker(&annotated, markers::STRING_DQ_END));
// Should have STRING_DQ markers
assert!(has_marker(&annotated, markers::STRING_DQ));
assert!(has_marker(&annotated, markers::STRING_DQ_END));
}
#[test]
fn annotate_single_quoted_string() {
let input = "echo 'hello world'";
let annotated = annotate_input(input);
let input = "echo 'hello world'";
let annotated = annotate_input(input);
// Should have STRING_SQ markers
assert!(has_marker(&annotated, markers::STRING_SQ));
assert!(has_marker(&annotated, markers::STRING_SQ_END));
// Should have STRING_SQ markers
assert!(has_marker(&annotated, markers::STRING_SQ));
assert!(has_marker(&annotated, markers::STRING_SQ_END));
}
#[test]
fn annotate_variable_in_string() {
let input = r#"echo "hello $USER""#;
let annotated = annotate_input(input);
let input = r#"echo "hello $USER""#;
let annotated = annotate_input(input);
// Should have both STRING_DQ and VAR_SUB markers
assert!(has_marker(&annotated, markers::STRING_DQ));
assert!(has_marker(&annotated, markers::VAR_SUB));
// Should have both STRING_DQ and VAR_SUB markers
assert!(has_marker(&annotated, markers::STRING_DQ));
assert!(has_marker(&annotated, markers::VAR_SUB));
// VAR_SUB should be inside STRING_DQ
assert!(marker_before(&annotated, markers::STRING_DQ, markers::VAR_SUB));
// VAR_SUB should be inside STRING_DQ
assert!(marker_before(
&annotated,
markers::STRING_DQ,
markers::VAR_SUB
));
}
#[test]
fn annotate_glob_asterisk() {
let input = "ls *.txt";
let annotated = annotate_input(input);
let input = "ls *.txt";
let annotated = annotate_input(input);
// Should have GLOB marker for *
assert!(has_marker(&annotated, markers::GLOB));
// Should have GLOB marker for *
assert!(has_marker(&annotated, markers::GLOB));
}
#[test]
fn annotate_glob_question() {
let input = "ls file?.txt";
let annotated = annotate_input(input);
let input = "ls file?.txt";
let annotated = annotate_input(input);
// Should have GLOB marker for ?
assert!(has_marker(&annotated, markers::GLOB));
// Should have GLOB marker for ?
assert!(has_marker(&annotated, markers::GLOB));
}
#[test]
fn annotate_glob_bracket() {
let input = "ls file[abc].txt";
let annotated = annotate_input(input);
let input = "ls file[abc].txt";
let annotated = annotate_input(input);
// Should have GLOB markers for bracket expression
let glob_count = annotated.chars().filter(|&c| c == markers::GLOB).count();
assert!(glob_count >= 2); // Opening and closing
// Should have GLOB markers for bracket expression
let glob_count = annotated.chars().filter(|&c| c == markers::GLOB).count();
assert!(glob_count >= 2); // Opening and closing
}
// ============================================================================
@@ -186,32 +193,32 @@ fn annotate_glob_bracket() {
#[test]
fn annotate_command_sub_basic() {
let input = "echo $(whoami)";
let annotated = annotate_input(input);
let input = "echo $(whoami)";
let annotated = annotate_input(input);
// Should have CMD_SUB markers (but not recursively annotated yet)
assert!(has_marker(&annotated, markers::CMD_SUB));
assert!(has_marker(&annotated, markers::CMD_SUB_END));
// Should have CMD_SUB markers (but not recursively annotated yet)
assert!(has_marker(&annotated, markers::CMD_SUB));
assert!(has_marker(&annotated, markers::CMD_SUB_END));
}
#[test]
fn annotate_subshell_basic() {
let input = "(cd /tmp && ls)";
let annotated = annotate_input(input);
let input = "(cd /tmp && ls)";
let annotated = annotate_input(input);
// Should have SUBSH markers
assert!(has_marker(&annotated, markers::SUBSH));
assert!(has_marker(&annotated, markers::SUBSH_END));
// Should have SUBSH markers
assert!(has_marker(&annotated, markers::SUBSH));
assert!(has_marker(&annotated, markers::SUBSH_END));
}
#[test]
fn annotate_process_sub_output() {
let input = "diff <(ls dir1) <(ls dir2)";
let annotated = annotate_input(input);
let input = "diff <(ls dir1) <(ls dir2)";
let annotated = annotate_input(input);
// Should have PROC_SUB markers
assert!(has_marker(&annotated, markers::PROC_SUB));
assert!(has_marker(&annotated, markers::PROC_SUB_END));
// Should have PROC_SUB markers
assert!(has_marker(&annotated, markers::PROC_SUB));
assert!(has_marker(&annotated, markers::PROC_SUB_END));
}
// ============================================================================
@@ -220,88 +227,97 @@ fn annotate_process_sub_output() {
#[test]
fn annotate_recursive_command_sub() {
let input = "echo $(whoami)";
let annotated = annotate_input_recursive(input);
let input = "echo $(whoami)";
let annotated = annotate_input_recursive(input);
// Should have CMD_SUB markers
assert!(has_marker(&annotated, markers::CMD_SUB));
assert!(has_marker(&annotated, markers::CMD_SUB_END));
// Should have CMD_SUB markers
assert!(has_marker(&annotated, markers::CMD_SUB));
assert!(has_marker(&annotated, markers::CMD_SUB_END));
// Inside the command sub, "whoami" should be marked as COMMAND
// The recursive annotator should have processed the inside
assert!(has_marker(&annotated, markers::COMMAND));
// Inside the command sub, "whoami" should be marked as COMMAND
// The recursive annotator should have processed the inside
assert!(has_marker(&annotated, markers::COMMAND));
}
#[test]
fn annotate_recursive_nested_command_sub() {
let input = "echo $(echo $(whoami))";
let annotated = annotate_input_recursive(input);
let input = "echo $(echo $(whoami))";
let annotated = annotate_input_recursive(input);
// Should have multiple CMD_SUB markers (nested)
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");
// Should have multiple CMD_SUB markers (nested)
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"
);
}
#[test]
fn annotate_recursive_command_sub_with_args() {
let input = "echo $(grep foo file.txt)";
let annotated = annotate_input_recursive(input);
let input = "echo $(grep foo file.txt)";
let annotated = annotate_input_recursive(input);
// Should have BUILTIN for echo and possibly COMMAND for grep (if in PATH)
// Just check that we have command-type markers
let builtin_count = annotated.chars().filter(|&c| c == markers::BUILTIN).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)");
// Should have BUILTIN for echo and possibly COMMAND for grep (if in PATH)
// Just check that we have command-type markers
let builtin_count = annotated.chars().filter(|&c| c == markers::BUILTIN).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)"
);
}
#[test]
fn annotate_recursive_subshell() {
let input = "(echo hello; echo world)";
let annotated = annotate_input_recursive(input);
let input = "(echo hello; echo world)";
let annotated = annotate_input_recursive(input);
// Should have SUBSH markers
assert!(has_marker(&annotated, markers::SUBSH));
assert!(has_marker(&annotated, markers::SUBSH_END));
// Should have SUBSH markers
assert!(has_marker(&annotated, markers::SUBSH));
assert!(has_marker(&annotated, markers::SUBSH_END));
// Inside should be annotated with BUILTIN (echo is a builtin) and CMD_SEP
assert!(has_marker(&annotated, markers::BUILTIN));
assert!(has_marker(&annotated, markers::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::CMD_SEP));
}
#[test]
fn annotate_recursive_process_sub() {
let input = "diff <(ls -la)";
let annotated = annotate_input_recursive(input);
let input = "diff <(ls -la)";
let annotated = annotate_input_recursive(input);
// Should have PROC_SUB markers
assert!(has_marker(&annotated, markers::PROC_SUB));
// Should have PROC_SUB markers
assert!(has_marker(&annotated, markers::PROC_SUB));
// ls should be marked as COMMAND inside the process sub
assert!(has_marker(&annotated, markers::COMMAND));
// ls should be marked as COMMAND inside the process sub
assert!(has_marker(&annotated, markers::COMMAND));
}
#[test]
fn annotate_recursive_command_sub_in_string() {
let input = r#"echo "current user: $(whoami)""#;
let annotated = annotate_input_recursive(input);
let input = r#"echo "current user: $(whoami)""#;
let annotated = annotate_input_recursive(input);
// Should have STRING_DQ, CMD_SUB, and COMMAND markers
assert!(has_marker(&annotated, markers::STRING_DQ));
assert!(has_marker(&annotated, markers::CMD_SUB));
assert!(has_marker(&annotated, markers::COMMAND));
// Should have STRING_DQ, CMD_SUB, and COMMAND markers
assert!(has_marker(&annotated, markers::STRING_DQ));
assert!(has_marker(&annotated, markers::CMD_SUB));
assert!(has_marker(&annotated, markers::COMMAND));
}
#[test]
fn annotate_recursive_deeply_nested() {
let input = r#"echo "outer: $(echo "inner: $(whoami)")""#;
let annotated = annotate_input_recursive(input);
let input = r#"echo "outer: $(echo "inner: $(whoami)")""#;
let annotated = annotate_input_recursive(input);
// Should have multiple STRING_DQ and CMD_SUB markers
let string_count = annotated.chars().filter(|&c| c == markers::STRING_DQ).count();
let cmd_sub_count = annotated.chars().filter(|&c| c == markers::CMD_SUB).count();
// Should have multiple STRING_DQ and CMD_SUB markers
let string_count = annotated
.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!(cmd_sub_count >= 2, "Should have multiple CMD_SUB markers");
assert!(string_count >= 2, "Should have multiple STRING_DQ markers");
assert!(cmd_sub_count >= 2, "Should have multiple CMD_SUB markers");
}
// ============================================================================
@@ -310,33 +326,37 @@ fn annotate_recursive_deeply_nested() {
#[test]
fn marker_priority_var_in_string() {
let input = r#""$foo""#;
let annotated = annotate_input(input);
let input = r#""$foo""#;
let annotated = annotate_input(input);
// STRING_DQ should come before VAR_SUB (outer before inner)
assert!(marker_before(&annotated, markers::STRING_DQ, markers::VAR_SUB));
// STRING_DQ should come before VAR_SUB (outer before inner)
assert!(marker_before(
&annotated,
markers::STRING_DQ,
markers::VAR_SUB
));
}
#[test]
fn marker_priority_arg_vs_string() {
let input = r#"echo "hello""#;
let annotated = annotate_input(input);
let input = r#"echo "hello""#;
let annotated = annotate_input(input);
// Both ARG and STRING_DQ should be present
// STRING_DQ should be inside the ARG token's span
assert!(has_marker(&annotated, markers::ARG));
assert!(has_marker(&annotated, markers::STRING_DQ));
// Both ARG and STRING_DQ should be present
// STRING_DQ should be inside the ARG token's span
assert!(has_marker(&annotated, markers::ARG));
assert!(has_marker(&annotated, markers::STRING_DQ));
}
#[test]
fn marker_priority_reset_placement() {
let input = "echo hello";
let annotated = annotate_input(input);
let input = "echo hello";
let annotated = annotate_input(input);
// RESET markers should appear after each token
// There should be multiple RESET markers
let reset_count = annotated.chars().filter(|&c| c == markers::RESET).count();
assert!(reset_count >= 2);
// RESET markers should appear after each token
// There should be multiple RESET markers
let reset_count = annotated.chars().filter(|&c| c == markers::RESET).count();
assert!(reset_count >= 2);
}
// ============================================================================
@@ -345,127 +365,131 @@ fn marker_priority_reset_placement() {
#[test]
fn highlighter_produces_ansi_codes() {
let mut highlighter = Highlighter::new();
highlighter.load_input("echo hello");
highlighter.highlight();
let output = highlighter.take();
let mut highlighter = Highlighter::new();
highlighter.load_input("echo hello");
highlighter.highlight();
let output = highlighter.take();
// Should contain ANSI escape codes
assert!(output.contains("\x1b["), "Output should contain ANSI escape sequences");
// Should contain ANSI escape codes
assert!(
output.contains("\x1b["),
"Output should contain ANSI escape sequences"
);
// Should still contain the original text
assert!(output.contains("echo"));
assert!(output.contains("hello"));
// Should still contain the original text
assert!(output.contains("echo"));
assert!(output.contains("hello"));
}
#[test]
fn highlighter_handles_empty_input() {
let mut highlighter = Highlighter::new();
highlighter.load_input("");
highlighter.highlight();
let output = highlighter.take();
let mut highlighter = Highlighter::new();
highlighter.load_input("");
highlighter.highlight();
let output = highlighter.take();
// Should not crash and should return empty or minimal output
assert!(output.len() < 10); // Just escape codes or empty
// Should not crash and should return empty or minimal output
assert!(output.len() < 10); // Just escape codes or empty
}
#[test]
fn highlighter_command_validation() {
let mut highlighter = Highlighter::new();
let mut highlighter = Highlighter::new();
// Valid command (echo exists)
highlighter.load_input("echo test");
highlighter.highlight();
let valid_output = highlighter.take();
// Valid command (echo exists)
highlighter.load_input("echo test");
highlighter.highlight();
let valid_output = highlighter.take();
// Invalid command (definitely doesn't exist)
highlighter.load_input("xyznotacommand123 test");
highlighter.highlight();
let invalid_output = highlighter.take();
// Invalid command (definitely doesn't exist)
highlighter.load_input("xyznotacommand123 test");
highlighter.highlight();
let invalid_output = highlighter.take();
// Both should have ANSI codes
assert!(valid_output.contains("\x1b["));
assert!(invalid_output.contains("\x1b["));
// Both should have ANSI codes
assert!(valid_output.contains("\x1b["));
assert!(invalid_output.contains("\x1b["));
// The color codes should be different (green vs red)
// Valid commands should have \x1b[32m (green)
// Invalid commands should have \x1b[31m (red) or \x1b[1;31m (bold red)
// The color codes should be different (green vs red)
// Valid commands should have \x1b[32m (green)
// Invalid commands should have \x1b[31m (red) or \x1b[1;31m (bold red)
}
#[test]
fn highlighter_preserves_text_content() {
let input = "echo hello world";
let mut highlighter = Highlighter::new();
highlighter.load_input(input);
highlighter.highlight();
let output = highlighter.take();
let input = "echo hello world";
let mut highlighter = Highlighter::new();
highlighter.load_input(input);
highlighter.highlight();
let output = highlighter.take();
// Remove ANSI codes to check text content
let text_only: String = output.chars()
.filter(|c| !c.is_control() && *c != '\x1b')
.collect();
// Remove ANSI codes to check text content
let text_only: String = output
.chars()
.filter(|c| !c.is_control() && *c != '\x1b')
.collect();
// Should still contain the words (might have escape sequence fragments)
assert!(output.contains("echo"));
assert!(output.contains("hello"));
assert!(output.contains("world"));
// Should still contain the words (might have escape sequence fragments)
assert!(output.contains("echo"));
assert!(output.contains("hello"));
assert!(output.contains("world"));
}
#[test]
fn highlighter_multiple_tokens() {
let mut highlighter = Highlighter::new();
highlighter.load_input("ls -la | grep foo");
highlighter.highlight();
let output = highlighter.take();
let mut highlighter = Highlighter::new();
highlighter.load_input("ls -la | grep foo");
highlighter.highlight();
let output = highlighter.take();
// Should contain all tokens
assert!(output.contains("ls"));
assert!(output.contains("-la"));
assert!(output.contains("|"));
assert!(output.contains("grep"));
assert!(output.contains("foo"));
// Should contain all tokens
assert!(output.contains("ls"));
assert!(output.contains("-la"));
assert!(output.contains("|"));
assert!(output.contains("grep"));
assert!(output.contains("foo"));
// Should have ANSI codes
assert!(output.contains("\x1b["));
// Should have ANSI codes
assert!(output.contains("\x1b["));
}
#[test]
fn highlighter_string_with_variable() {
let mut highlighter = Highlighter::new();
highlighter.load_input(r#"echo "hello $USER""#);
highlighter.highlight();
let output = highlighter.take();
let mut highlighter = Highlighter::new();
highlighter.load_input(r#"echo "hello $USER""#);
highlighter.highlight();
let output = highlighter.take();
// Should contain the text
assert!(output.contains("echo"));
assert!(output.contains("hello"));
assert!(output.contains("USER"));
// Should contain the text
assert!(output.contains("echo"));
assert!(output.contains("hello"));
assert!(output.contains("USER"));
// Should have ANSI codes for different elements
assert!(output.contains("\x1b["));
// Should have ANSI codes for different elements
assert!(output.contains("\x1b["));
}
#[test]
fn highlighter_reusable() {
let mut highlighter = Highlighter::new();
let mut highlighter = Highlighter::new();
// First input
highlighter.load_input("echo first");
highlighter.highlight();
let output1 = highlighter.take();
// First input
highlighter.load_input("echo first");
highlighter.highlight();
let output1 = highlighter.take();
// Second input (reusing same highlighter)
highlighter.load_input("echo second");
highlighter.highlight();
let output2 = highlighter.take();
// Second input (reusing same highlighter)
highlighter.load_input("echo second");
highlighter.highlight();
let output2 = highlighter.take();
// Both should work
assert!(output1.contains("first"));
assert!(output2.contains("second"));
// Both should work
assert!(output1.contains("first"));
assert!(output2.contains("second"));
// Should not contain each other's text
assert!(!output1.contains("second"));
assert!(!output2.contains("first"));
// Should not contain each other's text
assert!(!output1.contains("second"));
assert!(!output2.contains("first"));
}
// ============================================================================
@@ -474,133 +498,143 @@ fn highlighter_reusable() {
#[test]
fn annotate_unclosed_string() {
let input = r#"echo "hello"#;
let annotated = annotate_input(input);
let input = r#"echo "hello"#;
let annotated = annotate_input(input);
// Should handle unclosed string gracefully
assert!(has_marker(&annotated, markers::STRING_DQ));
// May or may not have STRING_DQ_END depending on implementation
// Should handle unclosed string gracefully
assert!(has_marker(&annotated, markers::STRING_DQ));
// May or may not have STRING_DQ_END depending on implementation
}
#[test]
fn annotate_unclosed_command_sub() {
let input = "echo $(whoami";
let annotated = annotate_input(input);
let input = "echo $(whoami";
let annotated = annotate_input(input);
// Should handle unclosed command sub gracefully
assert!(has_marker(&annotated, markers::CMD_SUB));
// Should handle unclosed command sub gracefully
assert!(has_marker(&annotated, markers::CMD_SUB));
}
#[test]
fn annotate_empty_command_sub() {
let input = "echo $()";
let annotated = annotate_input_recursive(input);
let input = "echo $()";
let annotated = annotate_input_recursive(input);
// Should handle empty command sub
assert!(has_marker(&annotated, markers::CMD_SUB));
assert!(has_marker(&annotated, markers::CMD_SUB_END));
// Should handle empty command sub
assert!(has_marker(&annotated, markers::CMD_SUB));
assert!(has_marker(&annotated, markers::CMD_SUB_END));
}
#[test]
fn annotate_escaped_characters() {
let input = r#"echo \$foo \`bar\` \"test\""#;
let annotated = annotate_input(input);
let input = r#"echo \$foo \`bar\` \"test\""#;
let annotated = annotate_input(input);
// Should not mark escaped $ as variable
// This is tricky - the behavior depends on implementation
// At minimum, should not crash
// Should not mark escaped $ as variable
// This is tricky - the behavior depends on implementation
// At minimum, should not crash
}
#[test]
fn annotate_special_variables() {
let input = "echo $0 $1 $2 $3 $4";
let annotated = annotate_input(input);
let input = "echo $0 $1 $2 $3 $4";
let annotated = annotate_input(input);
// Should mark positional parameters
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);
// Should mark positional parameters
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
);
}
#[test]
fn annotate_variable_no_expansion_in_single_quotes() {
let input = "echo '$foo'";
let annotated = annotate_input(input);
let input = "echo '$foo'";
let annotated = annotate_input(input);
// Should have STRING_SQ markers
assert!(has_marker(&annotated, markers::STRING_SQ));
// Should have STRING_SQ markers
assert!(has_marker(&annotated, markers::STRING_SQ));
// Should NOT have VAR_SUB markers (variables don't expand in single quotes)
// Note: The annotator might still mark it - depends on implementation
// Should NOT have VAR_SUB markers (variables don't expand in single quotes)
// Note: The annotator might still mark it - depends on implementation
}
#[test]
fn annotate_complex_pipeline() {
let input = "cat file.txt | grep pattern | sed 's/foo/bar/' | sort | uniq";
let annotated = annotate_input(input);
let input = "cat file.txt | grep pattern | sed 's/foo/bar/' | sort | uniq";
let annotated = annotate_input(input);
// Should have multiple OPERATOR markers for pipes
let operator_count = annotated.chars().filter(|&c| c == markers::OPERATOR).count();
assert!(operator_count >= 4);
// Should have multiple OPERATOR markers for pipes
let operator_count = annotated
.chars()
.filter(|&c| c == markers::OPERATOR)
.count();
assert!(operator_count >= 4);
// Should have multiple COMMAND markers
let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count();
assert!(command_count >= 5);
// Should have multiple COMMAND markers
let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count();
assert!(command_count >= 5);
}
#[test]
fn annotate_assignment_with_command_sub() {
let input = "FOO=$(whoami)";
let annotated = annotate_input_recursive(input);
let input = "FOO=$(whoami)";
let annotated = annotate_input_recursive(input);
// Should have ASSIGNMENT marker
assert!(has_marker(&annotated, markers::ASSIGNMENT));
// Should have ASSIGNMENT marker
assert!(has_marker(&annotated, markers::ASSIGNMENT));
// Should have CMD_SUB marker
assert!(has_marker(&annotated, markers::CMD_SUB));
// Should have CMD_SUB marker
assert!(has_marker(&annotated, markers::CMD_SUB));
// Inside command sub should have COMMAND marker
assert!(has_marker(&annotated, markers::COMMAND));
// Inside command sub should have COMMAND marker
assert!(has_marker(&annotated, markers::COMMAND));
}
#[test]
fn annotate_redirect_with_fd() {
let input = "command 2>&1";
let annotated = annotate_input(input);
let input = "command 2>&1";
let annotated = annotate_input(input);
// Should have REDIRECT marker for the redirect operator
assert!(has_marker(&annotated, markers::REDIRECT));
// Should have REDIRECT marker for the redirect operator
assert!(has_marker(&annotated, markers::REDIRECT));
}
#[test]
fn annotate_multiple_redirects() {
let input = "command > out.txt 2>&1";
let annotated = annotate_input(input);
let input = "command > out.txt 2>&1";
let annotated = annotate_input(input);
// Should have multiple REDIRECT markers
let redirect_count = annotated.chars().filter(|&c| c == markers::REDIRECT).count();
assert!(redirect_count >= 2);
// Should have multiple REDIRECT markers
let redirect_count = annotated
.chars()
.filter(|&c| c == markers::REDIRECT)
.count();
assert!(redirect_count >= 2);
}
#[test]
fn annotate_here_string() {
let input = "cat <<< 'hello world'";
let annotated = annotate_input(input);
let input = "cat <<< 'hello world'";
let annotated = annotate_input(input);
// Should have REDIRECT marker for <<<
assert!(has_marker(&annotated, markers::REDIRECT));
// Should have REDIRECT marker for <<<
assert!(has_marker(&annotated, markers::REDIRECT));
// Should have STRING_SQ markers
assert!(has_marker(&annotated, markers::STRING_SQ));
// Should have STRING_SQ markers
assert!(has_marker(&annotated, markers::STRING_SQ));
}
#[test]
fn annotate_unicode_content() {
let input = "echo 'hello 世界 🌍'";
let annotated = annotate_input(input);
let input = "echo 'hello 世界 🌍'";
let annotated = annotate_input(input);
// Should handle unicode gracefully
assert!(has_marker(&annotated, markers::BUILTIN));
assert!(has_marker(&annotated, markers::STRING_SQ));
// Should handle unicode gracefully
assert!(has_marker(&annotated, markers::BUILTIN));
assert!(has_marker(&annotated, markers::STRING_SQ));
}
// ============================================================================
@@ -609,26 +643,26 @@ fn annotate_unicode_content() {
#[test]
fn regression_arg_marker_at_position_zero() {
// Regression test: ARG marker was appearing at position 3 for input "ech"
// This was caused by SOI/EOI tokens falling through to ARG annotation
let input = "ech";
let annotated = annotate_input(input);
// Regression test: ARG marker was appearing at position 3 for input "ech"
// This was caused by SOI/EOI tokens falling through to ARG annotation
let input = "ech";
let annotated = annotate_input(input);
// Should only have COMMAND marker, not ARG
// (incomplete command should still be marked as command attempt)
assert!(has_marker(&annotated, markers::COMMAND));
// Should only have COMMAND marker, not ARG
// (incomplete command should still be marked as command attempt)
assert!(has_marker(&annotated, markers::COMMAND));
}
#[test]
fn regression_string_color_in_annotated_strings() {
// Regression test: ARG marker was overriding STRING_DQ color
let input = r#"echo "test""#;
let annotated = annotate_input(input);
// Regression test: ARG marker was overriding STRING_DQ color
let input = r#"echo "test""#;
let annotated = annotate_input(input);
// STRING_DQ should be present and properly positioned
assert!(has_marker(&annotated, markers::STRING_DQ));
assert!(has_marker(&annotated, markers::STRING_DQ_END));
// STRING_DQ should be present and properly positioned
assert!(has_marker(&annotated, markers::STRING_DQ));
assert!(has_marker(&annotated, markers::STRING_DQ_END));
// The string markers should come after the ARG marker
// (so they override it in the highlighting)
// The string markers should come after the ARG marker
// (so they override it in the highlighting)
}

View File

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

View File

@@ -1,14 +1,17 @@
use std::collections::VecDeque;
use crate::{
libsh::{error::ShErr, term::{Style, Styled}},
libsh::{
error::ShErr,
term::{Style, Styled},
},
prompt::readline::{
FernVi,
history::History,
keys::{KeyCode, KeyEvent, ModKeys},
linebuf::LineBuf,
term::{raw_mode, KeyReader, LineWriter},
term::{KeyReader, LineWriter, raw_mode},
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
// These test helpers are disabled until they can be properly updated
// NOTE: FernVi structure has changed significantly and readline() method no
// longer exists These test helpers are disabled until they can be properly
// updated
/*
impl FernVi {
pub fn new_test(prompt: Option<String>, input: &str, initial: &str) -> Self {
@@ -612,10 +616,10 @@ fn fernvi_test_mode_change() {
#[test]
fn fernvi_test_lorem_ipsum_1() {
assert_eq!(fernvi_test(
"\x1bwwwwwwww5dWdBdBjjdwjdwbbbcwasdasdasdasd\x1b\r",
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."
)
"\x1bwwwwwwww5dWdBdBjjdwjdwbbbcwasdasdasdasd\x1b\r",
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."
)
}
#[test]
@@ -632,9 +636,9 @@ fn fernvi_test_lorem_ipsum_undo() {
#[test]
fn fernvi_test_lorem_ipsum_ctrl_w() {
assert_eq!(fernvi_test(
"\x1bj5wiasdasdkjhaksjdhkajshd\x17wordswordswords\x17somemorewords\x17\x1b[D\x1b[D\x17\x1b\r",
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."
)
"\x1bj5wiasdasdkjhaksjdhkajshd\x17wordswordswords\x17somemorewords\x17\x1b[D\x1b[D\x17\x1b\r",
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."
)
}
*/

View File

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

View File

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