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},
|
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||||
parse::{NdRule, Node},
|
parse::{NdRule, Node},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
procio::{borrow_fd, IoStack},
|
procio::{IoStack, borrow_fd},
|
||||||
state::{self, read_logic, write_logic},
|
state::{self, read_logic, write_logic},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -43,15 +43,19 @@ pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if let Err(e) = env::set_current_dir(new_dir) {
|
if let Err(e) = env::set_current_dir(new_dir) {
|
||||||
return Err(ShErr::full(
|
return Err(ShErr::full(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
format!("cd: Failed to change directory: {}", e),
|
format!("cd: Failed to change directory: {}", e),
|
||||||
span,
|
span,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
let new_dir = env::current_dir().map_err(
|
let new_dir = env::current_dir().map_err(|e| {
|
||||||
|e| ShErr::full(ShErrKind::ExecFail, format!("cd: Failed to get current directory: {}", e), span)
|
ShErr::full(
|
||||||
)?;
|
ShErrKind::ExecFail,
|
||||||
|
format!("cd: Failed to get current directory: {}", e),
|
||||||
|
span,
|
||||||
|
)
|
||||||
|
})?;
|
||||||
unsafe { env::set_var("PWD", new_dir) };
|
unsafe { env::set_var("PWD", new_dir) };
|
||||||
|
|
||||||
state::set_status(0);
|
state::set_status(0);
|
||||||
|
|||||||
@@ -1,14 +1,34 @@
|
|||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
builtin::setup_builtin, expand::expand_prompt, getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node}, prelude::*, procio::{IoStack, borrow_fd}, state
|
builtin::setup_builtin,
|
||||||
|
expand::expand_prompt,
|
||||||
|
getopt::{Opt, OptSpec, get_opts_from_tokens},
|
||||||
|
jobs::JobBldr,
|
||||||
|
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
|
||||||
|
parse::{NdRule, Node},
|
||||||
|
prelude::*,
|
||||||
|
procio::{IoStack, borrow_fd},
|
||||||
|
state,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const ECHO_OPTS: [OptSpec;4] = [
|
pub const ECHO_OPTS: [OptSpec; 4] = [
|
||||||
OptSpec { opt: Opt::Short('n'), takes_arg: false },
|
OptSpec {
|
||||||
OptSpec { opt: Opt::Short('E'), takes_arg: false },
|
opt: Opt::Short('n'),
|
||||||
OptSpec { opt: Opt::Short('e'), takes_arg: false },
|
takes_arg: false,
|
||||||
OptSpec { opt: Opt::Short('p'), takes_arg: false },
|
},
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('E'),
|
||||||
|
takes_arg: false,
|
||||||
|
},
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('e'),
|
||||||
|
takes_arg: false,
|
||||||
|
},
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('p'),
|
||||||
|
takes_arg: false,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
bitflags! {
|
bitflags! {
|
||||||
@@ -16,7 +36,7 @@ bitflags! {
|
|||||||
const NO_NEWLINE = 0b000001;
|
const NO_NEWLINE = 0b000001;
|
||||||
const USE_STDERR = 0b000010;
|
const USE_STDERR = 0b000010;
|
||||||
const USE_ESCAPE = 0b000100;
|
const USE_ESCAPE = 0b000100;
|
||||||
const USE_PROMPT = 0b001000;
|
const USE_PROMPT = 0b001000;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,13 +60,15 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
|
|||||||
borrow_fd(STDOUT_FILENO)
|
borrow_fd(STDOUT_FILENO)
|
||||||
};
|
};
|
||||||
|
|
||||||
let mut echo_output = prepare_echo_args(argv
|
let mut echo_output = prepare_echo_args(
|
||||||
.into_iter()
|
argv
|
||||||
.map(|a| a.0) // Extract the String from the tuple of (String,Span)
|
.into_iter()
|
||||||
.collect::<Vec<_>>(),
|
.map(|a| a.0) // Extract the String from the tuple of (String,Span)
|
||||||
flags.contains(EchoFlags::USE_ESCAPE),
|
.collect::<Vec<_>>(),
|
||||||
flags.contains(EchoFlags::USE_PROMPT)
|
flags.contains(EchoFlags::USE_ESCAPE),
|
||||||
)?.join(" ");
|
flags.contains(EchoFlags::USE_PROMPT),
|
||||||
|
)?
|
||||||
|
.join(" ");
|
||||||
|
|
||||||
if !flags.contains(EchoFlags::NO_NEWLINE) && !echo_output.ends_with('\n') {
|
if !flags.contains(EchoFlags::NO_NEWLINE) && !echo_output.ends_with('\n') {
|
||||||
echo_output.push('\n')
|
echo_output.push('\n')
|
||||||
@@ -58,137 +80,141 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn prepare_echo_args(argv: Vec<String>, use_escape: bool, use_prompt: bool) -> ShResult<Vec<String>> {
|
pub fn prepare_echo_args(
|
||||||
if !use_escape {
|
argv: Vec<String>,
|
||||||
if use_prompt {
|
use_escape: bool,
|
||||||
let expanded: ShResult<Vec<String>> = argv
|
use_prompt: bool,
|
||||||
.into_iter()
|
) -> ShResult<Vec<String>> {
|
||||||
.map(|s| expand_prompt(s.as_str()))
|
if !use_escape {
|
||||||
.collect();
|
if use_prompt {
|
||||||
return expanded
|
let expanded: ShResult<Vec<String>> = argv
|
||||||
}
|
.into_iter()
|
||||||
return Ok(argv);
|
.map(|s| expand_prompt(s.as_str()))
|
||||||
}
|
.collect();
|
||||||
|
return expanded;
|
||||||
|
}
|
||||||
|
return Ok(argv);
|
||||||
|
}
|
||||||
|
|
||||||
let mut prepared_args = Vec::with_capacity(argv.len());
|
let mut prepared_args = Vec::with_capacity(argv.len());
|
||||||
|
|
||||||
for arg in argv {
|
for arg in argv {
|
||||||
let mut prepared_arg = String::new();
|
let mut prepared_arg = String::new();
|
||||||
if use_prompt {
|
if use_prompt {
|
||||||
prepared_arg = expand_prompt(&prepared_arg)?;
|
prepared_arg = expand_prompt(&prepared_arg)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut chars = arg.chars().peekable();
|
let mut chars = arg.chars().peekable();
|
||||||
|
|
||||||
while let Some(c) = chars.next() {
|
while let Some(c) = chars.next() {
|
||||||
if c == '\\' {
|
if c == '\\' {
|
||||||
if let Some(&next_char) = chars.peek() {
|
if let Some(&next_char) = chars.peek() {
|
||||||
match next_char {
|
match next_char {
|
||||||
'n' => {
|
'n' => {
|
||||||
prepared_arg.push('\n');
|
prepared_arg.push('\n');
|
||||||
chars.next();
|
chars.next();
|
||||||
}
|
}
|
||||||
't' => {
|
't' => {
|
||||||
prepared_arg.push('\t');
|
prepared_arg.push('\t');
|
||||||
chars.next();
|
chars.next();
|
||||||
}
|
}
|
||||||
'r' => {
|
'r' => {
|
||||||
prepared_arg.push('\r');
|
prepared_arg.push('\r');
|
||||||
chars.next();
|
chars.next();
|
||||||
}
|
}
|
||||||
'a' => {
|
'a' => {
|
||||||
prepared_arg.push('\x07');
|
prepared_arg.push('\x07');
|
||||||
chars.next();
|
chars.next();
|
||||||
}
|
}
|
||||||
'b' => {
|
'b' => {
|
||||||
prepared_arg.push('\x08');
|
prepared_arg.push('\x08');
|
||||||
chars.next();
|
chars.next();
|
||||||
}
|
}
|
||||||
'e' | 'E' => {
|
'e' | 'E' => {
|
||||||
prepared_arg.push('\x1b');
|
prepared_arg.push('\x1b');
|
||||||
chars.next();
|
chars.next();
|
||||||
}
|
}
|
||||||
'x' => {
|
'x' => {
|
||||||
chars.next(); // consume 'x'
|
chars.next(); // consume 'x'
|
||||||
let mut hex_digits = String::new();
|
let mut hex_digits = String::new();
|
||||||
for _ in 0..2 {
|
for _ in 0..2 {
|
||||||
if let Some(&hex_char) = chars.peek() {
|
if let Some(&hex_char) = chars.peek() {
|
||||||
if hex_char.is_ascii_hexdigit() {
|
if hex_char.is_ascii_hexdigit() {
|
||||||
hex_digits.push(hex_char);
|
hex_digits.push(hex_char);
|
||||||
chars.next();
|
chars.next();
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Ok(value) = u8::from_str_radix(&hex_digits, 16) {
|
if let Ok(value) = u8::from_str_radix(&hex_digits, 16) {
|
||||||
prepared_arg.push(value as char);
|
prepared_arg.push(value as char);
|
||||||
} else {
|
} else {
|
||||||
prepared_arg.push('\\');
|
prepared_arg.push('\\');
|
||||||
prepared_arg.push('x');
|
prepared_arg.push('x');
|
||||||
prepared_arg.push_str(&hex_digits);
|
prepared_arg.push_str(&hex_digits);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'0' => {
|
'0' => {
|
||||||
chars.next(); // consume '0'
|
chars.next(); // consume '0'
|
||||||
let mut octal_digits = String::new();
|
let mut octal_digits = String::new();
|
||||||
for _ in 0..3 {
|
for _ in 0..3 {
|
||||||
if let Some(&octal_char) = chars.peek() {
|
if let Some(&octal_char) = chars.peek() {
|
||||||
if ('0'..='7').contains(&octal_char) {
|
if ('0'..='7').contains(&octal_char) {
|
||||||
octal_digits.push(octal_char);
|
octal_digits.push(octal_char);
|
||||||
chars.next();
|
chars.next();
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Ok(value) = u8::from_str_radix(&octal_digits, 8) {
|
if let Ok(value) = u8::from_str_radix(&octal_digits, 8) {
|
||||||
prepared_arg.push(value as char);
|
prepared_arg.push(value as char);
|
||||||
} else {
|
} else {
|
||||||
prepared_arg.push('\\');
|
prepared_arg.push('\\');
|
||||||
prepared_arg.push('0');
|
prepared_arg.push('0');
|
||||||
prepared_arg.push_str(&octal_digits);
|
prepared_arg.push_str(&octal_digits);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'\\' => {
|
'\\' => {
|
||||||
prepared_arg.push('\\');
|
prepared_arg.push('\\');
|
||||||
chars.next();
|
chars.next();
|
||||||
}
|
}
|
||||||
_ => prepared_arg.push(c),
|
_ => prepared_arg.push(c),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
prepared_arg.push(c);
|
prepared_arg.push(c);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
prepared_arg.push(c);
|
prepared_arg.push(c);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
prepared_args.push(prepared_arg);
|
prepared_args.push(prepared_arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(prepared_args)
|
Ok(prepared_args)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_echo_flags(opts: Vec<Opt>) -> ShResult<EchoFlags> {
|
pub fn get_echo_flags(opts: Vec<Opt>) -> ShResult<EchoFlags> {
|
||||||
let mut flags = EchoFlags::empty();
|
let mut flags = EchoFlags::empty();
|
||||||
|
|
||||||
for opt in opts {
|
for opt in opts {
|
||||||
match opt {
|
match opt {
|
||||||
Opt::Short('n') => flags |= EchoFlags::NO_NEWLINE,
|
Opt::Short('n') => flags |= EchoFlags::NO_NEWLINE,
|
||||||
Opt::Short('r') => flags |= EchoFlags::USE_STDERR,
|
Opt::Short('r') => flags |= EchoFlags::USE_STDERR,
|
||||||
Opt::Short('e') => flags |= EchoFlags::USE_ESCAPE,
|
Opt::Short('e') => flags |= EchoFlags::USE_ESCAPE,
|
||||||
Opt::Short('p') => flags |= EchoFlags::USE_PROMPT,
|
Opt::Short('p') => flags |= EchoFlags::USE_PROMPT,
|
||||||
_ => {
|
_ => {
|
||||||
return Err(ShErr::simple(
|
return Err(ShErr::simple(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
format!("echo: Unexpected flag '{opt}'"),
|
format!("echo: Unexpected flag '{opt}'"),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -35,10 +35,10 @@ pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult
|
|||||||
for (arg, _) in argv {
|
for (arg, _) in argv {
|
||||||
if let Some((var, val)) = arg.split_once('=') {
|
if let Some((var, val)) = arg.split_once('=') {
|
||||||
write_vars(|v| v.set_var(var, val, VarFlags::EXPORT)); // Export an assignment like
|
write_vars(|v| v.set_var(var, val, VarFlags::EXPORT)); // Export an assignment like
|
||||||
// 'foo=bar'
|
// 'foo=bar'
|
||||||
} else {
|
} else {
|
||||||
write_vars(|v| v.export_var(&arg)); // Export an existing variable, if
|
write_vars(|v| v.export_var(&arg)); // Export an existing variable, if
|
||||||
// any
|
// any
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||||
parse::{execute::prepare_argv, NdRule, Node},
|
parse::{NdRule, Node, execute::prepare_argv},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -32,7 +32,6 @@ pub fn flowctl(node: Node, kind: ShErrKind) -> ShResult<()> {
|
|||||||
code = status;
|
code = status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
let kind = match kind {
|
let kind = match kind {
|
||||||
LoopContinue(_) => LoopContinue(code),
|
LoopContinue(_) => LoopContinue(code),
|
||||||
LoopBreak(_) => LoopBreak(code),
|
LoopBreak(_) => LoopBreak(code),
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
jobs::{JobBldr, JobCmdFlags, JobID},
|
jobs::{JobBldr, JobCmdFlags, JobID},
|
||||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||||
parse::{lex::Span, NdRule, Node},
|
parse::{NdRule, Node, lex::Span},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
procio::{borrow_fd, IoStack},
|
procio::{IoStack, borrow_fd},
|
||||||
state::{self, read_jobs, write_jobs},
|
state::{self, read_jobs, write_jobs},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -168,7 +168,7 @@ pub fn jobs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
|
|||||||
ShErrKind::SyntaxErr,
|
ShErrKind::SyntaxErr,
|
||||||
"Invalid flag in jobs call",
|
"Invalid flag in jobs call",
|
||||||
span,
|
span,
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
flags |= flag
|
flags |= flag
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ use crate::{
|
|||||||
jobs::{ChildProc, JobBldr},
|
jobs::{ChildProc, JobBldr},
|
||||||
libsh::error::ShResult,
|
libsh::error::ShResult,
|
||||||
parse::{
|
parse::{
|
||||||
Redir, execute::prepare_argv, lex::{Span, Tk}
|
Redir,
|
||||||
|
execute::prepare_argv,
|
||||||
|
lex::{Span, Tk},
|
||||||
},
|
},
|
||||||
procio::{IoFrame, IoStack, RedirGuard},
|
procio::{IoFrame, IoStack, RedirGuard},
|
||||||
};
|
};
|
||||||
@@ -16,19 +18,17 @@ pub mod export;
|
|||||||
pub mod flowctl;
|
pub mod flowctl;
|
||||||
pub mod jobctl;
|
pub mod jobctl;
|
||||||
pub mod pwd;
|
pub mod pwd;
|
||||||
|
pub mod read;
|
||||||
pub mod shift;
|
pub mod shift;
|
||||||
pub mod shopt;
|
pub mod shopt;
|
||||||
pub mod source;
|
pub mod source;
|
||||||
pub mod test; // [[ ]] thing
|
pub mod test; // [[ ]] thing
|
||||||
pub mod read;
|
|
||||||
pub mod zoltraak;
|
|
||||||
pub mod trap;
|
pub mod trap;
|
||||||
|
pub mod zoltraak;
|
||||||
|
|
||||||
pub const BUILTINS: [&str; 21] = [
|
pub const BUILTINS: [&str; 21] = [
|
||||||
"echo", "cd", "read", "export", "pwd", "source",
|
"echo", "cd", "read", "export", "pwd", "source", "shift", "jobs", "fg", "bg", "alias", "unalias",
|
||||||
"shift", "jobs", "fg", "bg", "alias", "unalias",
|
"return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "command", "trap",
|
||||||
"return", "break", "continue", "exit", "zoltraak",
|
|
||||||
"shopt", "builtin", "command", "trap"
|
|
||||||
];
|
];
|
||||||
|
|
||||||
/// Sets up a builtin command
|
/// Sets up a builtin command
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::{
|
|||||||
libsh::error::ShResult,
|
libsh::error::ShResult,
|
||||||
parse::{NdRule, Node},
|
parse::{NdRule, Node},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
procio::{borrow_fd, IoStack},
|
procio::{IoStack, borrow_fd},
|
||||||
state,
|
state,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,188 +1,237 @@
|
|||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
use nix::{errno::Errno, libc::{STDIN_FILENO, STDOUT_FILENO}, unistd::{isatty, read, write}};
|
use nix::{
|
||||||
|
errno::Errno,
|
||||||
|
libc::{STDIN_FILENO, STDOUT_FILENO},
|
||||||
|
unistd::{isatty, read, write},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{builtin::setup_builtin, getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node}, procio::{IoStack, borrow_fd}, prompt::readline::term::RawModeGuard, state::{self, VarFlags, read_vars, write_vars}};
|
use crate::{
|
||||||
|
builtin::setup_builtin,
|
||||||
|
getopt::{Opt, OptSpec, get_opts_from_tokens},
|
||||||
|
jobs::JobBldr,
|
||||||
|
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
|
||||||
|
parse::{NdRule, Node},
|
||||||
|
procio::{IoStack, borrow_fd},
|
||||||
|
prompt::readline::term::RawModeGuard,
|
||||||
|
state::{self, VarFlags, read_vars, write_vars},
|
||||||
|
};
|
||||||
|
|
||||||
pub const READ_OPTS: [OptSpec;7] = [
|
pub const READ_OPTS: [OptSpec; 7] = [
|
||||||
OptSpec { opt: Opt::Short('r'), takes_arg: false }, // don't allow backslash escapes
|
OptSpec {
|
||||||
OptSpec { opt: Opt::Short('s'), takes_arg: false }, // don't echo input
|
opt: Opt::Short('r'),
|
||||||
OptSpec { opt: Opt::Short('a'), takes_arg: false }, // read into array
|
takes_arg: false,
|
||||||
OptSpec { opt: Opt::Short('n'), takes_arg: false }, // read only N characters
|
}, // don't allow backslash escapes
|
||||||
OptSpec { opt: Opt::Short('t'), takes_arg: false }, // timeout
|
OptSpec {
|
||||||
OptSpec { opt: Opt::Short('p'), takes_arg: true }, // prompt
|
opt: Opt::Short('s'),
|
||||||
OptSpec { opt: Opt::Short('d'), takes_arg: true }, // read until delimiter
|
takes_arg: false,
|
||||||
|
}, // don't echo input
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('a'),
|
||||||
|
takes_arg: false,
|
||||||
|
}, // read into array
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('n'),
|
||||||
|
takes_arg: false,
|
||||||
|
}, // read only N characters
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('t'),
|
||||||
|
takes_arg: false,
|
||||||
|
}, // timeout
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('p'),
|
||||||
|
takes_arg: true,
|
||||||
|
}, // prompt
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('d'),
|
||||||
|
takes_arg: true,
|
||||||
|
}, // read until delimiter
|
||||||
];
|
];
|
||||||
|
|
||||||
bitflags! {
|
bitflags! {
|
||||||
pub struct ReadFlags: u32 {
|
pub struct ReadFlags: u32 {
|
||||||
const NO_ESCAPES = 0b000001;
|
const NO_ESCAPES = 0b000001;
|
||||||
const NO_ECHO = 0b000010; // TODO: unused
|
const NO_ECHO = 0b000010; // TODO: unused
|
||||||
const ARRAY = 0b000100; // TODO: unused
|
const ARRAY = 0b000100; // TODO: unused
|
||||||
const N_CHARS = 0b001000; // TODO: unused
|
const N_CHARS = 0b001000; // TODO: unused
|
||||||
const TIMEOUT = 0b010000; // TODO: unused
|
const TIMEOUT = 0b010000; // TODO: unused
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ReadOpts {
|
pub struct ReadOpts {
|
||||||
prompt: Option<String>,
|
prompt: Option<String>,
|
||||||
delim: u8, // byte representation of the delimiter character
|
delim: u8, // byte representation of the delimiter character
|
||||||
flags: ReadFlags,
|
flags: ReadFlags,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
|
pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
|
||||||
let blame = node.get_span().clone();
|
let blame = node.get_span().clone();
|
||||||
let NdRule::Command {
|
let NdRule::Command {
|
||||||
assignments: _,
|
assignments: _,
|
||||||
argv
|
argv,
|
||||||
} = node.class else {
|
} = node.class
|
||||||
unreachable!()
|
else {
|
||||||
};
|
unreachable!()
|
||||||
|
};
|
||||||
|
|
||||||
let (argv, opts) = get_opts_from_tokens(argv, &READ_OPTS);
|
let (argv, opts) = get_opts_from_tokens(argv, &READ_OPTS);
|
||||||
let read_opts = get_read_flags(opts).blame(blame.clone())?;
|
let read_opts = get_read_flags(opts).blame(blame.clone())?;
|
||||||
let (argv, _) = setup_builtin(argv, job, None).blame(blame.clone())?;
|
let (argv, _) = setup_builtin(argv, job, None).blame(blame.clone())?;
|
||||||
|
|
||||||
if let Some(prompt) = read_opts.prompt {
|
if let Some(prompt) = read_opts.prompt {
|
||||||
write(borrow_fd(STDOUT_FILENO), prompt.as_bytes())?;
|
write(borrow_fd(STDOUT_FILENO), prompt.as_bytes())?;
|
||||||
}
|
}
|
||||||
|
|
||||||
let input = if isatty(STDIN_FILENO)? {
|
let input = if isatty(STDIN_FILENO)? {
|
||||||
// Restore default terminal settings
|
// Restore default terminal settings
|
||||||
RawModeGuard::with_cooked_mode(|| {
|
RawModeGuard::with_cooked_mode(|| {
|
||||||
let mut input: Vec<u8> = vec![];
|
let mut input: Vec<u8> = vec![];
|
||||||
let mut escaped = false;
|
let mut escaped = false;
|
||||||
loop {
|
loop {
|
||||||
let mut buf = [0u8;1];
|
let mut buf = [0u8; 1];
|
||||||
match read(STDIN_FILENO, &mut buf) {
|
match read(STDIN_FILENO, &mut buf) {
|
||||||
Ok(0) => {
|
Ok(0) => {
|
||||||
state::set_status(1);
|
state::set_status(1);
|
||||||
let str_result = String::from_utf8(input.clone()).map_err(|e| ShErr::simple(
|
let str_result = String::from_utf8(input.clone()).map_err(|e| {
|
||||||
ShErrKind::ExecFail,
|
ShErr::simple(
|
||||||
format!("read: Input was not valid UTF-8: {e}"),
|
ShErrKind::ExecFail,
|
||||||
))?;
|
format!("read: Input was not valid UTF-8: {e}"),
|
||||||
return Ok(str_result); // EOF
|
)
|
||||||
}
|
})?;
|
||||||
Ok(_) => {
|
return Ok(str_result); // EOF
|
||||||
if buf[0] == read_opts.delim {
|
}
|
||||||
if read_opts.flags.contains(ReadFlags::NO_ESCAPES) && escaped {
|
Ok(_) => {
|
||||||
input.push(buf[0]);
|
if buf[0] == read_opts.delim {
|
||||||
} else {
|
if read_opts.flags.contains(ReadFlags::NO_ESCAPES) && escaped {
|
||||||
// Delimiter reached, stop reading
|
input.push(buf[0]);
|
||||||
break;
|
} else {
|
||||||
}
|
// Delimiter reached, stop reading
|
||||||
}
|
break;
|
||||||
else if read_opts.flags.contains(ReadFlags::NO_ESCAPES)
|
}
|
||||||
&& buf[0] == b'\\' {
|
} else if read_opts.flags.contains(ReadFlags::NO_ESCAPES) && buf[0] == b'\\' {
|
||||||
escaped = true;
|
escaped = true;
|
||||||
} else {
|
} else {
|
||||||
input.push(buf[0]);
|
input.push(buf[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(Errno::EINTR) => continue,
|
Err(Errno::EINTR) => continue,
|
||||||
Err(e) => return Err(ShErr::simple(
|
Err(e) => {
|
||||||
ShErrKind::ExecFail,
|
return Err(ShErr::simple(
|
||||||
format!("read: Failed to read from stdin: {e}"),
|
ShErrKind::ExecFail,
|
||||||
)),
|
format!("read: Failed to read from stdin: {e}"),
|
||||||
}
|
));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
state::set_status(0);
|
state::set_status(0);
|
||||||
let str_result = String::from_utf8(input.clone()).map_err(|e| ShErr::simple(
|
let str_result = String::from_utf8(input.clone()).map_err(|e| {
|
||||||
ShErrKind::ExecFail,
|
ShErr::simple(
|
||||||
format!("read: Input was not valid UTF-8: {e}"),
|
ShErrKind::ExecFail,
|
||||||
))?;
|
format!("read: Input was not valid UTF-8: {e}"),
|
||||||
Ok(str_result)
|
)
|
||||||
}).blame(blame)?
|
})?;
|
||||||
} else {
|
Ok(str_result)
|
||||||
let mut input: Vec<u8> = vec![];
|
})
|
||||||
loop {
|
.blame(blame)?
|
||||||
let mut buf = [0u8;1];
|
} else {
|
||||||
match read(STDIN_FILENO, &mut buf) {
|
let mut input: Vec<u8> = vec![];
|
||||||
Ok(0) => {
|
loop {
|
||||||
state::set_status(1);
|
let mut buf = [0u8; 1];
|
||||||
break; // EOF
|
match read(STDIN_FILENO, &mut buf) {
|
||||||
}
|
Ok(0) => {
|
||||||
Ok(_) => {
|
state::set_status(1);
|
||||||
if buf[0] == read_opts.delim {
|
break; // EOF
|
||||||
break; // Delimiter reached, stop reading
|
}
|
||||||
}
|
Ok(_) => {
|
||||||
input.push(buf[0]);
|
if buf[0] == read_opts.delim {
|
||||||
}
|
break; // Delimiter reached, stop reading
|
||||||
Err(Errno::EINTR) => continue,
|
}
|
||||||
Err(e) => return Err(ShErr::simple(
|
input.push(buf[0]);
|
||||||
ShErrKind::ExecFail,
|
}
|
||||||
format!("read: Failed to read from stdin: {e}"),
|
Err(Errno::EINTR) => continue,
|
||||||
)),
|
Err(e) => {
|
||||||
}
|
return Err(ShErr::simple(
|
||||||
}
|
ShErrKind::ExecFail,
|
||||||
String::from_utf8(input).map_err(|e| ShErr::simple(
|
format!("read: Failed to read from stdin: {e}"),
|
||||||
ShErrKind::ExecFail,
|
));
|
||||||
format!("read: Input was not valid UTF-8: {e}"),
|
}
|
||||||
))?
|
}
|
||||||
};
|
}
|
||||||
|
String::from_utf8(input).map_err(|e| {
|
||||||
|
ShErr::simple(
|
||||||
|
ShErrKind::ExecFail,
|
||||||
|
format!("read: Input was not valid UTF-8: {e}"),
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
};
|
||||||
|
|
||||||
if argv.is_empty() {
|
if argv.is_empty() {
|
||||||
write_vars(|v| {
|
write_vars(|v| {
|
||||||
v.set_var("REPLY", &input, VarFlags::NONE);
|
v.set_var("REPLY", &input, VarFlags::NONE);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// get our field separator
|
// get our field separator
|
||||||
let mut field_sep = read_vars(|v| v.get_var("IFS"));
|
let mut field_sep = read_vars(|v| v.get_var("IFS"));
|
||||||
if field_sep.is_empty() { field_sep = " ".to_string() }
|
if field_sep.is_empty() {
|
||||||
let mut remaining = input;
|
field_sep = " ".to_string()
|
||||||
|
}
|
||||||
|
let mut remaining = input;
|
||||||
|
|
||||||
for (i, arg) in argv.iter().enumerate() {
|
for (i, arg) in argv.iter().enumerate() {
|
||||||
if i == argv.len() - 1 {
|
if i == argv.len() - 1 {
|
||||||
// Last arg, stuff the rest of the input into it
|
// Last arg, stuff the rest of the input into it
|
||||||
write_vars(|v| v.set_var(&arg.0, &remaining, VarFlags::NONE));
|
write_vars(|v| v.set_var(&arg.0, &remaining, VarFlags::NONE));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// trim leading IFS characters
|
// trim leading IFS characters
|
||||||
let trimmed = remaining.trim_start_matches(|c: char| field_sep.contains(c));
|
let trimmed = remaining.trim_start_matches(|c: char| field_sep.contains(c));
|
||||||
|
|
||||||
if let Some(idx) = trimmed.find(|c: char| field_sep.contains(c)) {
|
if let Some(idx) = trimmed.find(|c: char| field_sep.contains(c)) {
|
||||||
// We found a field separator, split at the char index
|
// We found a field separator, split at the char index
|
||||||
let (field, rest) = trimmed.split_at(idx);
|
let (field, rest) = trimmed.split_at(idx);
|
||||||
write_vars(|v| v.set_var(&arg.0, field, VarFlags::NONE));
|
write_vars(|v| v.set_var(&arg.0, field, VarFlags::NONE));
|
||||||
|
|
||||||
// note that this doesn't account for consecutive IFS characters, which is what that trim above is for
|
// note that this doesn't account for consecutive IFS characters, which is what
|
||||||
remaining = rest.to_string();
|
// that trim above is for
|
||||||
} else {
|
remaining = rest.to_string();
|
||||||
write_vars(|v| v.set_var(&arg.0, trimmed, VarFlags::NONE));
|
} else {
|
||||||
remaining.clear();
|
write_vars(|v| v.set_var(&arg.0, trimmed, VarFlags::NONE));
|
||||||
}
|
remaining.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
state::set_status(0);
|
state::set_status(0);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_read_flags(opts: Vec<Opt>) -> ShResult<ReadOpts> {
|
pub fn get_read_flags(opts: Vec<Opt>) -> ShResult<ReadOpts> {
|
||||||
let mut read_opts = ReadOpts {
|
let mut read_opts = ReadOpts {
|
||||||
prompt: None,
|
prompt: None,
|
||||||
delim: b'\n',
|
delim: b'\n',
|
||||||
flags: ReadFlags::empty(),
|
flags: ReadFlags::empty(),
|
||||||
};
|
};
|
||||||
|
|
||||||
for opt in opts {
|
for opt in opts {
|
||||||
match opt {
|
match opt {
|
||||||
Opt::Short('r') => read_opts.flags |= ReadFlags::NO_ESCAPES,
|
Opt::Short('r') => read_opts.flags |= ReadFlags::NO_ESCAPES,
|
||||||
Opt::Short('s') => read_opts.flags |= ReadFlags::NO_ECHO,
|
Opt::Short('s') => read_opts.flags |= ReadFlags::NO_ECHO,
|
||||||
Opt::Short('a') => read_opts.flags |= ReadFlags::ARRAY,
|
Opt::Short('a') => read_opts.flags |= ReadFlags::ARRAY,
|
||||||
Opt::Short('n') => read_opts.flags |= ReadFlags::N_CHARS,
|
Opt::Short('n') => read_opts.flags |= ReadFlags::N_CHARS,
|
||||||
Opt::Short('t') => read_opts.flags |= ReadFlags::TIMEOUT,
|
Opt::Short('t') => read_opts.flags |= ReadFlags::TIMEOUT,
|
||||||
Opt::ShortWithArg('p', prompt) => read_opts.prompt = Some(prompt),
|
Opt::ShortWithArg('p', prompt) => read_opts.prompt = Some(prompt),
|
||||||
Opt::ShortWithArg('d', delim) => read_opts.delim = delim.chars().map(|c| c as u8).next().unwrap_or(b'\n'),
|
Opt::ShortWithArg('d', delim) => {
|
||||||
_ => {
|
read_opts.delim = delim.chars().map(|c| c as u8).next().unwrap_or(b'\n')
|
||||||
return Err(ShErr::simple(
|
}
|
||||||
ShErrKind::ExecFail,
|
_ => {
|
||||||
format!("read: Unexpected flag '{opt}'"),
|
return Err(ShErr::simple(
|
||||||
));
|
ShErrKind::ExecFail,
|
||||||
}
|
format!("read: Unexpected flag '{opt}'"),
|
||||||
}
|
));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok(read_opts)
|
Ok(read_opts)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use crate::{
|
|||||||
libsh::error::{ShResult, ShResultExt},
|
libsh::error::{ShResult, ShResultExt},
|
||||||
parse::{NdRule, Node},
|
parse::{NdRule, Node},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
procio::{borrow_fd, IoStack},
|
procio::{IoStack, borrow_fd},
|
||||||
state::write_shopts,
|
state::write_shopts,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use regex::Regex;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||||
parse::{ConjunctOp, NdRule, Node, TestCase, TEST_UNARY_OPS},
|
parse::{ConjunctOp, NdRule, Node, TEST_UNARY_OPS, TestCase},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -254,7 +254,7 @@ pub fn double_bracket_test(node: Node) -> ShResult<bool> {
|
|||||||
msg: "Expected a binary operator in this test call; found a unary operator".into(),
|
msg: "Expected a binary operator in this test call; found a unary operator".into(),
|
||||||
notes: vec![],
|
notes: vec![],
|
||||||
span: err_span,
|
span: err_span,
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
TestOp::StringEq => rhs.trim() == lhs.trim(),
|
TestOp::StringEq => rhs.trim() == lhs.trim(),
|
||||||
TestOp::StringNeq => rhs.trim() != lhs.trim(),
|
TestOp::StringNeq => rhs.trim() != lhs.trim(),
|
||||||
|
|||||||
@@ -1,162 +1,171 @@
|
|||||||
use std::{fmt::Display, str::FromStr};
|
use std::{fmt::Display, str::FromStr};
|
||||||
|
|
||||||
use nix::{libc::{STDERR_FILENO, STDOUT_FILENO}, sys::signal::Signal, unistd::write};
|
use nix::{
|
||||||
|
libc::{STDERR_FILENO, STDOUT_FILENO},
|
||||||
|
sys::signal::Signal,
|
||||||
|
unistd::write,
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{builtin::setup_builtin, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node}, procio::{IoStack, borrow_fd}, state::{self, read_logic, write_logic}};
|
use crate::{
|
||||||
|
builtin::setup_builtin,
|
||||||
|
jobs::JobBldr,
|
||||||
|
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||||
|
parse::{NdRule, Node},
|
||||||
|
procio::{IoStack, borrow_fd},
|
||||||
|
state::{self, read_logic, write_logic},
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash)]
|
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash)]
|
||||||
pub enum TrapTarget {
|
pub enum TrapTarget {
|
||||||
Exit,
|
Exit,
|
||||||
Error,
|
Error,
|
||||||
Signal(Signal)
|
Signal(Signal),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for TrapTarget {
|
impl FromStr for TrapTarget {
|
||||||
type Err = ShErr;
|
type Err = ShErr;
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
match s {
|
match s {
|
||||||
"EXIT" => Ok(TrapTarget::Exit),
|
"EXIT" => Ok(TrapTarget::Exit),
|
||||||
"ERR" => Ok(TrapTarget::Error),
|
"ERR" => Ok(TrapTarget::Error),
|
||||||
|
|
||||||
"INT" => Ok(TrapTarget::Signal(Signal::SIGINT)),
|
"INT" => Ok(TrapTarget::Signal(Signal::SIGINT)),
|
||||||
"QUIT" => Ok(TrapTarget::Signal(Signal::SIGQUIT)),
|
"QUIT" => Ok(TrapTarget::Signal(Signal::SIGQUIT)),
|
||||||
"ILL" => Ok(TrapTarget::Signal(Signal::SIGILL)),
|
"ILL" => Ok(TrapTarget::Signal(Signal::SIGILL)),
|
||||||
"TRAP" => Ok(TrapTarget::Signal(Signal::SIGTRAP)),
|
"TRAP" => Ok(TrapTarget::Signal(Signal::SIGTRAP)),
|
||||||
"ABRT" => Ok(TrapTarget::Signal(Signal::SIGABRT)),
|
"ABRT" => Ok(TrapTarget::Signal(Signal::SIGABRT)),
|
||||||
"BUS" => Ok(TrapTarget::Signal(Signal::SIGBUS)),
|
"BUS" => Ok(TrapTarget::Signal(Signal::SIGBUS)),
|
||||||
"FPE" => Ok(TrapTarget::Signal(Signal::SIGFPE)),
|
"FPE" => Ok(TrapTarget::Signal(Signal::SIGFPE)),
|
||||||
"KILL" => Ok(TrapTarget::Signal(Signal::SIGKILL)),
|
"KILL" => Ok(TrapTarget::Signal(Signal::SIGKILL)),
|
||||||
"USR1" => Ok(TrapTarget::Signal(Signal::SIGUSR1)),
|
"USR1" => Ok(TrapTarget::Signal(Signal::SIGUSR1)),
|
||||||
"SEGV" => Ok(TrapTarget::Signal(Signal::SIGSEGV)),
|
"SEGV" => Ok(TrapTarget::Signal(Signal::SIGSEGV)),
|
||||||
"USR2" => Ok(TrapTarget::Signal(Signal::SIGUSR2)),
|
"USR2" => Ok(TrapTarget::Signal(Signal::SIGUSR2)),
|
||||||
"PIPE" => Ok(TrapTarget::Signal(Signal::SIGPIPE)),
|
"PIPE" => Ok(TrapTarget::Signal(Signal::SIGPIPE)),
|
||||||
"ALRM" => Ok(TrapTarget::Signal(Signal::SIGALRM)),
|
"ALRM" => Ok(TrapTarget::Signal(Signal::SIGALRM)),
|
||||||
"TERM" => Ok(TrapTarget::Signal(Signal::SIGTERM)),
|
"TERM" => Ok(TrapTarget::Signal(Signal::SIGTERM)),
|
||||||
"STKFLT" => Ok(TrapTarget::Signal(Signal::SIGSTKFLT)),
|
"STKFLT" => Ok(TrapTarget::Signal(Signal::SIGSTKFLT)),
|
||||||
"CHLD" => Ok(TrapTarget::Signal(Signal::SIGCHLD)),
|
"CHLD" => Ok(TrapTarget::Signal(Signal::SIGCHLD)),
|
||||||
"CONT" => Ok(TrapTarget::Signal(Signal::SIGCONT)),
|
"CONT" => Ok(TrapTarget::Signal(Signal::SIGCONT)),
|
||||||
"STOP" => Ok(TrapTarget::Signal(Signal::SIGSTOP)),
|
"STOP" => Ok(TrapTarget::Signal(Signal::SIGSTOP)),
|
||||||
"TSTP" => Ok(TrapTarget::Signal(Signal::SIGTSTP)),
|
"TSTP" => Ok(TrapTarget::Signal(Signal::SIGTSTP)),
|
||||||
"TTIN" => Ok(TrapTarget::Signal(Signal::SIGTTIN)),
|
"TTIN" => Ok(TrapTarget::Signal(Signal::SIGTTIN)),
|
||||||
"TTOU" => Ok(TrapTarget::Signal(Signal::SIGTTOU)),
|
"TTOU" => Ok(TrapTarget::Signal(Signal::SIGTTOU)),
|
||||||
"URG" => Ok(TrapTarget::Signal(Signal::SIGURG)),
|
"URG" => Ok(TrapTarget::Signal(Signal::SIGURG)),
|
||||||
"XCPU" => Ok(TrapTarget::Signal(Signal::SIGXCPU)),
|
"XCPU" => Ok(TrapTarget::Signal(Signal::SIGXCPU)),
|
||||||
"XFSZ" => Ok(TrapTarget::Signal(Signal::SIGXFSZ)),
|
"XFSZ" => Ok(TrapTarget::Signal(Signal::SIGXFSZ)),
|
||||||
"VTALRM" => Ok(TrapTarget::Signal(Signal::SIGVTALRM)),
|
"VTALRM" => Ok(TrapTarget::Signal(Signal::SIGVTALRM)),
|
||||||
"PROF" => Ok(TrapTarget::Signal(Signal::SIGPROF)),
|
"PROF" => Ok(TrapTarget::Signal(Signal::SIGPROF)),
|
||||||
"WINCH" => Ok(TrapTarget::Signal(Signal::SIGWINCH)),
|
"WINCH" => Ok(TrapTarget::Signal(Signal::SIGWINCH)),
|
||||||
"IO" => Ok(TrapTarget::Signal(Signal::SIGIO)),
|
"IO" => Ok(TrapTarget::Signal(Signal::SIGIO)),
|
||||||
"PWR" => Ok(TrapTarget::Signal(Signal::SIGPWR)),
|
"PWR" => Ok(TrapTarget::Signal(Signal::SIGPWR)),
|
||||||
"SYS" => Ok(TrapTarget::Signal(Signal::SIGSYS)),
|
"SYS" => Ok(TrapTarget::Signal(Signal::SIGSYS)),
|
||||||
_ => {
|
_ => {
|
||||||
return Err(ShErr::simple(
|
return Err(ShErr::simple(
|
||||||
ShErrKind::ExecFail,
|
ShErrKind::ExecFail,
|
||||||
format!("invalid trap target '{}'", s),
|
format!("invalid trap target '{}'", s),
|
||||||
))
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for TrapTarget {
|
impl Display for TrapTarget {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
TrapTarget::Exit => write!(f, "EXIT"),
|
TrapTarget::Exit => write!(f, "EXIT"),
|
||||||
TrapTarget::Error => write!(f, "ERR"),
|
TrapTarget::Error => write!(f, "ERR"),
|
||||||
TrapTarget::Signal(s) => {
|
TrapTarget::Signal(s) => match s {
|
||||||
match s {
|
Signal::SIGHUP => write!(f, "HUP"),
|
||||||
Signal::SIGHUP => write!(f, "HUP"),
|
Signal::SIGINT => write!(f, "INT"),
|
||||||
Signal::SIGINT => write!(f, "INT"),
|
Signal::SIGQUIT => write!(f, "QUIT"),
|
||||||
Signal::SIGQUIT => write!(f, "QUIT"),
|
Signal::SIGILL => write!(f, "ILL"),
|
||||||
Signal::SIGILL => write!(f, "ILL"),
|
Signal::SIGTRAP => write!(f, "TRAP"),
|
||||||
Signal::SIGTRAP => write!(f, "TRAP"),
|
Signal::SIGABRT => write!(f, "ABRT"),
|
||||||
Signal::SIGABRT => write!(f, "ABRT"),
|
Signal::SIGBUS => write!(f, "BUS"),
|
||||||
Signal::SIGBUS => write!(f, "BUS"),
|
Signal::SIGFPE => write!(f, "FPE"),
|
||||||
Signal::SIGFPE => write!(f, "FPE"),
|
Signal::SIGKILL => write!(f, "KILL"),
|
||||||
Signal::SIGKILL => write!(f, "KILL"),
|
Signal::SIGUSR1 => write!(f, "USR1"),
|
||||||
Signal::SIGUSR1 => write!(f, "USR1"),
|
Signal::SIGSEGV => write!(f, "SEGV"),
|
||||||
Signal::SIGSEGV => write!(f, "SEGV"),
|
Signal::SIGUSR2 => write!(f, "USR2"),
|
||||||
Signal::SIGUSR2 => write!(f, "USR2"),
|
Signal::SIGPIPE => write!(f, "PIPE"),
|
||||||
Signal::SIGPIPE => write!(f, "PIPE"),
|
Signal::SIGALRM => write!(f, "ALRM"),
|
||||||
Signal::SIGALRM => write!(f, "ALRM"),
|
Signal::SIGTERM => write!(f, "TERM"),
|
||||||
Signal::SIGTERM => write!(f, "TERM"),
|
Signal::SIGSTKFLT => write!(f, "STKFLT"),
|
||||||
Signal::SIGSTKFLT => write!(f, "STKFLT"),
|
Signal::SIGCHLD => write!(f, "CHLD"),
|
||||||
Signal::SIGCHLD => write!(f, "CHLD"),
|
Signal::SIGCONT => write!(f, "CONT"),
|
||||||
Signal::SIGCONT => write!(f, "CONT"),
|
Signal::SIGSTOP => write!(f, "STOP"),
|
||||||
Signal::SIGSTOP => write!(f, "STOP"),
|
Signal::SIGTSTP => write!(f, "TSTP"),
|
||||||
Signal::SIGTSTP => write!(f, "TSTP"),
|
Signal::SIGTTIN => write!(f, "TTIN"),
|
||||||
Signal::SIGTTIN => write!(f, "TTIN"),
|
Signal::SIGTTOU => write!(f, "TTOU"),
|
||||||
Signal::SIGTTOU => write!(f, "TTOU"),
|
Signal::SIGURG => write!(f, "URG"),
|
||||||
Signal::SIGURG => write!(f, "URG"),
|
Signal::SIGXCPU => write!(f, "XCPU"),
|
||||||
Signal::SIGXCPU => write!(f, "XCPU"),
|
Signal::SIGXFSZ => write!(f, "XFSZ"),
|
||||||
Signal::SIGXFSZ => write!(f, "XFSZ"),
|
Signal::SIGVTALRM => write!(f, "VTALRM"),
|
||||||
Signal::SIGVTALRM => write!(f, "VTALRM"),
|
Signal::SIGPROF => write!(f, "PROF"),
|
||||||
Signal::SIGPROF => write!(f, "PROF"),
|
Signal::SIGWINCH => write!(f, "WINCH"),
|
||||||
Signal::SIGWINCH => write!(f, "WINCH"),
|
Signal::SIGIO => write!(f, "IO"),
|
||||||
Signal::SIGIO => write!(f, "IO"),
|
Signal::SIGPWR => write!(f, "PWR"),
|
||||||
Signal::SIGPWR => write!(f, "PWR"),
|
Signal::SIGSYS => write!(f, "SYS"),
|
||||||
Signal::SIGSYS => write!(f, "SYS"),
|
|
||||||
|
|
||||||
_ => {
|
_ => {
|
||||||
log::warn!("TrapTarget::fmt() : unrecognized signal {}", s);
|
log::warn!("TrapTarget::fmt() : unrecognized signal {}", s);
|
||||||
Err(std::fmt::Error)
|
Err(std::fmt::Error)
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn trap(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
|
pub fn trap(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
|
||||||
let span = node.get_span();
|
let span = node.get_span();
|
||||||
let NdRule::Command {
|
let NdRule::Command {
|
||||||
assignments: _,
|
assignments: _,
|
||||||
argv,
|
argv,
|
||||||
} = node.class
|
} = node.class
|
||||||
else {
|
else {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
};
|
};
|
||||||
|
|
||||||
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
|
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
|
||||||
|
|
||||||
if argv.is_empty() {
|
if argv.is_empty() {
|
||||||
let stdout = borrow_fd(STDOUT_FILENO);
|
let stdout = borrow_fd(STDOUT_FILENO);
|
||||||
|
|
||||||
return read_logic(|l| -> ShResult<()> {
|
return read_logic(|l| -> ShResult<()> {
|
||||||
for l in l.traps() {
|
for l in l.traps() {
|
||||||
let target = l.0;
|
let target = l.0;
|
||||||
let command = l.1;
|
let command = l.1;
|
||||||
write(stdout, format!("trap -- '{command}' {target}\n").as_bytes())?;
|
write(stdout, format!("trap -- '{command}' {target}\n").as_bytes())?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if argv.len() == 1 {
|
if argv.len() == 1 {
|
||||||
let stderr = borrow_fd(STDERR_FILENO);
|
let stderr = borrow_fd(STDERR_FILENO);
|
||||||
write(stderr, b"usage: trap <COMMAND> [SIGNAL...]\n")?;
|
write(stderr, b"usage: trap <COMMAND> [SIGNAL...]\n")?;
|
||||||
state::set_status(1);
|
state::set_status(1);
|
||||||
return Ok(())
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut args = argv.into_iter();
|
let mut args = argv.into_iter();
|
||||||
|
|
||||||
let command = args.next().unwrap().0;
|
let command = args.next().unwrap().0;
|
||||||
let mut targets = vec![];
|
let mut targets = vec![];
|
||||||
|
|
||||||
while let Some((arg, _)) = args.next() {
|
while let Some((arg, _)) = args.next() {
|
||||||
let target = arg.parse::<TrapTarget>()?;
|
let target = arg.parse::<TrapTarget>()?;
|
||||||
targets.push(target);
|
targets.push(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
for target in targets {
|
for target in targets {
|
||||||
if &command == "-" {
|
if &command == "-" {
|
||||||
write_logic(|l| l.remove_trap(target))
|
write_logic(|l| l.remove_trap(target))
|
||||||
} else {
|
} else {
|
||||||
write_logic(|l| l.insert_trap(target, command.clone()))
|
write_logic(|l| l.insert_trap(target, command.clone()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
state::set_status(0);
|
state::set_status(0);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,14 +37,32 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu
|
|||||||
else {
|
else {
|
||||||
unreachable!()
|
unreachable!()
|
||||||
};
|
};
|
||||||
let zolt_opts = [
|
let zolt_opts = [
|
||||||
OptSpec { opt: Opt::Long("dry-run".into()), takes_arg: false },
|
OptSpec {
|
||||||
OptSpec { opt: Opt::Long("confirm".into()), takes_arg: false },
|
opt: Opt::Long("dry-run".into()),
|
||||||
OptSpec { opt: Opt::Long("no-preserve-root".into()), takes_arg: false },
|
takes_arg: false,
|
||||||
OptSpec { opt: Opt::Short('r'), takes_arg: false },
|
},
|
||||||
OptSpec { opt: Opt::Short('f'), takes_arg: false },
|
OptSpec {
|
||||||
OptSpec { opt: Opt::Short('v'), takes_arg: false }
|
opt: Opt::Long("confirm".into()),
|
||||||
];
|
takes_arg: false,
|
||||||
|
},
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Long("no-preserve-root".into()),
|
||||||
|
takes_arg: false,
|
||||||
|
},
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('r'),
|
||||||
|
takes_arg: false,
|
||||||
|
},
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('f'),
|
||||||
|
takes_arg: false,
|
||||||
|
},
|
||||||
|
OptSpec {
|
||||||
|
opt: Opt::Short('v'),
|
||||||
|
takes_arg: false,
|
||||||
|
},
|
||||||
|
];
|
||||||
let mut flags = ZoltFlags::empty();
|
let mut flags = ZoltFlags::empty();
|
||||||
|
|
||||||
let (argv, opts) = get_opts_from_tokens(argv, &zolt_opts);
|
let (argv, opts) = get_opts_from_tokens(argv, &zolt_opts);
|
||||||
@@ -56,41 +74,40 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu
|
|||||||
"confirm" => flags |= ZoltFlags::CONFIRM,
|
"confirm" => flags |= ZoltFlags::CONFIRM,
|
||||||
"dry-run" => flags |= ZoltFlags::DRY,
|
"dry-run" => flags |= ZoltFlags::DRY,
|
||||||
_ => {
|
_ => {
|
||||||
return Err(ShErr::simple(
|
return Err(ShErr::simple(
|
||||||
ShErrKind::SyntaxErr,
|
ShErrKind::SyntaxErr,
|
||||||
format!("zoltraak: unrecognized option '{flag}'"),
|
format!("zoltraak: unrecognized option '{flag}'"),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Opt::Short(flag) => match flag {
|
Opt::Short(flag) => match flag {
|
||||||
'r' => flags |= ZoltFlags::RECURSIVE,
|
'r' => flags |= ZoltFlags::RECURSIVE,
|
||||||
'f' => flags |= ZoltFlags::FORCE,
|
'f' => flags |= ZoltFlags::FORCE,
|
||||||
'v' => flags |= ZoltFlags::VERBOSE,
|
'v' => flags |= ZoltFlags::VERBOSE,
|
||||||
_ => {
|
_ => {
|
||||||
return Err(ShErr::simple(
|
return Err(ShErr::simple(
|
||||||
ShErrKind::SyntaxErr,
|
ShErrKind::SyntaxErr,
|
||||||
format!("zoltraak: unrecognized option '{flag}'"),
|
format!("zoltraak: unrecognized option '{flag}'"),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Opt::LongWithArg(flag, _) => {
|
Opt::LongWithArg(flag, _) => {
|
||||||
return Err(ShErr::simple(
|
return Err(ShErr::simple(
|
||||||
ShErrKind::SyntaxErr,
|
ShErrKind::SyntaxErr,
|
||||||
format!("zoltraak: unrecognized option '{flag}'"),
|
format!("zoltraak: unrecognized option '{flag}'"),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
Opt::ShortWithArg(flag, _) => {
|
Opt::ShortWithArg(flag, _) => {
|
||||||
return Err(ShErr::simple(
|
return Err(ShErr::simple(
|
||||||
ShErrKind::SyntaxErr,
|
ShErrKind::SyntaxErr,
|
||||||
format!("zoltraak: unrecognized option '{flag}'"),
|
format!("zoltraak: unrecognized option '{flag}'"),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
|
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
|
||||||
|
|
||||||
|
|
||||||
for (arg, span) in argv {
|
for (arg, span) in argv {
|
||||||
if &arg == "/" && !flags.contains(ZoltFlags::NO_PRESERVE_ROOT) {
|
if &arg == "/" && !flags.contains(ZoltFlags::NO_PRESERVE_ROOT) {
|
||||||
return Err(
|
return Err(
|
||||||
@@ -109,7 +126,6 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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)]
|
#[derive(Clone, PartialEq, Eq, Debug)]
|
||||||
pub enum Opt {
|
pub enum Opt {
|
||||||
Long(String),
|
Long(String),
|
||||||
LongWithArg(String,String),
|
LongWithArg(String, String),
|
||||||
Short(char),
|
Short(char),
|
||||||
ShortWithArg(char,String),
|
ShortWithArg(char, String),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct OptSpec {
|
pub struct OptSpec {
|
||||||
pub opt: Opt,
|
pub opt: Opt,
|
||||||
pub takes_arg: bool,
|
pub takes_arg: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Opt {
|
impl Opt {
|
||||||
@@ -41,8 +41,8 @@ impl Display for Opt {
|
|||||||
match self {
|
match self {
|
||||||
Self::Long(opt) => write!(f, "--{}", opt),
|
Self::Long(opt) => write!(f, "--{}", opt),
|
||||||
Self::Short(opt) => write!(f, "-{}", opt),
|
Self::Short(opt) => write!(f, "-{}", opt),
|
||||||
Self::LongWithArg(opt, arg) => write!(f, "--{} {}", opt, arg),
|
Self::LongWithArg(opt, arg) => write!(f, "--{} {}", opt, arg),
|
||||||
Self::ShortWithArg(opt, arg) => write!(f, "-{} {}", opt, arg)
|
Self::ShortWithArg(opt, arg) => write!(f, "-{} {}", opt, arg),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -82,32 +82,33 @@ pub fn get_opts_from_tokens(tokens: Vec<Tk>, opt_specs: &[OptSpec]) -> (Vec<Tk>,
|
|||||||
if parsed_opts.is_empty() {
|
if parsed_opts.is_empty() {
|
||||||
non_opts.push(token)
|
non_opts.push(token)
|
||||||
} else {
|
} else {
|
||||||
for opt in parsed_opts {
|
for opt in parsed_opts {
|
||||||
let mut pushed = false;
|
let mut pushed = false;
|
||||||
for opt_spec in opt_specs {
|
for opt_spec in opt_specs {
|
||||||
if opt_spec.opt == opt {
|
if opt_spec.opt == opt {
|
||||||
if opt_spec.takes_arg {
|
if opt_spec.takes_arg {
|
||||||
let arg = tokens_iter.next()
|
let arg = tokens_iter
|
||||||
.map(|t| t.to_string())
|
.next()
|
||||||
.unwrap_or_default();
|
.map(|t| t.to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
let opt = match opt {
|
let opt = match opt {
|
||||||
Opt::Long(ref opt) => Opt::LongWithArg(opt.to_string(), arg),
|
Opt::Long(ref opt) => Opt::LongWithArg(opt.to_string(), arg),
|
||||||
Opt::Short(opt) => Opt::ShortWithArg(opt, arg),
|
Opt::Short(opt) => Opt::ShortWithArg(opt, arg),
|
||||||
_ => unreachable!(),
|
_ => unreachable!(),
|
||||||
};
|
};
|
||||||
opts.push(opt);
|
opts.push(opt);
|
||||||
pushed = true;
|
pushed = true;
|
||||||
} else {
|
} else {
|
||||||
opts.push(opt.clone());
|
opts.push(opt.clone());
|
||||||
pushed = true;
|
pushed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !pushed {
|
if !pushed {
|
||||||
non_opts.push(token.clone());
|
non_opts.push(token.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(non_opts, opts)
|
(non_opts, opts)
|
||||||
|
|||||||
10
src/jobs.rs
10
src/jobs.rs
@@ -2,7 +2,11 @@ use crate::{
|
|||||||
libsh::{
|
libsh::{
|
||||||
error::ShResult,
|
error::ShResult,
|
||||||
term::{Style, Styled},
|
term::{Style, Styled},
|
||||||
}, prelude::*, procio::{IoMode, borrow_fd}, signal::{disable_reaping, enable_reaping}, state::{self, set_status, read_jobs, write_jobs}
|
},
|
||||||
|
prelude::*,
|
||||||
|
procio::{IoMode, borrow_fd},
|
||||||
|
signal::{disable_reaping, enable_reaping},
|
||||||
|
state::{self, read_jobs, set_status, write_jobs},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const SIG_EXIT_OFFSET: i32 = 128;
|
pub const SIG_EXIT_OFFSET: i32 = 128;
|
||||||
@@ -685,7 +689,9 @@ pub fn wait_fg(job: Job) -> ShResult<()> {
|
|||||||
}
|
}
|
||||||
// If job wasn't stopped (moved to bg), clear the fg slot
|
// If job wasn't stopped (moved to bg), clear the fg slot
|
||||||
if !was_stopped {
|
if !was_stopped {
|
||||||
write_jobs(|j| { j.take_fg(); });
|
write_jobs(|j| {
|
||||||
|
j.take_fg();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
take_term()?;
|
take_term()?;
|
||||||
set_status(code);
|
set_status(code);
|
||||||
|
|||||||
@@ -408,12 +408,12 @@ pub enum ShErrKind {
|
|||||||
ReadlineIntr(String),
|
ReadlineIntr(String),
|
||||||
ReadlineErr,
|
ReadlineErr,
|
||||||
|
|
||||||
// Not really errors, more like internal signals
|
// Not really errors, more like internal signals
|
||||||
CleanExit(i32),
|
CleanExit(i32),
|
||||||
FuncReturn(i32),
|
FuncReturn(i32),
|
||||||
LoopContinue(i32),
|
LoopContinue(i32),
|
||||||
LoopBreak(i32),
|
LoopBreak(i32),
|
||||||
ClearReadline,
|
ClearReadline,
|
||||||
Null,
|
Null,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,7 +437,7 @@ impl Display for ShErrKind {
|
|||||||
Self::LoopBreak(_) => "",
|
Self::LoopBreak(_) => "",
|
||||||
Self::ReadlineIntr(_) => "",
|
Self::ReadlineIntr(_) => "",
|
||||||
Self::ReadlineErr => "Readline Error",
|
Self::ReadlineErr => "Readline Error",
|
||||||
Self::ClearReadline => "",
|
Self::ClearReadline => "",
|
||||||
Self::Null => "",
|
Self::Null => "",
|
||||||
};
|
};
|
||||||
write!(f, "{output}")
|
write!(f, "{output}")
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use termios::{LocalFlags, Termios};
|
use termios::{LocalFlags, Termios};
|
||||||
|
|
||||||
use crate::{prelude::*};
|
use crate::prelude::*;
|
||||||
///
|
///
|
||||||
/// The previous state of the terminal options.
|
/// The previous state of the terminal options.
|
||||||
///
|
///
|
||||||
@@ -33,44 +33,43 @@ pub(crate) static mut SAVED_TERMIOS: Option<Option<Termios>> = None;
|
|||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct TermiosGuard {
|
pub struct TermiosGuard {
|
||||||
saved_termios: Option<Termios>
|
saved_termios: Option<Termios>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TermiosGuard {
|
impl TermiosGuard {
|
||||||
pub fn new(new_termios: Termios) -> Self {
|
pub fn new(new_termios: Termios) -> Self {
|
||||||
let mut new = Self { saved_termios: None };
|
let mut new = Self {
|
||||||
|
saved_termios: None,
|
||||||
|
};
|
||||||
|
|
||||||
if isatty(std::io::stdin().as_raw_fd()).unwrap() {
|
if isatty(std::io::stdin().as_raw_fd()).unwrap() {
|
||||||
let current_termios = termios::tcgetattr(std::io::stdin()).unwrap();
|
let current_termios = termios::tcgetattr(std::io::stdin()).unwrap();
|
||||||
new.saved_termios = Some(current_termios);
|
new.saved_termios = Some(current_termios);
|
||||||
|
|
||||||
termios::tcsetattr(
|
termios::tcsetattr(
|
||||||
std::io::stdin(),
|
std::io::stdin(),
|
||||||
nix::sys::termios::SetArg::TCSANOW,
|
nix::sys::termios::SetArg::TCSANOW,
|
||||||
&new_termios,
|
&new_termios,
|
||||||
).unwrap();
|
)
|
||||||
}
|
.unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
new
|
new
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TermiosGuard {
|
impl Default for TermiosGuard {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let mut termios = termios::tcgetattr(std::io::stdin()).unwrap();
|
let mut termios = termios::tcgetattr(std::io::stdin()).unwrap();
|
||||||
termios.local_flags &= !LocalFlags::ECHOCTL;
|
termios.local_flags &= !LocalFlags::ECHOCTL;
|
||||||
Self::new(termios)
|
Self::new(termios)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for TermiosGuard {
|
impl Drop for TermiosGuard {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
if let Some(saved) = &self.saved_termios {
|
if let Some(saved) = &self.saved_termios {
|
||||||
termios::tcsetattr(
|
termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, saved).unwrap();
|
||||||
std::io::stdin(),
|
}
|
||||||
nix::sys::termios::SetArg::TCSANOW,
|
}
|
||||||
saved,
|
|
||||||
).unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -83,8 +83,7 @@ impl TkVecUtils<Tk> for Vec<Tk> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
fn debug_tokens(&self) {
|
fn debug_tokens(&self) {
|
||||||
for token in self {
|
for token in self {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
336
src/main.rs
336
src/main.rs
@@ -1,7 +1,7 @@
|
|||||||
#![allow(
|
#![allow(
|
||||||
clippy::derivable_impls,
|
clippy::derivable_impls,
|
||||||
clippy::tabs_in_doc_comments,
|
clippy::tabs_in_doc_comments,
|
||||||
clippy::while_let_on_iterator
|
clippy::while_let_on_iterator
|
||||||
)]
|
)]
|
||||||
pub mod builtin;
|
pub mod builtin;
|
||||||
pub mod expand;
|
pub mod expand;
|
||||||
@@ -22,10 +22,10 @@ use std::os::fd::BorrowedFd;
|
|||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
use std::sync::atomic::Ordering;
|
use std::sync::atomic::Ordering;
|
||||||
|
|
||||||
|
use nix::errno::Errno;
|
||||||
use nix::libc::STDIN_FILENO;
|
use nix::libc::STDIN_FILENO;
|
||||||
use nix::poll::{PollFd, PollFlags, PollTimeout, poll};
|
use nix::poll::{PollFd, PollFlags, PollTimeout, poll};
|
||||||
use nix::unistd::read;
|
use nix::unistd::read;
|
||||||
use nix::errno::Errno;
|
|
||||||
|
|
||||||
use crate::builtin::trap::TrapTarget;
|
use crate::builtin::trap::TrapTarget;
|
||||||
use crate::libsh::error::{ShErr, ShErrKind, ShResult};
|
use crate::libsh::error::{ShErr, ShErrKind, ShResult};
|
||||||
@@ -41,16 +41,16 @@ use state::{read_vars, write_vars};
|
|||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
struct FernArgs {
|
struct FernArgs {
|
||||||
script: Option<String>,
|
script: Option<String>,
|
||||||
|
|
||||||
#[arg(short)]
|
#[arg(short)]
|
||||||
command: Option<String>,
|
command: Option<String>,
|
||||||
|
|
||||||
#[arg(trailing_var_arg = true)]
|
#[arg(trailing_var_arg = true)]
|
||||||
script_args: Vec<String>,
|
script_args: Vec<String>,
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
version: bool,
|
version: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Force evaluation of lazily-initialized values early in shell startup.
|
/// Force evaluation of lazily-initialized values early in shell startup.
|
||||||
@@ -64,178 +64,192 @@ struct FernArgs {
|
|||||||
/// closure, which forces access to the variable table and causes its `LazyLock`
|
/// closure, which forces access to the variable table and causes its `LazyLock`
|
||||||
/// constructor to run.
|
/// constructor to run.
|
||||||
fn kickstart_lazy_evals() {
|
fn kickstart_lazy_evals() {
|
||||||
read_vars(|_| {});
|
read_vars(|_| {});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
kickstart_lazy_evals();
|
kickstart_lazy_evals();
|
||||||
let args = FernArgs::parse();
|
let args = FernArgs::parse();
|
||||||
if args.version {
|
if args.version {
|
||||||
println!("fern {}", env!("CARGO_PKG_VERSION"));
|
println!("fern {}", env!("CARGO_PKG_VERSION"));
|
||||||
return ExitCode::SUCCESS;
|
return ExitCode::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Err(e) = if let Some(path) = args.script {
|
if let Err(e) = if let Some(path) = args.script {
|
||||||
run_script(path, args.script_args)
|
run_script(path, args.script_args)
|
||||||
} else if let Some(cmd) = args.command {
|
} else if let Some(cmd) = args.command {
|
||||||
exec_input(cmd, None, false)
|
exec_input(cmd, None, false)
|
||||||
} else {
|
} else {
|
||||||
fern_interactive()
|
fern_interactive()
|
||||||
} {
|
} {
|
||||||
eprintln!("fern: {e}");
|
eprintln!("fern: {e}");
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Exit))
|
if let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Exit))
|
||||||
&& let Err(e) = exec_input(trap, None, false) {
|
&& let Err(e) = exec_input(trap, None, false)
|
||||||
eprintln!("fern: error running EXIT trap: {e}");
|
{
|
||||||
}
|
eprintln!("fern: error running EXIT trap: {e}");
|
||||||
|
}
|
||||||
|
|
||||||
ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8)
|
ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) -> ShResult<()> {
|
fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) -> ShResult<()> {
|
||||||
let path = path.as_ref();
|
let path = path.as_ref();
|
||||||
if !path.is_file() {
|
if !path.is_file() {
|
||||||
eprintln!("fern: Failed to open input file: {}", path.display());
|
eprintln!("fern: Failed to open input file: {}", path.display());
|
||||||
QUIT_CODE.store(1, Ordering::SeqCst);
|
QUIT_CODE.store(1, Ordering::SeqCst);
|
||||||
return Err(ShErr::simple(ShErrKind::CleanExit(1), "input file not found"));
|
return Err(ShErr::simple(
|
||||||
}
|
ShErrKind::CleanExit(1),
|
||||||
let Ok(input) = fs::read_to_string(path) else {
|
"input file not found",
|
||||||
eprintln!("fern: Failed to read input file: {}", path.display());
|
));
|
||||||
QUIT_CODE.store(1, Ordering::SeqCst);
|
}
|
||||||
return Err(ShErr::simple(ShErrKind::CleanExit(1), "failed to read input file"));
|
let Ok(input) = fs::read_to_string(path) else {
|
||||||
};
|
eprintln!("fern: Failed to read input file: {}", path.display());
|
||||||
|
QUIT_CODE.store(1, Ordering::SeqCst);
|
||||||
|
return Err(ShErr::simple(
|
||||||
|
ShErrKind::CleanExit(1),
|
||||||
|
"failed to read input file",
|
||||||
|
));
|
||||||
|
};
|
||||||
|
|
||||||
write_vars(|v| v.cur_scope_mut().bpush_arg(path.to_string_lossy().to_string()));
|
write_vars(|v| {
|
||||||
for arg in args {
|
v.cur_scope_mut()
|
||||||
write_vars(|v| v.cur_scope_mut().bpush_arg(arg))
|
.bpush_arg(path.to_string_lossy().to_string())
|
||||||
}
|
});
|
||||||
|
for arg in args {
|
||||||
|
write_vars(|v| v.cur_scope_mut().bpush_arg(arg))
|
||||||
|
}
|
||||||
|
|
||||||
exec_input(input, None, false)
|
exec_input(input, None, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fern_interactive() -> ShResult<()> {
|
fn fern_interactive() -> ShResult<()> {
|
||||||
let _raw_mode = raw_mode(); // sets raw mode, restores termios on drop
|
let _raw_mode = raw_mode(); // sets raw mode, restores termios on drop
|
||||||
sig_setup();
|
sig_setup();
|
||||||
|
|
||||||
if let Err(e) = source_rc() {
|
if let Err(e) = source_rc() {
|
||||||
eprintln!("{e}");
|
eprintln!("{e}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create readline instance with initial prompt
|
// Create readline instance with initial prompt
|
||||||
let mut readline = match FernVi::new(get_prompt().ok()) {
|
let mut readline = match FernVi::new(get_prompt().ok()) {
|
||||||
Ok(rl) => rl,
|
Ok(rl) => rl,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to initialize readline: {e}");
|
eprintln!("Failed to initialize readline: {e}");
|
||||||
QUIT_CODE.store(1, Ordering::SeqCst);
|
QUIT_CODE.store(1, Ordering::SeqCst);
|
||||||
return Err(ShErr::simple(ShErrKind::CleanExit(1), "readline initialization failed"));
|
return Err(ShErr::simple(
|
||||||
}
|
ShErrKind::CleanExit(1),
|
||||||
};
|
"readline initialization failed",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Main poll loop
|
// Main poll loop
|
||||||
loop {
|
loop {
|
||||||
// Handle any pending signals
|
// Handle any pending signals
|
||||||
while signals_pending() {
|
while signals_pending() {
|
||||||
if let Err(e) = check_signals() {
|
if let Err(e) = check_signals() {
|
||||||
match e.kind() {
|
match e.kind() {
|
||||||
ShErrKind::ClearReadline => {
|
ShErrKind::ClearReadline => {
|
||||||
// Ctrl+C - clear current input and show new prompt
|
// Ctrl+C - clear current input and show new prompt
|
||||||
readline.reset(get_prompt().ok());
|
readline.reset(get_prompt().ok());
|
||||||
}
|
}
|
||||||
ShErrKind::CleanExit(code) => {
|
ShErrKind::CleanExit(code) => {
|
||||||
QUIT_CODE.store(*code, Ordering::SeqCst);
|
QUIT_CODE.store(*code, Ordering::SeqCst);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
_ => eprintln!("{e}"),
|
_ => eprintln!("{e}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
readline.print_line()?;
|
readline.print_line()?;
|
||||||
|
|
||||||
// Poll for stdin input
|
// Poll for stdin input
|
||||||
let mut fds = [PollFd::new(
|
let mut fds = [PollFd::new(
|
||||||
unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) },
|
unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) },
|
||||||
PollFlags::POLLIN,
|
PollFlags::POLLIN,
|
||||||
)];
|
)];
|
||||||
|
|
||||||
match poll(&mut fds, PollTimeout::MAX) {
|
match poll(&mut fds, PollTimeout::MAX) {
|
||||||
Ok(_) => {}
|
Ok(_) => {}
|
||||||
Err(Errno::EINTR) => {
|
Err(Errno::EINTR) => {
|
||||||
// Interrupted by signal, loop back to handle it
|
// Interrupted by signal, loop back to handle it
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("poll error: {e}");
|
eprintln!("poll error: {e}");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if stdin has data
|
// Check if stdin has data
|
||||||
if fds[0].revents().is_some_and(|r| r.contains(PollFlags::POLLIN)) {
|
if fds[0]
|
||||||
let mut buffer = [0u8; 1024];
|
.revents()
|
||||||
match read(STDIN_FILENO, &mut buffer) {
|
.is_some_and(|r| r.contains(PollFlags::POLLIN))
|
||||||
Ok(0) => {
|
{
|
||||||
// EOF
|
let mut buffer = [0u8; 1024];
|
||||||
break;
|
match read(STDIN_FILENO, &mut buffer) {
|
||||||
}
|
Ok(0) => {
|
||||||
Ok(n) => {
|
// EOF
|
||||||
readline.feed_bytes(&buffer[..n]);
|
break;
|
||||||
}
|
}
|
||||||
Err(Errno::EINTR) => {
|
Ok(n) => {
|
||||||
// Interrupted, continue to handle signals
|
readline.feed_bytes(&buffer[..n]);
|
||||||
continue;
|
}
|
||||||
}
|
Err(Errno::EINTR) => {
|
||||||
Err(e) => {
|
// Interrupted, continue to handle signals
|
||||||
eprintln!("read error: {e}");
|
continue;
|
||||||
break;
|
}
|
||||||
}
|
Err(e) => {
|
||||||
}
|
eprintln!("read error: {e}");
|
||||||
}
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Process any available input
|
// Process any available input
|
||||||
match readline.process_input() {
|
match readline.process_input() {
|
||||||
Ok(ReadlineEvent::Line(input)) => {
|
Ok(ReadlineEvent::Line(input)) => {
|
||||||
let start = Instant::now();
|
let start = Instant::now();
|
||||||
write_meta(|m| m.start_timer());
|
write_meta(|m| m.start_timer());
|
||||||
if let Err(e) = exec_input(input, None, true) {
|
if let Err(e) = exec_input(input, None, true) {
|
||||||
match e.kind() {
|
match e.kind() {
|
||||||
ShErrKind::CleanExit(code) => {
|
ShErrKind::CleanExit(code) => {
|
||||||
QUIT_CODE.store(*code, Ordering::SeqCst);
|
QUIT_CODE.store(*code, Ordering::SeqCst);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
_ => eprintln!("{e}"),
|
_ => eprintln!("{e}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
let command_run_time = start.elapsed();
|
let command_run_time = start.elapsed();
|
||||||
log::info!("Command executed in {:.2?}", command_run_time);
|
log::info!("Command executed in {:.2?}", command_run_time);
|
||||||
write_meta(|m| m.stop_timer());
|
write_meta(|m| m.stop_timer());
|
||||||
|
|
||||||
// Reset for next command with fresh prompt
|
// Reset for next command with fresh prompt
|
||||||
readline.reset(get_prompt().ok());
|
readline.reset(get_prompt().ok());
|
||||||
let real_end = start.elapsed();
|
let real_end = start.elapsed();
|
||||||
log::info!("Total round trip time: {:.2?}", real_end);
|
log::info!("Total round trip time: {:.2?}", real_end);
|
||||||
}
|
}
|
||||||
Ok(ReadlineEvent::Eof) => {
|
Ok(ReadlineEvent::Eof) => {
|
||||||
// Ctrl+D on empty line
|
// Ctrl+D on empty line
|
||||||
QUIT_CODE.store(0, Ordering::SeqCst);
|
QUIT_CODE.store(0, Ordering::SeqCst);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
Ok(ReadlineEvent::Pending) => {
|
Ok(ReadlineEvent::Pending) => {
|
||||||
// No complete input yet, keep polling
|
// No complete input yet, keep polling
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => match e.kind() {
|
||||||
match e.kind() {
|
ShErrKind::CleanExit(code) => {
|
||||||
ShErrKind::CleanExit(code) => {
|
QUIT_CODE.store(*code, Ordering::SeqCst);
|
||||||
QUIT_CODE.store(*code, Ordering::SeqCst);
|
return Ok(());
|
||||||
return Ok(());
|
}
|
||||||
}
|
_ => eprintln!("{e}"),
|
||||||
_ => eprintln!("{e}"),
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
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> {
|
pub fn range(&self) -> Range<usize> {
|
||||||
self.range.clone()
|
self.range.clone()
|
||||||
}
|
}
|
||||||
/// With great power comes great responsibility
|
/// With great power comes great responsibility
|
||||||
/// Only use this in the most dire of circumstances
|
/// Only use this in the most dire of circumstances
|
||||||
pub fn set_range(&mut self, range: Range<usize>) {
|
pub fn set_range(&mut self, range: Range<usize>) {
|
||||||
self.range = range;
|
self.range = range;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Allows simple access to the underlying range wrapped by the span
|
/// Allows simple access to the underlying range wrapped by the span
|
||||||
@@ -324,13 +324,14 @@ impl LexStream {
|
|||||||
let can_be_subshell = chars.peek() == Some(&'(');
|
let can_be_subshell = chars.peek() == Some(&'(');
|
||||||
|
|
||||||
if self.flags.contains(LexFlags::IN_CASE)
|
if self.flags.contains(LexFlags::IN_CASE)
|
||||||
&& let Some(count) = case_pat_lookahead(chars.clone()) {
|
&& let Some(count) = case_pat_lookahead(chars.clone())
|
||||||
pos += count;
|
{
|
||||||
let casepat_tk = self.get_token(self.cursor..pos, TkRule::CasePattern);
|
pos += count;
|
||||||
self.cursor = pos;
|
let casepat_tk = self.get_token(self.cursor..pos, TkRule::CasePattern);
|
||||||
self.set_next_is_cmd(true);
|
self.cursor = pos;
|
||||||
return Ok(casepat_tk);
|
self.set_next_is_cmd(true);
|
||||||
}
|
return Ok(casepat_tk);
|
||||||
|
}
|
||||||
|
|
||||||
while let Some(ch) = chars.next() {
|
while let Some(ch) = chars.next() {
|
||||||
match ch {
|
match ch {
|
||||||
@@ -740,7 +741,10 @@ impl Iterator for LexStream {
|
|||||||
}
|
}
|
||||||
self.get_token(ch_idx..self.cursor, TkRule::Sep)
|
self.get_token(ch_idx..self.cursor, TkRule::Sep)
|
||||||
}
|
}
|
||||||
'#' if !self.flags.contains(LexFlags::INTERACTIVE) || crate::state::read_shopts(|s| s.core.interactive_comments) => {
|
'#'
|
||||||
|
if !self.flags.contains(LexFlags::INTERACTIVE)
|
||||||
|
|| crate::state::read_shopts(|s| s.core.interactive_comments) =>
|
||||||
|
{
|
||||||
let ch_idx = self.cursor;
|
let ch_idx = self.cursor;
|
||||||
self.cursor += 1;
|
self.cursor += 1;
|
||||||
|
|
||||||
|
|||||||
@@ -1160,7 +1160,7 @@ impl ParseStream {
|
|||||||
|
|
||||||
let cond_node: CondNode;
|
let cond_node: CondNode;
|
||||||
let mut node_tks = vec![];
|
let mut node_tks = vec![];
|
||||||
let mut redirs = vec![];
|
let mut redirs = vec![];
|
||||||
|
|
||||||
if (!self.check_keyword("while") && !self.check_keyword("until")) || !self.next_tk_is_some() {
|
if (!self.check_keyword("while") && !self.check_keyword("until")) || !self.next_tk_is_some() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@@ -1238,18 +1238,18 @@ impl ParseStream {
|
|||||||
fn parse_pipeln(&mut self) -> ShResult<Option<Node>> {
|
fn parse_pipeln(&mut self) -> ShResult<Option<Node>> {
|
||||||
let mut cmds = vec![];
|
let mut cmds = vec![];
|
||||||
let mut node_tks = vec![];
|
let mut node_tks = vec![];
|
||||||
let mut flags = NdFlags::empty();
|
let mut flags = NdFlags::empty();
|
||||||
|
|
||||||
while let Some(cmd) = self.parse_block(false)? {
|
while let Some(cmd) = self.parse_block(false)? {
|
||||||
let is_punctuated = node_is_punctuated(&cmd.tokens);
|
let is_punctuated = node_is_punctuated(&cmd.tokens);
|
||||||
node_tks.append(&mut cmd.tokens.clone());
|
node_tks.append(&mut cmd.tokens.clone());
|
||||||
cmds.push(cmd);
|
cmds.push(cmd);
|
||||||
if *self.next_tk_class() == TkRule::Bg {
|
if *self.next_tk_class() == TkRule::Bg {
|
||||||
let tk = self.next_tk().unwrap();
|
let tk = self.next_tk().unwrap();
|
||||||
node_tks.push(tk.clone());
|
node_tks.push(tk.clone());
|
||||||
flags |= NdFlags::BACKGROUND;
|
flags |= NdFlags::BACKGROUND;
|
||||||
break;
|
break;
|
||||||
} else if *self.next_tk_class() != TkRule::Pipe || is_punctuated {
|
} else if *self.next_tk_class() != TkRule::Pipe || is_punctuated {
|
||||||
break;
|
break;
|
||||||
} else if let Some(pipe) = self.next_tk() {
|
} else if let Some(pipe) = self.next_tk() {
|
||||||
node_tks.push(pipe)
|
node_tks.push(pipe)
|
||||||
@@ -1278,7 +1278,7 @@ impl ParseStream {
|
|||||||
let mut node_tks = vec![];
|
let mut node_tks = vec![];
|
||||||
let mut redirs = vec![];
|
let mut redirs = vec![];
|
||||||
let mut argv = vec![];
|
let mut argv = vec![];
|
||||||
let mut flags = NdFlags::empty();
|
let mut flags = NdFlags::empty();
|
||||||
let mut assignments = vec![];
|
let mut assignments = vec![];
|
||||||
|
|
||||||
while let Some(prefix_tk) = tk_iter.next() {
|
while let Some(prefix_tk) = tk_iter.next() {
|
||||||
@@ -1315,27 +1315,32 @@ impl ParseStream {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if argv.is_empty() {
|
if argv.is_empty() {
|
||||||
if assignments.is_empty() {
|
if assignments.is_empty() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
} else {
|
} else {
|
||||||
// If we have assignments but no command word,
|
// If we have assignments but no command word,
|
||||||
// return the assignment-only command without parsing more tokens
|
// return the assignment-only command without parsing more tokens
|
||||||
self.commit(node_tks.len());
|
self.commit(node_tks.len());
|
||||||
return Ok(Some(Node {
|
return Ok(Some(Node {
|
||||||
class: NdRule::Command { assignments, argv },
|
class: NdRule::Command { assignments, argv },
|
||||||
tokens: node_tks,
|
tokens: node_tks,
|
||||||
flags,
|
flags,
|
||||||
redirs,
|
redirs,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
while let Some(tk) = tk_iter.next() {
|
while let Some(tk) = tk_iter.next() {
|
||||||
if *self.next_tk_class() == TkRule::Bg {
|
if *self.next_tk_class() == TkRule::Bg {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
match tk.class {
|
match tk.class {
|
||||||
TkRule::EOI | TkRule::Pipe | TkRule::And | TkRule::BraceGrpEnd | TkRule::Or | TkRule::Bg => break,
|
TkRule::EOI
|
||||||
|
| TkRule::Pipe
|
||||||
|
| TkRule::And
|
||||||
|
| TkRule::BraceGrpEnd
|
||||||
|
| TkRule::Or
|
||||||
|
| TkRule::Bg => break,
|
||||||
TkRule::Sep => {
|
TkRule::Sep => {
|
||||||
node_tks.push(tk.clone());
|
node_tks.push(tk.clone());
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -19,17 +19,17 @@ pub use std::os::unix::io::{AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd,
|
|||||||
pub use bitflags::bitflags;
|
pub use bitflags::bitflags;
|
||||||
pub use nix::{
|
pub use nix::{
|
||||||
errno::Errno,
|
errno::Errno,
|
||||||
fcntl::{open, OFlag},
|
fcntl::{OFlag, open},
|
||||||
libc::{self, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO},
|
libc::{self, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO},
|
||||||
sys::{
|
sys::{
|
||||||
signal::{self, kill, killpg, pthread_sigmask, signal, SigHandler, SigSet, SigmaskHow, Signal},
|
signal::{self, SigHandler, SigSet, SigmaskHow, Signal, kill, killpg, pthread_sigmask, signal},
|
||||||
stat::Mode,
|
stat::Mode,
|
||||||
termios::{self},
|
termios::{self},
|
||||||
wait::{waitpid, WaitPidFlag as WtFlag, WaitStatus as WtStat},
|
wait::{WaitPidFlag as WtFlag, WaitStatus as WtStat, waitpid},
|
||||||
},
|
},
|
||||||
unistd::{
|
unistd::{
|
||||||
close, dup, dup2, execvpe, fork, getpgid, getpgrp, isatty, pipe, read, setpgid, tcgetpgrp,
|
ForkResult, Pid, close, dup, dup2, execvpe, fork, getpgid, getpgrp, isatty, pipe, read,
|
||||||
tcsetpgrp, write, ForkResult, Pid,
|
setpgid, tcgetpgrp, tcsetpgrp, write,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,13 @@ use std::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
expand::Expander, libsh::{
|
expand::Expander,
|
||||||
|
libsh::{
|
||||||
error::{ShErr, ShErrKind, ShResult},
|
error::{ShErr, ShErrKind, ShResult},
|
||||||
utils::RedirVecUtils,
|
utils::RedirVecUtils,
|
||||||
}, parse::{Redir, RedirType, get_redir_file}, prelude::*
|
},
|
||||||
|
parse::{Redir, RedirType, get_redir_file},
|
||||||
|
prelude::*,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Credit to fish-shell for many of the implementation ideas present in this
|
// Credit to fish-shell for many of the implementation ideas present in this
|
||||||
@@ -17,11 +20,11 @@ use crate::{
|
|||||||
pub enum IoMode {
|
pub enum IoMode {
|
||||||
Fd {
|
Fd {
|
||||||
tgt_fd: RawFd,
|
tgt_fd: RawFd,
|
||||||
src_fd: RawFd, // Just the fd number - dup2 will handle it at execution time
|
src_fd: RawFd, // Just the fd number - dup2 will handle it at execution time
|
||||||
},
|
},
|
||||||
OpenedFile {
|
OpenedFile {
|
||||||
tgt_fd: RawFd,
|
tgt_fd: RawFd,
|
||||||
file: Arc<OwnedFd>, // Owns the opened file descriptor
|
file: Arc<OwnedFd>, // Owns the opened file descriptor
|
||||||
},
|
},
|
||||||
File {
|
File {
|
||||||
tgt_fd: RawFd,
|
tgt_fd: RawFd,
|
||||||
@@ -70,17 +73,12 @@ impl IoMode {
|
|||||||
}
|
}
|
||||||
pub fn open_file(mut self) -> ShResult<Self> {
|
pub fn open_file(mut self) -> ShResult<Self> {
|
||||||
if let IoMode::File { tgt_fd, path, mode } = self {
|
if let IoMode::File { tgt_fd, path, mode } = self {
|
||||||
let path_raw = path
|
let path_raw = path.as_os_str().to_str().unwrap_or_default().to_string();
|
||||||
.as_os_str()
|
|
||||||
.to_str()
|
|
||||||
.unwrap_or_default()
|
|
||||||
.to_string();
|
|
||||||
|
|
||||||
let expanded_path = Expander::from_raw(&path_raw)?
|
let expanded_path = Expander::from_raw(&path_raw)?.expand()?.join(" "); // should just be one string, will have to find some way to handle a return of
|
||||||
.expand()?
|
// multiple
|
||||||
.join(" "); // should just be one string, will have to find some way to handle a return of multiple
|
|
||||||
|
|
||||||
let expanded_pathbuf = PathBuf::from(expanded_path);
|
let expanded_pathbuf = PathBuf::from(expanded_path);
|
||||||
|
|
||||||
let file = get_redir_file(mode, expanded_pathbuf)?;
|
let file = get_redir_file(mode, expanded_pathbuf)?;
|
||||||
self = IoMode::OpenedFile {
|
self = IoMode::OpenedFile {
|
||||||
@@ -155,9 +153,9 @@ impl<R: Read> IoBuf<R> {
|
|||||||
|
|
||||||
pub struct RedirGuard(IoFrame);
|
pub struct RedirGuard(IoFrame);
|
||||||
impl Drop for RedirGuard {
|
impl Drop for RedirGuard {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.0.restore().ok();
|
self.0.restore().ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A struct wrapping three fildescs representing `stdin`, `stdout`, and
|
/// A struct wrapping three fildescs representing `stdin`, `stdout`, and
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
pub mod readline;
|
pub mod readline;
|
||||||
pub mod statusline;
|
pub mod statusline;
|
||||||
|
|
||||||
|
|
||||||
use crate::{expand::expand_prompt, libsh::error::ShResult, prelude::*};
|
use crate::{expand::expand_prompt, libsh::error::ShResult, prelude::*};
|
||||||
|
|
||||||
/// Initialize the line editor
|
/// Initialize the line editor
|
||||||
@@ -16,7 +15,7 @@ pub fn get_prompt() -> ShResult<String> {
|
|||||||
"\\e[0m\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m ";
|
"\\e[0m\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m ";
|
||||||
return expand_prompt(default);
|
return expand_prompt(default);
|
||||||
};
|
};
|
||||||
let sanitized = format!("\\e[0m{prompt}");
|
let sanitized = format!("\\e[0m{prompt}");
|
||||||
|
|
||||||
expand_prompt(&sanitized)
|
expand_prompt(&sanitized)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,445 +1,467 @@
|
|||||||
use std::{env, os::unix::fs::PermissionsExt, path::PathBuf, sync::Arc};
|
use std::{env, os::unix::fs::PermissionsExt, path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
use crate::{builtin::BUILTINS, libsh::error::{ShErr, ShErrKind, ShResult}, parse::lex::{self, LexFlags, Tk, TkFlags}, prompt::readline::{Marker, annotate_input, annotate_input_recursive, get_insertions, markers::{self, is_marker}}, state::{read_logic, read_vars}};
|
use crate::{
|
||||||
|
builtin::BUILTINS,
|
||||||
|
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||||
|
parse::lex::{self, LexFlags, Tk, TkFlags},
|
||||||
|
prompt::readline::{
|
||||||
|
Marker, annotate_input, annotate_input_recursive, get_insertions,
|
||||||
|
markers::{self, is_marker},
|
||||||
|
},
|
||||||
|
state::{read_logic, read_vars},
|
||||||
|
};
|
||||||
|
|
||||||
pub enum CompCtx {
|
pub enum CompCtx {
|
||||||
CmdName,
|
CmdName,
|
||||||
FileName
|
FileName,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum CompResult {
|
pub enum CompResult {
|
||||||
NoMatch,
|
NoMatch,
|
||||||
Single {
|
Single { result: String },
|
||||||
result: String
|
Many { candidates: Vec<String> },
|
||||||
},
|
|
||||||
Many {
|
|
||||||
candidates: Vec<String>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CompResult {
|
impl CompResult {
|
||||||
pub fn from_candidates(candidates: Vec<String>) -> Self {
|
pub fn from_candidates(candidates: Vec<String>) -> Self {
|
||||||
if candidates.is_empty() {
|
if candidates.is_empty() {
|
||||||
Self::NoMatch
|
Self::NoMatch
|
||||||
} else if candidates.len() == 1 {
|
} else if candidates.len() == 1 {
|
||||||
Self::Single { result: candidates[0].clone() }
|
Self::Single {
|
||||||
} else {
|
result: candidates[0].clone(),
|
||||||
Self::Many { candidates }
|
}
|
||||||
}
|
} else {
|
||||||
}
|
Self::Many { candidates }
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Completer {
|
pub struct Completer {
|
||||||
pub candidates: Vec<String>,
|
pub candidates: Vec<String>,
|
||||||
pub selected_idx: usize,
|
pub selected_idx: usize,
|
||||||
pub original_input: String,
|
pub original_input: String,
|
||||||
pub token_span: (usize, usize),
|
pub token_span: (usize, usize),
|
||||||
pub active: bool,
|
pub active: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Completer {
|
impl Completer {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
candidates: vec![],
|
candidates: vec![],
|
||||||
selected_idx: 0,
|
selected_idx: 0,
|
||||||
original_input: String::new(),
|
original_input: String::new(),
|
||||||
token_span: (0, 0),
|
token_span: (0, 0),
|
||||||
active: false,
|
active: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn slice_line(line: &str, cursor_pos: usize) -> (&str, &str) {
|
pub fn slice_line(line: &str, cursor_pos: usize) -> (&str, &str) {
|
||||||
let (before_cursor, after_cursor) = line.split_at(cursor_pos);
|
let (before_cursor, after_cursor) = line.split_at(cursor_pos);
|
||||||
(before_cursor, after_cursor)
|
(before_cursor, after_cursor)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_completion_context(&self, line: &str, cursor_pos: usize) -> (Vec<Marker>, usize) {
|
pub fn get_completion_context(&self, line: &str, cursor_pos: usize) -> (Vec<Marker>, usize) {
|
||||||
let annotated = annotate_input_recursive(line);
|
let annotated = annotate_input_recursive(line);
|
||||||
log::debug!("Annotated input for completion context: {:?}", annotated);
|
let mut ctx = vec![markers::NULL];
|
||||||
let mut ctx = vec![markers::NULL];
|
let mut last_priority = 0;
|
||||||
let mut last_priority = 0;
|
let mut ctx_start = 0;
|
||||||
let mut ctx_start = 0;
|
let mut pos = 0;
|
||||||
let mut pos = 0;
|
|
||||||
|
|
||||||
for ch in annotated.chars() {
|
for ch in annotated.chars() {
|
||||||
match ch {
|
match ch {
|
||||||
_ if is_marker(ch) => {
|
_ if is_marker(ch) => match ch {
|
||||||
match ch {
|
markers::COMMAND | markers::BUILTIN => {
|
||||||
markers::COMMAND | markers::BUILTIN => {
|
if last_priority < 2 {
|
||||||
log::debug!("Found command marker at position {}", pos);
|
if last_priority > 0 {
|
||||||
if last_priority < 2 {
|
ctx.pop();
|
||||||
if last_priority > 0 {
|
}
|
||||||
ctx.pop();
|
ctx_start = pos;
|
||||||
}
|
last_priority = 2;
|
||||||
ctx_start = pos;
|
ctx.push(markers::COMMAND);
|
||||||
last_priority = 2;
|
}
|
||||||
ctx.push(markers::COMMAND);
|
}
|
||||||
}
|
markers::VAR_SUB => {
|
||||||
}
|
if last_priority < 3 {
|
||||||
markers::VAR_SUB => {
|
if last_priority > 0 {
|
||||||
log::debug!("Found variable substitution marker at position {}", pos);
|
ctx.pop();
|
||||||
if last_priority < 3 {
|
}
|
||||||
if last_priority > 0 {
|
ctx_start = pos;
|
||||||
ctx.pop();
|
last_priority = 3;
|
||||||
}
|
ctx.push(markers::VAR_SUB);
|
||||||
ctx_start = pos;
|
}
|
||||||
last_priority = 3;
|
}
|
||||||
ctx.push(markers::VAR_SUB);
|
markers::ARG | markers::ASSIGNMENT => {
|
||||||
}
|
if last_priority < 1 {
|
||||||
}
|
ctx_start = pos;
|
||||||
markers::ARG | markers::ASSIGNMENT => {
|
ctx.push(markers::ARG);
|
||||||
log::debug!("Found argument/assignment marker at position {}", pos);
|
}
|
||||||
if last_priority < 1 {
|
}
|
||||||
ctx_start = pos;
|
_ => {}
|
||||||
ctx.push(markers::ARG);
|
},
|
||||||
}
|
_ => {
|
||||||
}
|
last_priority = 0; // reset priority on normal characters
|
||||||
_ => {}
|
pos += 1; // we hit a normal character, advance our position
|
||||||
}
|
if pos >= cursor_pos {
|
||||||
}
|
break;
|
||||||
_ => {
|
}
|
||||||
last_priority = 0; // reset priority on normal characters
|
}
|
||||||
pos += 1; // we hit a normal character, advance our position
|
}
|
||||||
if pos >= cursor_pos {
|
}
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(ctx, ctx_start)
|
(ctx, ctx_start)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(&mut self) {
|
pub fn reset(&mut self) {
|
||||||
self.candidates.clear();
|
self.candidates.clear();
|
||||||
self.selected_idx = 0;
|
self.selected_idx = 0;
|
||||||
self.original_input.clear();
|
self.original_input.clear();
|
||||||
self.token_span = (0, 0);
|
self.token_span = (0, 0);
|
||||||
self.active = false;
|
self.active = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn complete(&mut self, line: String, cursor_pos: usize, direction: i32) -> ShResult<Option<String>> {
|
pub fn complete(
|
||||||
if self.active {
|
&mut self,
|
||||||
Ok(Some(self.cycle_completion(direction)))
|
line: String,
|
||||||
} else {
|
cursor_pos: usize,
|
||||||
self.start_completion(line, cursor_pos)
|
direction: i32,
|
||||||
}
|
) -> ShResult<Option<String>> {
|
||||||
}
|
if self.active {
|
||||||
|
Ok(Some(self.cycle_completion(direction)))
|
||||||
|
} else {
|
||||||
|
self.start_completion(line, cursor_pos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn selected_candidate(&self) -> Option<String> {
|
pub fn selected_candidate(&self) -> Option<String> {
|
||||||
self.candidates.get(self.selected_idx).cloned()
|
self.candidates.get(self.selected_idx).cloned()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cycle_completion(&mut self, direction: i32) -> String {
|
pub fn cycle_completion(&mut self, direction: i32) -> String {
|
||||||
if self.candidates.is_empty() {
|
if self.candidates.is_empty() {
|
||||||
return self.original_input.clone();
|
return self.original_input.clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
let len = self.candidates.len();
|
let len = self.candidates.len();
|
||||||
self.selected_idx = (self.selected_idx as i32 + direction).rem_euclid(len as i32) as usize;
|
self.selected_idx = (self.selected_idx as i32 + direction).rem_euclid(len as i32) as usize;
|
||||||
|
|
||||||
self.get_completed_line()
|
self.get_completed_line()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn start_completion(&mut self, line: String, cursor_pos: usize) -> ShResult<Option<String>> {
|
pub fn start_completion(&mut self, line: String, cursor_pos: usize) -> ShResult<Option<String>> {
|
||||||
let result = self.get_candidates(line.clone(), cursor_pos)?;
|
let result = self.get_candidates(line.clone(), cursor_pos)?;
|
||||||
match result {
|
match result {
|
||||||
CompResult::Many { candidates } => {
|
CompResult::Many { candidates } => {
|
||||||
self.candidates = candidates.clone();
|
self.candidates = candidates.clone();
|
||||||
self.selected_idx = 0;
|
self.selected_idx = 0;
|
||||||
self.original_input = line;
|
self.original_input = line;
|
||||||
self.active = true;
|
self.active = true;
|
||||||
|
|
||||||
Ok(Some(self.get_completed_line()))
|
Ok(Some(self.get_completed_line()))
|
||||||
}
|
}
|
||||||
CompResult::Single { result } => {
|
CompResult::Single { result } => {
|
||||||
self.candidates = vec![result.clone()];
|
self.candidates = vec![result.clone()];
|
||||||
self.selected_idx = 0;
|
self.selected_idx = 0;
|
||||||
self.original_input = line;
|
self.original_input = line;
|
||||||
self.active = false;
|
self.active = false;
|
||||||
|
|
||||||
Ok(Some(self.get_completed_line()))
|
Ok(Some(self.get_completed_line()))
|
||||||
}
|
}
|
||||||
CompResult::NoMatch => Ok(None)
|
CompResult::NoMatch => Ok(None),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
pub fn extract_var_name(text: &str) -> Option<(String, usize, usize)> {
|
||||||
}
|
let mut chars = text.chars().peekable();
|
||||||
|
let mut name = String::new();
|
||||||
|
let mut reading_name = false;
|
||||||
|
let mut pos = 0;
|
||||||
|
let mut name_start = 0;
|
||||||
|
let mut name_end = 0;
|
||||||
|
|
||||||
pub fn extract_var_name(text: &str) -> Option<(String,usize,usize)> {
|
while let Some(ch) = chars.next() {
|
||||||
let mut chars = text.chars().peekable();
|
match ch {
|
||||||
let mut name = String::new();
|
'$' => {
|
||||||
let mut reading_name = false;
|
if chars.peek() == Some(&'{') {
|
||||||
let mut pos = 0;
|
continue;
|
||||||
let mut name_start = 0;
|
}
|
||||||
let mut name_end = 0;
|
|
||||||
|
|
||||||
while let Some(ch) = chars.next() {
|
reading_name = true;
|
||||||
match ch {
|
name_start = pos + 1; // Start after the '$'
|
||||||
'$' => {
|
}
|
||||||
if chars.peek() == Some(&'{') {
|
'{' if !reading_name => {
|
||||||
continue;
|
reading_name = true;
|
||||||
}
|
name_start = pos + 1;
|
||||||
|
}
|
||||||
|
ch if ch.is_alphanumeric() || ch == '_' => {
|
||||||
|
if reading_name {
|
||||||
|
name.push(ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if reading_name {
|
||||||
|
name_end = pos; // End before the non-alphanumeric character
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
|
||||||
reading_name = true;
|
if !reading_name {
|
||||||
name_start = pos + 1; // Start after the '$'
|
return None;
|
||||||
}
|
}
|
||||||
'{' if !reading_name => {
|
|
||||||
reading_name = true;
|
|
||||||
name_start = pos + 1;
|
|
||||||
}
|
|
||||||
ch if ch.is_alphanumeric() || ch == '_' => {
|
|
||||||
if reading_name {
|
|
||||||
name.push(ch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
if reading_name {
|
|
||||||
name_end = pos; // End before the non-alphanumeric character
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
pos += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if !reading_name {
|
if name_end == 0 {
|
||||||
return None;
|
name_end = pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
if name_end == 0 {
|
Some((name, name_start, name_end))
|
||||||
name_end = pos;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Some((name, name_start, name_end))
|
pub fn get_completed_line(&self) -> String {
|
||||||
}
|
if self.candidates.is_empty() {
|
||||||
|
return self.original_input.clone();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_completed_line(&self) -> String {
|
let selected = &self.candidates[self.selected_idx];
|
||||||
if self.candidates.is_empty() {
|
let (start, end) = self.token_span;
|
||||||
return self.original_input.clone();
|
format!(
|
||||||
}
|
"{}{}{}",
|
||||||
|
&self.original_input[..start],
|
||||||
|
selected,
|
||||||
|
&self.original_input[end..]
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
let selected = &self.candidates[self.selected_idx];
|
pub fn get_candidates(&mut self, line: String, cursor_pos: usize) -> ShResult<CompResult> {
|
||||||
let (start, end) = self.token_span;
|
let source = Arc::new(line.clone());
|
||||||
format!("{}{}{}", &self.original_input[..start], selected, &self.original_input[end..])
|
let tokens =
|
||||||
}
|
lex::LexStream::new(source, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>()?;
|
||||||
|
|
||||||
pub fn get_candidates(&mut self, line: String, cursor_pos: usize) -> ShResult<CompResult> {
|
let Some(mut cur_token) = tokens.into_iter().find(|tk| {
|
||||||
let source = Arc::new(line.clone());
|
let start = tk.span.start;
|
||||||
let tokens = lex::LexStream::new(source, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>()?;
|
let end = tk.span.end;
|
||||||
|
(start..=end).contains(&cursor_pos)
|
||||||
|
}) else {
|
||||||
|
let candidates = Self::complete_filename("./"); // Default to filename completion if no token is found
|
||||||
|
let end_pos = line.len();
|
||||||
|
self.token_span = (end_pos, end_pos);
|
||||||
|
return Ok(CompResult::from_candidates(candidates));
|
||||||
|
};
|
||||||
|
|
||||||
let Some(mut cur_token) = tokens.into_iter().find(|tk| {
|
self.token_span = (cur_token.span.start, cur_token.span.end);
|
||||||
let start = tk.span.start;
|
|
||||||
let end = tk.span.end;
|
|
||||||
(start..=end).contains(&cursor_pos)
|
|
||||||
}) else {
|
|
||||||
log::debug!("No token found at cursor position");
|
|
||||||
let candidates = Self::complete_filename("./"); // Default to filename completion if no token is found
|
|
||||||
let end_pos = line.len();
|
|
||||||
self.token_span = (end_pos, end_pos);
|
|
||||||
return Ok(CompResult::from_candidates(candidates));
|
|
||||||
};
|
|
||||||
|
|
||||||
self.token_span = (cur_token.span.start, cur_token.span.end);
|
// Look for marker at the START of what we're completing, not at cursor
|
||||||
|
let (mut ctx, token_start) = self.get_completion_context(&line, cursor_pos);
|
||||||
|
self.token_span.0 = token_start; // Update start of token span based on context
|
||||||
|
cur_token
|
||||||
|
.span
|
||||||
|
.set_range(self.token_span.0..self.token_span.1); // Update token span to reflect context
|
||||||
|
|
||||||
|
// If token contains '=', only complete after the '='
|
||||||
|
let token_str = cur_token.span.as_str();
|
||||||
|
if let Some(eq_pos) = token_str.rfind('=') {
|
||||||
|
// Adjust span to only replace the part after '='
|
||||||
|
self.token_span.0 = cur_token.span.start + eq_pos + 1;
|
||||||
|
}
|
||||||
|
|
||||||
// Look for marker at the START of what we're completing, not at cursor
|
if ctx.last().is_some_and(|m| *m == markers::VAR_SUB) {
|
||||||
let (mut ctx, token_start) = self.get_completion_context(&line, cursor_pos);
|
let var_sub = &cur_token.as_str();
|
||||||
self.token_span.0 = token_start; // Update start of token span based on context
|
if let Some((var_name, start, end)) = Self::extract_var_name(var_sub) {
|
||||||
cur_token.span.set_range(self.token_span.0..self.token_span.1); // Update token span to reflect context
|
if read_vars(|v| v.get_var(&var_name)).is_empty() {
|
||||||
|
// if we are here, we have a variable substitution that isn't complete
|
||||||
|
// so let's try to complete it
|
||||||
|
let ret: ShResult<CompResult> = read_vars(|v| {
|
||||||
|
let var_matches = v
|
||||||
|
.flatten_vars()
|
||||||
|
.keys()
|
||||||
|
.filter(|k| k.starts_with(&var_name) && *k != &var_name)
|
||||||
|
.map(|k| k.to_string())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
// If token contains '=', only complete after the '='
|
if !var_matches.is_empty() {
|
||||||
let token_str = cur_token.span.as_str();
|
let name_start = cur_token.span.start + start;
|
||||||
if let Some(eq_pos) = token_str.rfind('=') {
|
let name_end = cur_token.span.start + end;
|
||||||
// Adjust span to only replace the part after '='
|
self.token_span = (name_start, name_end);
|
||||||
self.token_span.0 = cur_token.span.start + eq_pos + 1;
|
cur_token
|
||||||
}
|
.span
|
||||||
|
.set_range(self.token_span.0..self.token_span.1);
|
||||||
|
Ok(CompResult::from_candidates(var_matches))
|
||||||
|
} else {
|
||||||
|
Ok(CompResult::NoMatch)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
if ctx.last().is_some_and(|m| *m == markers::VAR_SUB) {
|
if !matches!(ret, Ok(CompResult::NoMatch)) {
|
||||||
let var_sub = &cur_token.as_str();
|
return ret;
|
||||||
if let Some((var_name,start,end)) = Self::extract_var_name(var_sub) {
|
} else {
|
||||||
log::debug!("Extracted variable name for completion: {}", var_name);
|
ctx.pop();
|
||||||
if read_vars(|v| v.get_var(&var_name)).is_empty() {
|
}
|
||||||
// if we are here, we have a variable substitution that isn't complete
|
} else {
|
||||||
// so let's try to complete it
|
ctx.pop();
|
||||||
let ret: ShResult<CompResult> = read_vars(|v| {
|
}
|
||||||
let var_matches = v.flatten_vars()
|
}
|
||||||
.keys()
|
}
|
||||||
.filter(|k| k.starts_with(&var_name) && *k != &var_name)
|
|
||||||
.map(|k| k.to_string())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
if !var_matches.is_empty() {
|
let raw_tk = cur_token.as_str().to_string();
|
||||||
let name_start = cur_token.span.start + start;
|
let expanded_tk = cur_token.expand()?;
|
||||||
let name_end = cur_token.span.start + end;
|
let expanded_words = expanded_tk.get_words().into_iter().collect::<Vec<_>>();
|
||||||
self.token_span = (name_start, name_end);
|
let expanded = expanded_words.join("\\ ");
|
||||||
cur_token.span.set_range(self.token_span.0..self.token_span.1);
|
|
||||||
Ok(CompResult::from_candidates(var_matches))
|
|
||||||
} else {
|
|
||||||
Ok(CompResult::NoMatch)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if !matches!(ret, Ok(CompResult::NoMatch)) {
|
let mut candidates = match ctx.pop() {
|
||||||
return ret;
|
Some(markers::COMMAND) => Self::complete_command(&expanded)?,
|
||||||
} else {
|
Some(markers::ARG) => Self::complete_filename(&expanded),
|
||||||
ctx.pop();
|
Some(_) => {
|
||||||
}
|
return Ok(CompResult::NoMatch);
|
||||||
} else {
|
}
|
||||||
ctx.pop();
|
None => {
|
||||||
}
|
return Ok(CompResult::NoMatch);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
let raw_tk = cur_token.as_str().to_string();
|
// Now we are just going to graft the completed text
|
||||||
let expanded_tk = cur_token.expand()?;
|
// onto the original token. This prevents something like
|
||||||
let expanded_words = expanded_tk.get_words().into_iter().collect::<Vec<_>>();
|
// $SOME_PATH/
|
||||||
let expanded = expanded_words.join("\\ ");
|
// from being completed into
|
||||||
|
// /path/to/some_path/file.txt
|
||||||
|
// and instead returns
|
||||||
|
// $SOME_PATH/file.txt
|
||||||
|
candidates = candidates
|
||||||
|
.into_iter()
|
||||||
|
.map(|c| match c.strip_prefix(&expanded) {
|
||||||
|
Some(suffix) => format!("{raw_tk}{suffix}"),
|
||||||
|
None => c,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
let mut candidates = match ctx.pop() {
|
let limit = crate::state::read_shopts(|s| s.prompt.comp_limit);
|
||||||
Some(markers::COMMAND) => {
|
candidates.truncate(limit);
|
||||||
log::debug!("Completing command: {}", &expanded);
|
|
||||||
Self::complete_command(&expanded)?
|
|
||||||
}
|
|
||||||
Some(markers::ARG) => {
|
|
||||||
log::debug!("Completing filename: {}", &expanded);
|
|
||||||
Self::complete_filename(&expanded)
|
|
||||||
}
|
|
||||||
Some(m) => {
|
|
||||||
log::warn!("Unknown marker {:?} in completion context", m);
|
|
||||||
return Ok(CompResult::NoMatch);
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
log::warn!("No marker found in completion context");
|
|
||||||
return Ok(CompResult::NoMatch);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Now we are just going to graft the completed text
|
Ok(CompResult::from_candidates(candidates))
|
||||||
// onto the original token. This prevents something like
|
}
|
||||||
// $SOME_PATH/
|
|
||||||
// from being completed into
|
|
||||||
// /path/to/some_path/file.txt
|
|
||||||
// and instead returns
|
|
||||||
// $SOME_PATH/file.txt
|
|
||||||
candidates = candidates.into_iter()
|
|
||||||
.map(|c| match c.strip_prefix(&expanded) {
|
|
||||||
Some(suffix) => format!("{raw_tk}{suffix}"),
|
|
||||||
None => c
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let limit = crate::state::read_shopts(|s| s.prompt.comp_limit);
|
fn complete_command(start: &str) -> ShResult<Vec<String>> {
|
||||||
candidates.truncate(limit);
|
let mut candidates = vec![];
|
||||||
|
|
||||||
Ok(CompResult::from_candidates(candidates))
|
let path = env::var("PATH").unwrap_or_default();
|
||||||
}
|
let paths = path.split(':').map(PathBuf::from).collect::<Vec<_>>();
|
||||||
|
for path in paths {
|
||||||
|
// Skip directories that don't exist (common in PATH)
|
||||||
|
let Ok(entries) = std::fs::read_dir(path) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
for entry in entries {
|
||||||
|
let Ok(entry) = entry else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Ok(meta) = entry.metadata() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
|
||||||
fn complete_command(start: &str) -> ShResult<Vec<String>> {
|
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||||
let mut candidates = vec![];
|
|
||||||
|
|
||||||
let path = env::var("PATH").unwrap_or_default();
|
if meta.is_file()
|
||||||
let paths = path.split(':').map(PathBuf::from).collect::<Vec<_>>();
|
&& (meta.permissions().mode() & 0o111) != 0
|
||||||
for path in paths {
|
&& file_name.starts_with(start)
|
||||||
// Skip directories that don't exist (common in PATH)
|
{
|
||||||
let Ok(entries) = std::fs::read_dir(path) else { continue; };
|
candidates.push(file_name);
|
||||||
for entry in entries {
|
}
|
||||||
let Ok(entry) = entry else { continue; };
|
}
|
||||||
let Ok(meta) = entry.metadata() else { continue; };
|
}
|
||||||
|
|
||||||
let file_name = entry.file_name().to_string_lossy().to_string();
|
let builtin_candidates = BUILTINS
|
||||||
|
.iter()
|
||||||
|
.filter(|b| b.starts_with(start))
|
||||||
|
.map(|s| s.to_string());
|
||||||
|
|
||||||
if meta.is_file()
|
candidates.extend(builtin_candidates);
|
||||||
&& (meta.permissions().mode() & 0o111) != 0
|
|
||||||
&& file_name.starts_with(start) {
|
|
||||||
candidates.push(file_name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let builtin_candidates = BUILTINS
|
read_logic(|l| {
|
||||||
.iter()
|
let func_table = l.funcs();
|
||||||
.filter(|b| b.starts_with(start))
|
let matches = func_table
|
||||||
.map(|s| s.to_string());
|
.keys()
|
||||||
|
.filter(|k| k.starts_with(start))
|
||||||
|
.map(|k| k.to_string());
|
||||||
|
|
||||||
candidates.extend(builtin_candidates);
|
candidates.extend(matches);
|
||||||
|
|
||||||
read_logic(|l| {
|
let aliases = l.aliases();
|
||||||
let func_table = l.funcs();
|
let matches = aliases
|
||||||
let matches = func_table
|
.keys()
|
||||||
.keys()
|
.filter(|k| k.starts_with(start))
|
||||||
.filter(|k| k.starts_with(start))
|
.map(|k| k.to_string());
|
||||||
.map(|k| k.to_string());
|
|
||||||
|
|
||||||
candidates.extend(matches);
|
candidates.extend(matches);
|
||||||
|
});
|
||||||
|
|
||||||
let aliases = l.aliases();
|
// Deduplicate (same command may appear in multiple PATH dirs)
|
||||||
let matches = aliases
|
candidates.sort();
|
||||||
.keys()
|
candidates.dedup();
|
||||||
.filter(|k| k.starts_with(start))
|
|
||||||
.map(|k| k.to_string());
|
|
||||||
|
|
||||||
candidates.extend(matches);
|
Ok(candidates)
|
||||||
});
|
}
|
||||||
|
|
||||||
// Deduplicate (same command may appear in multiple PATH dirs)
|
fn complete_filename(start: &str) -> Vec<String> {
|
||||||
candidates.sort();
|
let mut candidates = vec![];
|
||||||
candidates.dedup();
|
let has_dotslash = start.starts_with("./");
|
||||||
|
|
||||||
Ok(candidates)
|
// Split path into directory and filename parts
|
||||||
}
|
// Use "." if start is empty (e.g., after "foo=")
|
||||||
|
let path = PathBuf::from(if start.is_empty() { "." } else { start });
|
||||||
|
let (dir, prefix) = if start.ends_with('/') || start.is_empty() {
|
||||||
|
// Completing inside a directory: "src/" → dir="src/", prefix=""
|
||||||
|
(path, "")
|
||||||
|
} else if let Some(parent) = path.parent()
|
||||||
|
&& !parent.as_os_str().is_empty()
|
||||||
|
{
|
||||||
|
// Has directory component: "src/ma" → dir="src", prefix="ma"
|
||||||
|
(
|
||||||
|
parent.to_path_buf(),
|
||||||
|
path.file_name().unwrap().to_str().unwrap_or(""),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// No directory: "fil" → dir=".", prefix="fil"
|
||||||
|
(PathBuf::from("."), start)
|
||||||
|
};
|
||||||
|
|
||||||
fn complete_filename(start: &str) -> Vec<String> {
|
let Ok(entries) = std::fs::read_dir(&dir) else {
|
||||||
let mut candidates = vec![];
|
return candidates;
|
||||||
|
};
|
||||||
|
|
||||||
// If completing after '=', only use the part after it
|
for entry in entries.flatten() {
|
||||||
let start = if let Some(eq_pos) = start.rfind('=') {
|
let file_name = entry.file_name();
|
||||||
&start[eq_pos + 1..]
|
let file_str = file_name.to_string_lossy();
|
||||||
} else {
|
|
||||||
start
|
|
||||||
};
|
|
||||||
|
|
||||||
// Split path into directory and filename parts
|
// Skip hidden files unless explicitly requested
|
||||||
// Use "." if start is empty (e.g., after "foo=")
|
if !prefix.starts_with('.') && file_str.starts_with('.') {
|
||||||
let path = PathBuf::from(if start.is_empty() { "." } else { start });
|
continue;
|
||||||
let (dir, prefix) = if start.ends_with('/') || start.is_empty() {
|
}
|
||||||
// Completing inside a directory: "src/" → dir="src/", prefix=""
|
|
||||||
(path, "")
|
|
||||||
} else if let Some(parent) = path.parent()
|
|
||||||
&& !parent.as_os_str().is_empty() {
|
|
||||||
// Has directory component: "src/ma" → dir="src", prefix="ma"
|
|
||||||
(parent.to_path_buf(), path.file_name().unwrap().to_str().unwrap_or(""))
|
|
||||||
} else {
|
|
||||||
// No directory: "fil" → dir=".", prefix="fil"
|
|
||||||
(PathBuf::from("."), start)
|
|
||||||
};
|
|
||||||
|
|
||||||
let Ok(entries) = std::fs::read_dir(&dir) else {
|
if file_str.starts_with(prefix) {
|
||||||
return candidates;
|
// Reconstruct full path
|
||||||
};
|
let mut full_path = dir.join(&file_name);
|
||||||
|
|
||||||
for entry in entries.flatten() {
|
// Add trailing slash for directories
|
||||||
let file_name = entry.file_name();
|
if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
|
||||||
let file_str = file_name.to_string_lossy();
|
full_path.push(""); // adds trailing /
|
||||||
|
}
|
||||||
|
|
||||||
// Skip hidden files unless explicitly requested
|
let mut path_raw = full_path.to_string_lossy().to_string();
|
||||||
if !prefix.starts_with('.') && file_str.starts_with('.') {
|
if path_raw.starts_with("./") && !has_dotslash {
|
||||||
continue;
|
path_raw = path_raw.trim_start_matches("./").to_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
if file_str.starts_with(prefix) {
|
candidates.push(path_raw);
|
||||||
// Reconstruct full path
|
}
|
||||||
let mut full_path = dir.join(&file_name);
|
}
|
||||||
|
|
||||||
// Add trailing slash for directories
|
candidates.sort();
|
||||||
if entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false) {
|
candidates
|
||||||
full_path.push(""); // adds trailing /
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
candidates.push(full_path.to_string_lossy().to_string());
|
impl Default for Completer {
|
||||||
}
|
fn default() -> Self {
|
||||||
}
|
Self::new()
|
||||||
|
}
|
||||||
candidates.sort();
|
|
||||||
candidates
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,318 +1,395 @@
|
|||||||
use std::{env, os::unix::fs::PermissionsExt, path::{Path, PathBuf}};
|
use std::{
|
||||||
|
env,
|
||||||
|
os::unix::fs::PermissionsExt,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
};
|
||||||
|
|
||||||
use crate::{libsh::term::{Style, StyleSet, Styled}, prompt::readline::{annotate_input, markers}, state::{read_logic, read_shopts}};
|
use crate::{
|
||||||
|
libsh::term::{Style, StyleSet, Styled},
|
||||||
|
prompt::readline::{annotate_input, markers},
|
||||||
|
state::{read_logic, read_shopts},
|
||||||
|
};
|
||||||
|
|
||||||
/// Syntax highlighter for shell input using Unicode marker-based annotation
|
/// Syntax highlighter for shell input using Unicode marker-based annotation
|
||||||
///
|
///
|
||||||
/// The highlighter processes annotated input strings containing invisible Unicode markers
|
/// The highlighter processes annotated input strings containing invisible
|
||||||
/// (U+FDD0-U+FDEF range) that indicate syntax elements. It generates ANSI escape codes
|
/// Unicode markers (U+FDD0-U+FDEF range) that indicate syntax elements. It
|
||||||
/// for terminal display while maintaining a style stack for proper color restoration
|
/// generates ANSI escape codes for terminal display while maintaining a style
|
||||||
/// in nested constructs (e.g., variables inside strings inside command substitutions).
|
/// stack for proper color restoration in nested constructs (e.g., variables
|
||||||
|
/// inside strings inside command substitutions).
|
||||||
pub struct Highlighter {
|
pub struct Highlighter {
|
||||||
input: String,
|
input: String,
|
||||||
output: String,
|
output: String,
|
||||||
style_stack: Vec<StyleSet>,
|
style_stack: Vec<StyleSet>,
|
||||||
last_was_reset: bool,
|
last_was_reset: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Highlighter {
|
impl Highlighter {
|
||||||
/// Creates a new highlighter with empty buffers and reset state
|
/// Creates a new highlighter with empty buffers and reset state
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
input: String::new(),
|
input: String::new(),
|
||||||
output: String::new(),
|
output: String::new(),
|
||||||
style_stack: Vec::new(),
|
style_stack: Vec::new(),
|
||||||
last_was_reset: true, // start as true so we don't emit a leading reset
|
last_was_reset: true, // start as true so we don't emit a leading reset
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Loads raw input text and annotates it with syntax markers
|
/// Loads raw input text and annotates it with syntax markers
|
||||||
///
|
///
|
||||||
/// The input is passed through the annotator which inserts Unicode markers
|
/// The input is passed through the annotator which inserts Unicode markers
|
||||||
/// indicating token types and sub-token constructs (strings, variables, etc.)
|
/// indicating token types and sub-token constructs (strings, variables, etc.)
|
||||||
pub fn load_input(&mut self, input: &str) {
|
pub fn load_input(&mut self, input: &str) {
|
||||||
let input = annotate_input(input);
|
let input = annotate_input(input);
|
||||||
self.input = input;
|
self.input = input;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Processes the annotated input and generates ANSI-styled output
|
/// Processes the annotated input and generates ANSI-styled output
|
||||||
///
|
///
|
||||||
/// Walks through the input character by character, interpreting markers and
|
/// Walks through the input character by character, interpreting markers and
|
||||||
/// applying appropriate styles. Nested constructs (command substitutions,
|
/// applying appropriate styles. Nested constructs (command substitutions,
|
||||||
/// subshells, strings) are handled recursively with proper style restoration.
|
/// subshells, strings) are handled recursively with proper style restoration.
|
||||||
pub fn highlight(&mut self) {
|
pub fn highlight(&mut self) {
|
||||||
let input = self.input.clone();
|
let input = self.input.clone();
|
||||||
let mut input_chars = input.chars().peekable();
|
let mut input_chars = input.chars().peekable();
|
||||||
while let Some(ch) = input_chars.next() {
|
while let Some(ch) = input_chars.next() {
|
||||||
match ch {
|
match ch {
|
||||||
markers::STRING_DQ_END |
|
markers::STRING_DQ_END
|
||||||
markers::STRING_SQ_END |
|
| markers::STRING_SQ_END
|
||||||
markers::VAR_SUB_END |
|
| markers::VAR_SUB_END
|
||||||
markers::CMD_SUB_END |
|
| markers::CMD_SUB_END
|
||||||
markers::PROC_SUB_END |
|
| markers::PROC_SUB_END
|
||||||
markers::SUBSH_END => self.pop_style(),
|
| markers::SUBSH_END => self.pop_style(),
|
||||||
|
|
||||||
markers::CMD_SEP |
|
markers::CMD_SEP | markers::RESET => self.clear_styles(),
|
||||||
markers::RESET => self.clear_styles(),
|
|
||||||
|
|
||||||
|
markers::STRING_DQ | markers::STRING_SQ | markers::KEYWORD => {
|
||||||
|
self.push_style(Style::Yellow)
|
||||||
|
}
|
||||||
|
markers::BUILTIN => self.push_style(Style::Green),
|
||||||
|
markers::CASE_PAT => self.push_style(Style::Blue),
|
||||||
|
|
||||||
markers::STRING_DQ |
|
markers::COMMENT => self.push_style(Style::BrightBlack),
|
||||||
markers::STRING_SQ |
|
|
||||||
markers::KEYWORD => self.push_style(Style::Yellow),
|
|
||||||
markers::BUILTIN => self.push_style(Style::Green),
|
|
||||||
markers::CASE_PAT => self.push_style(Style::Blue),
|
|
||||||
markers::ARG => self.push_style(Style::White),
|
|
||||||
markers::COMMENT => self.push_style(Style::BrightBlack),
|
|
||||||
|
|
||||||
markers::GLOB => self.push_style(Style::Blue),
|
markers::GLOB => self.push_style(Style::Blue),
|
||||||
|
|
||||||
markers::REDIRECT |
|
markers::REDIRECT | markers::OPERATOR => self.push_style(Style::Magenta | Style::Bold),
|
||||||
markers::OPERATOR => self.push_style(Style::Magenta | Style::Bold),
|
|
||||||
|
|
||||||
markers::ASSIGNMENT => {
|
markers::ASSIGNMENT => {
|
||||||
let mut var_name = String::new();
|
let mut var_name = String::new();
|
||||||
|
|
||||||
while let Some(ch) = input_chars.peek() {
|
while let Some(ch) = input_chars.peek() {
|
||||||
if ch == &'=' {
|
if ch == &'=' {
|
||||||
input_chars.next(); // consume the '='
|
input_chars.next(); // consume the '='
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
match *ch {
|
match *ch {
|
||||||
markers::RESET => break,
|
markers::RESET => break,
|
||||||
_ => {
|
_ => {
|
||||||
var_name.push(*ch);
|
var_name.push(*ch);
|
||||||
input_chars.next();
|
input_chars.next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.output.push_str(&var_name);
|
self.output.push_str(&var_name);
|
||||||
self.push_style(Style::Blue);
|
self.push_style(Style::Blue);
|
||||||
self.output.push('=');
|
self.output.push('=');
|
||||||
self.pop_style();
|
self.pop_style();
|
||||||
}
|
}
|
||||||
|
|
||||||
markers::COMMAND => {
|
markers::ARG => {
|
||||||
let mut cmd_name = String::new();
|
let mut arg = String::new();
|
||||||
while let Some(ch) = input_chars.peek() {
|
let mut chars_clone = input_chars.clone();
|
||||||
if *ch == markers::RESET {
|
while let Some(ch) = chars_clone.next() {
|
||||||
break;
|
if ch == markers::RESET {
|
||||||
}
|
break;
|
||||||
cmd_name.push(*ch);
|
}
|
||||||
input_chars.next();
|
arg.push(ch);
|
||||||
}
|
}
|
||||||
let style = if Self::is_valid(&cmd_name) {
|
|
||||||
Style::Green.into()
|
|
||||||
} else {
|
|
||||||
Style::Red | Style::Bold
|
|
||||||
};
|
|
||||||
self.push_style(style);
|
|
||||||
self.output.push_str(&cmd_name);
|
|
||||||
self.last_was_reset = false;
|
|
||||||
}
|
|
||||||
markers::CMD_SUB | markers::SUBSH | markers::PROC_SUB => {
|
|
||||||
let mut inner = String::new();
|
|
||||||
let mut incomplete = true;
|
|
||||||
let end_marker = match ch {
|
|
||||||
markers::CMD_SUB => markers::CMD_SUB_END,
|
|
||||||
markers::SUBSH => markers::SUBSH_END,
|
|
||||||
markers::PROC_SUB => markers::PROC_SUB_END,
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
while let Some(ch) = input_chars.peek() {
|
|
||||||
if *ch == end_marker {
|
|
||||||
incomplete = false;
|
|
||||||
input_chars.next(); // consume the end marker
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
inner.push(*ch);
|
|
||||||
input_chars.next();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine prefix from content (handles both <( and >( for proc subs)
|
let style = if Self::is_filename(&arg) {
|
||||||
let prefix = match ch {
|
Style::White | Style::Underline
|
||||||
markers::CMD_SUB => "$(",
|
} else {
|
||||||
markers::SUBSH => "(",
|
Style::White.into()
|
||||||
markers::PROC_SUB => {
|
};
|
||||||
if inner.starts_with("<(") { "<(" }
|
|
||||||
else if inner.starts_with(">(") { ">(" }
|
|
||||||
else { "<(" } // fallback
|
|
||||||
}
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let inner_content = if incomplete {
|
self.push_style(style);
|
||||||
inner
|
self.last_was_reset = false;
|
||||||
.strip_prefix(prefix)
|
}
|
||||||
.unwrap_or(&inner)
|
|
||||||
} else {
|
|
||||||
inner
|
|
||||||
.strip_prefix(prefix)
|
|
||||||
.and_then(|s| s.strip_suffix(")"))
|
|
||||||
.unwrap_or(&inner)
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut recursive_highlighter = Self::new();
|
markers::COMMAND => {
|
||||||
recursive_highlighter.load_input(inner_content);
|
let mut cmd_name = String::new();
|
||||||
recursive_highlighter.highlight();
|
let mut chars_clone = input_chars.clone();
|
||||||
self.push_style(Style::Blue);
|
while let Some(ch) = chars_clone.next() {
|
||||||
self.output.push_str(prefix);
|
if ch == markers::RESET {
|
||||||
self.pop_style();
|
break;
|
||||||
self.output.push_str(&recursive_highlighter.take());
|
}
|
||||||
if !incomplete {
|
cmd_name.push(ch);
|
||||||
self.push_style(Style::Blue);
|
}
|
||||||
self.output.push(')');
|
let style = if Self::is_valid(&cmd_name) {
|
||||||
self.pop_style();
|
Style::Green.into()
|
||||||
}
|
} else {
|
||||||
self.last_was_reset = false;
|
Style::Red | Style::Bold
|
||||||
}
|
};
|
||||||
markers::VAR_SUB => {
|
self.push_style(style);
|
||||||
let mut var_sub = String::new();
|
self.last_was_reset = false;
|
||||||
while let Some(ch) = input_chars.peek() {
|
}
|
||||||
if *ch == markers::VAR_SUB_END {
|
markers::CMD_SUB | markers::SUBSH | markers::PROC_SUB => {
|
||||||
input_chars.next(); // consume the end marker
|
let mut inner = String::new();
|
||||||
break;
|
let mut incomplete = true;
|
||||||
} else if markers::is_marker(*ch) {
|
let end_marker = match ch {
|
||||||
input_chars.next(); // skip the marker
|
markers::CMD_SUB => markers::CMD_SUB_END,
|
||||||
continue;
|
markers::SUBSH => markers::SUBSH_END,
|
||||||
}
|
markers::PROC_SUB => markers::PROC_SUB_END,
|
||||||
var_sub.push(*ch);
|
_ => unreachable!(),
|
||||||
input_chars.next();
|
};
|
||||||
}
|
while let Some(ch) = input_chars.peek() {
|
||||||
let style = Style::Cyan;
|
if *ch == end_marker {
|
||||||
self.push_style(style);
|
incomplete = false;
|
||||||
self.output.push_str(&var_sub);
|
input_chars.next(); // consume the end marker
|
||||||
self.pop_style();
|
break;
|
||||||
}
|
}
|
||||||
_ => {
|
inner.push(*ch);
|
||||||
if markers::is_marker(ch) {
|
input_chars.next();
|
||||||
} else {
|
}
|
||||||
self.output.push(ch);
|
|
||||||
self.last_was_reset = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Extracts the highlighted output and resets the highlighter state
|
// Determine prefix from content (handles both <( and >( for proc subs)
|
||||||
///
|
let prefix = match ch {
|
||||||
/// Clears the input buffer, style stack, and returns the generated output
|
markers::CMD_SUB => "$(",
|
||||||
/// containing ANSI escape codes. The highlighter is ready for reuse after this.
|
markers::SUBSH => "(",
|
||||||
pub fn take(&mut self) -> String {
|
markers::PROC_SUB => {
|
||||||
self.input.clear();
|
if inner.starts_with("<(") {
|
||||||
self.clear_styles();
|
"<("
|
||||||
std::mem::take(&mut self.output)
|
} else if inner.starts_with(">(") {
|
||||||
}
|
">("
|
||||||
|
} else {
|
||||||
|
"<("
|
||||||
|
} // fallback
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
};
|
||||||
|
|
||||||
/// Checks if a command name is valid (exists in PATH, is a function, or is an alias)
|
let inner_content = if incomplete {
|
||||||
///
|
inner.strip_prefix(prefix).unwrap_or(&inner)
|
||||||
/// Searches:
|
} else {
|
||||||
/// 1. Current directory if command is a path
|
inner
|
||||||
/// 2. All directories in PATH environment variable
|
.strip_prefix(prefix)
|
||||||
/// 3. Shell functions and aliases in the current shell state
|
.and_then(|s| s.strip_suffix(")"))
|
||||||
fn is_valid(command: &str) -> bool {
|
.unwrap_or(&inner)
|
||||||
let path = env::var("PATH").unwrap_or_default();
|
};
|
||||||
let paths = path.split(':');
|
|
||||||
let cmd_path = PathBuf::from(&command);
|
|
||||||
|
|
||||||
if cmd_path.exists() {
|
let mut recursive_highlighter = Self::new();
|
||||||
// the user has given us an absolute path
|
recursive_highlighter.load_input(inner_content);
|
||||||
if cmd_path.is_dir() && read_shopts(|o| o.core.autocd) {
|
recursive_highlighter.highlight();
|
||||||
// this is a directory and autocd is enabled
|
self.push_style(Style::Blue);
|
||||||
return true;
|
self.output.push_str(prefix);
|
||||||
} else {
|
self.pop_style();
|
||||||
let Ok(meta) = cmd_path.metadata() else { return false };
|
self.output.push_str(&recursive_highlighter.take());
|
||||||
// this is a file that is executable by someone
|
if !incomplete {
|
||||||
return meta.permissions().mode() & 0o111 == 0
|
self.push_style(Style::Blue);
|
||||||
}
|
self.output.push(')');
|
||||||
} else {
|
self.pop_style();
|
||||||
// they gave us a command name
|
}
|
||||||
// now we must traverse the PATH env var
|
self.last_was_reset = false;
|
||||||
// and see if we find any matches
|
}
|
||||||
for path in paths {
|
markers::VAR_SUB => {
|
||||||
let path = PathBuf::from(path).join(command);
|
let mut var_sub = String::new();
|
||||||
if path.exists() {
|
while let Some(ch) = input_chars.peek() {
|
||||||
let Ok(meta) = path.metadata() else { continue };
|
if *ch == markers::VAR_SUB_END {
|
||||||
return meta.permissions().mode() & 0o111 != 0;
|
input_chars.next(); // consume the end marker
|
||||||
}
|
break;
|
||||||
}
|
} else if markers::is_marker(*ch) {
|
||||||
|
input_chars.next(); // skip the marker
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
var_sub.push(*ch);
|
||||||
|
input_chars.next();
|
||||||
|
}
|
||||||
|
let style = Style::Cyan;
|
||||||
|
self.push_style(style);
|
||||||
|
self.output.push_str(&var_sub);
|
||||||
|
self.pop_style();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
if markers::is_marker(ch) {
|
||||||
|
} else {
|
||||||
|
self.output.push(ch);
|
||||||
|
self.last_was_reset = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// also check shell functions and aliases for any matches
|
/// Extracts the highlighted output and resets the highlighter state
|
||||||
let found = read_logic(|l| l.get_func(command).is_some() || l.get_alias(command).is_some());
|
///
|
||||||
if found {
|
/// Clears the input buffer, style stack, and returns the generated output
|
||||||
return true;
|
/// containing ANSI escape codes. The highlighter is ready for reuse after
|
||||||
}
|
/// this.
|
||||||
}
|
pub fn take(&mut self) -> String {
|
||||||
|
self.input.clear();
|
||||||
|
self.clear_styles();
|
||||||
|
std::mem::take(&mut self.output)
|
||||||
|
}
|
||||||
|
|
||||||
false
|
/// Checks if a command name is valid (exists in PATH, is a function, or is an
|
||||||
}
|
/// alias)
|
||||||
|
///
|
||||||
|
/// Searches:
|
||||||
|
/// 1. Current directory if command is a path
|
||||||
|
/// 2. All directories in PATH environment variable
|
||||||
|
/// 3. Shell functions and aliases in the current shell state
|
||||||
|
fn is_valid(command: &str) -> bool {
|
||||||
|
let path = env::var("PATH").unwrap_or_default();
|
||||||
|
let paths = path.split(':');
|
||||||
|
let cmd_path = PathBuf::from(&command);
|
||||||
|
|
||||||
/// Emits a reset ANSI code to the output, with deduplication
|
if cmd_path.exists() {
|
||||||
///
|
// the user has given us an absolute path
|
||||||
/// Only emits the reset if the last emitted code was not already a reset,
|
if cmd_path.is_dir() && read_shopts(|o| o.core.autocd) {
|
||||||
/// preventing redundant `\x1b[0m` sequences in the output.
|
// this is a directory and autocd is enabled
|
||||||
fn emit_reset(&mut self) {
|
return true;
|
||||||
if !self.last_was_reset {
|
} else {
|
||||||
self.output.push_str(&Style::Reset.to_string());
|
let Ok(meta) = cmd_path.metadata() else {
|
||||||
self.last_was_reset = true;
|
return false;
|
||||||
}
|
};
|
||||||
}
|
// this is a file that is executable by someone
|
||||||
|
return meta.permissions().mode() & 0o111 == 0;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// they gave us a command name
|
||||||
|
// now we must traverse the PATH env var
|
||||||
|
// and see if we find any matches
|
||||||
|
for path in paths {
|
||||||
|
let path = PathBuf::from(path).join(command);
|
||||||
|
if path.exists() {
|
||||||
|
let Ok(meta) = path.metadata() else { continue };
|
||||||
|
return meta.permissions().mode() & 0o111 != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Emits a style ANSI code to the output
|
// also check shell functions and aliases for any matches
|
||||||
///
|
let found = read_logic(|l| l.get_func(command).is_some() || l.get_alias(command).is_some());
|
||||||
/// Unconditionally appends the ANSI escape sequence for the given style
|
if found {
|
||||||
/// and marks that we're no longer in a reset state.
|
return true;
|
||||||
fn emit_style(&mut self, style: &StyleSet) {
|
}
|
||||||
self.output.push_str(&style.to_string());
|
}
|
||||||
self.last_was_reset = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pushes a new style onto the stack and emits its ANSI code
|
false
|
||||||
///
|
}
|
||||||
/// Used when entering a new syntax context (string, variable, command, etc.).
|
|
||||||
/// The style stack allows proper restoration when exiting nested constructs.
|
|
||||||
pub fn push_style(&mut self, style: impl Into<StyleSet>) {
|
|
||||||
let set: StyleSet = style.into();
|
|
||||||
self.style_stack.push(set.clone());
|
|
||||||
self.emit_style(&set);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Pops a style from the stack and restores the previous style
|
fn is_filename(arg: &str) -> bool {
|
||||||
///
|
let path = PathBuf::from(arg);
|
||||||
/// Used when exiting a syntax context. If there's a parent style on the stack,
|
|
||||||
/// it's re-emitted to restore the previous color. Otherwise, emits a reset.
|
|
||||||
/// This ensures colors are properly restored in nested constructs like
|
|
||||||
/// `"string with $VAR"` where the string color resumes after the variable.
|
|
||||||
pub fn pop_style(&mut self) {
|
|
||||||
self.style_stack.pop();
|
|
||||||
if let Some(style) = self.style_stack.last().cloned() {
|
|
||||||
self.emit_style(&style);
|
|
||||||
} else {
|
|
||||||
self.emit_reset();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clears all styles from the stack and emits a reset
|
if path.exists() {
|
||||||
///
|
return true;
|
||||||
/// Used at command separators and explicit reset markers to return to
|
}
|
||||||
/// the default terminal color between independent commands.
|
|
||||||
pub fn clear_styles(&mut self) {
|
|
||||||
self.style_stack.clear();
|
|
||||||
self.emit_reset();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Simple marker-to-ANSI replacement (unused in favor of stack-based highlighting)
|
if let Some(parent_dir) = path.parent()
|
||||||
///
|
&& let Ok(entries) = parent_dir.read_dir()
|
||||||
/// Performs direct string replacement of markers with ANSI codes, without
|
{
|
||||||
/// handling nesting or proper color restoration. Kept for reference but not
|
let files = entries
|
||||||
/// used in the current implementation.
|
.filter_map(|e| e.ok())
|
||||||
pub fn trivial_replace(&mut self) {
|
.map(|e| e.file_name().to_string_lossy().to_string())
|
||||||
self.input = self.input
|
.collect::<Vec<_>>();
|
||||||
.replace([markers::RESET, markers::ARG], "\x1b[0m")
|
let Some(arg_filename) = PathBuf::from(arg)
|
||||||
.replace(markers::KEYWORD, "\x1b[33m")
|
.file_name()
|
||||||
.replace(markers::CASE_PAT, "\x1b[34m")
|
.map(|s| s.to_string_lossy().to_string())
|
||||||
.replace(markers::COMMENT, "\x1b[90m")
|
else {
|
||||||
.replace(markers::OPERATOR, "\x1b[35m");
|
return false;
|
||||||
}
|
};
|
||||||
|
for file in files {
|
||||||
|
if file.starts_with(&arg_filename) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(this_dir) = env::current_dir()
|
||||||
|
&& let Ok(entries) = this_dir.read_dir()
|
||||||
|
{
|
||||||
|
let this_dir_files = entries
|
||||||
|
.filter_map(|e| e.ok())
|
||||||
|
.map(|e| e.file_name().to_string_lossy().to_string())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
for file in this_dir_files {
|
||||||
|
if file.starts_with(arg) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emits a reset ANSI code to the output, with deduplication
|
||||||
|
///
|
||||||
|
/// Only emits the reset if the last emitted code was not already a reset,
|
||||||
|
/// preventing redundant `\x1b[0m` sequences in the output.
|
||||||
|
fn emit_reset(&mut self) {
|
||||||
|
if !self.last_was_reset {
|
||||||
|
self.output.push_str(&Style::Reset.to_string());
|
||||||
|
self.last_was_reset = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Emits a style ANSI code to the output
|
||||||
|
///
|
||||||
|
/// Unconditionally appends the ANSI escape sequence for the given style
|
||||||
|
/// and marks that we're no longer in a reset state.
|
||||||
|
fn emit_style(&mut self, style: &StyleSet) {
|
||||||
|
self.output.push_str(&style.to_string());
|
||||||
|
self.last_was_reset = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pushes a new style onto the stack and emits its ANSI code
|
||||||
|
///
|
||||||
|
/// Used when entering a new syntax context (string, variable, command, etc.).
|
||||||
|
/// The style stack allows proper restoration when exiting nested constructs.
|
||||||
|
pub fn push_style(&mut self, style: impl Into<StyleSet>) {
|
||||||
|
let set: StyleSet = style.into();
|
||||||
|
self.style_stack.push(set.clone());
|
||||||
|
self.emit_style(&set);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pops a style from the stack and restores the previous style
|
||||||
|
///
|
||||||
|
/// Used when exiting a syntax context. If there's a parent style on the
|
||||||
|
/// stack, it's re-emitted to restore the previous color. Otherwise, emits a
|
||||||
|
/// reset. This ensures colors are properly restored in nested constructs
|
||||||
|
/// like `"string with $VAR"` where the string color resumes after the
|
||||||
|
/// variable.
|
||||||
|
pub fn pop_style(&mut self) {
|
||||||
|
self.style_stack.pop();
|
||||||
|
if let Some(style) = self.style_stack.last().cloned() {
|
||||||
|
self.emit_style(&style);
|
||||||
|
} else {
|
||||||
|
self.emit_reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clears all styles from the stack and emits a reset
|
||||||
|
///
|
||||||
|
/// Used at command separators and explicit reset markers to return to
|
||||||
|
/// the default terminal color between independent commands.
|
||||||
|
pub fn clear_styles(&mut self) {
|
||||||
|
self.style_stack.clear();
|
||||||
|
self.emit_reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Simple marker-to-ANSI replacement (unused in favor of stack-based
|
||||||
|
/// highlighting)
|
||||||
|
///
|
||||||
|
/// Performs direct string replacement of markers with ANSI codes, without
|
||||||
|
/// handling nesting or proper color restoration. Kept for reference but not
|
||||||
|
/// used in the current implementation.
|
||||||
|
pub fn trivial_replace(&mut self) {
|
||||||
|
self.input = self
|
||||||
|
.input
|
||||||
|
.replace([markers::RESET, markers::ARG], "\x1b[0m")
|
||||||
|
.replace(markers::KEYWORD, "\x1b[33m")
|
||||||
|
.replace(markers::CASE_PAT, "\x1b[34m")
|
||||||
|
.replace(markers::COMMENT, "\x1b[90m")
|
||||||
|
.replace(markers::OPERATOR, "\x1b[35m");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -189,8 +189,8 @@ fn read_hist_file(path: &Path) -> ShResult<Vec<HistEntry>> {
|
|||||||
Ok(raw.parse::<HistEntries>()?.0)
|
Ok(raw.parse::<HistEntries>()?.0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deduplicate entries, keeping only the most recent occurrence of each command.
|
/// Deduplicate entries, keeping only the most recent occurrence of each
|
||||||
/// Preserves chronological order (oldest to newest).
|
/// command. Preserves chronological order (oldest to newest).
|
||||||
fn dedupe_entries(entries: &[HistEntry]) -> Vec<HistEntry> {
|
fn dedupe_entries(entries: &[HistEntry]) -> Vec<HistEntry> {
|
||||||
let mut seen = HashSet::new();
|
let mut seen = HashSet::new();
|
||||||
// Iterate backwards (newest first), keeping first occurrence of each command
|
// Iterate backwards (newest first), keeping first occurrence of each command
|
||||||
@@ -207,10 +207,10 @@ fn dedupe_entries(entries: &[HistEntry]) -> Vec<HistEntry> {
|
|||||||
|
|
||||||
pub struct History {
|
pub struct History {
|
||||||
path: PathBuf,
|
path: PathBuf,
|
||||||
pub pending: Option<String>,
|
pub pending: Option<(String, usize)>, // command, cursor_pos
|
||||||
entries: Vec<HistEntry>,
|
entries: Vec<HistEntry>,
|
||||||
search_mask: Vec<HistEntry>,
|
search_mask: Vec<HistEntry>,
|
||||||
no_matches: bool,
|
no_matches: bool,
|
||||||
pub cursor: usize,
|
pub cursor: usize,
|
||||||
search_direction: Direction,
|
search_direction: Direction,
|
||||||
ignore_dups: bool,
|
ignore_dups: bool,
|
||||||
@@ -235,9 +235,9 @@ impl History {
|
|||||||
Ok(Self {
|
Ok(Self {
|
||||||
path,
|
path,
|
||||||
entries,
|
entries,
|
||||||
pending: None,
|
pending: None,
|
||||||
search_mask,
|
search_mask,
|
||||||
no_matches: false,
|
no_matches: false,
|
||||||
cursor,
|
cursor,
|
||||||
search_direction: Direction::Backward,
|
search_direction: Direction::Backward,
|
||||||
ignore_dups,
|
ignore_dups,
|
||||||
@@ -245,10 +245,10 @@ impl History {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn reset(&mut self) {
|
pub fn reset(&mut self) {
|
||||||
self.search_mask = dedupe_entries(&self.entries);
|
self.search_mask = dedupe_entries(&self.entries);
|
||||||
self.cursor = self.search_mask.len();
|
self.cursor = self.search_mask.len();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn entries(&self) -> &[HistEntry] {
|
pub fn entries(&self) -> &[HistEntry] {
|
||||||
&self.entries
|
&self.entries
|
||||||
@@ -270,14 +270,14 @@ impl History {
|
|||||||
self.cursor = self.search_mask.len();
|
self.cursor = self.search_mask.len();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_pending_cmd(&mut self, command: &str) {
|
pub fn update_pending_cmd(&mut self, buf: (&str, usize)) {
|
||||||
let cmd = command.to_string();
|
let cmd = buf.0.to_string();
|
||||||
let constraint = SearchConstraint {
|
let constraint = SearchConstraint {
|
||||||
kind: SearchKind::Prefix,
|
kind: SearchKind::Prefix,
|
||||||
term: cmd.clone(),
|
term: cmd.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
self.pending = Some(cmd);
|
self.pending = Some((cmd, buf.1));
|
||||||
self.constrain_entries(constraint);
|
self.constrain_entries(constraint);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,11 +315,11 @@ impl History {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
self.search_mask = dedupe_entries(&filtered);
|
self.search_mask = dedupe_entries(&filtered);
|
||||||
self.no_matches = self.search_mask.is_empty();
|
self.no_matches = self.search_mask.is_empty();
|
||||||
if self.no_matches {
|
if self.no_matches {
|
||||||
// If no matches, reset to full history so user can still scroll through it
|
// If no matches, reset to full history so user can still scroll through it
|
||||||
self.search_mask = dedupe_entries(&self.entries);
|
self.search_mask = dedupe_entries(&self.entries);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.cursor = self.search_mask.len();
|
self.cursor = self.search_mask.len();
|
||||||
}
|
}
|
||||||
@@ -328,12 +328,14 @@ impl History {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn hint_entry(&self) -> Option<&HistEntry> {
|
pub fn hint_entry(&self) -> Option<&HistEntry> {
|
||||||
if self.no_matches { return None };
|
if self.no_matches {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
self.search_mask.last()
|
self.search_mask.last()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_hint(&self) -> Option<String> {
|
pub fn get_hint(&self) -> Option<String> {
|
||||||
if self.at_pending() && self.pending.as_ref().is_some_and(|p| !p.is_empty()) {
|
if self.at_pending() && self.pending.as_ref().is_some_and(|p| !p.0.is_empty()) {
|
||||||
let entry = self.hint_entry()?;
|
let entry = self.hint_entry()?;
|
||||||
Some(entry.command().to_string())
|
Some(entry.command().to_string())
|
||||||
} else {
|
} else {
|
||||||
@@ -342,9 +344,15 @@ impl History {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn scroll(&mut self, offset: isize) -> Option<&HistEntry> {
|
pub fn scroll(&mut self, offset: isize) -> Option<&HistEntry> {
|
||||||
self.cursor = self.cursor.saturating_add_signed(offset).clamp(0, self.search_mask.len());
|
self.cursor = self
|
||||||
|
.cursor
|
||||||
|
.saturating_add_signed(offset)
|
||||||
|
.clamp(0, self.search_mask.len());
|
||||||
|
|
||||||
log::debug!("Scrolling history by offset {offset} from cursor at index {}", self.cursor);
|
log::debug!(
|
||||||
|
"Scrolling history by offset {offset} from cursor at index {}",
|
||||||
|
self.cursor
|
||||||
|
);
|
||||||
self.search_mask.get(self.cursor)
|
self.search_mask.get(self.cursor)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,7 +386,8 @@ impl History {
|
|||||||
|
|
||||||
let last_file_entry = self
|
let last_file_entry = self
|
||||||
.entries
|
.entries
|
||||||
.iter().rfind(|ent| !ent.new)
|
.iter()
|
||||||
|
.rfind(|ent| !ent.new)
|
||||||
.map(|ent| ent.command.clone())
|
.map(|ent| ent.command.clone())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
@@ -399,8 +408,8 @@ impl History {
|
|||||||
}
|
}
|
||||||
|
|
||||||
file.write_all(data.as_bytes())?;
|
file.write_all(data.as_bytes())?;
|
||||||
self.pending = None;
|
self.pending = None;
|
||||||
self.reset();
|
self.reset();
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,10 +133,10 @@ impl SelectMode {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum MotionKind {
|
pub enum MotionKind {
|
||||||
To(usize), // Absolute position, exclusive
|
To(usize), // Absolute position, exclusive
|
||||||
On(usize), // Absolute position, inclusive
|
On(usize), // Absolute position, inclusive
|
||||||
Onto(usize), /* Absolute position, operations include the position but motions
|
Onto(usize), /* Absolute position, operations include the position but motions
|
||||||
* exclude it (wtf vim) */
|
* exclude it (wtf vim) */
|
||||||
Inclusive((usize, usize)), // Range, inclusive
|
Inclusive((usize, usize)), // Range, inclusive
|
||||||
Exclusive((usize, usize)), // Range, exclusive
|
Exclusive((usize, usize)), // Range, exclusive
|
||||||
|
|
||||||
@@ -360,12 +360,12 @@ impl LineBuf {
|
|||||||
pub fn set_hint(&mut self, hint: Option<String>) {
|
pub fn set_hint(&mut self, hint: Option<String>) {
|
||||||
if let Some(hint) = hint {
|
if let Some(hint) = hint {
|
||||||
if let Some(hint) = hint.strip_prefix(&self.buffer) {
|
if let Some(hint) = hint.strip_prefix(&self.buffer) {
|
||||||
if !hint.is_empty() {
|
if !hint.is_empty() {
|
||||||
self.hint = Some(hint.to_string())
|
self.hint = Some(hint.to_string())
|
||||||
} else {
|
} else {
|
||||||
self.hint = None
|
self.hint = None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.hint = None
|
self.hint = None
|
||||||
}
|
}
|
||||||
@@ -563,8 +563,8 @@ impl LineBuf {
|
|||||||
self.update_graphemes();
|
self.update_graphemes();
|
||||||
}
|
}
|
||||||
pub fn drain(&mut self, start: usize, end: usize) -> String {
|
pub fn drain(&mut self, start: usize, end: usize) -> String {
|
||||||
let start = start.max(0);
|
let start = start.max(0);
|
||||||
let end = end.min(self.grapheme_indices().len());
|
let end = end.min(self.grapheme_indices().len());
|
||||||
let drained = if end == self.grapheme_indices().len() {
|
let drained = if end == self.grapheme_indices().len() {
|
||||||
if start == self.grapheme_indices().len() {
|
if start == self.grapheme_indices().len() {
|
||||||
return String::new();
|
return String::new();
|
||||||
@@ -628,8 +628,9 @@ impl LineBuf {
|
|||||||
self.next_sentence_start_from_punctuation(pos).is_some()
|
self.next_sentence_start_from_punctuation(pos).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// If position is at sentence-ending punctuation, returns the position of the next sentence start.
|
/// If position is at sentence-ending punctuation, returns the position of the
|
||||||
/// Handles closing delimiters (`)`, `]`, `"`, `'`) after punctuation.
|
/// next sentence start. Handles closing delimiters (`)`, `]`, `"`, `'`)
|
||||||
|
/// after punctuation.
|
||||||
#[allow(clippy::collapsible_if)]
|
#[allow(clippy::collapsible_if)]
|
||||||
pub fn next_sentence_start_from_punctuation(&self, pos: usize) -> Option<usize> {
|
pub fn next_sentence_start_from_punctuation(&self, pos: usize) -> Option<usize> {
|
||||||
if let Some(gr) = self.read_grapheme_at(pos) {
|
if let Some(gr) = self.read_grapheme_at(pos) {
|
||||||
@@ -956,9 +957,10 @@ impl LineBuf {
|
|||||||
let start = start.unwrap_or(0);
|
let start = start.unwrap_or(0);
|
||||||
|
|
||||||
if count > 1
|
if count > 1
|
||||||
&& let Some((_, new_end)) = self.text_obj_sentence(end, count - 1, bound) {
|
&& let Some((_, new_end)) = self.text_obj_sentence(end, count - 1, bound)
|
||||||
end = new_end;
|
{
|
||||||
}
|
end = new_end;
|
||||||
|
}
|
||||||
|
|
||||||
Some((start, end))
|
Some((start, end))
|
||||||
}
|
}
|
||||||
@@ -1363,7 +1365,12 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Find the start of the next word forward
|
/// Find the start of the next word forward
|
||||||
pub fn start_of_word_forward(&mut self, mut pos: usize, word: Word, include_last_char: bool) -> usize {
|
pub fn start_of_word_forward(
|
||||||
|
&mut self,
|
||||||
|
mut pos: usize,
|
||||||
|
word: Word,
|
||||||
|
include_last_char: bool,
|
||||||
|
) -> usize {
|
||||||
let default = self.grapheme_indices().len();
|
let default = self.grapheme_indices().len();
|
||||||
let mut indices_iter = (pos..self.cursor.max).peekable();
|
let mut indices_iter = (pos..self.cursor.max).peekable();
|
||||||
|
|
||||||
@@ -1390,8 +1397,7 @@ impl LineBuf {
|
|||||||
let on_whitespace = is_whitespace(&cur_char);
|
let on_whitespace = is_whitespace(&cur_char);
|
||||||
|
|
||||||
if !on_whitespace {
|
if !on_whitespace {
|
||||||
let Some(ws_pos) =
|
let Some(ws_pos) = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
|
||||||
indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
|
|
||||||
else {
|
else {
|
||||||
return default;
|
return default;
|
||||||
};
|
};
|
||||||
@@ -1457,7 +1463,12 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Find the end of the previous word backward
|
/// Find the end of the previous word backward
|
||||||
pub fn end_of_word_backward(&mut self, mut pos: usize, word: Word, include_last_char: bool) -> usize {
|
pub fn end_of_word_backward(
|
||||||
|
&mut self,
|
||||||
|
mut pos: usize,
|
||||||
|
word: Word,
|
||||||
|
include_last_char: bool,
|
||||||
|
) -> usize {
|
||||||
let default = self.grapheme_indices().len();
|
let default = self.grapheme_indices().len();
|
||||||
let mut indices_iter = (0..pos).rev().peekable();
|
let mut indices_iter = (0..pos).rev().peekable();
|
||||||
|
|
||||||
@@ -1484,8 +1495,7 @@ impl LineBuf {
|
|||||||
let on_whitespace = is_whitespace(&cur_char);
|
let on_whitespace = is_whitespace(&cur_char);
|
||||||
|
|
||||||
if !on_whitespace {
|
if !on_whitespace {
|
||||||
let Some(ws_pos) =
|
let Some(ws_pos) = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
|
||||||
indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
|
|
||||||
else {
|
else {
|
||||||
return default;
|
return default;
|
||||||
};
|
};
|
||||||
@@ -1742,11 +1752,7 @@ impl LineBuf {
|
|||||||
};
|
};
|
||||||
pos = next_ws_pos;
|
pos = next_ws_pos;
|
||||||
|
|
||||||
if pos == 0 {
|
if pos == 0 { pos } else { pos + 1 }
|
||||||
pos
|
|
||||||
} else {
|
|
||||||
pos + 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1903,7 +1909,7 @@ impl LineBuf {
|
|||||||
&& self.grapheme_at(target_pos) == Some("\n")
|
&& self.grapheme_at(target_pos) == Some("\n")
|
||||||
{
|
{
|
||||||
target_pos = target_pos.saturating_sub(1); // Don't land on the
|
target_pos = target_pos.saturating_sub(1); // Don't land on the
|
||||||
// newline
|
// newline
|
||||||
}
|
}
|
||||||
MotionKind::InclusiveWithTargetCol((start, end), target_pos)
|
MotionKind::InclusiveWithTargetCol((start, end), target_pos)
|
||||||
}
|
}
|
||||||
@@ -2141,7 +2147,7 @@ impl LineBuf {
|
|||||||
&& self.grapheme_at(target_pos) == Some("\n")
|
&& self.grapheme_at(target_pos) == Some("\n")
|
||||||
{
|
{
|
||||||
target_pos = target_pos.saturating_sub(1); // Don't land on the
|
target_pos = target_pos.saturating_sub(1); // Don't land on the
|
||||||
// newline
|
// newline
|
||||||
}
|
}
|
||||||
|
|
||||||
let (start, end) = match motion.1 {
|
let (start, end) = match motion.1 {
|
||||||
@@ -2575,15 +2581,16 @@ impl LineBuf {
|
|||||||
}
|
}
|
||||||
Verb::SwapVisualAnchor => {
|
Verb::SwapVisualAnchor => {
|
||||||
if let Some((start, end)) = self.select_range()
|
if let Some((start, end)) = self.select_range()
|
||||||
&& let Some(mut mode) = self.select_mode {
|
&& let Some(mut mode) = self.select_mode
|
||||||
mode.invert_anchor();
|
{
|
||||||
let new_cursor_pos = match mode.anchor() {
|
mode.invert_anchor();
|
||||||
SelectAnchor::Start => start,
|
let new_cursor_pos = match mode.anchor() {
|
||||||
SelectAnchor::End => end,
|
SelectAnchor::Start => start,
|
||||||
};
|
SelectAnchor::End => end,
|
||||||
self.cursor.set(new_cursor_pos);
|
};
|
||||||
self.select_mode = Some(mode)
|
self.cursor.set(new_cursor_pos);
|
||||||
}
|
self.select_mode = Some(mode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Verb::JoinLines => {
|
Verb::JoinLines => {
|
||||||
let start = self.start_of_line();
|
let start = self.start_of_line();
|
||||||
@@ -2731,10 +2738,12 @@ impl LineBuf {
|
|||||||
let edit_is_merging = self.undo_stack.last().is_some_and(|edit| edit.merging);
|
let edit_is_merging = self.undo_stack.last().is_some_and(|edit| edit.merging);
|
||||||
|
|
||||||
// Merge character inserts into one edit
|
// Merge character inserts into one edit
|
||||||
if edit_is_merging && cmd.verb.as_ref().is_none_or(|v| !v.1.is_char_insert())
|
if edit_is_merging
|
||||||
&& let Some(edit) = self.undo_stack.last_mut() {
|
&& cmd.verb.as_ref().is_none_or(|v| !v.1.is_char_insert())
|
||||||
edit.stop_merge();
|
&& let Some(edit) = self.undo_stack.last_mut()
|
||||||
}
|
{
|
||||||
|
edit.stop_merge();
|
||||||
|
}
|
||||||
|
|
||||||
let ViCmd {
|
let ViCmd {
|
||||||
register,
|
register,
|
||||||
@@ -2821,10 +2830,9 @@ impl LineBuf {
|
|||||||
self.saved_col = None;
|
self.saved_col = None;
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_char_insert
|
if is_char_insert && let Some(edit) = self.undo_stack.last_mut() {
|
||||||
&& let Some(edit) = self.undo_stack.last_mut() {
|
edit.start_merge();
|
||||||
edit.start_merge();
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -2832,9 +2840,13 @@ impl LineBuf {
|
|||||||
&self.buffer // FIXME: this will have to be fixed up later
|
&self.buffer // FIXME: this will have to be fixed up later
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_hint_text(&self) -> String {
|
pub fn get_hint_text(&self) -> String {
|
||||||
self.hint.clone().map(|h| h.styled(Style::BrightBlack)).unwrap_or_default()
|
self
|
||||||
}
|
.hint
|
||||||
|
.clone()
|
||||||
|
.map(|h| h.styled(Style::BrightBlack))
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for LineBuf {
|
impl Display for LineBuf {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -17,11 +17,15 @@ use unicode_segmentation::UnicodeSegmentation;
|
|||||||
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||||
use vte::{Parser, Perform};
|
use vte::{Parser, Perform};
|
||||||
|
|
||||||
use crate::{prelude::*, procio::borrow_fd, state::{read_meta, write_meta}};
|
|
||||||
use crate::{
|
use crate::{
|
||||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||||
prompt::readline::keys::{KeyCode, ModKeys},
|
prompt::readline::keys::{KeyCode, ModKeys},
|
||||||
};
|
};
|
||||||
|
use crate::{
|
||||||
|
prelude::*,
|
||||||
|
procio::borrow_fd,
|
||||||
|
state::{read_meta, write_meta},
|
||||||
|
};
|
||||||
|
|
||||||
use super::{keys::KeyEvent, linebuf::LineBuf};
|
use super::{keys::KeyEvent, linebuf::LineBuf};
|
||||||
|
|
||||||
@@ -41,7 +45,7 @@ pub fn raw_mode() -> RawModeGuard {
|
|||||||
)
|
)
|
||||||
.expect("Failed to set terminal to raw mode");
|
.expect("Failed to set terminal to raw mode");
|
||||||
|
|
||||||
let (cols, rows) = get_win_size(STDIN_FILENO);
|
let (cols, rows) = get_win_size(STDIN_FILENO);
|
||||||
|
|
||||||
RawModeGuard {
|
RawModeGuard {
|
||||||
orig,
|
orig,
|
||||||
@@ -242,9 +246,7 @@ impl Read for TermBuffer {
|
|||||||
let result = nix::unistd::read(self.tty, buf);
|
let result = nix::unistd::read(self.tty, buf);
|
||||||
match result {
|
match result {
|
||||||
Ok(n) => Ok(n),
|
Ok(n) => Ok(n),
|
||||||
Err(Errno::EINTR) => {
|
Err(Errno::EINTR) => Err(Errno::EINTR.into()),
|
||||||
Err(Errno::EINTR.into())
|
|
||||||
}
|
|
||||||
Err(e) => Err(std::io::Error::from_raw_os_error(e as i32)),
|
Err(e) => Err(std::io::Error::from_raw_os_error(e as i32)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -280,17 +282,21 @@ impl RawModeGuard {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_cooked_mode<F, R>(f: F) -> R
|
pub fn with_cooked_mode<F, R>(f: F) -> R
|
||||||
where F: FnOnce() -> R {
|
where
|
||||||
let raw = tcgetattr(borrow_fd(STDIN_FILENO)).expect("Failed to get terminal attributes");
|
F: FnOnce() -> R,
|
||||||
let mut cooked = raw.clone();
|
{
|
||||||
cooked.local_flags |= termios::LocalFlags::ICANON | termios::LocalFlags::ECHO;
|
let raw = tcgetattr(borrow_fd(STDIN_FILENO)).expect("Failed to get terminal attributes");
|
||||||
cooked.input_flags |= termios::InputFlags::ICRNL;
|
let mut cooked = raw.clone();
|
||||||
tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &cooked).expect("Failed to set cooked mode");
|
cooked.local_flags |= termios::LocalFlags::ICANON | termios::LocalFlags::ECHO;
|
||||||
let res = f();
|
cooked.input_flags |= termios::InputFlags::ICRNL;
|
||||||
tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &raw).expect("Failed to restore raw mode");
|
tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &cooked)
|
||||||
res
|
.expect("Failed to set cooked mode");
|
||||||
}
|
let res = f();
|
||||||
|
tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &raw)
|
||||||
|
.expect("Failed to restore raw mode");
|
||||||
|
res
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Drop for RawModeGuard {
|
impl Drop for RawModeGuard {
|
||||||
@@ -333,9 +339,15 @@ impl KeyCollector {
|
|||||||
// CSI modifiers: param = 1 + (shift) + (alt*2) + (ctrl*4) + (meta*8)
|
// CSI modifiers: param = 1 + (shift) + (alt*2) + (ctrl*4) + (meta*8)
|
||||||
let bits = param.saturating_sub(1);
|
let bits = param.saturating_sub(1);
|
||||||
let mut mods = ModKeys::empty();
|
let mut mods = ModKeys::empty();
|
||||||
if bits & 1 != 0 { mods |= ModKeys::SHIFT; }
|
if bits & 1 != 0 {
|
||||||
if bits & 2 != 0 { mods |= ModKeys::ALT; }
|
mods |= ModKeys::SHIFT;
|
||||||
if bits & 4 != 0 { mods |= ModKeys::CTRL; }
|
}
|
||||||
|
if bits & 2 != 0 {
|
||||||
|
mods |= ModKeys::ALT;
|
||||||
|
}
|
||||||
|
if bits & 4 != 0 {
|
||||||
|
mods |= ModKeys::CTRL;
|
||||||
|
}
|
||||||
mods
|
mods
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -374,46 +386,72 @@ impl Perform for KeyCollector {
|
|||||||
self.push(event);
|
self.push(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn csi_dispatch(&mut self, params: &vte::Params, intermediates: &[u8], _ignore: bool, action: char) {
|
fn csi_dispatch(
|
||||||
let params: Vec<u16> = params.iter()
|
&mut self,
|
||||||
|
params: &vte::Params,
|
||||||
|
intermediates: &[u8],
|
||||||
|
_ignore: bool,
|
||||||
|
action: char,
|
||||||
|
) {
|
||||||
|
let params: Vec<u16> = params
|
||||||
|
.iter()
|
||||||
.map(|p| p.first().copied().unwrap_or(0))
|
.map(|p| p.first().copied().unwrap_or(0))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let event = match (intermediates, action) {
|
let event = match (intermediates, action) {
|
||||||
// Arrow keys: CSI A/B/C/D or CSI 1;mod A/B/C/D
|
// Arrow keys: CSI A/B/C/D or CSI 1;mod A/B/C/D
|
||||||
([], 'A') => {
|
([], 'A') => {
|
||||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
let mods = params
|
||||||
|
.get(1)
|
||||||
|
.map(|&m| Self::parse_modifiers(m))
|
||||||
|
.unwrap_or(ModKeys::empty());
|
||||||
KeyEvent(KeyCode::Up, mods)
|
KeyEvent(KeyCode::Up, mods)
|
||||||
}
|
}
|
||||||
([], 'B') => {
|
([], 'B') => {
|
||||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
let mods = params
|
||||||
|
.get(1)
|
||||||
|
.map(|&m| Self::parse_modifiers(m))
|
||||||
|
.unwrap_or(ModKeys::empty());
|
||||||
KeyEvent(KeyCode::Down, mods)
|
KeyEvent(KeyCode::Down, mods)
|
||||||
}
|
}
|
||||||
([], 'C') => {
|
([], 'C') => {
|
||||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
let mods = params
|
||||||
|
.get(1)
|
||||||
|
.map(|&m| Self::parse_modifiers(m))
|
||||||
|
.unwrap_or(ModKeys::empty());
|
||||||
KeyEvent(KeyCode::Right, mods)
|
KeyEvent(KeyCode::Right, mods)
|
||||||
}
|
}
|
||||||
([], 'D') => {
|
([], 'D') => {
|
||||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
let mods = params
|
||||||
|
.get(1)
|
||||||
|
.map(|&m| Self::parse_modifiers(m))
|
||||||
|
.unwrap_or(ModKeys::empty());
|
||||||
KeyEvent(KeyCode::Left, mods)
|
KeyEvent(KeyCode::Left, mods)
|
||||||
}
|
}
|
||||||
// Home/End: CSI H/F or CSI 1;mod H/F
|
// Home/End: CSI H/F or CSI 1;mod H/F
|
||||||
([], 'H') => {
|
([], 'H') => {
|
||||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
let mods = params
|
||||||
|
.get(1)
|
||||||
|
.map(|&m| Self::parse_modifiers(m))
|
||||||
|
.unwrap_or(ModKeys::empty());
|
||||||
KeyEvent(KeyCode::Home, mods)
|
KeyEvent(KeyCode::Home, mods)
|
||||||
}
|
}
|
||||||
([], 'F') => {
|
([], 'F') => {
|
||||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
let mods = params
|
||||||
|
.get(1)
|
||||||
|
.map(|&m| Self::parse_modifiers(m))
|
||||||
|
.unwrap_or(ModKeys::empty());
|
||||||
KeyEvent(KeyCode::End, mods)
|
KeyEvent(KeyCode::End, mods)
|
||||||
}
|
}
|
||||||
// Shift+Tab: CSI Z
|
// Shift+Tab: CSI Z
|
||||||
([], 'Z') => {
|
([], 'Z') => KeyEvent(KeyCode::Tab, ModKeys::SHIFT),
|
||||||
KeyEvent(KeyCode::Tab, ModKeys::SHIFT)
|
|
||||||
}
|
|
||||||
// Special keys with tilde: CSI num ~ or CSI num;mod ~
|
// Special keys with tilde: CSI num ~ or CSI num;mod ~
|
||||||
([], '~') => {
|
([], '~') => {
|
||||||
let key_num = params.first().copied().unwrap_or(0);
|
let key_num = params.first().copied().unwrap_or(0);
|
||||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
let mods = params
|
||||||
|
.get(1)
|
||||||
|
.map(|&m| Self::parse_modifiers(m))
|
||||||
|
.unwrap_or(ModKeys::empty());
|
||||||
let key = match key_num {
|
let key = match key_num {
|
||||||
1 | 7 => KeyCode::Home,
|
1 | 7 => KeyCode::Home,
|
||||||
2 => KeyCode::Insert,
|
2 => KeyCode::Insert,
|
||||||
@@ -473,7 +511,9 @@ impl PollReader {
|
|||||||
pub fn feed_bytes(&mut self, bytes: &[u8]) {
|
pub fn feed_bytes(&mut self, bytes: &[u8]) {
|
||||||
if bytes == [b'\x1b'] {
|
if bytes == [b'\x1b'] {
|
||||||
// Single escape byte - user pressed ESC key
|
// Single escape byte - user pressed ESC key
|
||||||
self.collector.push(KeyEvent(KeyCode::Esc, ModKeys::empty()));
|
self
|
||||||
|
.collector
|
||||||
|
.push(KeyEvent(KeyCode::Esc, ModKeys::empty()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -914,13 +954,13 @@ impl LineWriter for TermWriter {
|
|||||||
let end = new_layout.end;
|
let end = new_layout.end;
|
||||||
let cursor = new_layout.cursor;
|
let cursor = new_layout.cursor;
|
||||||
|
|
||||||
if read_meta(|m| m.system_msg_pending()) {
|
if read_meta(|m| m.system_msg_pending()) {
|
||||||
let mut system_msg = String::new();
|
let mut system_msg = String::new();
|
||||||
while let Some(msg) = write_meta(|m| m.pop_system_message()) {
|
while let Some(msg) = write_meta(|m| m.pop_system_message()) {
|
||||||
writeln!(system_msg, "{msg}").map_err(err)?;
|
writeln!(system_msg, "{msg}").map_err(err)?;
|
||||||
}
|
}
|
||||||
self.buffer.push_str(&system_msg);
|
self.buffer.push_str(&system_msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.buffer.push_str(prompt);
|
self.buffer.push_str(prompt);
|
||||||
self.buffer.push_str(line);
|
self.buffer.push_str(line);
|
||||||
|
|||||||
@@ -161,14 +161,16 @@ impl ViCmd {
|
|||||||
}
|
}
|
||||||
/// If a ViCmd has a linewise motion, but no verb, we change it to charwise
|
/// If a ViCmd has a linewise motion, but no verb, we change it to charwise
|
||||||
pub fn alter_line_motion_if_no_verb(&mut self) {
|
pub fn alter_line_motion_if_no_verb(&mut self) {
|
||||||
if self.is_line_motion() && self.verb.is_none()
|
if self.is_line_motion()
|
||||||
&& let Some(motion) = self.motion.as_mut() {
|
&& self.verb.is_none()
|
||||||
match motion.1 {
|
&& let Some(motion) = self.motion.as_mut()
|
||||||
Motion::LineUp => motion.1 = Motion::LineUpCharwise,
|
{
|
||||||
Motion::LineDown => motion.1 = Motion::LineDownCharwise,
|
match motion.1 {
|
||||||
_ => unreachable!(),
|
Motion::LineUp => motion.1 = Motion::LineUpCharwise,
|
||||||
}
|
Motion::LineDown => motion.1 = Motion::LineDownCharwise,
|
||||||
|
_ => unreachable!(),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
pub fn is_mode_transition(&self) -> bool {
|
pub fn is_mode_transition(&self) -> bool {
|
||||||
self.verb.as_ref().is_some_and(|v| {
|
self.verb.as_ref().is_some_and(|v| {
|
||||||
|
|||||||
@@ -315,7 +315,7 @@ impl ViNormal {
|
|||||||
return match obj {
|
return match obj {
|
||||||
TextObj::Sentence(_) | TextObj::Paragraph(_) => CmdState::Complete,
|
TextObj::Sentence(_) | TextObj::Paragraph(_) => CmdState::Complete,
|
||||||
_ => CmdState::Invalid,
|
_ => CmdState::Invalid,
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
Some(_) => return CmdState::Complete,
|
Some(_) => return CmdState::Complete,
|
||||||
None => return CmdState::Pending,
|
None => return CmdState::Pending,
|
||||||
@@ -410,7 +410,7 @@ impl ViNormal {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'~' => {
|
'~' => {
|
||||||
chars_clone.next();
|
chars_clone.next();
|
||||||
@@ -445,7 +445,7 @@ impl ViNormal {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'x' => {
|
'x' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -454,7 +454,7 @@ impl ViNormal {
|
|||||||
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'X' => {
|
'X' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -463,7 +463,7 @@ impl ViNormal {
|
|||||||
motion: Some(MotionCmd(1, Motion::BackwardChar)),
|
motion: Some(MotionCmd(1, Motion::BackwardChar)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
's' => {
|
's' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -472,7 +472,7 @@ impl ViNormal {
|
|||||||
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'S' => {
|
'S' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -481,7 +481,7 @@ impl ViNormal {
|
|||||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'p' => {
|
'p' => {
|
||||||
chars = chars_clone;
|
chars = chars_clone;
|
||||||
@@ -516,7 +516,7 @@ impl ViNormal {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'~' => {
|
'~' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -525,7 +525,7 @@ impl ViNormal {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'u' => {
|
'u' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -534,7 +534,7 @@ impl ViNormal {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'v' => {
|
'v' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -543,7 +543,7 @@ impl ViNormal {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'V' => {
|
'V' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -552,7 +552,7 @@ impl ViNormal {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'o' => {
|
'o' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -561,7 +561,7 @@ impl ViNormal {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'O' => {
|
'O' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -570,7 +570,7 @@ impl ViNormal {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'a' => {
|
'a' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -579,7 +579,7 @@ impl ViNormal {
|
|||||||
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'A' => {
|
'A' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -588,7 +588,7 @@ impl ViNormal {
|
|||||||
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'i' => {
|
'i' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -597,7 +597,7 @@ impl ViNormal {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'I' => {
|
'I' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -606,7 +606,7 @@ impl ViNormal {
|
|||||||
motion: Some(MotionCmd(1, Motion::BeginningOfFirstWord)),
|
motion: Some(MotionCmd(1, Motion::BeginningOfFirstWord)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'J' => {
|
'J' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -615,7 +615,7 @@ impl ViNormal {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'y' => {
|
'y' => {
|
||||||
chars = chars_clone;
|
chars = chars_clone;
|
||||||
@@ -636,7 +636,7 @@ impl ViNormal {
|
|||||||
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'D' => {
|
'D' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -645,7 +645,7 @@ impl ViNormal {
|
|||||||
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'C' => {
|
'C' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -654,7 +654,7 @@ impl ViNormal {
|
|||||||
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: self.flags(),
|
flags: self.flags(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'=' => {
|
'=' => {
|
||||||
chars = chars_clone;
|
chars = chars_clone;
|
||||||
@@ -684,7 +684,7 @@ impl ViNormal {
|
|||||||
| ('~', Some(VerbCmd(_, Verb::ToggleCaseRange)))
|
| ('~', Some(VerbCmd(_, Verb::ToggleCaseRange)))
|
||||||
| ('>', Some(VerbCmd(_, Verb::Indent)))
|
| ('>', Some(VerbCmd(_, Verb::Indent)))
|
||||||
| ('<', Some(VerbCmd(_, Verb::Dedent))) => {
|
| ('<', Some(VerbCmd(_, Verb::Dedent))) => {
|
||||||
break 'motion_parse Some(MotionCmd(count, Motion::WholeLine))
|
break 'motion_parse Some(MotionCmd(count, Motion::WholeLine));
|
||||||
}
|
}
|
||||||
('W', Some(VerbCmd(_, Verb::Change))) => {
|
('W', Some(VerbCmd(_, Verb::Change))) => {
|
||||||
// Same with 'W'
|
// Same with 'W'
|
||||||
@@ -994,8 +994,7 @@ impl ViNormal {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if chars.peek().is_some() {
|
if chars.peek().is_some() {}
|
||||||
}
|
|
||||||
|
|
||||||
let verb_ref = verb.as_ref().map(|v| &v.1);
|
let verb_ref = verb.as_ref().map(|v| &v.1);
|
||||||
let motion_ref = motion.as_ref().map(|m| &m.1);
|
let motion_ref = motion.as_ref().map(|m| &m.1);
|
||||||
@@ -1185,7 +1184,7 @@ impl ViVisual {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'?' => {
|
'?' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -1194,7 +1193,7 @@ impl ViVisual {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
_ => break 'verb_parse None,
|
_ => break 'verb_parse None,
|
||||||
}
|
}
|
||||||
@@ -1209,7 +1208,7 @@ impl ViVisual {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'x' => {
|
'x' => {
|
||||||
chars = chars_clone;
|
chars = chars_clone;
|
||||||
@@ -1222,7 +1221,7 @@ impl ViVisual {
|
|||||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'Y' => {
|
'Y' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -1231,7 +1230,7 @@ impl ViVisual {
|
|||||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'D' => {
|
'D' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -1240,7 +1239,7 @@ impl ViVisual {
|
|||||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'R' | 'C' => {
|
'R' | 'C' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -1249,7 +1248,7 @@ impl ViVisual {
|
|||||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'>' => {
|
'>' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -1258,7 +1257,7 @@ impl ViVisual {
|
|||||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'<' => {
|
'<' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -1267,7 +1266,7 @@ impl ViVisual {
|
|||||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'=' => {
|
'=' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -1276,7 +1275,7 @@ impl ViVisual {
|
|||||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'p' | 'P' => {
|
'p' | 'P' => {
|
||||||
chars = chars_clone;
|
chars = chars_clone;
|
||||||
@@ -1299,7 +1298,7 @@ impl ViVisual {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'u' => {
|
'u' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -1308,7 +1307,7 @@ impl ViVisual {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'U' => {
|
'U' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -1317,7 +1316,7 @@ impl ViVisual {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'O' | 'o' => {
|
'O' | 'o' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -1326,7 +1325,7 @@ impl ViVisual {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'A' => {
|
'A' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -1335,7 +1334,7 @@ impl ViVisual {
|
|||||||
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'I' => {
|
'I' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -1344,7 +1343,7 @@ impl ViVisual {
|
|||||||
motion: Some(MotionCmd(1, Motion::BeginningOfLine)),
|
motion: Some(MotionCmd(1, Motion::BeginningOfLine)),
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'J' => {
|
'J' => {
|
||||||
return Some(ViCmd {
|
return Some(ViCmd {
|
||||||
@@ -1353,7 +1352,7 @@ impl ViVisual {
|
|||||||
motion: None,
|
motion: None,
|
||||||
raw_seq: self.take_cmd(),
|
raw_seq: self.take_cmd(),
|
||||||
flags: CmdFlags::empty(),
|
flags: CmdFlags::empty(),
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
'y' => {
|
'y' => {
|
||||||
chars = chars_clone;
|
chars = chars_clone;
|
||||||
@@ -1395,7 +1394,7 @@ impl ViVisual {
|
|||||||
| ('=', Some(VerbCmd(_, Verb::Equalize)))
|
| ('=', Some(VerbCmd(_, Verb::Equalize)))
|
||||||
| ('>', Some(VerbCmd(_, Verb::Indent)))
|
| ('>', Some(VerbCmd(_, Verb::Indent)))
|
||||||
| ('<', Some(VerbCmd(_, Verb::Dedent))) => {
|
| ('<', Some(VerbCmd(_, Verb::Dedent))) => {
|
||||||
break 'motion_parse Some(MotionCmd(count, Motion::WholeLine))
|
break 'motion_parse Some(MotionCmd(count, Motion::WholeLine));
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
@@ -1652,8 +1651,7 @@ impl ViVisual {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
if chars.peek().is_some() {
|
if chars.peek().is_some() {}
|
||||||
}
|
|
||||||
|
|
||||||
let verb_ref = verb.as_ref().map(|v| &v.1);
|
let verb_ref = verb.as_ref().map(|v| &v.1);
|
||||||
let motion_ref = motion.as_ref().map(|m| &m.1);
|
let motion_ref = motion.as_ref().map(|m| &m.1);
|
||||||
|
|||||||
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")
|
Note::new("'shopt' takes arguments separated by periods to denote namespaces")
|
||||||
.with_sub_notes(vec!["Example: 'shopt core.autocd=true'"]),
|
.with_sub_notes(vec!["Example: 'shopt core.autocd=true'"]),
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -263,7 +263,7 @@ impl ShOptCore {
|
|||||||
"max_recurse_depth",
|
"max_recurse_depth",
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -445,18 +445,20 @@ impl ShOptPrompt {
|
|||||||
ShErrKind::SyntaxErr,
|
ShErrKind::SyntaxErr,
|
||||||
format!("shopt: Unexpected 'prompt' option '{opt}'"),
|
format!("shopt: Unexpected 'prompt' option '{opt}'"),
|
||||||
)
|
)
|
||||||
.with_note(Note::new("options can be accessed like 'prompt.option_name'"))
|
.with_note(Note::new(
|
||||||
|
"options can be accessed like 'prompt.option_name'",
|
||||||
|
))
|
||||||
.with_note(
|
.with_note(
|
||||||
Note::new("'prompt' contains the following options").with_sub_notes(vec![
|
Note::new("'prompt' contains the following options").with_sub_notes(vec![
|
||||||
"trunc_prompt_path",
|
"trunc_prompt_path",
|
||||||
"edit_mode",
|
"edit_mode",
|
||||||
"comp_limit",
|
"comp_limit",
|
||||||
"highlight",
|
"highlight",
|
||||||
"tab_stop",
|
"tab_stop",
|
||||||
"custom",
|
"custom",
|
||||||
]),
|
]),
|
||||||
),
|
),
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
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 nix::sys::signal::{SaFlags, SigAction, sigaction};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
builtin::trap::TrapTarget, jobs::{JobCmdFlags, JobID, take_term}, libsh::error::{ShErr, ShErrKind, ShResult}, parse::execute::exec_input, prelude::*, state::{read_jobs, read_logic, write_jobs, write_meta}
|
builtin::trap::TrapTarget,
|
||||||
|
jobs::{JobCmdFlags, JobID, take_term},
|
||||||
|
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||||
|
parse::execute::exec_input,
|
||||||
|
prelude::*,
|
||||||
|
state::{read_jobs, read_logic, write_jobs, write_meta},
|
||||||
};
|
};
|
||||||
|
|
||||||
static SIGNALS: AtomicU64 = AtomicU64::new(0);
|
static SIGNALS: AtomicU64 = AtomicU64::new(0);
|
||||||
@@ -12,92 +17,91 @@ pub static REAPING_ENABLED: AtomicBool = AtomicBool::new(true);
|
|||||||
pub static SHOULD_QUIT: AtomicBool = AtomicBool::new(false);
|
pub static SHOULD_QUIT: AtomicBool = AtomicBool::new(false);
|
||||||
pub static QUIT_CODE: AtomicI32 = AtomicI32::new(0);
|
pub static QUIT_CODE: AtomicI32 = AtomicI32::new(0);
|
||||||
|
|
||||||
const MISC_SIGNALS: [Signal;22] = [
|
const MISC_SIGNALS: [Signal; 22] = [
|
||||||
Signal::SIGILL,
|
Signal::SIGILL,
|
||||||
Signal::SIGTRAP,
|
Signal::SIGTRAP,
|
||||||
Signal::SIGABRT,
|
Signal::SIGABRT,
|
||||||
Signal::SIGBUS,
|
Signal::SIGBUS,
|
||||||
Signal::SIGFPE,
|
Signal::SIGFPE,
|
||||||
Signal::SIGUSR1,
|
Signal::SIGUSR1,
|
||||||
Signal::SIGSEGV,
|
Signal::SIGSEGV,
|
||||||
Signal::SIGUSR2,
|
Signal::SIGUSR2,
|
||||||
Signal::SIGPIPE,
|
Signal::SIGPIPE,
|
||||||
Signal::SIGALRM,
|
Signal::SIGALRM,
|
||||||
Signal::SIGTERM,
|
Signal::SIGTERM,
|
||||||
Signal::SIGSTKFLT,
|
Signal::SIGSTKFLT,
|
||||||
Signal::SIGCONT,
|
Signal::SIGCONT,
|
||||||
Signal::SIGURG,
|
Signal::SIGURG,
|
||||||
Signal::SIGXCPU,
|
Signal::SIGXCPU,
|
||||||
Signal::SIGXFSZ,
|
Signal::SIGXFSZ,
|
||||||
Signal::SIGVTALRM,
|
Signal::SIGVTALRM,
|
||||||
Signal::SIGPROF,
|
Signal::SIGPROF,
|
||||||
Signal::SIGWINCH,
|
Signal::SIGWINCH,
|
||||||
Signal::SIGIO,
|
Signal::SIGIO,
|
||||||
Signal::SIGPWR,
|
Signal::SIGPWR,
|
||||||
Signal::SIGSYS,
|
Signal::SIGSYS,
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn signals_pending() -> bool {
|
pub fn signals_pending() -> bool {
|
||||||
SIGNALS.load(Ordering::SeqCst) != 0 || SHOULD_QUIT.load(Ordering::SeqCst)
|
SIGNALS.load(Ordering::SeqCst) != 0 || SHOULD_QUIT.load(Ordering::SeqCst)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn check_signals() -> ShResult<()> {
|
pub fn check_signals() -> ShResult<()> {
|
||||||
let pending = SIGNALS.swap(0, Ordering::SeqCst);
|
let pending = SIGNALS.swap(0, Ordering::SeqCst);
|
||||||
let got_signal = |sig: Signal| -> bool { pending & (1 << sig as u64) != 0 };
|
let got_signal = |sig: Signal| -> bool { pending & (1 << sig as u64) != 0 };
|
||||||
let run_trap = |sig: Signal| -> ShResult<()> {
|
let run_trap = |sig: Signal| -> ShResult<()> {
|
||||||
if let Some(command) = read_logic(|l| l.get_trap(TrapTarget::Signal(sig))) {
|
if let Some(command) = read_logic(|l| l.get_trap(TrapTarget::Signal(sig))) {
|
||||||
exec_input(command, None, false)?;
|
exec_input(command, None, false)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
};
|
};
|
||||||
|
|
||||||
if got_signal(Signal::SIGINT) {
|
if got_signal(Signal::SIGINT) {
|
||||||
interrupt()?;
|
interrupt()?;
|
||||||
run_trap(Signal::SIGINT)?;
|
run_trap(Signal::SIGINT)?;
|
||||||
return Err(ShErr::simple(ShErrKind::ClearReadline, ""));
|
return Err(ShErr::simple(ShErrKind::ClearReadline, ""));
|
||||||
}
|
}
|
||||||
if got_signal(Signal::SIGHUP) {
|
if got_signal(Signal::SIGHUP) {
|
||||||
run_trap(Signal::SIGHUP)?;
|
run_trap(Signal::SIGHUP)?;
|
||||||
hang_up(0);
|
hang_up(0);
|
||||||
}
|
}
|
||||||
if got_signal(Signal::SIGQUIT) {
|
if got_signal(Signal::SIGQUIT) {
|
||||||
run_trap(Signal::SIGQUIT)?;
|
run_trap(Signal::SIGQUIT)?;
|
||||||
hang_up(0);
|
hang_up(0);
|
||||||
}
|
}
|
||||||
if got_signal(Signal::SIGTSTP) {
|
if got_signal(Signal::SIGTSTP) {
|
||||||
run_trap(Signal::SIGTSTP)?;
|
run_trap(Signal::SIGTSTP)?;
|
||||||
terminal_stop()?;
|
terminal_stop()?;
|
||||||
}
|
}
|
||||||
if got_signal(Signal::SIGCHLD) && REAPING_ENABLED.load(Ordering::SeqCst) {
|
if got_signal(Signal::SIGCHLD) && REAPING_ENABLED.load(Ordering::SeqCst) {
|
||||||
run_trap(Signal::SIGCHLD)?;
|
run_trap(Signal::SIGCHLD)?;
|
||||||
wait_child()?;
|
wait_child()?;
|
||||||
}
|
}
|
||||||
|
|
||||||
for sig in MISC_SIGNALS {
|
for sig in MISC_SIGNALS {
|
||||||
if got_signal(sig) {
|
if got_signal(sig) {
|
||||||
run_trap(sig)?;
|
run_trap(sig)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if SHOULD_QUIT.load(Ordering::SeqCst) {
|
if SHOULD_QUIT.load(Ordering::SeqCst) {
|
||||||
let code = QUIT_CODE.load(Ordering::SeqCst);
|
let code = QUIT_CODE.load(Ordering::SeqCst);
|
||||||
return Err(ShErr::simple(ShErrKind::CleanExit(code), "exit"));
|
return Err(ShErr::simple(ShErrKind::CleanExit(code), "exit"));
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn disable_reaping() {
|
pub fn disable_reaping() {
|
||||||
REAPING_ENABLED.store(false, Ordering::SeqCst);
|
REAPING_ENABLED.store(false, Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
pub fn enable_reaping() {
|
pub fn enable_reaping() {
|
||||||
REAPING_ENABLED.store(true, Ordering::SeqCst);
|
REAPING_ENABLED.store(true, Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn sig_setup() {
|
pub fn sig_setup() {
|
||||||
let flags = SaFlags::empty();
|
let flags = SaFlags::empty();
|
||||||
|
|
||||||
let action = SigAction::new(SigHandler::Handler(handle_signal), flags, SigSet::empty());
|
|
||||||
|
|
||||||
|
let action = SigAction::new(SigHandler::Handler(handle_signal), flags, SigSet::empty());
|
||||||
|
|
||||||
let ignore = SigAction::new(SigHandler::SigIgn, flags, SigSet::empty());
|
let ignore = SigAction::new(SigHandler::SigIgn, flags, SigSet::empty());
|
||||||
|
|
||||||
@@ -136,12 +140,12 @@ pub fn sig_setup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extern "C" fn handle_signal(sig: libc::c_int) {
|
extern "C" fn handle_signal(sig: libc::c_int) {
|
||||||
SIGNALS.fetch_or(1 << sig, Ordering::SeqCst);
|
SIGNALS.fetch_or(1 << sig, Ordering::SeqCst);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn hang_up(_: libc::c_int) {
|
pub fn hang_up(_: libc::c_int) {
|
||||||
SHOULD_QUIT.store(true, Ordering::SeqCst);
|
SHOULD_QUIT.store(true, Ordering::SeqCst);
|
||||||
QUIT_CODE.store(1, Ordering::SeqCst);
|
QUIT_CODE.store(1, Ordering::SeqCst);
|
||||||
write_jobs(|j| {
|
write_jobs(|j| {
|
||||||
for job in j.jobs_mut().iter_mut().flatten() {
|
for job in j.jobs_mut().iter_mut().flatten() {
|
||||||
job.killpg(Signal::SIGTERM).ok();
|
job.killpg(Signal::SIGTERM).ok();
|
||||||
@@ -154,10 +158,10 @@ pub fn terminal_stop() -> ShResult<()> {
|
|||||||
if let Some(job) = j.get_fg_mut() {
|
if let Some(job) = j.get_fg_mut() {
|
||||||
job.killpg(Signal::SIGTSTP)
|
job.killpg(Signal::SIGTSTP)
|
||||||
} else {
|
} else {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
// TODO: It seems like there is supposed to be a take_term() call here
|
// TODO: It seems like there is supposed to be a take_term() call here
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn interrupt() -> ShResult<()> {
|
pub fn interrupt() -> ShResult<()> {
|
||||||
@@ -269,19 +273,19 @@ pub fn child_exited(pid: Pid, status: WtStat) -> ShResult<()> {
|
|||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
})
|
}) && is_finished
|
||||||
&& is_finished {
|
{
|
||||||
if is_fg {
|
if is_fg {
|
||||||
take_term()?;
|
take_term()?;
|
||||||
} else {
|
} else {
|
||||||
println!();
|
println!();
|
||||||
let job_order = read_jobs(|j| j.order().to_vec());
|
let job_order = read_jobs(|j| j.order().to_vec());
|
||||||
let result = read_jobs(|j| j.query(JobID::Pgid(pgid)).cloned());
|
let result = read_jobs(|j| j.query(JobID::Pgid(pgid)).cloned());
|
||||||
if let Some(job) = result {
|
if let Some(job) = result {
|
||||||
let job_complete_msg = job.display(&job_order, JobCmdFlags::PIDS).to_string();
|
let job_complete_msg = job.display(&job_order, JobCmdFlags::PIDS).to_string();
|
||||||
write_meta(|m| m.post_system_message(job_complete_msg))
|
write_meta(|m| m.post_system_message(job_complete_msg))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
783
src/state.rs
783
src/state.rs
@@ -1,249 +1,262 @@
|
|||||||
use std::{
|
use std::{
|
||||||
cell::RefCell, collections::{HashMap, VecDeque}, fmt::Display, ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref}, str::FromStr, time::Duration
|
cell::RefCell,
|
||||||
|
collections::{HashMap, VecDeque},
|
||||||
|
fmt::Display,
|
||||||
|
ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref},
|
||||||
|
str::FromStr,
|
||||||
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use nix::unistd::{gethostname, getppid, User};
|
use nix::unistd::{User, gethostname, getppid};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
builtin::trap::TrapTarget, exec_input, jobs::JobTab, libsh::{
|
builtin::trap::TrapTarget,
|
||||||
|
exec_input,
|
||||||
|
jobs::JobTab,
|
||||||
|
libsh::{
|
||||||
error::{ShErr, ShErrKind, ShResult},
|
error::{ShErr, ShErrKind, ShResult},
|
||||||
utils::VecDequeExt,
|
utils::VecDequeExt,
|
||||||
}, parse::{ConjunctNode, NdRule, Node, ParsedSrc}, prelude::*, shopt::ShOpts
|
},
|
||||||
|
parse::{ConjunctNode, NdRule, Node, ParsedSrc},
|
||||||
|
prelude::*,
|
||||||
|
shopt::ShOpts,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Fern {
|
pub struct Fern {
|
||||||
pub jobs: RefCell<JobTab>,
|
pub jobs: RefCell<JobTab>,
|
||||||
pub var_scopes: RefCell<ScopeStack>,
|
pub var_scopes: RefCell<ScopeStack>,
|
||||||
pub meta: RefCell<MetaTab>,
|
pub meta: RefCell<MetaTab>,
|
||||||
pub logic: RefCell<LogTab>,
|
pub logic: RefCell<LogTab>,
|
||||||
pub shopts: RefCell<ShOpts>,
|
pub shopts: RefCell<ShOpts>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Fern {
|
impl Fern {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
Self {
|
Self {
|
||||||
jobs: RefCell::new(JobTab::new()),
|
jobs: RefCell::new(JobTab::new()),
|
||||||
var_scopes: RefCell::new(ScopeStack::new()),
|
var_scopes: RefCell::new(ScopeStack::new()),
|
||||||
meta: RefCell::new(MetaTab::new()),
|
meta: RefCell::new(MetaTab::new()),
|
||||||
logic: RefCell::new(LogTab::new()),
|
logic: RefCell::new(LogTab::new()),
|
||||||
shopts: RefCell::new(ShOpts::default()),
|
shopts: RefCell::new(ShOpts::default()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Fern {
|
impl Default for Fern {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self::new()
|
Self::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Hash, Eq, PartialEq, Debug, Clone, Copy)]
|
#[derive(Hash, Eq, PartialEq, Debug, Clone, Copy)]
|
||||||
pub enum ShellParam {
|
pub enum ShellParam {
|
||||||
// Global
|
// Global
|
||||||
Status,
|
Status,
|
||||||
ShPid,
|
ShPid,
|
||||||
LastJob,
|
LastJob,
|
||||||
ShellName,
|
ShellName,
|
||||||
|
|
||||||
// Local
|
// Local
|
||||||
Pos(usize),
|
Pos(usize),
|
||||||
AllArgs,
|
AllArgs,
|
||||||
AllArgsStr,
|
AllArgsStr,
|
||||||
ArgCount
|
ArgCount,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ShellParam {
|
impl ShellParam {
|
||||||
pub fn is_global(&self) -> bool {
|
pub fn is_global(&self) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self,
|
self,
|
||||||
Self::Status | Self::ShPid | Self::LastJob | Self::ShellName
|
Self::Status | Self::ShPid | Self::LastJob | Self::ShellName
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for ShellParam {
|
impl Display for ShellParam {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
Self::Status => write!(f, "?"),
|
Self::Status => write!(f, "?"),
|
||||||
Self::ShPid => write!(f, "$"),
|
Self::ShPid => write!(f, "$"),
|
||||||
Self::LastJob => write!(f, "!"),
|
Self::LastJob => write!(f, "!"),
|
||||||
Self::ShellName => write!(f, "0"),
|
Self::ShellName => write!(f, "0"),
|
||||||
Self::Pos(n) => write!(f, "{}", n),
|
Self::Pos(n) => write!(f, "{}", n),
|
||||||
Self::AllArgs => write!(f, "@"),
|
Self::AllArgs => write!(f, "@"),
|
||||||
Self::AllArgsStr => write!(f, "*"),
|
Self::AllArgsStr => write!(f, "*"),
|
||||||
Self::ArgCount => write!(f, "#"),
|
Self::ArgCount => write!(f, "#"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ShellParam {
|
impl FromStr for ShellParam {
|
||||||
type Err = ShErr;
|
type Err = ShErr;
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
match s {
|
match s {
|
||||||
"?" => Ok(Self::Status),
|
"?" => Ok(Self::Status),
|
||||||
"$" => Ok(Self::ShPid),
|
"$" => Ok(Self::ShPid),
|
||||||
"!" => Ok(Self::LastJob),
|
"!" => Ok(Self::LastJob),
|
||||||
"0" => Ok(Self::ShellName),
|
"0" => Ok(Self::ShellName),
|
||||||
"@" => Ok(Self::AllArgs),
|
"@" => Ok(Self::AllArgs),
|
||||||
"*" => Ok(Self::AllArgsStr),
|
"*" => Ok(Self::AllArgsStr),
|
||||||
"#" => Ok(Self::ArgCount),
|
"#" => Ok(Self::ArgCount),
|
||||||
n if n.parse::<usize>().is_ok() => {
|
n if n.parse::<usize>().is_ok() => {
|
||||||
let idx = n.parse::<usize>().unwrap();
|
let idx = n.parse::<usize>().unwrap();
|
||||||
Ok(Self::Pos(idx))
|
Ok(Self::Pos(idx))
|
||||||
}
|
}
|
||||||
_ => Err(ShErr::simple(
|
_ => Err(ShErr::simple(
|
||||||
ShErrKind::InternalErr,
|
ShErrKind::InternalErr,
|
||||||
format!("Invalid shell parameter: {}", s),
|
format!("Invalid shell parameter: {}", s),
|
||||||
)),
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default, Debug)]
|
#[derive(Clone, Default, Debug)]
|
||||||
pub struct ScopeStack {
|
pub struct ScopeStack {
|
||||||
// ALWAYS keep one scope.
|
// ALWAYS keep one scope.
|
||||||
// The bottom scope is the global variable space.
|
// The bottom scope is the global variable space.
|
||||||
// Scopes that come after that are pushed in functions,
|
// Scopes that come after that are pushed in functions,
|
||||||
// and only contain variables that are defined using `local`.
|
// and only contain variables that are defined using `local`.
|
||||||
scopes: Vec<VarTab>,
|
scopes: Vec<VarTab>,
|
||||||
depth: u32,
|
depth: u32,
|
||||||
|
|
||||||
// Global parameters such as $?, $!, $$, etc
|
// Global parameters such as $?, $!, $$, etc
|
||||||
global_params: HashMap<String, String>,
|
global_params: HashMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ScopeStack {
|
impl ScopeStack {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let mut new = Self::default();
|
let mut new = Self::default();
|
||||||
new.scopes.push(VarTab::new());
|
new.scopes.push(VarTab::new());
|
||||||
let shell_name = std::env::args().next().unwrap_or_else(|| "fern".to_string());
|
let shell_name = std::env::args()
|
||||||
new.global_params.insert(ShellParam::ShellName.to_string(), shell_name);
|
.next()
|
||||||
new
|
.unwrap_or_else(|| "fern".to_string());
|
||||||
}
|
new
|
||||||
pub fn descend(&mut self, argv: Option<Vec<String>>) {
|
.global_params
|
||||||
let mut new_vars = VarTab::new();
|
.insert(ShellParam::ShellName.to_string(), shell_name);
|
||||||
if let Some(argv) = argv {
|
new
|
||||||
for arg in argv {
|
}
|
||||||
new_vars.bpush_arg(arg);
|
pub fn descend(&mut self, argv: Option<Vec<String>>) {
|
||||||
}
|
let mut new_vars = VarTab::new();
|
||||||
}
|
if let Some(argv) = argv {
|
||||||
self.scopes.push(new_vars);
|
for arg in argv {
|
||||||
self.depth += 1;
|
new_vars.bpush_arg(arg);
|
||||||
}
|
}
|
||||||
pub fn ascend(&mut self) {
|
}
|
||||||
if self.depth >= 1 {
|
self.scopes.push(new_vars);
|
||||||
self.scopes.pop();
|
self.depth += 1;
|
||||||
self.depth -= 1;
|
}
|
||||||
}
|
pub fn ascend(&mut self) {
|
||||||
}
|
if self.depth >= 1 {
|
||||||
pub fn cur_scope(&self) -> &VarTab {
|
self.scopes.pop();
|
||||||
self.scopes.last().unwrap()
|
self.depth -= 1;
|
||||||
}
|
}
|
||||||
pub fn cur_scope_mut(&mut self) -> &mut VarTab {
|
}
|
||||||
self.scopes.last_mut().unwrap()
|
pub fn cur_scope(&self) -> &VarTab {
|
||||||
}
|
self.scopes.last().unwrap()
|
||||||
pub fn unset_var(&mut self, var_name: &str) {
|
}
|
||||||
for scope in self.scopes.iter_mut().rev() {
|
pub fn cur_scope_mut(&mut self) -> &mut VarTab {
|
||||||
if scope.var_exists(var_name) {
|
self.scopes.last_mut().unwrap()
|
||||||
scope.unset_var(var_name);
|
}
|
||||||
return;
|
pub fn unset_var(&mut self, var_name: &str) {
|
||||||
}
|
for scope in self.scopes.iter_mut().rev() {
|
||||||
}
|
if scope.var_exists(var_name) {
|
||||||
}
|
scope.unset_var(var_name);
|
||||||
pub fn export_var(&mut self, var_name: &str) {
|
return;
|
||||||
for scope in self.scopes.iter_mut().rev() {
|
}
|
||||||
if scope.var_exists(var_name) {
|
}
|
||||||
scope.export_var(var_name);
|
}
|
||||||
return;
|
pub fn export_var(&mut self, var_name: &str) {
|
||||||
}
|
for scope in self.scopes.iter_mut().rev() {
|
||||||
}
|
if scope.var_exists(var_name) {
|
||||||
}
|
scope.export_var(var_name);
|
||||||
pub fn var_exists(&self, var_name: &str) -> bool {
|
return;
|
||||||
for scope in self.scopes.iter().rev() {
|
}
|
||||||
if scope.var_exists(var_name) {
|
}
|
||||||
return true;
|
}
|
||||||
}
|
pub fn var_exists(&self, var_name: &str) -> bool {
|
||||||
}
|
for scope in self.scopes.iter().rev() {
|
||||||
if let Ok(param) = var_name.parse::<ShellParam>() {
|
if scope.var_exists(var_name) {
|
||||||
return self.global_params.contains_key(¶m.to_string());
|
return true;
|
||||||
}
|
}
|
||||||
false
|
}
|
||||||
}
|
if let Ok(param) = var_name.parse::<ShellParam>() {
|
||||||
pub fn flatten_vars(&self) -> HashMap<String, Var> {
|
return self.global_params.contains_key(¶m.to_string());
|
||||||
let mut flat_vars = HashMap::new();
|
}
|
||||||
for scope in self.scopes.iter() {
|
false
|
||||||
for (var_name, var) in scope.vars() {
|
}
|
||||||
flat_vars.insert(var_name.clone(), var.clone());
|
pub fn flatten_vars(&self) -> HashMap<String, Var> {
|
||||||
}
|
let mut flat_vars = HashMap::new();
|
||||||
}
|
for scope in self.scopes.iter() {
|
||||||
flat_vars
|
for (var_name, var) in scope.vars() {
|
||||||
}
|
flat_vars.insert(var_name.clone(), var.clone());
|
||||||
pub fn set_var(&mut self, var_name: &str, val: &str, flags: VarFlags) {
|
}
|
||||||
if flags.contains(VarFlags::LOCAL) {
|
}
|
||||||
self.set_var_local(var_name, val, flags);
|
flat_vars
|
||||||
} else {
|
}
|
||||||
self.set_var_global(var_name, val, flags);
|
pub fn set_var(&mut self, var_name: &str, val: &str, flags: VarFlags) {
|
||||||
}
|
if flags.contains(VarFlags::LOCAL) {
|
||||||
}
|
self.set_var_local(var_name, val, flags);
|
||||||
fn set_var_global(&mut self, var_name: &str, val: &str, flags: VarFlags) {
|
} else {
|
||||||
if let Some(scope) = self.scopes.first_mut() {
|
self.set_var_global(var_name, val, flags);
|
||||||
scope.set_var(var_name, val, flags);
|
}
|
||||||
}
|
}
|
||||||
}
|
fn set_var_global(&mut self, var_name: &str, val: &str, flags: VarFlags) {
|
||||||
fn set_var_local(&mut self, var_name: &str, val: &str, flags: VarFlags) {
|
if let Some(scope) = self.scopes.first_mut() {
|
||||||
if let Some(scope) = self.scopes.last_mut() {
|
scope.set_var(var_name, val, flags);
|
||||||
scope.set_var(var_name, val, flags);
|
}
|
||||||
}
|
}
|
||||||
}
|
fn set_var_local(&mut self, var_name: &str, val: &str, flags: VarFlags) {
|
||||||
pub fn get_var(&self, var_name: &str) -> String {
|
if let Some(scope) = self.scopes.last_mut() {
|
||||||
if let Ok(param) = var_name.parse::<ShellParam>() {
|
scope.set_var(var_name, val, flags);
|
||||||
return self.get_param(param);
|
}
|
||||||
}
|
}
|
||||||
for scope in self.scopes.iter().rev() {
|
pub fn get_var(&self, var_name: &str) -> String {
|
||||||
if scope.var_exists(var_name) {
|
if let Ok(param) = var_name.parse::<ShellParam>() {
|
||||||
return scope.get_var(var_name);
|
return self.get_param(param);
|
||||||
}
|
}
|
||||||
}
|
for scope in self.scopes.iter().rev() {
|
||||||
// Fallback to env var
|
if scope.var_exists(var_name) {
|
||||||
std::env::var(var_name).unwrap_or_default()
|
return scope.get_var(var_name);
|
||||||
}
|
}
|
||||||
pub fn get_param(&self, param: ShellParam) -> String {
|
}
|
||||||
if param.is_global() && let Some(val) = self.global_params.get(¶m.to_string()) {
|
// Fallback to env var
|
||||||
return val.clone();
|
std::env::var(var_name).unwrap_or_default()
|
||||||
}
|
}
|
||||||
for scope in self.scopes.iter().rev() {
|
pub fn get_param(&self, param: ShellParam) -> String {
|
||||||
let val = scope.get_param(param);
|
if param.is_global()
|
||||||
if !val.is_empty() {
|
&& let Some(val) = self.global_params.get(¶m.to_string())
|
||||||
return val;
|
{
|
||||||
}
|
return val.clone();
|
||||||
}
|
}
|
||||||
// Fallback to empty string
|
for scope in self.scopes.iter().rev() {
|
||||||
"".into()
|
let val = scope.get_param(param);
|
||||||
}
|
if !val.is_empty() {
|
||||||
/// Set a shell parameter
|
return val;
|
||||||
/// Therefore, these are global state and we use the global scope
|
}
|
||||||
pub fn set_param(&mut self, param: ShellParam, val: &str) {
|
}
|
||||||
match param {
|
// Fallback to empty string
|
||||||
ShellParam::ShPid |
|
"".into()
|
||||||
ShellParam::Status |
|
}
|
||||||
ShellParam::LastJob |
|
/// Set a shell parameter
|
||||||
ShellParam::ShellName => {
|
/// Therefore, these are global state and we use the global scope
|
||||||
self.global_params.insert(param.to_string(), val.to_string());
|
pub fn set_param(&mut self, param: ShellParam, val: &str) {
|
||||||
}
|
match param {
|
||||||
ShellParam::Pos(_) |
|
ShellParam::ShPid | ShellParam::Status | ShellParam::LastJob | ShellParam::ShellName => {
|
||||||
ShellParam::AllArgs |
|
self
|
||||||
ShellParam::AllArgsStr |
|
.global_params
|
||||||
ShellParam::ArgCount => {
|
.insert(param.to_string(), val.to_string());
|
||||||
if let Some(scope) = self.scopes.first_mut() {
|
}
|
||||||
scope.set_param(param, val);
|
ShellParam::Pos(_) | ShellParam::AllArgs | ShellParam::AllArgsStr | ShellParam::ArgCount => {
|
||||||
}
|
if let Some(scope) = self.scopes.first_mut() {
|
||||||
}
|
scope.set_param(param, val);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
pub static FERN: Fern = Fern::new();
|
pub static FERN: Fern = Fern::new();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A shell function
|
/// A shell function
|
||||||
@@ -287,7 +300,7 @@ impl Deref for ShFunc {
|
|||||||
pub struct LogTab {
|
pub struct LogTab {
|
||||||
functions: HashMap<String, ShFunc>,
|
functions: HashMap<String, ShFunc>,
|
||||||
aliases: HashMap<String, String>,
|
aliases: HashMap<String, String>,
|
||||||
traps: HashMap<TrapTarget, String>,
|
traps: HashMap<TrapTarget, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LogTab {
|
impl LogTab {
|
||||||
@@ -297,18 +310,18 @@ impl LogTab {
|
|||||||
pub fn insert_func(&mut self, name: &str, src: ShFunc) {
|
pub fn insert_func(&mut self, name: &str, src: ShFunc) {
|
||||||
self.functions.insert(name.into(), src);
|
self.functions.insert(name.into(), src);
|
||||||
}
|
}
|
||||||
pub fn insert_trap(&mut self, target: TrapTarget, command: String) {
|
pub fn insert_trap(&mut self, target: TrapTarget, command: String) {
|
||||||
self.traps.insert(target, command);
|
self.traps.insert(target, command);
|
||||||
}
|
}
|
||||||
pub fn get_trap(&self, target: TrapTarget) -> Option<String> {
|
pub fn get_trap(&self, target: TrapTarget) -> Option<String> {
|
||||||
self.traps.get(&target).cloned()
|
self.traps.get(&target).cloned()
|
||||||
}
|
}
|
||||||
pub fn remove_trap(&mut self, target: TrapTarget) {
|
pub fn remove_trap(&mut self, target: TrapTarget) {
|
||||||
self.traps.remove(&target);
|
self.traps.remove(&target);
|
||||||
}
|
}
|
||||||
pub fn traps(&self) -> &HashMap<TrapTarget, String> {
|
pub fn traps(&self) -> &HashMap<TrapTarget, String> {
|
||||||
&self.traps
|
&self.traps
|
||||||
}
|
}
|
||||||
pub fn get_func(&self, name: &str) -> Option<ShFunc> {
|
pub fn get_func(&self, name: &str) -> Option<ShFunc> {
|
||||||
self.functions.get(name).cloned()
|
self.functions.get(name).cloned()
|
||||||
}
|
}
|
||||||
@@ -339,103 +352,103 @@ impl LogTab {
|
|||||||
pub struct VarFlags(u8);
|
pub struct VarFlags(u8);
|
||||||
|
|
||||||
impl VarFlags {
|
impl VarFlags {
|
||||||
pub const NONE : Self = Self(0);
|
pub const NONE: Self = Self(0);
|
||||||
pub const EXPORT : Self = Self(1 << 0);
|
pub const EXPORT: Self = Self(1 << 0);
|
||||||
pub const LOCAL : Self = Self(1 << 1);
|
pub const LOCAL: Self = Self(1 << 1);
|
||||||
pub const READONLY : Self = Self(1 << 2);
|
pub const READONLY: Self = Self(1 << 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BitOr for VarFlags {
|
impl BitOr for VarFlags {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
fn bitor(self, rhs: Self) -> Self::Output {
|
fn bitor(self, rhs: Self) -> Self::Output {
|
||||||
Self(self.0 | rhs.0)
|
Self(self.0 | rhs.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BitOrAssign for VarFlags {
|
impl BitOrAssign for VarFlags {
|
||||||
fn bitor_assign(&mut self, rhs: Self) {
|
fn bitor_assign(&mut self, rhs: Self) {
|
||||||
self.0 |= rhs.0;
|
self.0 |= rhs.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BitAnd for VarFlags {
|
impl BitAnd for VarFlags {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
fn bitand(self, rhs: Self) -> Self::Output {
|
fn bitand(self, rhs: Self) -> Self::Output {
|
||||||
Self(self.0 & rhs.0)
|
Self(self.0 & rhs.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BitAndAssign for VarFlags {
|
impl BitAndAssign for VarFlags {
|
||||||
fn bitand_assign(&mut self, rhs: Self) {
|
fn bitand_assign(&mut self, rhs: Self) {
|
||||||
self.0 &= rhs.0;
|
self.0 &= rhs.0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VarFlags {
|
impl VarFlags {
|
||||||
pub fn contains(&self, flag: Self) -> bool {
|
pub fn contains(&self, flag: Self) -> bool {
|
||||||
(self.0 & flag.0) == flag.0
|
(self.0 & flag.0) == flag.0
|
||||||
}
|
}
|
||||||
pub fn intersects(&self, flag: Self) -> bool {
|
pub fn intersects(&self, flag: Self) -> bool {
|
||||||
(self.0 & flag.0) != 0
|
(self.0 & flag.0) != 0
|
||||||
}
|
}
|
||||||
pub fn is_empty(&self) -> bool {
|
pub fn is_empty(&self) -> bool {
|
||||||
self.0 == 0
|
self.0 == 0
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn insert(&mut self, flag: Self) {
|
pub fn insert(&mut self, flag: Self) {
|
||||||
self.0 |= flag.0;
|
self.0 |= flag.0;
|
||||||
}
|
}
|
||||||
pub fn remove(&mut self, flag: Self) {
|
pub fn remove(&mut self, flag: Self) {
|
||||||
self.0 &= !flag.0;
|
self.0 &= !flag.0;
|
||||||
}
|
}
|
||||||
pub fn toggle(&mut self, flag: Self) {
|
pub fn toggle(&mut self, flag: Self) {
|
||||||
self.0 ^= flag.0;
|
self.0 ^= flag.0;
|
||||||
}
|
}
|
||||||
pub fn set(&mut self, flag: Self, value: bool) {
|
pub fn set(&mut self, flag: Self, value: bool) {
|
||||||
if value {
|
if value {
|
||||||
self.insert(flag);
|
self.insert(flag);
|
||||||
} else {
|
} else {
|
||||||
self.remove(flag);
|
self.remove(flag);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub enum VarKind {
|
pub enum VarKind {
|
||||||
Str(String),
|
Str(String),
|
||||||
Int(i32),
|
Int(i32),
|
||||||
Arr(Vec<String>),
|
Arr(Vec<String>),
|
||||||
AssocArr(Vec<(String, String)>),
|
AssocArr(Vec<(String, String)>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for VarKind {
|
impl Display for VarKind {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
VarKind::Str(s) => write!(f, "{s}"),
|
VarKind::Str(s) => write!(f, "{s}"),
|
||||||
VarKind::Int(i) => write!(f, "{i}"),
|
VarKind::Int(i) => write!(f, "{i}"),
|
||||||
VarKind::Arr(items) => {
|
VarKind::Arr(items) => {
|
||||||
let mut item_iter = items.iter().peekable();
|
let mut item_iter = items.iter().peekable();
|
||||||
while let Some(item) = item_iter.next() {
|
while let Some(item) = item_iter.next() {
|
||||||
write!(f, "{item}")?;
|
write!(f, "{item}")?;
|
||||||
if item_iter.peek().is_some() {
|
if item_iter.peek().is_some() {
|
||||||
write!(f, " ")?;
|
write!(f, " ")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
VarKind::AssocArr(items) => {
|
VarKind::AssocArr(items) => {
|
||||||
let mut item_iter = items.iter().peekable();
|
let mut item_iter = items.iter().peekable();
|
||||||
while let Some(item) = item_iter.next() {
|
while let Some(item) = item_iter.next() {
|
||||||
let (k,v) = item;
|
let (k, v) = item;
|
||||||
write!(f, "{k}={v}")?;
|
write!(f, "{k}={v}")?;
|
||||||
if item_iter.peek().is_some() {
|
if item_iter.peek().is_some() {
|
||||||
write!(f, " ")?;
|
write!(f, " ")?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -446,26 +459,23 @@ pub struct Var {
|
|||||||
|
|
||||||
impl Var {
|
impl Var {
|
||||||
pub fn new(kind: VarKind, flags: VarFlags) -> Self {
|
pub fn new(kind: VarKind, flags: VarFlags) -> Self {
|
||||||
Self {
|
Self { flags, kind }
|
||||||
flags,
|
}
|
||||||
kind
|
pub fn kind(&self) -> &VarKind {
|
||||||
}
|
&self.kind
|
||||||
|
}
|
||||||
|
pub fn kind_mut(&mut self) -> &mut VarKind {
|
||||||
|
&mut self.kind
|
||||||
}
|
}
|
||||||
pub fn kind(&self) -> &VarKind {
|
|
||||||
&self.kind
|
|
||||||
}
|
|
||||||
pub fn kind_mut(&mut self) -> &mut VarKind {
|
|
||||||
&mut self.kind
|
|
||||||
}
|
|
||||||
pub fn mark_for_export(&mut self) {
|
pub fn mark_for_export(&mut self) {
|
||||||
self.flags.set(VarFlags::EXPORT, true);
|
self.flags.set(VarFlags::EXPORT, true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Var {
|
impl Display for Var {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
self.kind.fmt(f)
|
self.kind.fmt(f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Clone, Debug)]
|
#[derive(Default, Clone, Debug)]
|
||||||
@@ -528,23 +538,23 @@ impl VarTab {
|
|||||||
.map(|hname| hname.to_string_lossy().to_string())
|
.map(|hname| hname.to_string_lossy().to_string())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
unsafe {
|
unsafe {
|
||||||
env::set_var("IFS", " \t\n");
|
env::set_var("IFS", " \t\n");
|
||||||
env::set_var("HOST", hostname.clone());
|
env::set_var("HOST", hostname.clone());
|
||||||
env::set_var("UID", uid.to_string());
|
env::set_var("UID", uid.to_string());
|
||||||
env::set_var("PPID", getppid().to_string());
|
env::set_var("PPID", getppid().to_string());
|
||||||
env::set_var("TMPDIR", "/tmp");
|
env::set_var("TMPDIR", "/tmp");
|
||||||
env::set_var("TERM", term);
|
env::set_var("TERM", term);
|
||||||
env::set_var("LANG", "en_US.UTF-8");
|
env::set_var("LANG", "en_US.UTF-8");
|
||||||
env::set_var("USER", username.clone());
|
env::set_var("USER", username.clone());
|
||||||
env::set_var("LOGNAME", username);
|
env::set_var("LOGNAME", username);
|
||||||
env::set_var("PWD", pathbuf_to_string(std::env::current_dir()));
|
env::set_var("PWD", pathbuf_to_string(std::env::current_dir()));
|
||||||
env::set_var("OLDPWD", pathbuf_to_string(std::env::current_dir()));
|
env::set_var("OLDPWD", pathbuf_to_string(std::env::current_dir()));
|
||||||
env::set_var("HOME", home.clone());
|
env::set_var("HOME", home.clone());
|
||||||
env::set_var("SHELL", pathbuf_to_string(std::env::current_exe()));
|
env::set_var("SHELL", pathbuf_to_string(std::env::current_exe()));
|
||||||
env::set_var("FERN_HIST", format!("{}/.fernhist", home));
|
env::set_var("FERN_HIST", format!("{}/.fernhist", home));
|
||||||
env::set_var("FERN_RC", format!("{}/.fernrc", home));
|
env::set_var("FERN_RC", format!("{}/.fernrc", home));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn init_sh_argv(&mut self) {
|
pub fn init_sh_argv(&mut self) {
|
||||||
for arg in env::args() {
|
for arg in env::args() {
|
||||||
@@ -575,7 +585,10 @@ impl VarTab {
|
|||||||
self.bpush_arg(env::current_exe().unwrap().to_str().unwrap().to_string());
|
self.bpush_arg(env::current_exe().unwrap().to_str().unwrap().to_string());
|
||||||
}
|
}
|
||||||
fn update_arg_params(&mut self) {
|
fn update_arg_params(&mut self) {
|
||||||
self.set_param(ShellParam::AllArgs, &self.sh_argv.clone().to_vec()[1..].join(" "));
|
self.set_param(
|
||||||
|
ShellParam::AllArgs,
|
||||||
|
&self.sh_argv.clone().to_vec()[1..].join(" "),
|
||||||
|
);
|
||||||
self.set_param(ShellParam::ArgCount, &(self.sh_argv.len() - 1).to_string());
|
self.set_param(ShellParam::ArgCount, &(self.sh_argv.len() - 1).to_string());
|
||||||
}
|
}
|
||||||
/// Push an arg to the front of the arg deque
|
/// Push an arg to the front of the arg deque
|
||||||
@@ -619,29 +632,29 @@ impl VarTab {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn get_var(&self, var: &str) -> String {
|
pub fn get_var(&self, var: &str) -> String {
|
||||||
if let Ok(param) = var.parse::<ShellParam>() {
|
if let Ok(param) = var.parse::<ShellParam>() {
|
||||||
let param = self.get_param(param);
|
let param = self.get_param(param);
|
||||||
if !param.is_empty() {
|
if !param.is_empty() {
|
||||||
return param;
|
return param;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(var) = self.vars.get(var).map(|s| s.to_string()) {
|
if let Some(var) = self.vars.get(var).map(|s| s.to_string()) {
|
||||||
var
|
var
|
||||||
} else {
|
} else {
|
||||||
std::env::var(var).unwrap_or_default()
|
std::env::var(var).unwrap_or_default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn unset_var(&mut self, var_name: &str) {
|
pub fn unset_var(&mut self, var_name: &str) {
|
||||||
self.vars.remove(var_name);
|
self.vars.remove(var_name);
|
||||||
unsafe { env::remove_var(var_name) };
|
unsafe { env::remove_var(var_name) };
|
||||||
}
|
}
|
||||||
pub fn set_var(&mut self, var_name: &str, val: &str, flags: VarFlags) {
|
pub fn set_var(&mut self, var_name: &str, val: &str, flags: VarFlags) {
|
||||||
if let Some(var) = self.vars.get_mut(var_name) {
|
if let Some(var) = self.vars.get_mut(var_name) {
|
||||||
var.kind = VarKind::Str(val.to_string());
|
var.kind = VarKind::Str(val.to_string());
|
||||||
if var.flags.contains(VarFlags::EXPORT) || flags.contains(VarFlags::EXPORT) {
|
if var.flags.contains(VarFlags::EXPORT) || flags.contains(VarFlags::EXPORT) {
|
||||||
if flags.contains(VarFlags::EXPORT) && !var.flags.contains(VarFlags::EXPORT) {
|
if flags.contains(VarFlags::EXPORT) && !var.flags.contains(VarFlags::EXPORT) {
|
||||||
var.mark_for_export();
|
var.mark_for_export();
|
||||||
}
|
}
|
||||||
unsafe { env::set_var(var_name, val) };
|
unsafe { env::set_var(var_name, val) };
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -663,39 +676,35 @@ impl VarTab {
|
|||||||
self.params.insert(param, val.to_string());
|
self.params.insert(param, val.to_string());
|
||||||
}
|
}
|
||||||
pub fn get_param(&self, param: ShellParam) -> String {
|
pub fn get_param(&self, param: ShellParam) -> String {
|
||||||
match param {
|
match param {
|
||||||
ShellParam::Pos(n) => {
|
ShellParam::Pos(n) => self
|
||||||
self
|
.sh_argv()
|
||||||
.sh_argv()
|
.get(n)
|
||||||
.get(n)
|
.map(|s| s.to_string())
|
||||||
.map(|s| s.to_string())
|
.unwrap_or_default(),
|
||||||
.unwrap_or_default()
|
ShellParam::Status => self
|
||||||
}
|
.params
|
||||||
ShellParam::Status => {
|
.get(&ShellParam::Status)
|
||||||
self
|
.map(|s| s.to_string())
|
||||||
.params
|
.unwrap_or("0".into()),
|
||||||
.get(&ShellParam::Status)
|
_ => self
|
||||||
.map(|s| s.to_string())
|
.params
|
||||||
.unwrap_or("0".into())
|
.get(¶m)
|
||||||
}
|
.map(|s| s.to_string())
|
||||||
_ => self
|
.unwrap_or_default(),
|
||||||
.params
|
}
|
||||||
.get(¶m)
|
|
||||||
.map(|s| s.to_string())
|
|
||||||
.unwrap_or_default(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A table of metadata for the shell
|
/// A table of metadata for the shell
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
pub struct MetaTab {
|
pub struct MetaTab {
|
||||||
// command running duration
|
// command running duration
|
||||||
runtime_start: Option<Instant>,
|
runtime_start: Option<Instant>,
|
||||||
runtime_stop: Option<Instant>,
|
runtime_stop: Option<Instant>,
|
||||||
|
|
||||||
// pending system messages
|
// pending system messages
|
||||||
system_msg: Vec<String>
|
system_msg: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MetaTab {
|
impl MetaTab {
|
||||||
@@ -708,76 +717,76 @@ impl MetaTab {
|
|||||||
pub fn stop_timer(&mut self) {
|
pub fn stop_timer(&mut self) {
|
||||||
self.runtime_stop = Some(Instant::now());
|
self.runtime_stop = Some(Instant::now());
|
||||||
}
|
}
|
||||||
pub fn get_time(&self) -> Option<Duration> {
|
pub fn get_time(&self) -> Option<Duration> {
|
||||||
if let (Some(start), Some(stop)) = (self.runtime_start, self.runtime_stop) {
|
if let (Some(start), Some(stop)) = (self.runtime_start, self.runtime_stop) {
|
||||||
Some(stop.duration_since(start))
|
Some(stop.duration_since(start))
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn post_system_message(&mut self, message: String) {
|
pub fn post_system_message(&mut self, message: String) {
|
||||||
self.system_msg.push(message);
|
self.system_msg.push(message);
|
||||||
}
|
}
|
||||||
pub fn pop_system_message(&mut self) -> Option<String> {
|
pub fn pop_system_message(&mut self) -> Option<String> {
|
||||||
self.system_msg.pop()
|
self.system_msg.pop()
|
||||||
}
|
}
|
||||||
pub fn system_msg_pending(&self) -> bool {
|
pub fn system_msg_pending(&self) -> bool {
|
||||||
!self.system_msg.is_empty()
|
!self.system_msg.is_empty()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read from the job table
|
/// Read from the job table
|
||||||
pub fn read_jobs<T, F: FnOnce(&JobTab) -> T>(f: F) -> T {
|
pub fn read_jobs<T, F: FnOnce(&JobTab) -> T>(f: F) -> T {
|
||||||
FERN.with(|fern| f(&fern.jobs.borrow()))
|
FERN.with(|fern| f(&fern.jobs.borrow()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write to the job table
|
/// Write to the job table
|
||||||
pub fn write_jobs<T, F: FnOnce(&mut JobTab) -> T>(f: F) -> T {
|
pub fn write_jobs<T, F: FnOnce(&mut JobTab) -> T>(f: F) -> T {
|
||||||
FERN.with(|fern| f(&mut fern.jobs.borrow_mut()))
|
FERN.with(|fern| f(&mut fern.jobs.borrow_mut()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read from the var scope stack
|
/// Read from the var scope stack
|
||||||
pub fn read_vars<T, F: FnOnce(&ScopeStack) -> T>(f: F) -> T {
|
pub fn read_vars<T, F: FnOnce(&ScopeStack) -> T>(f: F) -> T {
|
||||||
FERN.with(|fern| f(&fern.var_scopes.borrow()))
|
FERN.with(|fern| f(&fern.var_scopes.borrow()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write to the variable table
|
/// Write to the variable table
|
||||||
pub fn write_vars<T, F: FnOnce(&mut ScopeStack) -> T>(f: F) -> T {
|
pub fn write_vars<T, F: FnOnce(&mut ScopeStack) -> T>(f: F) -> T {
|
||||||
FERN.with(|fern| f(&mut fern.var_scopes.borrow_mut()))
|
FERN.with(|fern| f(&mut fern.var_scopes.borrow_mut()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_meta<T, F: FnOnce(&MetaTab) -> T>(f: F) -> T {
|
pub fn read_meta<T, F: FnOnce(&MetaTab) -> T>(f: F) -> T {
|
||||||
FERN.with(|fern| f(&fern.meta.borrow()))
|
FERN.with(|fern| f(&fern.meta.borrow()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write to the meta table
|
/// Write to the meta table
|
||||||
pub fn write_meta<T, F: FnOnce(&mut MetaTab) -> T>(f: F) -> T {
|
pub fn write_meta<T, F: FnOnce(&mut MetaTab) -> T>(f: F) -> T {
|
||||||
FERN.with(|fern| f(&mut fern.meta.borrow_mut()))
|
FERN.with(|fern| f(&mut fern.meta.borrow_mut()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Read from the logic table
|
/// Read from the logic table
|
||||||
pub fn read_logic<T, F: FnOnce(&LogTab) -> T>(f: F) -> T {
|
pub fn read_logic<T, F: FnOnce(&LogTab) -> T>(f: F) -> T {
|
||||||
FERN.with(|fern| f(&fern.logic.borrow()))
|
FERN.with(|fern| f(&fern.logic.borrow()))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Write to the logic table
|
/// Write to the logic table
|
||||||
pub fn write_logic<T, F: FnOnce(&mut LogTab) -> T>(f: F) -> T {
|
pub fn write_logic<T, F: FnOnce(&mut LogTab) -> T>(f: F) -> T {
|
||||||
FERN.with(|fern| f(&mut fern.logic.borrow_mut()))
|
FERN.with(|fern| f(&mut fern.logic.borrow_mut()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn read_shopts<T, F: FnOnce(&ShOpts) -> T>(f: F) -> T {
|
pub fn read_shopts<T, F: FnOnce(&ShOpts) -> T>(f: F) -> T {
|
||||||
FERN.with(|fern| f(&fern.shopts.borrow()))
|
FERN.with(|fern| f(&fern.shopts.borrow()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write_shopts<T, F: FnOnce(&mut ShOpts) -> T>(f: F) -> T {
|
pub fn write_shopts<T, F: FnOnce(&mut ShOpts) -> T>(f: F) -> T {
|
||||||
FERN.with(|fern| f(&mut fern.shopts.borrow_mut()))
|
FERN.with(|fern| f(&mut fern.shopts.borrow_mut()))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn descend_scope(argv: Option<Vec<String>>) {
|
pub fn descend_scope(argv: Option<Vec<String>>) {
|
||||||
write_vars(|v| v.descend(argv));
|
write_vars(|v| v.descend(argv));
|
||||||
}
|
}
|
||||||
pub fn ascend_scope() {
|
pub fn ascend_scope() {
|
||||||
write_vars(|v| v.ascend());
|
write_vars(|v| v.ascend());
|
||||||
}
|
}
|
||||||
|
|
||||||
/// This function is used internally and ideally never sees user input
|
/// This function is used internally and ideally never sees user input
|
||||||
@@ -788,7 +797,9 @@ pub fn get_shopt(path: &str) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_status() -> i32 {
|
pub fn get_status() -> i32 {
|
||||||
read_vars(|v| v.get_param(ShellParam::Status)).parse::<i32>().unwrap()
|
read_vars(|v| v.get_param(ShellParam::Status))
|
||||||
|
.parse::<i32>()
|
||||||
|
.unwrap()
|
||||||
}
|
}
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn set_status(code: i32) {
|
pub fn set_status(code: i32) {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
|||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
|
|
||||||
use crate::expand::{perform_param_expansion, DUB_QUOTE, VAR_SUB};
|
use crate::expand::{DUB_QUOTE, VAR_SUB, perform_param_expansion};
|
||||||
use crate::state::VarFlags;
|
use crate::state::VarFlags;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -293,70 +293,78 @@ fn param_expansion_replacesuffix() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dquote_escape_dollar() {
|
fn dquote_escape_dollar() {
|
||||||
// "\$foo" should strip backslash, produce literal $foo (no expansion)
|
// "\$foo" should strip backslash, produce literal $foo (no expansion)
|
||||||
let result = unescape_str(r#""\$foo""#);
|
let result = unescape_str(r#""\$foo""#);
|
||||||
assert!(!result.contains(VAR_SUB), "Escaped $ should not become VAR_SUB");
|
assert!(
|
||||||
assert!(result.contains('$'), "Literal $ should be preserved");
|
!result.contains(VAR_SUB),
|
||||||
assert!(!result.contains('\\'), "Backslash should be stripped");
|
"Escaped $ should not become VAR_SUB"
|
||||||
|
);
|
||||||
|
assert!(result.contains('$'), "Literal $ should be preserved");
|
||||||
|
assert!(!result.contains('\\'), "Backslash should be stripped");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dquote_escape_backslash() {
|
fn dquote_escape_backslash() {
|
||||||
// "\\" in double quotes should produce a single backslash
|
// "\\" in double quotes should produce a single backslash
|
||||||
let result = unescape_str(r#""\\""#);
|
let result = unescape_str(r#""\\""#);
|
||||||
let inner: String = result.chars()
|
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
|
||||||
.filter(|&c| c != DUB_QUOTE)
|
assert_eq!(
|
||||||
.collect();
|
inner, "\\",
|
||||||
assert_eq!(inner, "\\", "Double backslash should produce single backslash");
|
"Double backslash should produce single backslash"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dquote_escape_quote() {
|
fn dquote_escape_quote() {
|
||||||
// "\"" should produce a literal double quote
|
// "\"" should produce a literal double quote
|
||||||
let result = unescape_str(r#""\"""#);
|
let result = unescape_str(r#""\"""#);
|
||||||
let inner: String = result.chars()
|
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
|
||||||
.filter(|&c| c != DUB_QUOTE)
|
assert!(
|
||||||
.collect();
|
inner.contains('"'),
|
||||||
assert!(inner.contains('"'), "Escaped quote should produce literal quote");
|
"Escaped quote should produce literal quote"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dquote_escape_backtick() {
|
fn dquote_escape_backtick() {
|
||||||
// "\`" should strip backslash, produce literal backtick
|
// "\`" should strip backslash, produce literal backtick
|
||||||
let result = unescape_str(r#""\`""#);
|
let result = unescape_str(r#""\`""#);
|
||||||
let inner: String = result.chars()
|
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
|
||||||
.filter(|&c| c != DUB_QUOTE)
|
assert_eq!(
|
||||||
.collect();
|
inner, "`",
|
||||||
assert_eq!(inner, "`", "Escaped backtick should produce literal backtick");
|
"Escaped backtick should produce literal backtick"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dquote_escape_nonspecial_preserves_backslash() {
|
fn dquote_escape_nonspecial_preserves_backslash() {
|
||||||
// "\a" inside double quotes should preserve the backslash (a is not special)
|
// "\a" inside double quotes should preserve the backslash (a is not special)
|
||||||
let result = unescape_str(r#""\a""#);
|
let result = unescape_str(r#""\a""#);
|
||||||
let inner: String = result.chars()
|
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
|
||||||
.filter(|&c| c != DUB_QUOTE)
|
assert_eq!(
|
||||||
.collect();
|
inner, "\\a",
|
||||||
assert_eq!(inner, "\\a", "Backslash before non-special char should be preserved");
|
"Backslash before non-special char should be preserved"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dquote_unescaped_dollar_expands() {
|
fn dquote_unescaped_dollar_expands() {
|
||||||
// "$foo" inside double quotes should produce VAR_SUB (expansion marker)
|
// "$foo" inside double quotes should produce VAR_SUB (expansion marker)
|
||||||
let result = unescape_str(r#""$foo""#);
|
let result = unescape_str(r#""$foo""#);
|
||||||
assert!(result.contains(VAR_SUB), "Unescaped $ should become VAR_SUB");
|
assert!(
|
||||||
|
result.contains(VAR_SUB),
|
||||||
|
"Unescaped $ should become VAR_SUB"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn dquote_mixed_escapes() {
|
fn dquote_mixed_escapes() {
|
||||||
// "hello \$world \\end" should have literal $, single backslash
|
// "hello \$world \\end" should have literal $, single backslash
|
||||||
let result = unescape_str(r#""hello \$world \\end""#);
|
let result = unescape_str(r#""hello \$world \\end""#);
|
||||||
assert!(!result.contains(VAR_SUB), "Escaped $ should not expand");
|
assert!(!result.contains(VAR_SUB), "Escaped $ should not expand");
|
||||||
assert!(result.contains('$'), "Literal $ should be in output");
|
assert!(result.contains('$'), "Literal $ should be in output");
|
||||||
// Should have exactly one backslash (from \\)
|
// Should have exactly one backslash (from \\)
|
||||||
let inner: String = result.chars()
|
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
|
||||||
.filter(|&c| c != DUB_QUOTE)
|
let backslash_count = inner.chars().filter(|&c| c == '\\').count();
|
||||||
.collect();
|
assert_eq!(backslash_count, 1, "\\\\ should produce one backslash");
|
||||||
let backslash_count = inner.chars().filter(|&c| c == '\\').count();
|
|
||||||
assert_eq!(backslash_count, 1, "\\\\ should produce one backslash");
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,29 @@
|
|||||||
use crate::prompt::readline::{
|
use crate::prompt::readline::{
|
||||||
annotate_input, annotate_input_recursive, markers,
|
annotate_input, annotate_input_recursive, highlight::Highlighter, markers,
|
||||||
highlight::Highlighter,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
/// Helper to check if a marker exists at any position in the annotated string
|
/// Helper to check if a marker exists at any position in the annotated string
|
||||||
fn has_marker(annotated: &str, marker: char) -> bool {
|
fn has_marker(annotated: &str, marker: char) -> bool {
|
||||||
annotated.contains(marker)
|
annotated.contains(marker)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper to find the position of a marker in the annotated string
|
/// Helper to find the position of a marker in the annotated string
|
||||||
fn find_marker(annotated: &str, marker: char) -> Option<usize> {
|
fn find_marker(annotated: &str, marker: char) -> Option<usize> {
|
||||||
annotated.find(marker)
|
annotated.find(marker)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper to check if markers appear in the correct order
|
/// Helper to check if markers appear in the correct order
|
||||||
fn marker_before(annotated: &str, first: char, second: char) -> bool {
|
fn marker_before(annotated: &str, first: char, second: char) -> bool {
|
||||||
if let (Some(pos1), Some(pos2)) = (find_marker(annotated, first), find_marker(annotated, second)) {
|
if let (Some(pos1), Some(pos2)) = (
|
||||||
pos1 < pos2
|
find_marker(annotated, first),
|
||||||
} else {
|
find_marker(annotated, second),
|
||||||
false
|
) {
|
||||||
}
|
pos1 < pos2
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -30,69 +32,70 @@ fn marker_before(annotated: &str, first: char, second: char) -> bool {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_simple_command() {
|
fn annotate_simple_command() {
|
||||||
let input = "/bin/ls -la";
|
let input = "/bin/ls -la";
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should have COMMAND marker for "/bin/ls" (external command)
|
// Should have COMMAND marker for "/bin/ls" (external command)
|
||||||
assert!(has_marker(&annotated, markers::COMMAND));
|
assert!(has_marker(&annotated, markers::COMMAND));
|
||||||
|
|
||||||
// Should have ARG marker for "-la"
|
// Should have ARG marker for "-la"
|
||||||
assert!(has_marker(&annotated, markers::ARG));
|
assert!(has_marker(&annotated, markers::ARG));
|
||||||
|
|
||||||
// Should have RESET markers
|
// Should have RESET markers
|
||||||
assert!(has_marker(&annotated, markers::RESET));
|
assert!(has_marker(&annotated, markers::RESET));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_builtin_command() {
|
fn annotate_builtin_command() {
|
||||||
let input = "export FOO=bar";
|
let input = "export FOO=bar";
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should mark "export" as BUILTIN
|
// Should mark "export" as BUILTIN
|
||||||
assert!(has_marker(&annotated, markers::BUILTIN));
|
assert!(has_marker(&annotated, markers::BUILTIN));
|
||||||
|
|
||||||
// Should mark assignment (or ARG if assignment isn't specifically marked separately)
|
// Should mark assignment (or ARG if assignment isn't specifically marked
|
||||||
assert!(has_marker(&annotated, markers::ASSIGNMENT) || has_marker(&annotated, markers::ARG));
|
// separately)
|
||||||
|
assert!(has_marker(&annotated, markers::ASSIGNMENT) || has_marker(&annotated, markers::ARG));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_operator() {
|
fn annotate_operator() {
|
||||||
let input = "ls | grep foo";
|
let input = "ls | grep foo";
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should have OPERATOR marker for pipe
|
// Should have OPERATOR marker for pipe
|
||||||
assert!(has_marker(&annotated, markers::OPERATOR));
|
assert!(has_marker(&annotated, markers::OPERATOR));
|
||||||
|
|
||||||
// Should have COMMAND markers for both commands
|
// Should have COMMAND markers for both commands
|
||||||
let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count();
|
let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count();
|
||||||
assert_eq!(command_count, 2);
|
assert_eq!(command_count, 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_redirect() {
|
fn annotate_redirect() {
|
||||||
let input = "echo hello > output.txt";
|
let input = "echo hello > output.txt";
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should have REDIRECT marker
|
// Should have REDIRECT marker
|
||||||
assert!(has_marker(&annotated, markers::REDIRECT));
|
assert!(has_marker(&annotated, markers::REDIRECT));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_keyword() {
|
fn annotate_keyword() {
|
||||||
let input = "if true; then echo yes; fi";
|
let input = "if true; then echo yes; fi";
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should have KEYWORD markers for if/then/fi
|
// Should have KEYWORD markers for if/then/fi
|
||||||
assert!(has_marker(&annotated, markers::KEYWORD));
|
assert!(has_marker(&annotated, markers::KEYWORD));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_command_separator() {
|
fn annotate_command_separator() {
|
||||||
let input = "echo foo; echo bar";
|
let input = "echo foo; echo bar";
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should have CMD_SEP marker for semicolon
|
// Should have CMD_SEP marker for semicolon
|
||||||
assert!(has_marker(&annotated, markers::CMD_SEP));
|
assert!(has_marker(&annotated, markers::CMD_SEP));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -101,83 +104,87 @@ fn annotate_command_separator() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_variable_simple() {
|
fn annotate_variable_simple() {
|
||||||
let input = "echo $foo";
|
let input = "echo $foo";
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should have VAR_SUB markers
|
// Should have VAR_SUB markers
|
||||||
assert!(has_marker(&annotated, markers::VAR_SUB));
|
assert!(has_marker(&annotated, markers::VAR_SUB));
|
||||||
assert!(has_marker(&annotated, markers::VAR_SUB_END));
|
assert!(has_marker(&annotated, markers::VAR_SUB_END));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_variable_braces() {
|
fn annotate_variable_braces() {
|
||||||
let input = "echo ${foo}";
|
let input = "echo ${foo}";
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should have VAR_SUB markers for ${foo}
|
// Should have VAR_SUB markers for ${foo}
|
||||||
assert!(has_marker(&annotated, markers::VAR_SUB));
|
assert!(has_marker(&annotated, markers::VAR_SUB));
|
||||||
assert!(has_marker(&annotated, markers::VAR_SUB_END));
|
assert!(has_marker(&annotated, markers::VAR_SUB_END));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_double_quoted_string() {
|
fn annotate_double_quoted_string() {
|
||||||
let input = r#"echo "hello world""#;
|
let input = r#"echo "hello world""#;
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should have STRING_DQ markers
|
// Should have STRING_DQ markers
|
||||||
assert!(has_marker(&annotated, markers::STRING_DQ));
|
assert!(has_marker(&annotated, markers::STRING_DQ));
|
||||||
assert!(has_marker(&annotated, markers::STRING_DQ_END));
|
assert!(has_marker(&annotated, markers::STRING_DQ_END));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_single_quoted_string() {
|
fn annotate_single_quoted_string() {
|
||||||
let input = "echo 'hello world'";
|
let input = "echo 'hello world'";
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should have STRING_SQ markers
|
// Should have STRING_SQ markers
|
||||||
assert!(has_marker(&annotated, markers::STRING_SQ));
|
assert!(has_marker(&annotated, markers::STRING_SQ));
|
||||||
assert!(has_marker(&annotated, markers::STRING_SQ_END));
|
assert!(has_marker(&annotated, markers::STRING_SQ_END));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_variable_in_string() {
|
fn annotate_variable_in_string() {
|
||||||
let input = r#"echo "hello $USER""#;
|
let input = r#"echo "hello $USER""#;
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should have both STRING_DQ and VAR_SUB markers
|
// Should have both STRING_DQ and VAR_SUB markers
|
||||||
assert!(has_marker(&annotated, markers::STRING_DQ));
|
assert!(has_marker(&annotated, markers::STRING_DQ));
|
||||||
assert!(has_marker(&annotated, markers::VAR_SUB));
|
assert!(has_marker(&annotated, markers::VAR_SUB));
|
||||||
|
|
||||||
// VAR_SUB should be inside STRING_DQ
|
// VAR_SUB should be inside STRING_DQ
|
||||||
assert!(marker_before(&annotated, markers::STRING_DQ, markers::VAR_SUB));
|
assert!(marker_before(
|
||||||
|
&annotated,
|
||||||
|
markers::STRING_DQ,
|
||||||
|
markers::VAR_SUB
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_glob_asterisk() {
|
fn annotate_glob_asterisk() {
|
||||||
let input = "ls *.txt";
|
let input = "ls *.txt";
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should have GLOB marker for *
|
// Should have GLOB marker for *
|
||||||
assert!(has_marker(&annotated, markers::GLOB));
|
assert!(has_marker(&annotated, markers::GLOB));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_glob_question() {
|
fn annotate_glob_question() {
|
||||||
let input = "ls file?.txt";
|
let input = "ls file?.txt";
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should have GLOB marker for ?
|
// Should have GLOB marker for ?
|
||||||
assert!(has_marker(&annotated, markers::GLOB));
|
assert!(has_marker(&annotated, markers::GLOB));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_glob_bracket() {
|
fn annotate_glob_bracket() {
|
||||||
let input = "ls file[abc].txt";
|
let input = "ls file[abc].txt";
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should have GLOB markers for bracket expression
|
// Should have GLOB markers for bracket expression
|
||||||
let glob_count = annotated.chars().filter(|&c| c == markers::GLOB).count();
|
let glob_count = annotated.chars().filter(|&c| c == markers::GLOB).count();
|
||||||
assert!(glob_count >= 2); // Opening and closing
|
assert!(glob_count >= 2); // Opening and closing
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -186,32 +193,32 @@ fn annotate_glob_bracket() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_command_sub_basic() {
|
fn annotate_command_sub_basic() {
|
||||||
let input = "echo $(whoami)";
|
let input = "echo $(whoami)";
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should have CMD_SUB markers (but not recursively annotated yet)
|
// Should have CMD_SUB markers (but not recursively annotated yet)
|
||||||
assert!(has_marker(&annotated, markers::CMD_SUB));
|
assert!(has_marker(&annotated, markers::CMD_SUB));
|
||||||
assert!(has_marker(&annotated, markers::CMD_SUB_END));
|
assert!(has_marker(&annotated, markers::CMD_SUB_END));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_subshell_basic() {
|
fn annotate_subshell_basic() {
|
||||||
let input = "(cd /tmp && ls)";
|
let input = "(cd /tmp && ls)";
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should have SUBSH markers
|
// Should have SUBSH markers
|
||||||
assert!(has_marker(&annotated, markers::SUBSH));
|
assert!(has_marker(&annotated, markers::SUBSH));
|
||||||
assert!(has_marker(&annotated, markers::SUBSH_END));
|
assert!(has_marker(&annotated, markers::SUBSH_END));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_process_sub_output() {
|
fn annotate_process_sub_output() {
|
||||||
let input = "diff <(ls dir1) <(ls dir2)";
|
let input = "diff <(ls dir1) <(ls dir2)";
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should have PROC_SUB markers
|
// Should have PROC_SUB markers
|
||||||
assert!(has_marker(&annotated, markers::PROC_SUB));
|
assert!(has_marker(&annotated, markers::PROC_SUB));
|
||||||
assert!(has_marker(&annotated, markers::PROC_SUB_END));
|
assert!(has_marker(&annotated, markers::PROC_SUB_END));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -220,88 +227,97 @@ fn annotate_process_sub_output() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_recursive_command_sub() {
|
fn annotate_recursive_command_sub() {
|
||||||
let input = "echo $(whoami)";
|
let input = "echo $(whoami)";
|
||||||
let annotated = annotate_input_recursive(input);
|
let annotated = annotate_input_recursive(input);
|
||||||
|
|
||||||
// Should have CMD_SUB markers
|
// Should have CMD_SUB markers
|
||||||
assert!(has_marker(&annotated, markers::CMD_SUB));
|
assert!(has_marker(&annotated, markers::CMD_SUB));
|
||||||
assert!(has_marker(&annotated, markers::CMD_SUB_END));
|
assert!(has_marker(&annotated, markers::CMD_SUB_END));
|
||||||
|
|
||||||
// Inside the command sub, "whoami" should be marked as COMMAND
|
// Inside the command sub, "whoami" should be marked as COMMAND
|
||||||
// The recursive annotator should have processed the inside
|
// The recursive annotator should have processed the inside
|
||||||
assert!(has_marker(&annotated, markers::COMMAND));
|
assert!(has_marker(&annotated, markers::COMMAND));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_recursive_nested_command_sub() {
|
fn annotate_recursive_nested_command_sub() {
|
||||||
let input = "echo $(echo $(whoami))";
|
let input = "echo $(echo $(whoami))";
|
||||||
let annotated = annotate_input_recursive(input);
|
let annotated = annotate_input_recursive(input);
|
||||||
|
|
||||||
// Should have multiple CMD_SUB markers (nested)
|
// Should have multiple CMD_SUB markers (nested)
|
||||||
let cmd_sub_count = annotated.chars().filter(|&c| c == markers::CMD_SUB).count();
|
let cmd_sub_count = annotated.chars().filter(|&c| c == markers::CMD_SUB).count();
|
||||||
assert!(cmd_sub_count >= 2, "Should have at least 2 CMD_SUB markers for nested substitutions");
|
assert!(
|
||||||
|
cmd_sub_count >= 2,
|
||||||
|
"Should have at least 2 CMD_SUB markers for nested substitutions"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_recursive_command_sub_with_args() {
|
fn annotate_recursive_command_sub_with_args() {
|
||||||
let input = "echo $(grep foo file.txt)";
|
let input = "echo $(grep foo file.txt)";
|
||||||
let annotated = annotate_input_recursive(input);
|
let annotated = annotate_input_recursive(input);
|
||||||
|
|
||||||
// Should have BUILTIN for echo and possibly COMMAND for grep (if in PATH)
|
// Should have BUILTIN for echo and possibly COMMAND for grep (if in PATH)
|
||||||
// Just check that we have command-type markers
|
// Just check that we have command-type markers
|
||||||
let builtin_count = annotated.chars().filter(|&c| c == markers::BUILTIN).count();
|
let builtin_count = annotated.chars().filter(|&c| c == markers::BUILTIN).count();
|
||||||
let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count();
|
let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count();
|
||||||
assert!(builtin_count + command_count >= 2, "Expected at least 2 command markers (BUILTIN or COMMAND)");
|
assert!(
|
||||||
|
builtin_count + command_count >= 2,
|
||||||
|
"Expected at least 2 command markers (BUILTIN or COMMAND)"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_recursive_subshell() {
|
fn annotate_recursive_subshell() {
|
||||||
let input = "(echo hello; echo world)";
|
let input = "(echo hello; echo world)";
|
||||||
let annotated = annotate_input_recursive(input);
|
let annotated = annotate_input_recursive(input);
|
||||||
|
|
||||||
// Should have SUBSH markers
|
// Should have SUBSH markers
|
||||||
assert!(has_marker(&annotated, markers::SUBSH));
|
assert!(has_marker(&annotated, markers::SUBSH));
|
||||||
assert!(has_marker(&annotated, markers::SUBSH_END));
|
assert!(has_marker(&annotated, markers::SUBSH_END));
|
||||||
|
|
||||||
// Inside should be annotated with BUILTIN (echo is a builtin) and CMD_SEP
|
// Inside should be annotated with BUILTIN (echo is a builtin) and CMD_SEP
|
||||||
assert!(has_marker(&annotated, markers::BUILTIN));
|
assert!(has_marker(&annotated, markers::BUILTIN));
|
||||||
assert!(has_marker(&annotated, markers::CMD_SEP));
|
assert!(has_marker(&annotated, markers::CMD_SEP));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_recursive_process_sub() {
|
fn annotate_recursive_process_sub() {
|
||||||
let input = "diff <(ls -la)";
|
let input = "diff <(ls -la)";
|
||||||
let annotated = annotate_input_recursive(input);
|
let annotated = annotate_input_recursive(input);
|
||||||
|
|
||||||
// Should have PROC_SUB markers
|
// Should have PROC_SUB markers
|
||||||
assert!(has_marker(&annotated, markers::PROC_SUB));
|
assert!(has_marker(&annotated, markers::PROC_SUB));
|
||||||
|
|
||||||
// ls should be marked as COMMAND inside the process sub
|
// ls should be marked as COMMAND inside the process sub
|
||||||
assert!(has_marker(&annotated, markers::COMMAND));
|
assert!(has_marker(&annotated, markers::COMMAND));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_recursive_command_sub_in_string() {
|
fn annotate_recursive_command_sub_in_string() {
|
||||||
let input = r#"echo "current user: $(whoami)""#;
|
let input = r#"echo "current user: $(whoami)""#;
|
||||||
let annotated = annotate_input_recursive(input);
|
let annotated = annotate_input_recursive(input);
|
||||||
|
|
||||||
// Should have STRING_DQ, CMD_SUB, and COMMAND markers
|
// Should have STRING_DQ, CMD_SUB, and COMMAND markers
|
||||||
assert!(has_marker(&annotated, markers::STRING_DQ));
|
assert!(has_marker(&annotated, markers::STRING_DQ));
|
||||||
assert!(has_marker(&annotated, markers::CMD_SUB));
|
assert!(has_marker(&annotated, markers::CMD_SUB));
|
||||||
assert!(has_marker(&annotated, markers::COMMAND));
|
assert!(has_marker(&annotated, markers::COMMAND));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_recursive_deeply_nested() {
|
fn annotate_recursive_deeply_nested() {
|
||||||
let input = r#"echo "outer: $(echo "inner: $(whoami)")""#;
|
let input = r#"echo "outer: $(echo "inner: $(whoami)")""#;
|
||||||
let annotated = annotate_input_recursive(input);
|
let annotated = annotate_input_recursive(input);
|
||||||
|
|
||||||
// Should have multiple STRING_DQ and CMD_SUB markers
|
// Should have multiple STRING_DQ and CMD_SUB markers
|
||||||
let string_count = annotated.chars().filter(|&c| c == markers::STRING_DQ).count();
|
let string_count = annotated
|
||||||
let cmd_sub_count = annotated.chars().filter(|&c| c == markers::CMD_SUB).count();
|
.chars()
|
||||||
|
.filter(|&c| c == markers::STRING_DQ)
|
||||||
|
.count();
|
||||||
|
let cmd_sub_count = annotated.chars().filter(|&c| c == markers::CMD_SUB).count();
|
||||||
|
|
||||||
assert!(string_count >= 2, "Should have multiple STRING_DQ markers");
|
assert!(string_count >= 2, "Should have multiple STRING_DQ markers");
|
||||||
assert!(cmd_sub_count >= 2, "Should have multiple CMD_SUB markers");
|
assert!(cmd_sub_count >= 2, "Should have multiple CMD_SUB markers");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -310,33 +326,37 @@ fn annotate_recursive_deeply_nested() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn marker_priority_var_in_string() {
|
fn marker_priority_var_in_string() {
|
||||||
let input = r#""$foo""#;
|
let input = r#""$foo""#;
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// STRING_DQ should come before VAR_SUB (outer before inner)
|
// STRING_DQ should come before VAR_SUB (outer before inner)
|
||||||
assert!(marker_before(&annotated, markers::STRING_DQ, markers::VAR_SUB));
|
assert!(marker_before(
|
||||||
|
&annotated,
|
||||||
|
markers::STRING_DQ,
|
||||||
|
markers::VAR_SUB
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn marker_priority_arg_vs_string() {
|
fn marker_priority_arg_vs_string() {
|
||||||
let input = r#"echo "hello""#;
|
let input = r#"echo "hello""#;
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Both ARG and STRING_DQ should be present
|
// Both ARG and STRING_DQ should be present
|
||||||
// STRING_DQ should be inside the ARG token's span
|
// STRING_DQ should be inside the ARG token's span
|
||||||
assert!(has_marker(&annotated, markers::ARG));
|
assert!(has_marker(&annotated, markers::ARG));
|
||||||
assert!(has_marker(&annotated, markers::STRING_DQ));
|
assert!(has_marker(&annotated, markers::STRING_DQ));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn marker_priority_reset_placement() {
|
fn marker_priority_reset_placement() {
|
||||||
let input = "echo hello";
|
let input = "echo hello";
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// RESET markers should appear after each token
|
// RESET markers should appear after each token
|
||||||
// There should be multiple RESET markers
|
// There should be multiple RESET markers
|
||||||
let reset_count = annotated.chars().filter(|&c| c == markers::RESET).count();
|
let reset_count = annotated.chars().filter(|&c| c == markers::RESET).count();
|
||||||
assert!(reset_count >= 2);
|
assert!(reset_count >= 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -345,127 +365,131 @@ fn marker_priority_reset_placement() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn highlighter_produces_ansi_codes() {
|
fn highlighter_produces_ansi_codes() {
|
||||||
let mut highlighter = Highlighter::new();
|
let mut highlighter = Highlighter::new();
|
||||||
highlighter.load_input("echo hello");
|
highlighter.load_input("echo hello");
|
||||||
highlighter.highlight();
|
highlighter.highlight();
|
||||||
let output = highlighter.take();
|
let output = highlighter.take();
|
||||||
|
|
||||||
// Should contain ANSI escape codes
|
// Should contain ANSI escape codes
|
||||||
assert!(output.contains("\x1b["), "Output should contain ANSI escape sequences");
|
assert!(
|
||||||
|
output.contains("\x1b["),
|
||||||
|
"Output should contain ANSI escape sequences"
|
||||||
|
);
|
||||||
|
|
||||||
// Should still contain the original text
|
// Should still contain the original text
|
||||||
assert!(output.contains("echo"));
|
assert!(output.contains("echo"));
|
||||||
assert!(output.contains("hello"));
|
assert!(output.contains("hello"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn highlighter_handles_empty_input() {
|
fn highlighter_handles_empty_input() {
|
||||||
let mut highlighter = Highlighter::new();
|
let mut highlighter = Highlighter::new();
|
||||||
highlighter.load_input("");
|
highlighter.load_input("");
|
||||||
highlighter.highlight();
|
highlighter.highlight();
|
||||||
let output = highlighter.take();
|
let output = highlighter.take();
|
||||||
|
|
||||||
// Should not crash and should return empty or minimal output
|
// Should not crash and should return empty or minimal output
|
||||||
assert!(output.len() < 10); // Just escape codes or empty
|
assert!(output.len() < 10); // Just escape codes or empty
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn highlighter_command_validation() {
|
fn highlighter_command_validation() {
|
||||||
let mut highlighter = Highlighter::new();
|
let mut highlighter = Highlighter::new();
|
||||||
|
|
||||||
// Valid command (echo exists)
|
// Valid command (echo exists)
|
||||||
highlighter.load_input("echo test");
|
highlighter.load_input("echo test");
|
||||||
highlighter.highlight();
|
highlighter.highlight();
|
||||||
let valid_output = highlighter.take();
|
let valid_output = highlighter.take();
|
||||||
|
|
||||||
// Invalid command (definitely doesn't exist)
|
// Invalid command (definitely doesn't exist)
|
||||||
highlighter.load_input("xyznotacommand123 test");
|
highlighter.load_input("xyznotacommand123 test");
|
||||||
highlighter.highlight();
|
highlighter.highlight();
|
||||||
let invalid_output = highlighter.take();
|
let invalid_output = highlighter.take();
|
||||||
|
|
||||||
// Both should have ANSI codes
|
// Both should have ANSI codes
|
||||||
assert!(valid_output.contains("\x1b["));
|
assert!(valid_output.contains("\x1b["));
|
||||||
assert!(invalid_output.contains("\x1b["));
|
assert!(invalid_output.contains("\x1b["));
|
||||||
|
|
||||||
// The color codes should be different (green vs red)
|
// The color codes should be different (green vs red)
|
||||||
// Valid commands should have \x1b[32m (green)
|
// Valid commands should have \x1b[32m (green)
|
||||||
// Invalid commands should have \x1b[31m (red) or \x1b[1;31m (bold red)
|
// Invalid commands should have \x1b[31m (red) or \x1b[1;31m (bold red)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn highlighter_preserves_text_content() {
|
fn highlighter_preserves_text_content() {
|
||||||
let input = "echo hello world";
|
let input = "echo hello world";
|
||||||
let mut highlighter = Highlighter::new();
|
let mut highlighter = Highlighter::new();
|
||||||
highlighter.load_input(input);
|
highlighter.load_input(input);
|
||||||
highlighter.highlight();
|
highlighter.highlight();
|
||||||
let output = highlighter.take();
|
let output = highlighter.take();
|
||||||
|
|
||||||
// Remove ANSI codes to check text content
|
// Remove ANSI codes to check text content
|
||||||
let text_only: String = output.chars()
|
let text_only: String = output
|
||||||
.filter(|c| !c.is_control() && *c != '\x1b')
|
.chars()
|
||||||
.collect();
|
.filter(|c| !c.is_control() && *c != '\x1b')
|
||||||
|
.collect();
|
||||||
|
|
||||||
// Should still contain the words (might have escape sequence fragments)
|
// Should still contain the words (might have escape sequence fragments)
|
||||||
assert!(output.contains("echo"));
|
assert!(output.contains("echo"));
|
||||||
assert!(output.contains("hello"));
|
assert!(output.contains("hello"));
|
||||||
assert!(output.contains("world"));
|
assert!(output.contains("world"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn highlighter_multiple_tokens() {
|
fn highlighter_multiple_tokens() {
|
||||||
let mut highlighter = Highlighter::new();
|
let mut highlighter = Highlighter::new();
|
||||||
highlighter.load_input("ls -la | grep foo");
|
highlighter.load_input("ls -la | grep foo");
|
||||||
highlighter.highlight();
|
highlighter.highlight();
|
||||||
let output = highlighter.take();
|
let output = highlighter.take();
|
||||||
|
|
||||||
// Should contain all tokens
|
// Should contain all tokens
|
||||||
assert!(output.contains("ls"));
|
assert!(output.contains("ls"));
|
||||||
assert!(output.contains("-la"));
|
assert!(output.contains("-la"));
|
||||||
assert!(output.contains("|"));
|
assert!(output.contains("|"));
|
||||||
assert!(output.contains("grep"));
|
assert!(output.contains("grep"));
|
||||||
assert!(output.contains("foo"));
|
assert!(output.contains("foo"));
|
||||||
|
|
||||||
// Should have ANSI codes
|
// Should have ANSI codes
|
||||||
assert!(output.contains("\x1b["));
|
assert!(output.contains("\x1b["));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn highlighter_string_with_variable() {
|
fn highlighter_string_with_variable() {
|
||||||
let mut highlighter = Highlighter::new();
|
let mut highlighter = Highlighter::new();
|
||||||
highlighter.load_input(r#"echo "hello $USER""#);
|
highlighter.load_input(r#"echo "hello $USER""#);
|
||||||
highlighter.highlight();
|
highlighter.highlight();
|
||||||
let output = highlighter.take();
|
let output = highlighter.take();
|
||||||
|
|
||||||
// Should contain the text
|
// Should contain the text
|
||||||
assert!(output.contains("echo"));
|
assert!(output.contains("echo"));
|
||||||
assert!(output.contains("hello"));
|
assert!(output.contains("hello"));
|
||||||
assert!(output.contains("USER"));
|
assert!(output.contains("USER"));
|
||||||
|
|
||||||
// Should have ANSI codes for different elements
|
// Should have ANSI codes for different elements
|
||||||
assert!(output.contains("\x1b["));
|
assert!(output.contains("\x1b["));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn highlighter_reusable() {
|
fn highlighter_reusable() {
|
||||||
let mut highlighter = Highlighter::new();
|
let mut highlighter = Highlighter::new();
|
||||||
|
|
||||||
// First input
|
// First input
|
||||||
highlighter.load_input("echo first");
|
highlighter.load_input("echo first");
|
||||||
highlighter.highlight();
|
highlighter.highlight();
|
||||||
let output1 = highlighter.take();
|
let output1 = highlighter.take();
|
||||||
|
|
||||||
// Second input (reusing same highlighter)
|
// Second input (reusing same highlighter)
|
||||||
highlighter.load_input("echo second");
|
highlighter.load_input("echo second");
|
||||||
highlighter.highlight();
|
highlighter.highlight();
|
||||||
let output2 = highlighter.take();
|
let output2 = highlighter.take();
|
||||||
|
|
||||||
// Both should work
|
// Both should work
|
||||||
assert!(output1.contains("first"));
|
assert!(output1.contains("first"));
|
||||||
assert!(output2.contains("second"));
|
assert!(output2.contains("second"));
|
||||||
|
|
||||||
// Should not contain each other's text
|
// Should not contain each other's text
|
||||||
assert!(!output1.contains("second"));
|
assert!(!output1.contains("second"));
|
||||||
assert!(!output2.contains("first"));
|
assert!(!output2.contains("first"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -474,133 +498,143 @@ fn highlighter_reusable() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_unclosed_string() {
|
fn annotate_unclosed_string() {
|
||||||
let input = r#"echo "hello"#;
|
let input = r#"echo "hello"#;
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should handle unclosed string gracefully
|
// Should handle unclosed string gracefully
|
||||||
assert!(has_marker(&annotated, markers::STRING_DQ));
|
assert!(has_marker(&annotated, markers::STRING_DQ));
|
||||||
// May or may not have STRING_DQ_END depending on implementation
|
// May or may not have STRING_DQ_END depending on implementation
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_unclosed_command_sub() {
|
fn annotate_unclosed_command_sub() {
|
||||||
let input = "echo $(whoami";
|
let input = "echo $(whoami";
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should handle unclosed command sub gracefully
|
// Should handle unclosed command sub gracefully
|
||||||
assert!(has_marker(&annotated, markers::CMD_SUB));
|
assert!(has_marker(&annotated, markers::CMD_SUB));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_empty_command_sub() {
|
fn annotate_empty_command_sub() {
|
||||||
let input = "echo $()";
|
let input = "echo $()";
|
||||||
let annotated = annotate_input_recursive(input);
|
let annotated = annotate_input_recursive(input);
|
||||||
|
|
||||||
// Should handle empty command sub
|
// Should handle empty command sub
|
||||||
assert!(has_marker(&annotated, markers::CMD_SUB));
|
assert!(has_marker(&annotated, markers::CMD_SUB));
|
||||||
assert!(has_marker(&annotated, markers::CMD_SUB_END));
|
assert!(has_marker(&annotated, markers::CMD_SUB_END));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_escaped_characters() {
|
fn annotate_escaped_characters() {
|
||||||
let input = r#"echo \$foo \`bar\` \"test\""#;
|
let input = r#"echo \$foo \`bar\` \"test\""#;
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should not mark escaped $ as variable
|
// Should not mark escaped $ as variable
|
||||||
// This is tricky - the behavior depends on implementation
|
// This is tricky - the behavior depends on implementation
|
||||||
// At minimum, should not crash
|
// At minimum, should not crash
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_special_variables() {
|
fn annotate_special_variables() {
|
||||||
let input = "echo $0 $1 $2 $3 $4";
|
let input = "echo $0 $1 $2 $3 $4";
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should mark positional parameters
|
// Should mark positional parameters
|
||||||
let var_count = annotated.chars().filter(|&c| c == markers::VAR_SUB).count();
|
let var_count = annotated.chars().filter(|&c| c == markers::VAR_SUB).count();
|
||||||
assert!(var_count >= 5, "Expected at least 5 VAR_SUB markers, found {}", var_count);
|
assert!(
|
||||||
|
var_count >= 5,
|
||||||
|
"Expected at least 5 VAR_SUB markers, found {}",
|
||||||
|
var_count
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_variable_no_expansion_in_single_quotes() {
|
fn annotate_variable_no_expansion_in_single_quotes() {
|
||||||
let input = "echo '$foo'";
|
let input = "echo '$foo'";
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should have STRING_SQ markers
|
// Should have STRING_SQ markers
|
||||||
assert!(has_marker(&annotated, markers::STRING_SQ));
|
assert!(has_marker(&annotated, markers::STRING_SQ));
|
||||||
|
|
||||||
// Should NOT have VAR_SUB markers (variables don't expand in single quotes)
|
// Should NOT have VAR_SUB markers (variables don't expand in single quotes)
|
||||||
// Note: The annotator might still mark it - depends on implementation
|
// Note: The annotator might still mark it - depends on implementation
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_complex_pipeline() {
|
fn annotate_complex_pipeline() {
|
||||||
let input = "cat file.txt | grep pattern | sed 's/foo/bar/' | sort | uniq";
|
let input = "cat file.txt | grep pattern | sed 's/foo/bar/' | sort | uniq";
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should have multiple OPERATOR markers for pipes
|
// Should have multiple OPERATOR markers for pipes
|
||||||
let operator_count = annotated.chars().filter(|&c| c == markers::OPERATOR).count();
|
let operator_count = annotated
|
||||||
assert!(operator_count >= 4);
|
.chars()
|
||||||
|
.filter(|&c| c == markers::OPERATOR)
|
||||||
|
.count();
|
||||||
|
assert!(operator_count >= 4);
|
||||||
|
|
||||||
// Should have multiple COMMAND markers
|
// Should have multiple COMMAND markers
|
||||||
let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count();
|
let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count();
|
||||||
assert!(command_count >= 5);
|
assert!(command_count >= 5);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_assignment_with_command_sub() {
|
fn annotate_assignment_with_command_sub() {
|
||||||
let input = "FOO=$(whoami)";
|
let input = "FOO=$(whoami)";
|
||||||
let annotated = annotate_input_recursive(input);
|
let annotated = annotate_input_recursive(input);
|
||||||
|
|
||||||
// Should have ASSIGNMENT marker
|
// Should have ASSIGNMENT marker
|
||||||
assert!(has_marker(&annotated, markers::ASSIGNMENT));
|
assert!(has_marker(&annotated, markers::ASSIGNMENT));
|
||||||
|
|
||||||
// Should have CMD_SUB marker
|
// Should have CMD_SUB marker
|
||||||
assert!(has_marker(&annotated, markers::CMD_SUB));
|
assert!(has_marker(&annotated, markers::CMD_SUB));
|
||||||
|
|
||||||
// Inside command sub should have COMMAND marker
|
// Inside command sub should have COMMAND marker
|
||||||
assert!(has_marker(&annotated, markers::COMMAND));
|
assert!(has_marker(&annotated, markers::COMMAND));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_redirect_with_fd() {
|
fn annotate_redirect_with_fd() {
|
||||||
let input = "command 2>&1";
|
let input = "command 2>&1";
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should have REDIRECT marker for the redirect operator
|
// Should have REDIRECT marker for the redirect operator
|
||||||
assert!(has_marker(&annotated, markers::REDIRECT));
|
assert!(has_marker(&annotated, markers::REDIRECT));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_multiple_redirects() {
|
fn annotate_multiple_redirects() {
|
||||||
let input = "command > out.txt 2>&1";
|
let input = "command > out.txt 2>&1";
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should have multiple REDIRECT markers
|
// Should have multiple REDIRECT markers
|
||||||
let redirect_count = annotated.chars().filter(|&c| c == markers::REDIRECT).count();
|
let redirect_count = annotated
|
||||||
assert!(redirect_count >= 2);
|
.chars()
|
||||||
|
.filter(|&c| c == markers::REDIRECT)
|
||||||
|
.count();
|
||||||
|
assert!(redirect_count >= 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_here_string() {
|
fn annotate_here_string() {
|
||||||
let input = "cat <<< 'hello world'";
|
let input = "cat <<< 'hello world'";
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should have REDIRECT marker for <<<
|
// Should have REDIRECT marker for <<<
|
||||||
assert!(has_marker(&annotated, markers::REDIRECT));
|
assert!(has_marker(&annotated, markers::REDIRECT));
|
||||||
|
|
||||||
// Should have STRING_SQ markers
|
// Should have STRING_SQ markers
|
||||||
assert!(has_marker(&annotated, markers::STRING_SQ));
|
assert!(has_marker(&annotated, markers::STRING_SQ));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn annotate_unicode_content() {
|
fn annotate_unicode_content() {
|
||||||
let input = "echo 'hello 世界 🌍'";
|
let input = "echo 'hello 世界 🌍'";
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should handle unicode gracefully
|
// Should handle unicode gracefully
|
||||||
assert!(has_marker(&annotated, markers::BUILTIN));
|
assert!(has_marker(&annotated, markers::BUILTIN));
|
||||||
assert!(has_marker(&annotated, markers::STRING_SQ));
|
assert!(has_marker(&annotated, markers::STRING_SQ));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -609,26 +643,26 @@ fn annotate_unicode_content() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn regression_arg_marker_at_position_zero() {
|
fn regression_arg_marker_at_position_zero() {
|
||||||
// Regression test: ARG marker was appearing at position 3 for input "ech"
|
// Regression test: ARG marker was appearing at position 3 for input "ech"
|
||||||
// This was caused by SOI/EOI tokens falling through to ARG annotation
|
// This was caused by SOI/EOI tokens falling through to ARG annotation
|
||||||
let input = "ech";
|
let input = "ech";
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// Should only have COMMAND marker, not ARG
|
// Should only have COMMAND marker, not ARG
|
||||||
// (incomplete command should still be marked as command attempt)
|
// (incomplete command should still be marked as command attempt)
|
||||||
assert!(has_marker(&annotated, markers::COMMAND));
|
assert!(has_marker(&annotated, markers::COMMAND));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn regression_string_color_in_annotated_strings() {
|
fn regression_string_color_in_annotated_strings() {
|
||||||
// Regression test: ARG marker was overriding STRING_DQ color
|
// Regression test: ARG marker was overriding STRING_DQ color
|
||||||
let input = r#"echo "test""#;
|
let input = r#"echo "test""#;
|
||||||
let annotated = annotate_input(input);
|
let annotated = annotate_input(input);
|
||||||
|
|
||||||
// STRING_DQ should be present and properly positioned
|
// STRING_DQ should be present and properly positioned
|
||||||
assert!(has_marker(&annotated, markers::STRING_DQ));
|
assert!(has_marker(&annotated, markers::STRING_DQ));
|
||||||
assert!(has_marker(&annotated, markers::STRING_DQ_END));
|
assert!(has_marker(&annotated, markers::STRING_DQ_END));
|
||||||
|
|
||||||
// The string markers should come after the ARG marker
|
// The string markers should come after the ARG marker
|
||||||
// (so they override it in the highlighting)
|
// (so they override it in the highlighting)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ use super::*;
|
|||||||
use crate::expand::{expand_aliases, unescape_str};
|
use crate::expand::{expand_aliases, unescape_str};
|
||||||
use crate::libsh::error::{Note, ShErr, ShErrKind};
|
use crate::libsh::error::{Note, ShErr, ShErrKind};
|
||||||
use crate::parse::{
|
use crate::parse::{
|
||||||
|
NdRule, Node, ParseStream,
|
||||||
lex::{LexFlags, LexStream, Tk, TkRule},
|
lex::{LexFlags, LexStream, Tk, TkRule},
|
||||||
node_operation, NdRule, Node, ParseStream,
|
node_operation,
|
||||||
};
|
};
|
||||||
use crate::state::{write_logic, write_vars};
|
use crate::state::{write_logic, write_vars};
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
libsh::{error::ShErr, term::{Style, Styled}},
|
libsh::{
|
||||||
|
error::ShErr,
|
||||||
|
term::{Style, Styled},
|
||||||
|
},
|
||||||
prompt::readline::{
|
prompt::readline::{
|
||||||
|
FernVi,
|
||||||
history::History,
|
history::History,
|
||||||
keys::{KeyCode, KeyEvent, ModKeys},
|
keys::{KeyCode, KeyEvent, ModKeys},
|
||||||
linebuf::LineBuf,
|
linebuf::LineBuf,
|
||||||
term::{raw_mode, KeyReader, LineWriter},
|
term::{KeyReader, LineWriter, raw_mode},
|
||||||
vimode::{ViInsert, ViMode, ViNormal},
|
vimode::{ViInsert, ViMode, ViNormal},
|
||||||
FernVi,
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -173,8 +176,9 @@ impl LineWriter for TestWriter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: FernVi structure has changed significantly and readline() method no longer exists
|
// NOTE: FernVi structure has changed significantly and readline() method no
|
||||||
// These test helpers are disabled until they can be properly updated
|
// longer exists These test helpers are disabled until they can be properly
|
||||||
|
// updated
|
||||||
/*
|
/*
|
||||||
impl FernVi {
|
impl FernVi {
|
||||||
pub fn new_test(prompt: Option<String>, input: &str, initial: &str) -> Self {
|
pub fn new_test(prompt: Option<String>, input: &str, initial: &str) -> Self {
|
||||||
@@ -612,10 +616,10 @@ fn fernvi_test_mode_change() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn fernvi_test_lorem_ipsum_1() {
|
fn fernvi_test_lorem_ipsum_1() {
|
||||||
assert_eq!(fernvi_test(
|
assert_eq!(fernvi_test(
|
||||||
"\x1bwwwwwwww5dWdBdBjjdwjdwbbbcwasdasdasdasd\x1b\r",
|
"\x1bwwwwwwww5dWdBdBjjdwjdwbbbcwasdasdasdasd\x1b\r",
|
||||||
LOREM_IPSUM),
|
LOREM_IPSUM),
|
||||||
"Lorem ipsum dolor sit amet, incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in repin voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur asdasdasdasd occaecat cupinon proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra."
|
"Lorem ipsum dolor sit amet, incididunt ut labore et dolore magna aliqua.\nUt enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in repin voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur asdasdasdasd occaecat cupinon proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -632,9 +636,9 @@ fn fernvi_test_lorem_ipsum_undo() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn fernvi_test_lorem_ipsum_ctrl_w() {
|
fn fernvi_test_lorem_ipsum_ctrl_w() {
|
||||||
assert_eq!(fernvi_test(
|
assert_eq!(fernvi_test(
|
||||||
"\x1bj5wiasdasdkjhaksjdhkajshd\x17wordswordswords\x17somemorewords\x17\x1b[D\x1b[D\x17\x1b\r",
|
"\x1bj5wiasdasdkjhaksjdhkajshd\x17wordswordswords\x17somemorewords\x17\x1b[D\x1b[D\x17\x1b\r",
|
||||||
LOREM_IPSUM),
|
LOREM_IPSUM),
|
||||||
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim am, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra."
|
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.\nUt enim ad minim am, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\nDuis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur.\nExcepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.\nCurabitur pretium tincidunt lacus. Nulla gravida orci a odio. Nullam varius, turpis et commodo pharetra."
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use crate::parse::{
|
use crate::parse::{
|
||||||
lex::{LexFlags, LexStream},
|
NdRule, Node, ParseStream, Redir, RedirType,
|
||||||
Node, NdRule, ParseStream, RedirType, Redir,
|
lex::{LexFlags, LexStream},
|
||||||
};
|
};
|
||||||
use crate::procio::{IoFrame, IoMode, IoStack};
|
use crate::procio::{IoFrame, IoMode, IoStack};
|
||||||
|
|
||||||
@@ -11,187 +11,238 @@ use crate::procio::{IoFrame, IoMode, IoStack};
|
|||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
fn parse_command(input: &str) -> Node {
|
fn parse_command(input: &str) -> Node {
|
||||||
let source = Arc::new(input.to_string());
|
let source = Arc::new(input.to_string());
|
||||||
let tokens = LexStream::new(source, LexFlags::empty())
|
let tokens = LexStream::new(source, LexFlags::empty())
|
||||||
.flatten()
|
.flatten()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let mut nodes = ParseStream::new(tokens)
|
let mut nodes = ParseStream::new(tokens).flatten().collect::<Vec<_>>();
|
||||||
.flatten()
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
assert_eq!(nodes.len(), 1, "Expected exactly one node");
|
assert_eq!(nodes.len(), 1, "Expected exactly one node");
|
||||||
let top_node = nodes.remove(0);
|
let top_node = nodes.remove(0);
|
||||||
|
|
||||||
// Navigate to the actual Command node within the AST structure
|
// Navigate to the actual Command node within the AST structure
|
||||||
// Structure is typically: Conjunction -> Pipeline -> Command
|
// Structure is typically: Conjunction -> Pipeline -> Command
|
||||||
match top_node.class {
|
match top_node.class {
|
||||||
NdRule::Conjunction { elements } => {
|
NdRule::Conjunction { elements } => {
|
||||||
let first_element = elements.into_iter().next().expect("Expected at least one conjunction element");
|
let first_element = elements
|
||||||
match first_element.cmd.class {
|
.into_iter()
|
||||||
NdRule::Pipeline { cmds, .. } => {
|
.next()
|
||||||
let mut commands = cmds;
|
.expect("Expected at least one conjunction element");
|
||||||
assert_eq!(commands.len(), 1, "Expected exactly one command in pipeline");
|
match first_element.cmd.class {
|
||||||
commands.remove(0)
|
NdRule::Pipeline { cmds, .. } => {
|
||||||
}
|
let mut commands = cmds;
|
||||||
NdRule::Command { .. } => *first_element.cmd,
|
assert_eq!(
|
||||||
_ => panic!("Expected Command or Pipeline node, got {:?}", first_element.cmd.class),
|
commands.len(),
|
||||||
}
|
1,
|
||||||
}
|
"Expected exactly one command in pipeline"
|
||||||
NdRule::Pipeline { cmds, .. } => {
|
);
|
||||||
let mut commands = cmds;
|
commands.remove(0)
|
||||||
assert_eq!(commands.len(), 1, "Expected exactly one command in pipeline");
|
}
|
||||||
commands.remove(0)
|
NdRule::Command { .. } => *first_element.cmd,
|
||||||
}
|
_ => panic!(
|
||||||
NdRule::Command { .. } => top_node,
|
"Expected Command or Pipeline node, got {:?}",
|
||||||
_ => panic!("Expected Conjunction, Pipeline, or Command node, got {:?}", top_node.class),
|
first_element.cmd.class
|
||||||
}
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
NdRule::Pipeline { cmds, .. } => {
|
||||||
|
let mut commands = cmds;
|
||||||
|
assert_eq!(
|
||||||
|
commands.len(),
|
||||||
|
1,
|
||||||
|
"Expected exactly one command in pipeline"
|
||||||
|
);
|
||||||
|
commands.remove(0)
|
||||||
|
}
|
||||||
|
NdRule::Command { .. } => top_node,
|
||||||
|
_ => panic!(
|
||||||
|
"Expected Conjunction, Pipeline, or Command node, got {:?}",
|
||||||
|
top_node.class
|
||||||
|
),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_output_redirect() {
|
fn parse_output_redirect() {
|
||||||
let node = parse_command("echo hello > output.txt");
|
let node = parse_command("echo hello > output.txt");
|
||||||
|
|
||||||
assert_eq!(node.redirs.len(), 1);
|
assert_eq!(node.redirs.len(), 1);
|
||||||
let redir = &node.redirs[0];
|
let redir = &node.redirs[0];
|
||||||
|
|
||||||
assert!(matches!(redir.class, RedirType::Output));
|
assert!(matches!(redir.class, RedirType::Output));
|
||||||
assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 1, .. }));
|
assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 1, .. }));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_append_redirect() {
|
fn parse_append_redirect() {
|
||||||
let node = parse_command("echo hello >> output.txt");
|
let node = parse_command("echo hello >> output.txt");
|
||||||
|
|
||||||
assert_eq!(node.redirs.len(), 1);
|
assert_eq!(node.redirs.len(), 1);
|
||||||
let redir = &node.redirs[0];
|
let redir = &node.redirs[0];
|
||||||
|
|
||||||
assert!(matches!(redir.class, RedirType::Append));
|
assert!(matches!(redir.class, RedirType::Append));
|
||||||
assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 1, .. }));
|
assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 1, .. }));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_input_redirect() {
|
fn parse_input_redirect() {
|
||||||
let node = parse_command("cat < input.txt");
|
let node = parse_command("cat < input.txt");
|
||||||
|
|
||||||
assert_eq!(node.redirs.len(), 1);
|
assert_eq!(node.redirs.len(), 1);
|
||||||
let redir = &node.redirs[0];
|
let redir = &node.redirs[0];
|
||||||
|
|
||||||
assert!(matches!(redir.class, RedirType::Input));
|
assert!(matches!(redir.class, RedirType::Input));
|
||||||
assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 0, .. }));
|
assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 0, .. }));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_stderr_redirect() {
|
fn parse_stderr_redirect() {
|
||||||
let node = parse_command("ls 2> errors.txt");
|
let node = parse_command("ls 2> errors.txt");
|
||||||
|
|
||||||
assert_eq!(node.redirs.len(), 1);
|
assert_eq!(node.redirs.len(), 1);
|
||||||
let redir = &node.redirs[0];
|
let redir = &node.redirs[0];
|
||||||
|
|
||||||
assert!(matches!(redir.class, RedirType::Output));
|
assert!(matches!(redir.class, RedirType::Output));
|
||||||
assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 2, .. }));
|
assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 2, .. }));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_stderr_to_stdout() {
|
fn parse_stderr_to_stdout() {
|
||||||
let node = parse_command("ls 2>&1");
|
let node = parse_command("ls 2>&1");
|
||||||
|
|
||||||
assert_eq!(node.redirs.len(), 1);
|
assert_eq!(node.redirs.len(), 1);
|
||||||
let redir = &node.redirs[0];
|
let redir = &node.redirs[0];
|
||||||
|
|
||||||
assert!(matches!(redir.io_mode, IoMode::Fd { tgt_fd: 2, src_fd: 1 }));
|
assert!(matches!(
|
||||||
|
redir.io_mode,
|
||||||
|
IoMode::Fd {
|
||||||
|
tgt_fd: 2,
|
||||||
|
src_fd: 1
|
||||||
|
}
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_stdout_to_stderr() {
|
fn parse_stdout_to_stderr() {
|
||||||
let node = parse_command("echo test 1>&2");
|
let node = parse_command("echo test 1>&2");
|
||||||
|
|
||||||
assert_eq!(node.redirs.len(), 1);
|
assert_eq!(node.redirs.len(), 1);
|
||||||
let redir = &node.redirs[0];
|
let redir = &node.redirs[0];
|
||||||
|
|
||||||
assert!(matches!(redir.io_mode, IoMode::Fd { tgt_fd: 1, src_fd: 2 }));
|
assert!(matches!(
|
||||||
|
redir.io_mode,
|
||||||
|
IoMode::Fd {
|
||||||
|
tgt_fd: 1,
|
||||||
|
src_fd: 2
|
||||||
|
}
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_multiple_redirects() {
|
fn parse_multiple_redirects() {
|
||||||
let node = parse_command("cmd < input.txt > output.txt 2> errors.txt");
|
let node = parse_command("cmd < input.txt > output.txt 2> errors.txt");
|
||||||
|
|
||||||
assert_eq!(node.redirs.len(), 3);
|
assert_eq!(node.redirs.len(), 3);
|
||||||
|
|
||||||
// Input redirect
|
// Input redirect
|
||||||
assert!(matches!(node.redirs[0].class, RedirType::Input));
|
assert!(matches!(node.redirs[0].class, RedirType::Input));
|
||||||
assert!(matches!(node.redirs[0].io_mode, IoMode::File { tgt_fd: 0, .. }));
|
assert!(matches!(
|
||||||
|
node.redirs[0].io_mode,
|
||||||
|
IoMode::File { tgt_fd: 0, .. }
|
||||||
|
));
|
||||||
|
|
||||||
// Stdout redirect
|
// Stdout redirect
|
||||||
assert!(matches!(node.redirs[1].class, RedirType::Output));
|
assert!(matches!(node.redirs[1].class, RedirType::Output));
|
||||||
assert!(matches!(node.redirs[1].io_mode, IoMode::File { tgt_fd: 1, .. }));
|
assert!(matches!(
|
||||||
|
node.redirs[1].io_mode,
|
||||||
|
IoMode::File { tgt_fd: 1, .. }
|
||||||
|
));
|
||||||
|
|
||||||
// Stderr redirect
|
// Stderr redirect
|
||||||
assert!(matches!(node.redirs[2].class, RedirType::Output));
|
assert!(matches!(node.redirs[2].class, RedirType::Output));
|
||||||
assert!(matches!(node.redirs[2].io_mode, IoMode::File { tgt_fd: 2, .. }));
|
assert!(matches!(
|
||||||
|
node.redirs[2].io_mode,
|
||||||
|
IoMode::File { tgt_fd: 2, .. }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_custom_fd_redirect() {
|
fn parse_custom_fd_redirect() {
|
||||||
let node = parse_command("echo test 3> fd3.txt");
|
let node = parse_command("echo test 3> fd3.txt");
|
||||||
|
|
||||||
assert_eq!(node.redirs.len(), 1);
|
assert_eq!(node.redirs.len(), 1);
|
||||||
let redir = &node.redirs[0];
|
let redir = &node.redirs[0];
|
||||||
|
|
||||||
assert!(matches!(redir.class, RedirType::Output));
|
assert!(matches!(redir.class, RedirType::Output));
|
||||||
assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 3, .. }));
|
assert!(matches!(redir.io_mode, IoMode::File { tgt_fd: 3, .. }));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_custom_fd_dup() {
|
fn parse_custom_fd_dup() {
|
||||||
let node = parse_command("cmd 3>&4");
|
let node = parse_command("cmd 3>&4");
|
||||||
|
|
||||||
assert_eq!(node.redirs.len(), 1);
|
assert_eq!(node.redirs.len(), 1);
|
||||||
let redir = &node.redirs[0];
|
let redir = &node.redirs[0];
|
||||||
|
|
||||||
assert!(matches!(redir.io_mode, IoMode::Fd { tgt_fd: 3, src_fd: 4 }));
|
assert!(matches!(
|
||||||
|
redir.io_mode,
|
||||||
|
IoMode::Fd {
|
||||||
|
tgt_fd: 3,
|
||||||
|
src_fd: 4
|
||||||
|
}
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_heredoc() {
|
fn parse_heredoc() {
|
||||||
let node = parse_command("cat << EOF");
|
let node = parse_command("cat << EOF");
|
||||||
|
|
||||||
assert_eq!(node.redirs.len(), 1);
|
assert_eq!(node.redirs.len(), 1);
|
||||||
let redir = &node.redirs[0];
|
let redir = &node.redirs[0];
|
||||||
|
|
||||||
assert!(matches!(redir.class, RedirType::HereDoc));
|
assert!(matches!(redir.class, RedirType::HereDoc));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_herestring() {
|
fn parse_herestring() {
|
||||||
let node = parse_command("cat <<< 'hello world'");
|
let node = parse_command("cat <<< 'hello world'");
|
||||||
|
|
||||||
assert_eq!(node.redirs.len(), 1);
|
assert_eq!(node.redirs.len(), 1);
|
||||||
let redir = &node.redirs[0];
|
let redir = &node.redirs[0];
|
||||||
|
|
||||||
assert!(matches!(redir.class, RedirType::HereString));
|
assert!(matches!(redir.class, RedirType::HereString));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_redirect_with_no_space() {
|
fn parse_redirect_with_no_space() {
|
||||||
let node = parse_command("echo hello >output.txt");
|
let node = parse_command("echo hello >output.txt");
|
||||||
|
|
||||||
assert_eq!(node.redirs.len(), 1);
|
assert_eq!(node.redirs.len(), 1);
|
||||||
assert!(matches!(node.redirs[0].class, RedirType::Output));
|
assert!(matches!(node.redirs[0].class, RedirType::Output));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_redirect_order_preserved() {
|
fn parse_redirect_order_preserved() {
|
||||||
let node = parse_command("cmd 2>&1 > file.txt");
|
let node = parse_command("cmd 2>&1 > file.txt");
|
||||||
|
|
||||||
assert_eq!(node.redirs.len(), 2);
|
assert_eq!(node.redirs.len(), 2);
|
||||||
|
|
||||||
// First redirect: 2>&1
|
// First redirect: 2>&1
|
||||||
assert!(matches!(node.redirs[0].io_mode, IoMode::Fd { tgt_fd: 2, src_fd: 1 }));
|
assert!(matches!(
|
||||||
|
node.redirs[0].io_mode,
|
||||||
|
IoMode::Fd {
|
||||||
|
tgt_fd: 2,
|
||||||
|
src_fd: 1
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
// Second redirect: > file.txt
|
// Second redirect: > file.txt
|
||||||
assert!(matches!(node.redirs[1].class, RedirType::Output));
|
assert!(matches!(node.redirs[1].class, RedirType::Output));
|
||||||
assert!(matches!(node.redirs[1].io_mode, IoMode::File { tgt_fd: 1, .. }));
|
assert!(matches!(
|
||||||
|
node.redirs[1].io_mode,
|
||||||
|
IoMode::File { tgt_fd: 1, .. }
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -200,148 +251,148 @@ fn parse_redirect_order_preserved() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn iostack_new() {
|
fn iostack_new() {
|
||||||
let stack = IoStack::new();
|
let stack = IoStack::new();
|
||||||
|
|
||||||
assert_eq!(stack.len(), 1, "IoStack should start with one frame");
|
assert_eq!(stack.len(), 1, "IoStack should start with one frame");
|
||||||
assert_eq!(stack.curr_frame().len(), 0, "Initial frame should be empty");
|
assert_eq!(stack.curr_frame().len(), 0, "Initial frame should be empty");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn iostack_push_pop_frame() {
|
fn iostack_push_pop_frame() {
|
||||||
let mut stack = IoStack::new();
|
let mut stack = IoStack::new();
|
||||||
|
|
||||||
// Push a new frame
|
// Push a new frame
|
||||||
stack.push_frame(IoFrame::new());
|
stack.push_frame(IoFrame::new());
|
||||||
assert_eq!(stack.len(), 2);
|
assert_eq!(stack.len(), 2);
|
||||||
|
|
||||||
// Pop it back
|
// Pop it back
|
||||||
let frame = stack.pop_frame();
|
let frame = stack.pop_frame();
|
||||||
assert_eq!(frame.len(), 0);
|
assert_eq!(frame.len(), 0);
|
||||||
assert_eq!(stack.len(), 1);
|
assert_eq!(stack.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn iostack_never_empties() {
|
fn iostack_never_empties() {
|
||||||
let mut stack = IoStack::new();
|
let mut stack = IoStack::new();
|
||||||
|
|
||||||
// Try to pop the last frame
|
// Try to pop the last frame
|
||||||
let frame = stack.pop_frame();
|
let frame = stack.pop_frame();
|
||||||
assert_eq!(frame.len(), 0);
|
assert_eq!(frame.len(), 0);
|
||||||
|
|
||||||
// Stack should still have one frame
|
// Stack should still have one frame
|
||||||
assert_eq!(stack.len(), 1);
|
assert_eq!(stack.len(), 1);
|
||||||
|
|
||||||
// Pop again - should still have one frame
|
// Pop again - should still have one frame
|
||||||
let frame = stack.pop_frame();
|
let frame = stack.pop_frame();
|
||||||
assert_eq!(frame.len(), 0);
|
assert_eq!(frame.len(), 0);
|
||||||
assert_eq!(stack.len(), 1);
|
assert_eq!(stack.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn iostack_push_to_frame() {
|
fn iostack_push_to_frame() {
|
||||||
let mut stack = IoStack::new();
|
let mut stack = IoStack::new();
|
||||||
|
|
||||||
let redir = crate::parse::Redir::new(
|
let redir = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output);
|
||||||
IoMode::fd(1, 2),
|
|
||||||
RedirType::Output,
|
|
||||||
);
|
|
||||||
|
|
||||||
stack.push_to_frame(redir);
|
stack.push_to_frame(redir);
|
||||||
assert_eq!(stack.curr_frame().len(), 1);
|
assert_eq!(stack.curr_frame().len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn iostack_append_to_frame() {
|
fn iostack_append_to_frame() {
|
||||||
let mut stack = IoStack::new();
|
let mut stack = IoStack::new();
|
||||||
|
|
||||||
let redirs = vec![
|
let redirs = vec![
|
||||||
crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output),
|
crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output),
|
||||||
crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output),
|
crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output),
|
||||||
];
|
];
|
||||||
|
|
||||||
stack.append_to_frame(redirs);
|
stack.append_to_frame(redirs);
|
||||||
assert_eq!(stack.curr_frame().len(), 2);
|
assert_eq!(stack.curr_frame().len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn iostack_frame_isolation() {
|
fn iostack_frame_isolation() {
|
||||||
let mut stack = IoStack::new();
|
let mut stack = IoStack::new();
|
||||||
|
|
||||||
// Add redir to first frame
|
// Add redir to first frame
|
||||||
let redir1 = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output);
|
let redir1 = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output);
|
||||||
stack.push_to_frame(redir1);
|
stack.push_to_frame(redir1);
|
||||||
assert_eq!(stack.curr_frame().len(), 1);
|
assert_eq!(stack.curr_frame().len(), 1);
|
||||||
|
|
||||||
// Push new frame
|
// Push new frame
|
||||||
stack.push_frame(IoFrame::new());
|
stack.push_frame(IoFrame::new());
|
||||||
assert_eq!(stack.curr_frame().len(), 0, "New frame should be empty");
|
assert_eq!(stack.curr_frame().len(), 0, "New frame should be empty");
|
||||||
|
|
||||||
// Add redir to second frame
|
// Add redir to second frame
|
||||||
let redir2 = crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output);
|
let redir2 = crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output);
|
||||||
stack.push_to_frame(redir2);
|
stack.push_to_frame(redir2);
|
||||||
assert_eq!(stack.curr_frame().len(), 1);
|
assert_eq!(stack.curr_frame().len(), 1);
|
||||||
|
|
||||||
// Pop second frame
|
// Pop second frame
|
||||||
let frame2 = stack.pop_frame();
|
let frame2 = stack.pop_frame();
|
||||||
assert_eq!(frame2.len(), 1);
|
assert_eq!(frame2.len(), 1);
|
||||||
|
|
||||||
// First frame should still have its redir
|
// First frame should still have its redir
|
||||||
assert_eq!(stack.curr_frame().len(), 1);
|
assert_eq!(stack.curr_frame().len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn iostack_flatten() {
|
fn iostack_flatten() {
|
||||||
let mut stack = IoStack::new();
|
let mut stack = IoStack::new();
|
||||||
|
|
||||||
// Add redir to first frame
|
// Add redir to first frame
|
||||||
let redir1 = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output);
|
let redir1 = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output);
|
||||||
stack.push_to_frame(redir1);
|
stack.push_to_frame(redir1);
|
||||||
|
|
||||||
// Push new frame with redir
|
// Push new frame with redir
|
||||||
let mut frame2 = IoFrame::new();
|
let mut frame2 = IoFrame::new();
|
||||||
frame2.push(crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output));
|
frame2.push(crate::parse::Redir::new(
|
||||||
stack.push_frame(frame2);
|
IoMode::fd(2, 1),
|
||||||
|
RedirType::Output,
|
||||||
|
));
|
||||||
|
stack.push_frame(frame2);
|
||||||
|
|
||||||
// Push third frame with redir
|
// Push third frame with redir
|
||||||
let mut frame3 = IoFrame::new();
|
let mut frame3 = IoFrame::new();
|
||||||
frame3.push(crate::parse::Redir::new(IoMode::fd(0, 3), RedirType::Input));
|
frame3.push(crate::parse::Redir::new(IoMode::fd(0, 3), RedirType::Input));
|
||||||
stack.push_frame(frame3);
|
stack.push_frame(frame3);
|
||||||
|
|
||||||
assert_eq!(stack.len(), 3);
|
assert_eq!(stack.len(), 3);
|
||||||
|
|
||||||
// Flatten
|
// Flatten
|
||||||
stack.flatten();
|
stack.flatten();
|
||||||
|
|
||||||
// Should have one frame with all redirects
|
// Should have one frame with all redirects
|
||||||
assert_eq!(stack.len(), 1);
|
assert_eq!(stack.len(), 1);
|
||||||
assert_eq!(stack.curr_frame().len(), 3);
|
assert_eq!(stack.curr_frame().len(), 3);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ioframe_new() {
|
fn ioframe_new() {
|
||||||
let frame = IoFrame::new();
|
let frame = IoFrame::new();
|
||||||
assert_eq!(frame.len(), 0);
|
assert_eq!(frame.len(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ioframe_from_redirs() {
|
fn ioframe_from_redirs() {
|
||||||
let redirs = vec![
|
let redirs = vec![
|
||||||
crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output),
|
crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output),
|
||||||
crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output),
|
crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output),
|
||||||
];
|
];
|
||||||
|
|
||||||
let frame = IoFrame::from_redirs(redirs);
|
let frame = IoFrame::from_redirs(redirs);
|
||||||
assert_eq!(frame.len(), 2);
|
assert_eq!(frame.len(), 2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn ioframe_push() {
|
fn ioframe_push() {
|
||||||
let mut frame = IoFrame::new();
|
let mut frame = IoFrame::new();
|
||||||
|
|
||||||
let redir = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output);
|
let redir = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output);
|
||||||
frame.push(redir);
|
frame.push(redir);
|
||||||
|
|
||||||
assert_eq!(frame.len(), 1);
|
assert_eq!(frame.len(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -350,28 +401,28 @@ fn ioframe_push() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn iomode_fd_construction() {
|
fn iomode_fd_construction() {
|
||||||
let io_mode = IoMode::fd(2, 1);
|
let io_mode = IoMode::fd(2, 1);
|
||||||
|
|
||||||
match io_mode {
|
match io_mode {
|
||||||
IoMode::Fd { tgt_fd, src_fd } => {
|
IoMode::Fd { tgt_fd, src_fd } => {
|
||||||
assert_eq!(tgt_fd, 2);
|
assert_eq!(tgt_fd, 2);
|
||||||
assert_eq!(src_fd, 1);
|
assert_eq!(src_fd, 1);
|
||||||
}
|
}
|
||||||
_ => panic!("Expected IoMode::Fd"),
|
_ => panic!("Expected IoMode::Fd"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn iomode_tgt_fd() {
|
fn iomode_tgt_fd() {
|
||||||
let fd_mode = IoMode::fd(2, 1);
|
let fd_mode = IoMode::fd(2, 1);
|
||||||
assert_eq!(fd_mode.tgt_fd(), 2);
|
assert_eq!(fd_mode.tgt_fd(), 2);
|
||||||
|
|
||||||
let file_mode = IoMode::file(1, std::path::PathBuf::from("test.txt"), RedirType::Output);
|
let file_mode = IoMode::file(1, std::path::PathBuf::from("test.txt"), RedirType::Output);
|
||||||
assert_eq!(file_mode.tgt_fd(), 1);
|
assert_eq!(file_mode.tgt_fd(), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn iomode_src_fd() {
|
fn iomode_src_fd() {
|
||||||
let fd_mode = IoMode::fd(2, 1);
|
let fd_mode = IoMode::fd(2, 1);
|
||||||
assert_eq!(fd_mode.src_fd(), 1);
|
assert_eq!(fd_mode.src_fd(), 1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,264 +6,280 @@ use crate::state::{LogTab, ScopeStack, ShellParam, VarFlags, VarTab};
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scopestack_new() {
|
fn scopestack_new() {
|
||||||
let stack = ScopeStack::new();
|
let stack = ScopeStack::new();
|
||||||
|
|
||||||
// Should start with one global scope
|
// Should start with one global scope
|
||||||
assert!(stack.var_exists("PATH") || !stack.var_exists("PATH")); // Just check it doesn't panic
|
assert!(stack.var_exists("PATH") || !stack.var_exists("PATH")); // Just check
|
||||||
|
// it doesn't
|
||||||
|
// panic
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scopestack_descend_ascend() {
|
fn scopestack_descend_ascend() {
|
||||||
let mut stack = ScopeStack::new();
|
let mut stack = ScopeStack::new();
|
||||||
|
|
||||||
// Set a global variable
|
// Set a global variable
|
||||||
stack.set_var("GLOBAL", "value1", VarFlags::NONE);
|
stack.set_var("GLOBAL", "value1", VarFlags::NONE);
|
||||||
assert_eq!(stack.get_var("GLOBAL"), "value1");
|
assert_eq!(stack.get_var("GLOBAL"), "value1");
|
||||||
|
|
||||||
// Descend into a new scope
|
// Descend into a new scope
|
||||||
stack.descend(None);
|
stack.descend(None);
|
||||||
|
|
||||||
// Global should still be visible
|
// Global should still be visible
|
||||||
assert_eq!(stack.get_var("GLOBAL"), "value1");
|
assert_eq!(stack.get_var("GLOBAL"), "value1");
|
||||||
|
|
||||||
// Set a local variable
|
// Set a local variable
|
||||||
stack.set_var("LOCAL", "value2", VarFlags::LOCAL);
|
stack.set_var("LOCAL", "value2", VarFlags::LOCAL);
|
||||||
assert_eq!(stack.get_var("LOCAL"), "value2");
|
assert_eq!(stack.get_var("LOCAL"), "value2");
|
||||||
|
|
||||||
// Ascend back to global scope
|
// Ascend back to global scope
|
||||||
stack.ascend();
|
stack.ascend();
|
||||||
|
|
||||||
// Global should still exist
|
// Global should still exist
|
||||||
assert_eq!(stack.get_var("GLOBAL"), "value1");
|
assert_eq!(stack.get_var("GLOBAL"), "value1");
|
||||||
|
|
||||||
// Local should no longer be visible
|
// Local should no longer be visible
|
||||||
assert_eq!(stack.get_var("LOCAL"), "");
|
assert_eq!(stack.get_var("LOCAL"), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scopestack_variable_shadowing() {
|
fn scopestack_variable_shadowing() {
|
||||||
let mut stack = ScopeStack::new();
|
let mut stack = ScopeStack::new();
|
||||||
|
|
||||||
// Set global variable
|
// Set global variable
|
||||||
stack.set_var("VAR", "global", VarFlags::NONE);
|
stack.set_var("VAR", "global", VarFlags::NONE);
|
||||||
assert_eq!(stack.get_var("VAR"), "global");
|
assert_eq!(stack.get_var("VAR"), "global");
|
||||||
|
|
||||||
// Descend into local scope
|
// Descend into local scope
|
||||||
stack.descend(None);
|
stack.descend(None);
|
||||||
|
|
||||||
// Set local variable with same name
|
// Set local variable with same name
|
||||||
stack.set_var("VAR", "local", VarFlags::LOCAL);
|
stack.set_var("VAR", "local", VarFlags::LOCAL);
|
||||||
assert_eq!(stack.get_var("VAR"), "local", "Local should shadow global");
|
assert_eq!(stack.get_var("VAR"), "local", "Local should shadow global");
|
||||||
|
|
||||||
// Ascend back
|
// Ascend back
|
||||||
stack.ascend();
|
stack.ascend();
|
||||||
|
|
||||||
// Global should be restored
|
// Global should be restored
|
||||||
assert_eq!(stack.get_var("VAR"), "global", "Global should be unchanged after ascend");
|
assert_eq!(
|
||||||
|
stack.get_var("VAR"),
|
||||||
|
"global",
|
||||||
|
"Global should be unchanged after ascend"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scopestack_local_vs_global_flag() {
|
fn scopestack_local_vs_global_flag() {
|
||||||
let mut stack = ScopeStack::new();
|
let mut stack = ScopeStack::new();
|
||||||
|
|
||||||
// Descend into a local scope
|
// Descend into a local scope
|
||||||
stack.descend(None);
|
stack.descend(None);
|
||||||
|
|
||||||
// Set with LOCAL flag - should go in current scope
|
// Set with LOCAL flag - should go in current scope
|
||||||
stack.set_var("LOCAL_VAR", "local", VarFlags::LOCAL);
|
stack.set_var("LOCAL_VAR", "local", VarFlags::LOCAL);
|
||||||
|
|
||||||
// Set without LOCAL flag - should go in global scope
|
// Set without LOCAL flag - should go in global scope
|
||||||
stack.set_var("GLOBAL_VAR", "global", VarFlags::NONE);
|
stack.set_var("GLOBAL_VAR", "global", VarFlags::NONE);
|
||||||
|
|
||||||
// Both visible from local scope
|
// Both visible from local scope
|
||||||
assert_eq!(stack.get_var("LOCAL_VAR"), "local");
|
assert_eq!(stack.get_var("LOCAL_VAR"), "local");
|
||||||
assert_eq!(stack.get_var("GLOBAL_VAR"), "global");
|
assert_eq!(stack.get_var("GLOBAL_VAR"), "global");
|
||||||
|
|
||||||
// Ascend to global
|
// Ascend to global
|
||||||
stack.ascend();
|
stack.ascend();
|
||||||
|
|
||||||
// Only global var should be visible
|
// Only global var should be visible
|
||||||
assert_eq!(stack.get_var("GLOBAL_VAR"), "global");
|
assert_eq!(stack.get_var("GLOBAL_VAR"), "global");
|
||||||
assert_eq!(stack.get_var("LOCAL_VAR"), "");
|
assert_eq!(stack.get_var("LOCAL_VAR"), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scopestack_multiple_levels() {
|
fn scopestack_multiple_levels() {
|
||||||
let mut stack = ScopeStack::new();
|
let mut stack = ScopeStack::new();
|
||||||
|
|
||||||
stack.set_var("LEVEL0", "global", VarFlags::NONE);
|
stack.set_var("LEVEL0", "global", VarFlags::NONE);
|
||||||
|
|
||||||
// Level 1
|
// Level 1
|
||||||
stack.descend(None);
|
stack.descend(None);
|
||||||
stack.set_var("LEVEL1", "first", VarFlags::LOCAL);
|
stack.set_var("LEVEL1", "first", VarFlags::LOCAL);
|
||||||
|
|
||||||
// Level 2
|
// Level 2
|
||||||
stack.descend(None);
|
stack.descend(None);
|
||||||
stack.set_var("LEVEL2", "second", VarFlags::LOCAL);
|
stack.set_var("LEVEL2", "second", VarFlags::LOCAL);
|
||||||
|
|
||||||
// All variables visible from deepest scope
|
// All variables visible from deepest scope
|
||||||
assert_eq!(stack.get_var("LEVEL0"), "global");
|
assert_eq!(stack.get_var("LEVEL0"), "global");
|
||||||
assert_eq!(stack.get_var("LEVEL1"), "first");
|
assert_eq!(stack.get_var("LEVEL1"), "first");
|
||||||
assert_eq!(stack.get_var("LEVEL2"), "second");
|
assert_eq!(stack.get_var("LEVEL2"), "second");
|
||||||
|
|
||||||
// Ascend to level 1
|
// Ascend to level 1
|
||||||
stack.ascend();
|
stack.ascend();
|
||||||
assert_eq!(stack.get_var("LEVEL0"), "global");
|
assert_eq!(stack.get_var("LEVEL0"), "global");
|
||||||
assert_eq!(stack.get_var("LEVEL1"), "first");
|
assert_eq!(stack.get_var("LEVEL1"), "first");
|
||||||
assert_eq!(stack.get_var("LEVEL2"), "");
|
assert_eq!(stack.get_var("LEVEL2"), "");
|
||||||
|
|
||||||
// Ascend to global
|
// Ascend to global
|
||||||
stack.ascend();
|
stack.ascend();
|
||||||
assert_eq!(stack.get_var("LEVEL0"), "global");
|
assert_eq!(stack.get_var("LEVEL0"), "global");
|
||||||
assert_eq!(stack.get_var("LEVEL1"), "");
|
assert_eq!(stack.get_var("LEVEL1"), "");
|
||||||
assert_eq!(stack.get_var("LEVEL2"), "");
|
assert_eq!(stack.get_var("LEVEL2"), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scopestack_cannot_ascend_past_global() {
|
fn scopestack_cannot_ascend_past_global() {
|
||||||
let mut stack = ScopeStack::new();
|
let mut stack = ScopeStack::new();
|
||||||
|
|
||||||
stack.set_var("VAR", "value", VarFlags::NONE);
|
stack.set_var("VAR", "value", VarFlags::NONE);
|
||||||
|
|
||||||
// Try to ascend from global scope (should be no-op)
|
// Try to ascend from global scope (should be no-op)
|
||||||
stack.ascend();
|
stack.ascend();
|
||||||
stack.ascend();
|
stack.ascend();
|
||||||
stack.ascend();
|
stack.ascend();
|
||||||
|
|
||||||
// Variable should still exist
|
// Variable should still exist
|
||||||
assert_eq!(stack.get_var("VAR"), "value");
|
assert_eq!(stack.get_var("VAR"), "value");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scopestack_descend_with_args() {
|
fn scopestack_descend_with_args() {
|
||||||
let mut stack = ScopeStack::new();
|
let mut stack = ScopeStack::new();
|
||||||
|
|
||||||
// Get initial param values from global scope (test process args)
|
// Get initial param values from global scope (test process args)
|
||||||
let global_param_1 = stack.get_param(ShellParam::Pos(1));
|
let global_param_1 = stack.get_param(ShellParam::Pos(1));
|
||||||
|
|
||||||
// Descend with positional parameters
|
// Descend with positional parameters
|
||||||
let args = vec!["local_arg1".to_string(), "local_arg2".to_string()];
|
let args = vec!["local_arg1".to_string(), "local_arg2".to_string()];
|
||||||
stack.descend(Some(args));
|
stack.descend(Some(args));
|
||||||
|
|
||||||
// In local scope, positional params come from the VarTab created during descend
|
// In local scope, positional params come from the VarTab created during descend
|
||||||
// VarTab::new() initializes with process args, then our args are appended
|
// VarTab::new() initializes with process args, then our args are appended
|
||||||
// So we check that SOME positional parameter exists (implementation detail may vary)
|
// So we check that SOME positional parameter exists (implementation detail may
|
||||||
let local_param = stack.get_param(ShellParam::Pos(1));
|
// vary)
|
||||||
assert!(!local_param.is_empty(), "Should have positional parameters in local scope");
|
let local_param = stack.get_param(ShellParam::Pos(1));
|
||||||
|
assert!(
|
||||||
|
!local_param.is_empty(),
|
||||||
|
"Should have positional parameters in local scope"
|
||||||
|
);
|
||||||
|
|
||||||
// Ascend back
|
// Ascend back
|
||||||
stack.ascend();
|
stack.ascend();
|
||||||
|
|
||||||
// Should be back to global scope parameters
|
// Should be back to global scope parameters
|
||||||
assert_eq!(stack.get_param(ShellParam::Pos(1)), global_param_1);
|
assert_eq!(stack.get_param(ShellParam::Pos(1)), global_param_1);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scopestack_global_parameters() {
|
fn scopestack_global_parameters() {
|
||||||
let mut stack = ScopeStack::new();
|
let mut stack = ScopeStack::new();
|
||||||
|
|
||||||
// Set global parameters
|
// Set global parameters
|
||||||
stack.set_param(ShellParam::Status, "0");
|
stack.set_param(ShellParam::Status, "0");
|
||||||
stack.set_param(ShellParam::LastJob, "1234");
|
stack.set_param(ShellParam::LastJob, "1234");
|
||||||
|
|
||||||
assert_eq!(stack.get_param(ShellParam::Status), "0");
|
assert_eq!(stack.get_param(ShellParam::Status), "0");
|
||||||
assert_eq!(stack.get_param(ShellParam::LastJob), "1234");
|
assert_eq!(stack.get_param(ShellParam::LastJob), "1234");
|
||||||
|
|
||||||
// Descend into local scope
|
// Descend into local scope
|
||||||
stack.descend(None);
|
stack.descend(None);
|
||||||
|
|
||||||
// Global parameters should still be visible
|
// Global parameters should still be visible
|
||||||
assert_eq!(stack.get_param(ShellParam::Status), "0");
|
assert_eq!(stack.get_param(ShellParam::Status), "0");
|
||||||
assert_eq!(stack.get_param(ShellParam::LastJob), "1234");
|
assert_eq!(stack.get_param(ShellParam::LastJob), "1234");
|
||||||
|
|
||||||
// Modify global parameter from local scope
|
// Modify global parameter from local scope
|
||||||
stack.set_param(ShellParam::Status, "1");
|
stack.set_param(ShellParam::Status, "1");
|
||||||
assert_eq!(stack.get_param(ShellParam::Status), "1");
|
assert_eq!(stack.get_param(ShellParam::Status), "1");
|
||||||
|
|
||||||
// Ascend
|
// Ascend
|
||||||
stack.ascend();
|
stack.ascend();
|
||||||
|
|
||||||
// Global parameter should retain modified value
|
// Global parameter should retain modified value
|
||||||
assert_eq!(stack.get_param(ShellParam::Status), "1");
|
assert_eq!(stack.get_param(ShellParam::Status), "1");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scopestack_unset_var() {
|
fn scopestack_unset_var() {
|
||||||
let mut stack = ScopeStack::new();
|
let mut stack = ScopeStack::new();
|
||||||
|
|
||||||
stack.set_var("VAR", "value", VarFlags::NONE);
|
stack.set_var("VAR", "value", VarFlags::NONE);
|
||||||
assert_eq!(stack.get_var("VAR"), "value");
|
assert_eq!(stack.get_var("VAR"), "value");
|
||||||
|
|
||||||
stack.unset_var("VAR");
|
stack.unset_var("VAR");
|
||||||
assert_eq!(stack.get_var("VAR"), "");
|
assert_eq!(stack.get_var("VAR"), "");
|
||||||
assert!(!stack.var_exists("VAR"));
|
assert!(!stack.var_exists("VAR"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scopestack_unset_finds_innermost() {
|
fn scopestack_unset_finds_innermost() {
|
||||||
let mut stack = ScopeStack::new();
|
let mut stack = ScopeStack::new();
|
||||||
|
|
||||||
// Set global
|
// Set global
|
||||||
stack.set_var("VAR", "global", VarFlags::NONE);
|
stack.set_var("VAR", "global", VarFlags::NONE);
|
||||||
|
|
||||||
// Descend and shadow
|
// Descend and shadow
|
||||||
stack.descend(None);
|
stack.descend(None);
|
||||||
stack.set_var("VAR", "local", VarFlags::LOCAL);
|
stack.set_var("VAR", "local", VarFlags::LOCAL);
|
||||||
assert_eq!(stack.get_var("VAR"), "local");
|
assert_eq!(stack.get_var("VAR"), "local");
|
||||||
|
|
||||||
// Unset should remove local, revealing global
|
// Unset should remove local, revealing global
|
||||||
stack.unset_var("VAR");
|
stack.unset_var("VAR");
|
||||||
assert_eq!(stack.get_var("VAR"), "global");
|
assert_eq!(stack.get_var("VAR"), "global");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scopestack_export_var() {
|
fn scopestack_export_var() {
|
||||||
let mut stack = ScopeStack::new();
|
let mut stack = ScopeStack::new();
|
||||||
|
|
||||||
stack.set_var("VAR", "value", VarFlags::NONE);
|
stack.set_var("VAR", "value", VarFlags::NONE);
|
||||||
|
|
||||||
// Export the variable
|
// Export the variable
|
||||||
stack.export_var("VAR");
|
stack.export_var("VAR");
|
||||||
|
|
||||||
// Variable should still be accessible (flag is internal detail)
|
// Variable should still be accessible (flag is internal detail)
|
||||||
assert_eq!(stack.get_var("VAR"), "value");
|
assert_eq!(stack.get_var("VAR"), "value");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scopestack_var_exists() {
|
fn scopestack_var_exists() {
|
||||||
let mut stack = ScopeStack::new();
|
let mut stack = ScopeStack::new();
|
||||||
|
|
||||||
assert!(!stack.var_exists("NONEXISTENT"));
|
assert!(!stack.var_exists("NONEXISTENT"));
|
||||||
|
|
||||||
stack.set_var("EXISTS", "yes", VarFlags::NONE);
|
stack.set_var("EXISTS", "yes", VarFlags::NONE);
|
||||||
assert!(stack.var_exists("EXISTS"));
|
assert!(stack.var_exists("EXISTS"));
|
||||||
|
|
||||||
stack.descend(None);
|
stack.descend(None);
|
||||||
assert!(stack.var_exists("EXISTS"), "Global var should be visible in local scope");
|
assert!(
|
||||||
|
stack.var_exists("EXISTS"),
|
||||||
|
"Global var should be visible in local scope"
|
||||||
|
);
|
||||||
|
|
||||||
stack.set_var("LOCAL", "yes", VarFlags::LOCAL);
|
stack.set_var("LOCAL", "yes", VarFlags::LOCAL);
|
||||||
assert!(stack.var_exists("LOCAL"));
|
assert!(stack.var_exists("LOCAL"));
|
||||||
|
|
||||||
stack.ascend();
|
stack.ascend();
|
||||||
assert!(!stack.var_exists("LOCAL"), "Local var should not exist after ascend");
|
assert!(
|
||||||
|
!stack.var_exists("LOCAL"),
|
||||||
|
"Local var should not exist after ascend"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scopestack_flatten_vars() {
|
fn scopestack_flatten_vars() {
|
||||||
let mut stack = ScopeStack::new();
|
let mut stack = ScopeStack::new();
|
||||||
|
|
||||||
stack.set_var("GLOBAL1", "g1", VarFlags::NONE);
|
stack.set_var("GLOBAL1", "g1", VarFlags::NONE);
|
||||||
stack.set_var("GLOBAL2", "g2", VarFlags::NONE);
|
stack.set_var("GLOBAL2", "g2", VarFlags::NONE);
|
||||||
|
|
||||||
stack.descend(None);
|
stack.descend(None);
|
||||||
stack.set_var("LOCAL1", "l1", VarFlags::LOCAL);
|
stack.set_var("LOCAL1", "l1", VarFlags::LOCAL);
|
||||||
|
|
||||||
let flattened = stack.flatten_vars();
|
let flattened = stack.flatten_vars();
|
||||||
|
|
||||||
// Should contain variables from all scopes
|
// Should contain variables from all scopes
|
||||||
assert!(flattened.contains_key("GLOBAL1"));
|
assert!(flattened.contains_key("GLOBAL1"));
|
||||||
assert!(flattened.contains_key("GLOBAL2"));
|
assert!(flattened.contains_key("GLOBAL2"));
|
||||||
assert!(flattened.contains_key("LOCAL1"));
|
assert!(flattened.contains_key("LOCAL1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -272,78 +288,81 @@ fn scopestack_flatten_vars() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn logtab_new() {
|
fn logtab_new() {
|
||||||
let logtab = LogTab::new();
|
let logtab = LogTab::new();
|
||||||
assert_eq!(logtab.funcs().len(), 0);
|
assert_eq!(logtab.funcs().len(), 0);
|
||||||
assert_eq!(logtab.aliases().len(), 0);
|
assert_eq!(logtab.aliases().len(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn logtab_insert_get_alias() {
|
fn logtab_insert_get_alias() {
|
||||||
let mut logtab = LogTab::new();
|
let mut logtab = LogTab::new();
|
||||||
|
|
||||||
logtab.insert_alias("ll", "ls -la");
|
logtab.insert_alias("ll", "ls -la");
|
||||||
assert_eq!(logtab.get_alias("ll"), Some("ls -la".to_string()));
|
assert_eq!(logtab.get_alias("ll"), Some("ls -la".to_string()));
|
||||||
assert_eq!(logtab.get_alias("nonexistent"), None);
|
assert_eq!(logtab.get_alias("nonexistent"), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn logtab_overwrite_alias() {
|
fn logtab_overwrite_alias() {
|
||||||
let mut logtab = LogTab::new();
|
let mut logtab = LogTab::new();
|
||||||
|
|
||||||
logtab.insert_alias("ll", "ls -la");
|
logtab.insert_alias("ll", "ls -la");
|
||||||
assert_eq!(logtab.get_alias("ll"), Some("ls -la".to_string()));
|
assert_eq!(logtab.get_alias("ll"), Some("ls -la".to_string()));
|
||||||
|
|
||||||
logtab.insert_alias("ll", "ls -lah");
|
logtab.insert_alias("ll", "ls -lah");
|
||||||
assert_eq!(logtab.get_alias("ll"), Some("ls -lah".to_string()));
|
assert_eq!(logtab.get_alias("ll"), Some("ls -lah".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn logtab_remove_alias() {
|
fn logtab_remove_alias() {
|
||||||
let mut logtab = LogTab::new();
|
let mut logtab = LogTab::new();
|
||||||
|
|
||||||
logtab.insert_alias("ll", "ls -la");
|
logtab.insert_alias("ll", "ls -la");
|
||||||
assert!(logtab.get_alias("ll").is_some());
|
assert!(logtab.get_alias("ll").is_some());
|
||||||
|
|
||||||
logtab.remove_alias("ll");
|
logtab.remove_alias("ll");
|
||||||
assert!(logtab.get_alias("ll").is_none());
|
assert!(logtab.get_alias("ll").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn logtab_clear_aliases() {
|
fn logtab_clear_aliases() {
|
||||||
let mut logtab = LogTab::new();
|
let mut logtab = LogTab::new();
|
||||||
|
|
||||||
logtab.insert_alias("ll", "ls -la");
|
logtab.insert_alias("ll", "ls -la");
|
||||||
logtab.insert_alias("la", "ls -A");
|
logtab.insert_alias("la", "ls -A");
|
||||||
logtab.insert_alias("l", "ls -CF");
|
logtab.insert_alias("l", "ls -CF");
|
||||||
|
|
||||||
assert_eq!(logtab.aliases().len(), 3);
|
assert_eq!(logtab.aliases().len(), 3);
|
||||||
|
|
||||||
logtab.clear_aliases();
|
logtab.clear_aliases();
|
||||||
assert_eq!(logtab.aliases().len(), 0);
|
assert_eq!(logtab.aliases().len(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn logtab_multiple_aliases() {
|
fn logtab_multiple_aliases() {
|
||||||
let mut logtab = LogTab::new();
|
let mut logtab = LogTab::new();
|
||||||
|
|
||||||
logtab.insert_alias("ll", "ls -la");
|
logtab.insert_alias("ll", "ls -la");
|
||||||
logtab.insert_alias("la", "ls -A");
|
logtab.insert_alias("la", "ls -A");
|
||||||
logtab.insert_alias("grep", "grep --color=auto");
|
logtab.insert_alias("grep", "grep --color=auto");
|
||||||
|
|
||||||
assert_eq!(logtab.aliases().len(), 3);
|
assert_eq!(logtab.aliases().len(), 3);
|
||||||
assert_eq!(logtab.get_alias("ll"), Some("ls -la".to_string()));
|
assert_eq!(logtab.get_alias("ll"), Some("ls -la".to_string()));
|
||||||
assert_eq!(logtab.get_alias("la"), Some("ls -A".to_string()));
|
assert_eq!(logtab.get_alias("la"), Some("ls -A".to_string()));
|
||||||
assert_eq!(logtab.get_alias("grep"), Some("grep --color=auto".to_string()));
|
assert_eq!(
|
||||||
|
logtab.get_alias("grep"),
|
||||||
|
Some("grep --color=auto".to_string())
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Note: Function tests are limited because ShFunc requires complex setup (parsed AST)
|
// Note: Function tests are limited because ShFunc requires complex setup
|
||||||
// We'll test the basic storage/retrieval mechanics
|
// (parsed AST) We'll test the basic storage/retrieval mechanics
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn logtab_funcs_empty_initially() {
|
fn logtab_funcs_empty_initially() {
|
||||||
let logtab = LogTab::new();
|
let logtab = LogTab::new();
|
||||||
assert_eq!(logtab.funcs().len(), 0);
|
assert_eq!(logtab.funcs().len(), 0);
|
||||||
assert!(logtab.get_func("nonexistent").is_none());
|
assert!(logtab.get_func("nonexistent").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -352,109 +371,112 @@ fn logtab_funcs_empty_initially() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn vartab_new() {
|
fn vartab_new() {
|
||||||
let vartab = VarTab::new();
|
let vartab = VarTab::new();
|
||||||
// VarTab initializes with some default params, just check it doesn't panic
|
// VarTab initializes with some default params, just check it doesn't panic
|
||||||
assert!(vartab.get_var("NONEXISTENT").is_empty());
|
assert!(vartab.get_var("NONEXISTENT").is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn vartab_set_get_var() {
|
fn vartab_set_get_var() {
|
||||||
let mut vartab = VarTab::new();
|
let mut vartab = VarTab::new();
|
||||||
|
|
||||||
vartab.set_var("TEST", "value", VarFlags::NONE);
|
vartab.set_var("TEST", "value", VarFlags::NONE);
|
||||||
assert_eq!(vartab.get_var("TEST"), "value");
|
assert_eq!(vartab.get_var("TEST"), "value");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn vartab_overwrite_var() {
|
fn vartab_overwrite_var() {
|
||||||
let mut vartab = VarTab::new();
|
let mut vartab = VarTab::new();
|
||||||
|
|
||||||
vartab.set_var("VAR", "value1", VarFlags::NONE);
|
vartab.set_var("VAR", "value1", VarFlags::NONE);
|
||||||
assert_eq!(vartab.get_var("VAR"), "value1");
|
assert_eq!(vartab.get_var("VAR"), "value1");
|
||||||
|
|
||||||
vartab.set_var("VAR", "value2", VarFlags::NONE);
|
vartab.set_var("VAR", "value2", VarFlags::NONE);
|
||||||
assert_eq!(vartab.get_var("VAR"), "value2");
|
assert_eq!(vartab.get_var("VAR"), "value2");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn vartab_var_exists() {
|
fn vartab_var_exists() {
|
||||||
let mut vartab = VarTab::new();
|
let mut vartab = VarTab::new();
|
||||||
|
|
||||||
assert!(!vartab.var_exists("TEST"));
|
assert!(!vartab.var_exists("TEST"));
|
||||||
|
|
||||||
vartab.set_var("TEST", "value", VarFlags::NONE);
|
vartab.set_var("TEST", "value", VarFlags::NONE);
|
||||||
assert!(vartab.var_exists("TEST"));
|
assert!(vartab.var_exists("TEST"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn vartab_unset_var() {
|
fn vartab_unset_var() {
|
||||||
let mut vartab = VarTab::new();
|
let mut vartab = VarTab::new();
|
||||||
|
|
||||||
vartab.set_var("VAR", "value", VarFlags::NONE);
|
vartab.set_var("VAR", "value", VarFlags::NONE);
|
||||||
assert!(vartab.var_exists("VAR"));
|
assert!(vartab.var_exists("VAR"));
|
||||||
|
|
||||||
vartab.unset_var("VAR");
|
vartab.unset_var("VAR");
|
||||||
assert!(!vartab.var_exists("VAR"));
|
assert!(!vartab.var_exists("VAR"));
|
||||||
assert_eq!(vartab.get_var("VAR"), "");
|
assert_eq!(vartab.get_var("VAR"), "");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn vartab_export_var() {
|
fn vartab_export_var() {
|
||||||
let mut vartab = VarTab::new();
|
let mut vartab = VarTab::new();
|
||||||
|
|
||||||
vartab.set_var("VAR", "value", VarFlags::NONE);
|
vartab.set_var("VAR", "value", VarFlags::NONE);
|
||||||
vartab.export_var("VAR");
|
vartab.export_var("VAR");
|
||||||
|
|
||||||
// Variable should still be accessible
|
// Variable should still be accessible
|
||||||
assert_eq!(vartab.get_var("VAR"), "value");
|
assert_eq!(vartab.get_var("VAR"), "value");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn vartab_positional_params() {
|
fn vartab_positional_params() {
|
||||||
let mut vartab = VarTab::new();
|
let mut vartab = VarTab::new();
|
||||||
|
|
||||||
// Get the current argv length
|
// Get the current argv length
|
||||||
let initial_len = vartab.sh_argv().len();
|
let initial_len = vartab.sh_argv().len();
|
||||||
|
|
||||||
// Clear and reinitialize with known args
|
// Clear and reinitialize with known args
|
||||||
vartab.clear_args(); // This keeps $0 as current exe
|
vartab.clear_args(); // This keeps $0 as current exe
|
||||||
|
|
||||||
// After clear_args, should have just $0
|
// After clear_args, should have just $0
|
||||||
// Push additional args
|
// Push additional args
|
||||||
vartab.bpush_arg("test_arg1".to_string());
|
vartab.bpush_arg("test_arg1".to_string());
|
||||||
vartab.bpush_arg("test_arg2".to_string());
|
vartab.bpush_arg("test_arg2".to_string());
|
||||||
|
|
||||||
// Now sh_argv should be: [exe_path, test_arg1, test_arg2]
|
// Now sh_argv should be: [exe_path, test_arg1, test_arg2]
|
||||||
// Pos(0) = exe_path, Pos(1) = test_arg1, Pos(2) = test_arg2
|
// Pos(0) = exe_path, Pos(1) = test_arg1, Pos(2) = test_arg2
|
||||||
let final_len = vartab.sh_argv().len();
|
let final_len = vartab.sh_argv().len();
|
||||||
assert!(final_len > initial_len || final_len >= 1, "Should have arguments");
|
assert!(
|
||||||
|
final_len > initial_len || final_len >= 1,
|
||||||
|
"Should have arguments"
|
||||||
|
);
|
||||||
|
|
||||||
// Just verify we can retrieve the last args we pushed
|
// Just verify we can retrieve the last args we pushed
|
||||||
let last_idx = final_len - 1;
|
let last_idx = final_len - 1;
|
||||||
assert_eq!(vartab.get_param(ShellParam::Pos(last_idx)), "test_arg2");
|
assert_eq!(vartab.get_param(ShellParam::Pos(last_idx)), "test_arg2");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn vartab_shell_argv_operations() {
|
fn vartab_shell_argv_operations() {
|
||||||
let mut vartab = VarTab::new();
|
let mut vartab = VarTab::new();
|
||||||
|
|
||||||
// Clear initial args and set fresh ones
|
// Clear initial args and set fresh ones
|
||||||
vartab.clear_args();
|
vartab.clear_args();
|
||||||
|
|
||||||
// Push args (clear_args leaves $0, so these become $1, $2, $3)
|
// Push args (clear_args leaves $0, so these become $1, $2, $3)
|
||||||
vartab.bpush_arg("arg1".to_string());
|
vartab.bpush_arg("arg1".to_string());
|
||||||
vartab.bpush_arg("arg2".to_string());
|
vartab.bpush_arg("arg2".to_string());
|
||||||
vartab.bpush_arg("arg3".to_string());
|
vartab.bpush_arg("arg3".to_string());
|
||||||
|
|
||||||
// Get initial arg count
|
// Get initial arg count
|
||||||
let initial_len = vartab.sh_argv().len();
|
let initial_len = vartab.sh_argv().len();
|
||||||
|
|
||||||
// Pop first arg (removes $0)
|
// Pop first arg (removes $0)
|
||||||
let popped = vartab.fpop_arg();
|
let popped = vartab.fpop_arg();
|
||||||
assert!(popped.is_some());
|
assert!(popped.is_some());
|
||||||
|
|
||||||
// Should have one fewer arg
|
// Should have one fewer arg
|
||||||
assert_eq!(vartab.sh_argv().len(), initial_len - 1);
|
assert_eq!(vartab.sh_argv().len(), initial_len - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -463,39 +485,39 @@ fn vartab_shell_argv_operations() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn varflags_none() {
|
fn varflags_none() {
|
||||||
let flags = VarFlags::NONE;
|
let flags = VarFlags::NONE;
|
||||||
assert!(!flags.contains(VarFlags::EXPORT));
|
assert!(!flags.contains(VarFlags::EXPORT));
|
||||||
assert!(!flags.contains(VarFlags::LOCAL));
|
assert!(!flags.contains(VarFlags::LOCAL));
|
||||||
assert!(!flags.contains(VarFlags::READONLY));
|
assert!(!flags.contains(VarFlags::READONLY));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn varflags_export() {
|
fn varflags_export() {
|
||||||
let flags = VarFlags::EXPORT;
|
let flags = VarFlags::EXPORT;
|
||||||
assert!(flags.contains(VarFlags::EXPORT));
|
assert!(flags.contains(VarFlags::EXPORT));
|
||||||
assert!(!flags.contains(VarFlags::LOCAL));
|
assert!(!flags.contains(VarFlags::LOCAL));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn varflags_local() {
|
fn varflags_local() {
|
||||||
let flags = VarFlags::LOCAL;
|
let flags = VarFlags::LOCAL;
|
||||||
assert!(!flags.contains(VarFlags::EXPORT));
|
assert!(!flags.contains(VarFlags::EXPORT));
|
||||||
assert!(flags.contains(VarFlags::LOCAL));
|
assert!(flags.contains(VarFlags::LOCAL));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn varflags_combine() {
|
fn varflags_combine() {
|
||||||
let flags = VarFlags::EXPORT | VarFlags::LOCAL;
|
let flags = VarFlags::EXPORT | VarFlags::LOCAL;
|
||||||
assert!(flags.contains(VarFlags::EXPORT));
|
assert!(flags.contains(VarFlags::EXPORT));
|
||||||
assert!(flags.contains(VarFlags::LOCAL));
|
assert!(flags.contains(VarFlags::LOCAL));
|
||||||
assert!(!flags.contains(VarFlags::READONLY));
|
assert!(!flags.contains(VarFlags::READONLY));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn varflags_readonly() {
|
fn varflags_readonly() {
|
||||||
let flags = VarFlags::READONLY;
|
let flags = VarFlags::READONLY;
|
||||||
assert!(flags.contains(VarFlags::READONLY));
|
assert!(flags.contains(VarFlags::READONLY));
|
||||||
assert!(!flags.contains(VarFlags::EXPORT));
|
assert!(!flags.contains(VarFlags::EXPORT));
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -504,49 +526,70 @@ fn varflags_readonly() {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn shellparam_is_global() {
|
fn shellparam_is_global() {
|
||||||
assert!(ShellParam::Status.is_global());
|
assert!(ShellParam::Status.is_global());
|
||||||
assert!(ShellParam::ShPid.is_global());
|
assert!(ShellParam::ShPid.is_global());
|
||||||
assert!(ShellParam::LastJob.is_global());
|
assert!(ShellParam::LastJob.is_global());
|
||||||
assert!(ShellParam::ShellName.is_global());
|
assert!(ShellParam::ShellName.is_global());
|
||||||
|
|
||||||
assert!(!ShellParam::Pos(1).is_global());
|
assert!(!ShellParam::Pos(1).is_global());
|
||||||
assert!(!ShellParam::AllArgs.is_global());
|
assert!(!ShellParam::AllArgs.is_global());
|
||||||
assert!(!ShellParam::AllArgsStr.is_global());
|
assert!(!ShellParam::AllArgsStr.is_global());
|
||||||
assert!(!ShellParam::ArgCount.is_global());
|
assert!(!ShellParam::ArgCount.is_global());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn shellparam_from_str() {
|
fn shellparam_from_str() {
|
||||||
assert!(matches!("?".parse::<ShellParam>().unwrap(), ShellParam::Status));
|
assert!(matches!(
|
||||||
assert!(matches!("$".parse::<ShellParam>().unwrap(), ShellParam::ShPid));
|
"?".parse::<ShellParam>().unwrap(),
|
||||||
assert!(matches!("!".parse::<ShellParam>().unwrap(), ShellParam::LastJob));
|
ShellParam::Status
|
||||||
assert!(matches!("0".parse::<ShellParam>().unwrap(), ShellParam::ShellName));
|
));
|
||||||
assert!(matches!("@".parse::<ShellParam>().unwrap(), ShellParam::AllArgs));
|
assert!(matches!(
|
||||||
assert!(matches!("*".parse::<ShellParam>().unwrap(), ShellParam::AllArgsStr));
|
"$".parse::<ShellParam>().unwrap(),
|
||||||
assert!(matches!("#".parse::<ShellParam>().unwrap(), ShellParam::ArgCount));
|
ShellParam::ShPid
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
"!".parse::<ShellParam>().unwrap(),
|
||||||
|
ShellParam::LastJob
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
"0".parse::<ShellParam>().unwrap(),
|
||||||
|
ShellParam::ShellName
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
"@".parse::<ShellParam>().unwrap(),
|
||||||
|
ShellParam::AllArgs
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
"*".parse::<ShellParam>().unwrap(),
|
||||||
|
ShellParam::AllArgsStr
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
"#".parse::<ShellParam>().unwrap(),
|
||||||
|
ShellParam::ArgCount
|
||||||
|
));
|
||||||
|
|
||||||
match "1".parse::<ShellParam>().unwrap() {
|
match "1".parse::<ShellParam>().unwrap() {
|
||||||
ShellParam::Pos(n) => assert_eq!(n, 1),
|
ShellParam::Pos(n) => assert_eq!(n, 1),
|
||||||
_ => panic!("Expected Pos(1)"),
|
_ => panic!("Expected Pos(1)"),
|
||||||
}
|
}
|
||||||
|
|
||||||
match "42".parse::<ShellParam>().unwrap() {
|
match "42".parse::<ShellParam>().unwrap() {
|
||||||
ShellParam::Pos(n) => assert_eq!(n, 42),
|
ShellParam::Pos(n) => assert_eq!(n, 42),
|
||||||
_ => panic!("Expected Pos(42)"),
|
_ => panic!("Expected Pos(42)"),
|
||||||
}
|
}
|
||||||
|
|
||||||
assert!("invalid".parse::<ShellParam>().is_err());
|
assert!("invalid".parse::<ShellParam>().is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn shellparam_display() {
|
fn shellparam_display() {
|
||||||
assert_eq!(ShellParam::Status.to_string(), "?");
|
assert_eq!(ShellParam::Status.to_string(), "?");
|
||||||
assert_eq!(ShellParam::ShPid.to_string(), "$");
|
assert_eq!(ShellParam::ShPid.to_string(), "$");
|
||||||
assert_eq!(ShellParam::LastJob.to_string(), "!");
|
assert_eq!(ShellParam::LastJob.to_string(), "!");
|
||||||
assert_eq!(ShellParam::ShellName.to_string(), "0");
|
assert_eq!(ShellParam::ShellName.to_string(), "0");
|
||||||
assert_eq!(ShellParam::AllArgs.to_string(), "@");
|
assert_eq!(ShellParam::AllArgs.to_string(), "@");
|
||||||
assert_eq!(ShellParam::AllArgsStr.to_string(), "*");
|
assert_eq!(ShellParam::AllArgsStr.to_string(), "*");
|
||||||
assert_eq!(ShellParam::ArgCount.to_string(), "#");
|
assert_eq!(ShellParam::ArgCount.to_string(), "#");
|
||||||
assert_eq!(ShellParam::Pos(1).to_string(), "1");
|
assert_eq!(ShellParam::Pos(1).to_string(), "1");
|
||||||
assert_eq!(ShellParam::Pos(99).to_string(), "99");
|
assert_eq!(ShellParam::Pos(99).to_string(), "99");
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user