command arguments are now underlined if they match an existing path -m ran rustfmt on the entire codebase
This commit is contained in:
@@ -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},
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}'"),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
libsh::error::ShResult,
|
||||
parse::{NdRule, Node},
|
||||
prelude::*,
|
||||
procio::{borrow_fd, IoStack},
|
||||
procio::{IoStack, borrow_fd},
|
||||
state,
|
||||
};
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
1060
src/expand.rs
1060
src/expand.rs
File diff suppressed because it is too large
Load Diff
@@ -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)
|
||||
|
||||
10
src/jobs.rs
10
src/jobs.rs
@@ -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);
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,8 +83,7 @@ impl TkVecUtils<Tk> for Vec<Tk> {
|
||||
}
|
||||
}
|
||||
fn debug_tokens(&self) {
|
||||
for token in self {
|
||||
}
|
||||
for token in self {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
336
src/main.rs
336
src/main.rs
@@ -1,7 +1,7 @@
|
||||
#![allow(
|
||||
clippy::derivable_impls,
|
||||
clippy::tabs_in_doc_comments,
|
||||
clippy::while_let_on_iterator
|
||||
clippy::derivable_impls,
|
||||
clippy::tabs_in_doc_comments,
|
||||
clippy::while_let_on_iterator
|
||||
)]
|
||||
pub mod builtin;
|
||||
pub mod expand;
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
1311
src/parse/execute.rs
1311
src/parse/execute.rs
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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);
|
||||
|
||||
22
src/shopt.rs
22
src/shopt.rs
@@ -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(())
|
||||
|
||||
178
src/signal.rs
178
src/signal.rs
@@ -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(())
|
||||
}
|
||||
|
||||
783
src/state.rs
783
src/state.rs
@@ -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(¶m.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(¶m.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(¶m.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(¶m.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(¶m)
|
||||
.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(¶m)
|
||||
.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
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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};
|
||||
|
||||
|
||||
@@ -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."
|
||||
)
|
||||
}
|
||||
*/
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user