command arguments are now underlined if they match an existing path -m ran rustfmt on the entire codebase
This commit is contained in:
@@ -3,7 +3,7 @@ use crate::{
|
||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||
parse::{NdRule, Node},
|
||||
prelude::*,
|
||||
procio::{borrow_fd, IoStack},
|
||||
procio::{IoStack, borrow_fd},
|
||||
state::{self, read_logic, write_logic},
|
||||
};
|
||||
|
||||
|
||||
@@ -49,9 +49,13 @@ pub fn cd(node: Node, job: &mut JobBldr) -> ShResult<()> {
|
||||
span,
|
||||
));
|
||||
}
|
||||
let new_dir = env::current_dir().map_err(
|
||||
|e| ShErr::full(ShErrKind::ExecFail, format!("cd: Failed to get current directory: {}", e), span)
|
||||
)?;
|
||||
let new_dir = env::current_dir().map_err(|e| {
|
||||
ShErr::full(
|
||||
ShErrKind::ExecFail,
|
||||
format!("cd: Failed to get current directory: {}", e),
|
||||
span,
|
||||
)
|
||||
})?;
|
||||
unsafe { env::set_var("PWD", new_dir) };
|
||||
|
||||
state::set_status(0);
|
||||
|
||||
@@ -1,14 +1,34 @@
|
||||
use std::sync::LazyLock;
|
||||
|
||||
use crate::{
|
||||
builtin::setup_builtin, expand::expand_prompt, getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node}, prelude::*, procio::{IoStack, borrow_fd}, state
|
||||
builtin::setup_builtin,
|
||||
expand::expand_prompt,
|
||||
getopt::{Opt, OptSpec, get_opts_from_tokens},
|
||||
jobs::JobBldr,
|
||||
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
|
||||
parse::{NdRule, Node},
|
||||
prelude::*,
|
||||
procio::{IoStack, borrow_fd},
|
||||
state,
|
||||
};
|
||||
|
||||
pub const ECHO_OPTS: [OptSpec; 4] = [
|
||||
OptSpec { opt: Opt::Short('n'), takes_arg: false },
|
||||
OptSpec { opt: Opt::Short('E'), takes_arg: false },
|
||||
OptSpec { opt: Opt::Short('e'), takes_arg: false },
|
||||
OptSpec { opt: Opt::Short('p'), takes_arg: false },
|
||||
OptSpec {
|
||||
opt: Opt::Short('n'),
|
||||
takes_arg: false,
|
||||
},
|
||||
OptSpec {
|
||||
opt: Opt::Short('E'),
|
||||
takes_arg: false,
|
||||
},
|
||||
OptSpec {
|
||||
opt: Opt::Short('e'),
|
||||
takes_arg: false,
|
||||
},
|
||||
OptSpec {
|
||||
opt: Opt::Short('p'),
|
||||
takes_arg: false,
|
||||
},
|
||||
];
|
||||
|
||||
bitflags! {
|
||||
@@ -40,13 +60,15 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
|
||||
borrow_fd(STDOUT_FILENO)
|
||||
};
|
||||
|
||||
let mut echo_output = prepare_echo_args(argv
|
||||
let mut echo_output = prepare_echo_args(
|
||||
argv
|
||||
.into_iter()
|
||||
.map(|a| a.0) // Extract the String from the tuple of (String,Span)
|
||||
.collect::<Vec<_>>(),
|
||||
flags.contains(EchoFlags::USE_ESCAPE),
|
||||
flags.contains(EchoFlags::USE_PROMPT)
|
||||
)?.join(" ");
|
||||
flags.contains(EchoFlags::USE_PROMPT),
|
||||
)?
|
||||
.join(" ");
|
||||
|
||||
if !flags.contains(EchoFlags::NO_NEWLINE) && !echo_output.ends_with('\n') {
|
||||
echo_output.push('\n')
|
||||
@@ -58,14 +80,18 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn prepare_echo_args(argv: Vec<String>, use_escape: bool, use_prompt: bool) -> ShResult<Vec<String>> {
|
||||
pub fn prepare_echo_args(
|
||||
argv: Vec<String>,
|
||||
use_escape: bool,
|
||||
use_prompt: bool,
|
||||
) -> ShResult<Vec<String>> {
|
||||
if !use_escape {
|
||||
if use_prompt {
|
||||
let expanded: ShResult<Vec<String>> = argv
|
||||
.into_iter()
|
||||
.map(|s| expand_prompt(s.as_str()))
|
||||
.collect();
|
||||
return expanded
|
||||
return expanded;
|
||||
}
|
||||
return Ok(argv);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||
parse::{execute::prepare_argv, NdRule, Node},
|
||||
parse::{NdRule, Node, execute::prepare_argv},
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
@@ -32,7 +32,6 @@ pub fn flowctl(node: Node, kind: ShErrKind) -> ShResult<()> {
|
||||
code = status;
|
||||
}
|
||||
|
||||
|
||||
let kind = match kind {
|
||||
LoopContinue(_) => LoopContinue(code),
|
||||
LoopBreak(_) => LoopBreak(code),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
use crate::{
|
||||
jobs::{JobBldr, JobCmdFlags, JobID},
|
||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||
parse::{lex::Span, NdRule, Node},
|
||||
parse::{NdRule, Node, lex::Span},
|
||||
prelude::*,
|
||||
procio::{borrow_fd, IoStack},
|
||||
procio::{IoStack, borrow_fd},
|
||||
state::{self, read_jobs, write_jobs},
|
||||
};
|
||||
|
||||
@@ -168,7 +168,7 @@ pub fn jobs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
|
||||
ShErrKind::SyntaxErr,
|
||||
"Invalid flag in jobs call",
|
||||
span,
|
||||
))
|
||||
));
|
||||
}
|
||||
};
|
||||
flags |= flag
|
||||
|
||||
@@ -4,7 +4,9 @@ use crate::{
|
||||
jobs::{ChildProc, JobBldr},
|
||||
libsh::error::ShResult,
|
||||
parse::{
|
||||
Redir, execute::prepare_argv, lex::{Span, Tk}
|
||||
Redir,
|
||||
execute::prepare_argv,
|
||||
lex::{Span, Tk},
|
||||
},
|
||||
procio::{IoFrame, IoStack, RedirGuard},
|
||||
};
|
||||
@@ -16,19 +18,17 @@ pub mod export;
|
||||
pub mod flowctl;
|
||||
pub mod jobctl;
|
||||
pub mod pwd;
|
||||
pub mod read;
|
||||
pub mod shift;
|
||||
pub mod shopt;
|
||||
pub mod source;
|
||||
pub mod test; // [[ ]] thing
|
||||
pub mod read;
|
||||
pub mod zoltraak;
|
||||
pub mod trap;
|
||||
pub mod zoltraak;
|
||||
|
||||
pub const BUILTINS: [&str; 21] = [
|
||||
"echo", "cd", "read", "export", "pwd", "source",
|
||||
"shift", "jobs", "fg", "bg", "alias", "unalias",
|
||||
"return", "break", "continue", "exit", "zoltraak",
|
||||
"shopt", "builtin", "command", "trap"
|
||||
"echo", "cd", "read", "export", "pwd", "source", "shift", "jobs", "fg", "bg", "alias", "unalias",
|
||||
"return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "command", "trap",
|
||||
];
|
||||
|
||||
/// Sets up a builtin command
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
libsh::error::ShResult,
|
||||
parse::{NdRule, Node},
|
||||
prelude::*,
|
||||
procio::{borrow_fd, IoStack},
|
||||
procio::{IoStack, borrow_fd},
|
||||
state,
|
||||
};
|
||||
|
||||
|
||||
@@ -1,16 +1,50 @@
|
||||
use bitflags::bitflags;
|
||||
use nix::{errno::Errno, libc::{STDIN_FILENO, STDOUT_FILENO}, unistd::{isatty, read, write}};
|
||||
use nix::{
|
||||
errno::Errno,
|
||||
libc::{STDIN_FILENO, STDOUT_FILENO},
|
||||
unistd::{isatty, read, write},
|
||||
};
|
||||
|
||||
use crate::{builtin::setup_builtin, getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node}, procio::{IoStack, borrow_fd}, prompt::readline::term::RawModeGuard, state::{self, VarFlags, read_vars, write_vars}};
|
||||
use crate::{
|
||||
builtin::setup_builtin,
|
||||
getopt::{Opt, OptSpec, get_opts_from_tokens},
|
||||
jobs::JobBldr,
|
||||
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
|
||||
parse::{NdRule, Node},
|
||||
procio::{IoStack, borrow_fd},
|
||||
prompt::readline::term::RawModeGuard,
|
||||
state::{self, VarFlags, read_vars, write_vars},
|
||||
};
|
||||
|
||||
pub const READ_OPTS: [OptSpec; 7] = [
|
||||
OptSpec { opt: Opt::Short('r'), takes_arg: false }, // don't allow backslash escapes
|
||||
OptSpec { opt: Opt::Short('s'), takes_arg: false }, // don't echo input
|
||||
OptSpec { opt: Opt::Short('a'), takes_arg: false }, // read into array
|
||||
OptSpec { opt: Opt::Short('n'), takes_arg: false }, // read only N characters
|
||||
OptSpec { opt: Opt::Short('t'), takes_arg: false }, // timeout
|
||||
OptSpec { opt: Opt::Short('p'), takes_arg: true }, // prompt
|
||||
OptSpec { opt: Opt::Short('d'), takes_arg: true }, // read until delimiter
|
||||
OptSpec {
|
||||
opt: Opt::Short('r'),
|
||||
takes_arg: false,
|
||||
}, // don't allow backslash escapes
|
||||
OptSpec {
|
||||
opt: Opt::Short('s'),
|
||||
takes_arg: false,
|
||||
}, // don't echo input
|
||||
OptSpec {
|
||||
opt: Opt::Short('a'),
|
||||
takes_arg: false,
|
||||
}, // read into array
|
||||
OptSpec {
|
||||
opt: Opt::Short('n'),
|
||||
takes_arg: false,
|
||||
}, // read only N characters
|
||||
OptSpec {
|
||||
opt: Opt::Short('t'),
|
||||
takes_arg: false,
|
||||
}, // timeout
|
||||
OptSpec {
|
||||
opt: Opt::Short('p'),
|
||||
takes_arg: true,
|
||||
}, // prompt
|
||||
OptSpec {
|
||||
opt: Opt::Short('d'),
|
||||
takes_arg: true,
|
||||
}, // read until delimiter
|
||||
];
|
||||
|
||||
bitflags! {
|
||||
@@ -33,8 +67,9 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S
|
||||
let blame = node.get_span().clone();
|
||||
let NdRule::Command {
|
||||
assignments: _,
|
||||
argv
|
||||
} = node.class else {
|
||||
argv,
|
||||
} = node.class
|
||||
else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
@@ -56,10 +91,12 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S
|
||||
match read(STDIN_FILENO, &mut buf) {
|
||||
Ok(0) => {
|
||||
state::set_status(1);
|
||||
let str_result = String::from_utf8(input.clone()).map_err(|e| ShErr::simple(
|
||||
let str_result = String::from_utf8(input.clone()).map_err(|e| {
|
||||
ShErr::simple(
|
||||
ShErrKind::ExecFail,
|
||||
format!("read: Input was not valid UTF-8: {e}"),
|
||||
))?;
|
||||
)
|
||||
})?;
|
||||
return Ok(str_result); // EOF
|
||||
}
|
||||
Ok(_) => {
|
||||
@@ -70,29 +107,32 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S
|
||||
// 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;
|
||||
} else {
|
||||
input.push(buf[0]);
|
||||
}
|
||||
}
|
||||
Err(Errno::EINTR) => continue,
|
||||
Err(e) => return Err(ShErr::simple(
|
||||
Err(e) => {
|
||||
return Err(ShErr::simple(
|
||||
ShErrKind::ExecFail,
|
||||
format!("read: Failed to read from stdin: {e}"),
|
||||
)),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
state::set_status(0);
|
||||
let str_result = String::from_utf8(input.clone()).map_err(|e| ShErr::simple(
|
||||
let str_result = String::from_utf8(input.clone()).map_err(|e| {
|
||||
ShErr::simple(
|
||||
ShErrKind::ExecFail,
|
||||
format!("read: Input was not valid UTF-8: {e}"),
|
||||
))?;
|
||||
)
|
||||
})?;
|
||||
Ok(str_result)
|
||||
}).blame(blame)?
|
||||
})
|
||||
.blame(blame)?
|
||||
} else {
|
||||
let mut input: Vec<u8> = vec![];
|
||||
loop {
|
||||
@@ -109,16 +149,20 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S
|
||||
input.push(buf[0]);
|
||||
}
|
||||
Err(Errno::EINTR) => continue,
|
||||
Err(e) => return Err(ShErr::simple(
|
||||
Err(e) => {
|
||||
return Err(ShErr::simple(
|
||||
ShErrKind::ExecFail,
|
||||
format!("read: Failed to read from stdin: {e}"),
|
||||
)),
|
||||
));
|
||||
}
|
||||
}
|
||||
String::from_utf8(input).map_err(|e| ShErr::simple(
|
||||
}
|
||||
String::from_utf8(input).map_err(|e| {
|
||||
ShErr::simple(
|
||||
ShErrKind::ExecFail,
|
||||
format!("read: Input was not valid UTF-8: {e}"),
|
||||
))?
|
||||
)
|
||||
})?
|
||||
};
|
||||
|
||||
if argv.is_empty() {
|
||||
@@ -128,7 +172,9 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S
|
||||
} else {
|
||||
// get our field separator
|
||||
let mut field_sep = read_vars(|v| v.get_var("IFS"));
|
||||
if field_sep.is_empty() { field_sep = " ".to_string() }
|
||||
if field_sep.is_empty() {
|
||||
field_sep = " ".to_string()
|
||||
}
|
||||
let mut remaining = input;
|
||||
|
||||
for (i, arg) in argv.iter().enumerate() {
|
||||
@@ -146,7 +192,8 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S
|
||||
let (field, rest) = trimmed.split_at(idx);
|
||||
write_vars(|v| v.set_var(&arg.0, field, VarFlags::NONE));
|
||||
|
||||
// note that this doesn't account for consecutive IFS characters, which is what that trim above is for
|
||||
// note that this doesn't account for consecutive IFS characters, which is what
|
||||
// that trim above is for
|
||||
remaining = rest.to_string();
|
||||
} else {
|
||||
write_vars(|v| v.set_var(&arg.0, trimmed, VarFlags::NONE));
|
||||
@@ -174,7 +221,9 @@ pub fn get_read_flags(opts: Vec<Opt>) -> ShResult<ReadOpts> {
|
||||
Opt::Short('n') => read_opts.flags |= ReadFlags::N_CHARS,
|
||||
Opt::Short('t') => read_opts.flags |= ReadFlags::TIMEOUT,
|
||||
Opt::ShortWithArg('p', prompt) => read_opts.prompt = Some(prompt),
|
||||
Opt::ShortWithArg('d', delim) => read_opts.delim = delim.chars().map(|c| c as u8).next().unwrap_or(b'\n'),
|
||||
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,
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
libsh::error::{ShResult, ShResultExt},
|
||||
parse::{NdRule, Node},
|
||||
prelude::*,
|
||||
procio::{borrow_fd, IoStack},
|
||||
procio::{IoStack, borrow_fd},
|
||||
state::write_shopts,
|
||||
};
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ use regex::Regex;
|
||||
|
||||
use crate::{
|
||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||
parse::{ConjunctOp, NdRule, Node, TestCase, TEST_UNARY_OPS},
|
||||
parse::{ConjunctOp, NdRule, Node, TEST_UNARY_OPS, TestCase},
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
@@ -254,7 +254,7 @@ pub fn double_bracket_test(node: Node) -> ShResult<bool> {
|
||||
msg: "Expected a binary operator in this test call; found a unary operator".into(),
|
||||
notes: vec![],
|
||||
span: err_span,
|
||||
})
|
||||
});
|
||||
}
|
||||
TestOp::StringEq => rhs.trim() == lhs.trim(),
|
||||
TestOp::StringNeq => rhs.trim() != lhs.trim(),
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
use std::{fmt::Display, str::FromStr};
|
||||
|
||||
use nix::{libc::{STDERR_FILENO, STDOUT_FILENO}, sys::signal::Signal, unistd::write};
|
||||
use nix::{
|
||||
libc::{STDERR_FILENO, STDOUT_FILENO},
|
||||
sys::signal::Signal,
|
||||
unistd::write,
|
||||
};
|
||||
|
||||
use crate::{builtin::setup_builtin, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node}, procio::{IoStack, borrow_fd}, state::{self, read_logic, write_logic}};
|
||||
use crate::{
|
||||
builtin::setup_builtin,
|
||||
jobs::JobBldr,
|
||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||
parse::{NdRule, Node},
|
||||
procio::{IoStack, borrow_fd},
|
||||
state::{self, read_logic, write_logic},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Hash)]
|
||||
pub enum TrapTarget {
|
||||
Exit,
|
||||
Error,
|
||||
Signal(Signal)
|
||||
Signal(Signal),
|
||||
}
|
||||
|
||||
impl FromStr for TrapTarget {
|
||||
@@ -52,7 +63,7 @@ impl FromStr for TrapTarget {
|
||||
return Err(ShErr::simple(
|
||||
ShErrKind::ExecFail,
|
||||
format!("invalid trap target '{}'", s),
|
||||
))
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -63,8 +74,7 @@ impl Display for TrapTarget {
|
||||
match self {
|
||||
TrapTarget::Exit => write!(f, "EXIT"),
|
||||
TrapTarget::Error => write!(f, "ERR"),
|
||||
TrapTarget::Signal(s) => {
|
||||
match s {
|
||||
TrapTarget::Signal(s) => match s {
|
||||
Signal::SIGHUP => write!(f, "HUP"),
|
||||
Signal::SIGINT => write!(f, "INT"),
|
||||
Signal::SIGQUIT => write!(f, "QUIT"),
|
||||
@@ -101,8 +111,7 @@ impl Display for TrapTarget {
|
||||
log::warn!("TrapTarget::fmt() : unrecognized signal {}", s);
|
||||
Err(std::fmt::Error)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -136,7 +145,7 @@ pub fn trap(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
|
||||
let stderr = borrow_fd(STDERR_FILENO);
|
||||
write(stderr, b"usage: trap <COMMAND> [SIGNAL...]\n")?;
|
||||
state::set_status(1);
|
||||
return Ok(())
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut args = argv.into_iter();
|
||||
|
||||
@@ -38,12 +38,30 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu
|
||||
unreachable!()
|
||||
};
|
||||
let zolt_opts = [
|
||||
OptSpec { opt: Opt::Long("dry-run".into()), takes_arg: false },
|
||||
OptSpec { opt: Opt::Long("confirm".into()), takes_arg: false },
|
||||
OptSpec { opt: Opt::Long("no-preserve-root".into()), takes_arg: false },
|
||||
OptSpec { opt: Opt::Short('r'), takes_arg: false },
|
||||
OptSpec { opt: Opt::Short('f'), takes_arg: false },
|
||||
OptSpec { opt: Opt::Short('v'), takes_arg: false }
|
||||
OptSpec {
|
||||
opt: Opt::Long("dry-run".into()),
|
||||
takes_arg: false,
|
||||
},
|
||||
OptSpec {
|
||||
opt: Opt::Long("confirm".into()),
|
||||
takes_arg: false,
|
||||
},
|
||||
OptSpec {
|
||||
opt: Opt::Long("no-preserve-root".into()),
|
||||
takes_arg: false,
|
||||
},
|
||||
OptSpec {
|
||||
opt: Opt::Short('r'),
|
||||
takes_arg: false,
|
||||
},
|
||||
OptSpec {
|
||||
opt: Opt::Short('f'),
|
||||
takes_arg: false,
|
||||
},
|
||||
OptSpec {
|
||||
opt: Opt::Short('v'),
|
||||
takes_arg: false,
|
||||
},
|
||||
];
|
||||
let mut flags = ZoltFlags::empty();
|
||||
|
||||
@@ -90,7 +108,6 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu
|
||||
|
||||
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
|
||||
|
||||
|
||||
for (arg, span) in argv {
|
||||
if &arg == "/" && !flags.contains(ZoltFlags::NO_PRESERVE_ROOT) {
|
||||
return Err(
|
||||
@@ -109,7 +126,6 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
134
src/expand.rs
134
src/expand.rs
@@ -7,11 +7,13 @@ use regex::Regex;
|
||||
|
||||
use crate::libsh::error::{ShErr, ShErrKind, ShResult};
|
||||
use crate::parse::execute::exec_input;
|
||||
use crate::parse::lex::{is_field_sep, is_hard_sep, LexFlags, LexStream, Tk, TkFlags, TkRule};
|
||||
use crate::parse::lex::{LexFlags, LexStream, Tk, TkFlags, TkRule, is_field_sep, is_hard_sep};
|
||||
use crate::parse::{Redir, RedirType};
|
||||
use crate::{jobs, prelude::*};
|
||||
use crate::procio::{IoBuf, IoFrame, IoMode, IoStack};
|
||||
use crate::state::{LogTab, VarFlags, read_jobs, read_logic, read_vars, write_jobs, write_meta, write_vars};
|
||||
use crate::state::{
|
||||
LogTab, VarFlags, read_jobs, read_logic, read_vars, write_jobs, write_meta, write_vars,
|
||||
};
|
||||
use crate::{jobs, prelude::*};
|
||||
|
||||
const PARAMETERS: [char; 7] = ['@', '*', '#', '$', '?', '!', '0'];
|
||||
|
||||
@@ -30,8 +32,9 @@ pub const PROC_SUB_IN: char = '\u{fdd5}';
|
||||
/// Output process sub marker
|
||||
pub const PROC_SUB_OUT: char = '\u{fdd6}';
|
||||
/// Marker for null expansion
|
||||
/// This is used for when "$@" or "$*" are used in quotes and there are no arguments
|
||||
/// Without this marker, it would be handled like an empty string, which breaks some commands
|
||||
/// This is used for when "$@" or "$*" are used in quotes and there are no
|
||||
/// arguments Without this marker, it would be handled like an empty string,
|
||||
/// which breaks some commands
|
||||
pub const NULL_EXPAND: char = '\u{fdd7}';
|
||||
|
||||
impl Tk {
|
||||
@@ -74,13 +77,14 @@ impl Expander {
|
||||
let has_leading_dot_slash = self.raw.starts_with("./");
|
||||
|
||||
if let Ok(glob_exp) = expand_glob(&self.raw)
|
||||
&& !glob_exp.is_empty() {
|
||||
&& !glob_exp.is_empty()
|
||||
{
|
||||
self.raw = glob_exp;
|
||||
}
|
||||
|
||||
if has_trailing_slash && !self.raw.ends_with('/') {
|
||||
// glob expansion can remove trailing slashes and leading dot-slashes, but we want to preserve them
|
||||
// so that things like tab completion don't break
|
||||
// glob expansion can remove trailing slashes and leading dot-slashes, but we
|
||||
// want to preserve them so that things like tab completion don't break
|
||||
self.raw.push('/');
|
||||
}
|
||||
if has_leading_dot_slash && !self.raw.starts_with("./") {
|
||||
@@ -132,7 +136,8 @@ impl Expander {
|
||||
}
|
||||
|
||||
/// Check if a string contains valid brace expansion patterns.
|
||||
/// Returns true if there's a valid {a,b} or {1..5} pattern at the outermost level.
|
||||
/// Returns true if there's a valid {a,b} or {1..5} pattern at the outermost
|
||||
/// level.
|
||||
fn has_braces(s: &str) -> bool {
|
||||
let mut chars = s.chars().peekable();
|
||||
let mut depth = 0;
|
||||
@@ -143,7 +148,9 @@ fn has_braces(s: &str) -> bool {
|
||||
|
||||
while let Some(ch) = chars.next() {
|
||||
match ch {
|
||||
'\\' => { chars.next(); } // skip escaped char
|
||||
'\\' => {
|
||||
chars.next();
|
||||
} // skip escaped char
|
||||
'\'' if cur_quote.is_none() => cur_quote = Some('\''),
|
||||
'\'' if cur_quote == Some('\'') => cur_quote = None,
|
||||
'"' if cur_quote.is_none() => cur_quote = Some('"'),
|
||||
@@ -222,19 +229,23 @@ fn expand_one_brace(word: &str) -> ShResult<Vec<String>> {
|
||||
if parts.len() == 1 && parts[0] == inner {
|
||||
// Check if it's a range
|
||||
if let Some(range_parts) = try_expand_range(&inner) {
|
||||
return Ok(range_parts
|
||||
return Ok(
|
||||
range_parts
|
||||
.into_iter()
|
||||
.map(|p| format!("{}{}{}", prefix, p, suffix))
|
||||
.collect());
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
// Not a valid brace expression, return as-is with literal braces
|
||||
return Ok(vec![format!("{}{{{}}}{}", prefix, inner, suffix)]);
|
||||
}
|
||||
|
||||
Ok(parts
|
||||
Ok(
|
||||
parts
|
||||
.into_iter()
|
||||
.map(|p| format!("{}{}{}", prefix, p, suffix))
|
||||
.collect())
|
||||
.collect(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Extract prefix, inner, and suffix from a brace expression.
|
||||
@@ -253,12 +264,22 @@ fn get_brace_parts(word: &str) -> Option<(String, String, String)> {
|
||||
prefix.push(next);
|
||||
}
|
||||
}
|
||||
'\'' if cur_quote.is_none() => { cur_quote = Some('\'');
|
||||
prefix.push(ch); }
|
||||
'\'' if cur_quote == Some('\'') => { cur_quote = None;
|
||||
prefix.push(ch); }
|
||||
'"' if cur_quote.is_none() => { cur_quote = Some('"'); prefix.push(ch); }
|
||||
'"' if cur_quote == Some('"') => { cur_quote = None; prefix.push(ch); }
|
||||
'\'' if cur_quote.is_none() => {
|
||||
cur_quote = Some('\'');
|
||||
prefix.push(ch);
|
||||
}
|
||||
'\'' if cur_quote == Some('\'') => {
|
||||
cur_quote = None;
|
||||
prefix.push(ch);
|
||||
}
|
||||
'"' if cur_quote.is_none() => {
|
||||
cur_quote = Some('"');
|
||||
prefix.push(ch);
|
||||
}
|
||||
'"' if cur_quote == Some('"') => {
|
||||
cur_quote = None;
|
||||
prefix.push(ch);
|
||||
}
|
||||
'{' if cur_quote.is_none() => {
|
||||
break;
|
||||
}
|
||||
@@ -279,10 +300,22 @@ fn get_brace_parts(word: &str) -> Option<(String, String, String)> {
|
||||
inner.push(next);
|
||||
}
|
||||
}
|
||||
'\'' if cur_quote.is_none() => { cur_quote = Some('\''); inner.push(ch); }
|
||||
'\'' if cur_quote == Some('\'') => { cur_quote = None; inner.push(ch); }
|
||||
'"' if cur_quote.is_none() => { cur_quote = Some('"'); inner.push(ch); }
|
||||
'"' if cur_quote == Some('"') => { cur_quote = None; inner.push(ch); }
|
||||
'\'' if cur_quote.is_none() => {
|
||||
cur_quote = Some('\'');
|
||||
inner.push(ch);
|
||||
}
|
||||
'\'' if cur_quote == Some('\'') => {
|
||||
cur_quote = None;
|
||||
inner.push(ch);
|
||||
}
|
||||
'"' if cur_quote.is_none() => {
|
||||
cur_quote = Some('"');
|
||||
inner.push(ch);
|
||||
}
|
||||
'"' if cur_quote == Some('"') => {
|
||||
cur_quote = None;
|
||||
inner.push(ch);
|
||||
}
|
||||
'{' if cur_quote.is_none() => {
|
||||
depth += 1;
|
||||
inner.push(ch);
|
||||
@@ -326,10 +359,22 @@ fn split_brace_inner(inner: &str) -> Vec<String> {
|
||||
current.push(next);
|
||||
}
|
||||
}
|
||||
'\'' if cur_quote.is_none() => { cur_quote = Some('\''); current.push(ch); }
|
||||
'\'' if cur_quote == Some('\'') => { cur_quote = None; current.push(ch); }
|
||||
'"' if cur_quote.is_none() => { cur_quote = Some('"'); current.push(ch); }
|
||||
'"' if cur_quote == Some('"') => { cur_quote = None; current.push(ch); }
|
||||
'\'' if cur_quote.is_none() => {
|
||||
cur_quote = Some('\'');
|
||||
current.push(ch);
|
||||
}
|
||||
'\'' if cur_quote == Some('\'') => {
|
||||
cur_quote = None;
|
||||
current.push(ch);
|
||||
}
|
||||
'"' if cur_quote.is_none() => {
|
||||
cur_quote = Some('"');
|
||||
current.push(ch);
|
||||
}
|
||||
'"' if cur_quote == Some('"') => {
|
||||
cur_quote = None;
|
||||
current.push(ch);
|
||||
}
|
||||
'{' if cur_quote.is_none() => {
|
||||
depth += 1;
|
||||
current.push(ch);
|
||||
@@ -364,15 +409,16 @@ fn try_expand_range(inner: &str) -> Option<Vec<String>> {
|
||||
let start = parts[0];
|
||||
let end = parts[1];
|
||||
let step: i32 = parts[2].parse().ok()?;
|
||||
if step == 0 { return None; }
|
||||
if step == 0 {
|
||||
return None;
|
||||
}
|
||||
expand_range(start, end, step.unsigned_abs() as usize)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn expand_range(start: &str, end: &str, step: usize) ->
|
||||
Option<Vec<String>> {
|
||||
fn expand_range(start: &str, end: &str, step: usize) -> Option<Vec<String>> {
|
||||
// Try character range first
|
||||
if is_alpha_range_bound(start) && is_alpha_range_bound(end) {
|
||||
let start_char = start.chars().next()? as u8;
|
||||
@@ -405,8 +451,7 @@ Option<Vec<String>> {
|
||||
|
||||
// Handle zero-padding
|
||||
let pad_width = start.len().max(end.len());
|
||||
let needs_padding = start.starts_with('0') ||
|
||||
end.starts_with('0');
|
||||
let needs_padding = start.starts_with('0') || end.starts_with('0');
|
||||
|
||||
let (lo, hi) = if reverse {
|
||||
(end_num, start_num)
|
||||
@@ -435,7 +480,6 @@ Option<Vec<String>> {
|
||||
None
|
||||
}
|
||||
|
||||
|
||||
fn is_alpha_range_bound(word: &str) -> bool {
|
||||
word.len() == 1 && word.chars().all(|c| c.is_ascii_alphabetic())
|
||||
}
|
||||
@@ -558,8 +602,8 @@ pub fn expand_glob(raw: &str) -> ShResult<String> {
|
||||
require_literal_leading_dot: !crate::state::read_shopts(|s| s.core.dotglob),
|
||||
..Default::default()
|
||||
};
|
||||
for entry in
|
||||
glob::glob_with(raw, opts).map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid glob pattern"))?
|
||||
for entry in glob::glob_with(raw, opts)
|
||||
.map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid glob pattern"))?
|
||||
{
|
||||
let entry =
|
||||
entry.map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid filename found in glob"))?;
|
||||
@@ -625,7 +669,7 @@ impl ArithTk {
|
||||
kind: ShErrKind::ParseErr,
|
||||
msg: "Invalid character in arithmetic substitution".into(),
|
||||
notes: vec![],
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -707,7 +751,7 @@ impl ArithTk {
|
||||
kind: ShErrKind::ParseErr,
|
||||
msg: "Unexpected token during evaluation".into(),
|
||||
notes: vec![],
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -807,8 +851,10 @@ pub fn expand_proc_sub(raw: &str, is_input: bool) -> ShResult<String> {
|
||||
|
||||
/// Get the command output of a given command input as a String
|
||||
pub fn expand_cmd_sub(raw: &str) -> ShResult<String> {
|
||||
if raw.starts_with('(') && raw.ends_with(')')
|
||||
&& let Ok(output) = expand_arithmetic(raw) {
|
||||
if raw.starts_with('(')
|
||||
&& raw.ends_with(')')
|
||||
&& let Ok(output) = expand_arithmetic(raw)
|
||||
{
|
||||
return Ok(output); // It's actually an arithmetic sub
|
||||
}
|
||||
let (rpipe, wpipe) = IoMode::get_pipes();
|
||||
@@ -829,7 +875,8 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult<String> {
|
||||
ForkResult::Parent { child } => {
|
||||
std::mem::drop(cmd_sub_io_frame); // Closes the write pipe
|
||||
|
||||
// Read output first (before waiting) to avoid deadlock if child fills pipe buffer
|
||||
// Read output first (before waiting) to avoid deadlock if child fills pipe
|
||||
// buffer
|
||||
loop {
|
||||
match io_buf.fill_buffer() {
|
||||
Ok(()) => break,
|
||||
@@ -851,9 +898,7 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult<String> {
|
||||
jobs::take_term()?;
|
||||
|
||||
match status {
|
||||
WtStat::Exited(_, _) => {
|
||||
Ok(io_buf.as_str()?.trim_end().to_string())
|
||||
}
|
||||
WtStat::Exited(_, _) => Ok(io_buf.as_str()?.trim_end().to_string()),
|
||||
_ => Err(ShErr::simple(ShErrKind::InternalErr, "Command sub failed")),
|
||||
}
|
||||
}
|
||||
@@ -1785,7 +1830,6 @@ fn tokenize_prompt(raw: &str) -> Vec<PromptTk> {
|
||||
let mut func_name = String::new();
|
||||
let is_braced = chars.peek() == Some(&'{');
|
||||
while let Some(ch) = chars.peek() {
|
||||
|
||||
match ch {
|
||||
'}' if is_braced => {
|
||||
chars.next();
|
||||
|
||||
@@ -42,7 +42,7 @@ impl Display for Opt {
|
||||
Self::Long(opt) => write!(f, "--{}", opt),
|
||||
Self::Short(opt) => write!(f, "-{}", opt),
|
||||
Self::LongWithArg(opt, arg) => write!(f, "--{} {}", opt, arg),
|
||||
Self::ShortWithArg(opt, arg) => write!(f, "-{} {}", opt, arg)
|
||||
Self::ShortWithArg(opt, arg) => write!(f, "-{} {}", opt, arg),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,8 @@ pub fn get_opts_from_tokens(tokens: Vec<Tk>, opt_specs: &[OptSpec]) -> (Vec<Tk>,
|
||||
for opt_spec in opt_specs {
|
||||
if opt_spec.opt == opt {
|
||||
if opt_spec.takes_arg {
|
||||
let arg = tokens_iter.next()
|
||||
let arg = tokens_iter
|
||||
.next()
|
||||
.map(|t| t.to_string())
|
||||
.unwrap_or_default();
|
||||
|
||||
|
||||
10
src/jobs.rs
10
src/jobs.rs
@@ -2,7 +2,11 @@ use crate::{
|
||||
libsh::{
|
||||
error::ShResult,
|
||||
term::{Style, Styled},
|
||||
}, prelude::*, procio::{IoMode, borrow_fd}, signal::{disable_reaping, enable_reaping}, state::{self, set_status, read_jobs, write_jobs}
|
||||
},
|
||||
prelude::*,
|
||||
procio::{IoMode, borrow_fd},
|
||||
signal::{disable_reaping, enable_reaping},
|
||||
state::{self, read_jobs, set_status, write_jobs},
|
||||
};
|
||||
|
||||
pub const SIG_EXIT_OFFSET: i32 = 128;
|
||||
@@ -685,7 +689,9 @@ pub fn wait_fg(job: Job) -> ShResult<()> {
|
||||
}
|
||||
// If job wasn't stopped (moved to bg), clear the fg slot
|
||||
if !was_stopped {
|
||||
write_jobs(|j| { j.take_fg(); });
|
||||
write_jobs(|j| {
|
||||
j.take_fg();
|
||||
});
|
||||
}
|
||||
take_term()?;
|
||||
set_status(code);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use termios::{LocalFlags, Termios};
|
||||
|
||||
use crate::{prelude::*};
|
||||
use crate::prelude::*;
|
||||
///
|
||||
/// The previous state of the terminal options.
|
||||
///
|
||||
@@ -33,12 +33,14 @@ pub(crate) static mut SAVED_TERMIOS: Option<Option<Termios>> = None;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TermiosGuard {
|
||||
saved_termios: Option<Termios>
|
||||
saved_termios: Option<Termios>,
|
||||
}
|
||||
|
||||
impl TermiosGuard {
|
||||
pub fn new(new_termios: Termios) -> Self {
|
||||
let mut new = Self { saved_termios: None };
|
||||
let mut new = Self {
|
||||
saved_termios: None,
|
||||
};
|
||||
|
||||
if isatty(std::io::stdin().as_raw_fd()).unwrap() {
|
||||
let current_termios = termios::tcgetattr(std::io::stdin()).unwrap();
|
||||
@@ -48,7 +50,8 @@ impl TermiosGuard {
|
||||
std::io::stdin(),
|
||||
nix::sys::termios::SetArg::TCSANOW,
|
||||
&new_termios,
|
||||
).unwrap();
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
new
|
||||
@@ -66,11 +69,7 @@ impl Default for TermiosGuard {
|
||||
impl Drop for TermiosGuard {
|
||||
fn drop(&mut self) {
|
||||
if let Some(saved) = &self.saved_termios {
|
||||
termios::tcsetattr(
|
||||
std::io::stdin(),
|
||||
nix::sys::termios::SetArg::TCSANOW,
|
||||
saved,
|
||||
).unwrap();
|
||||
termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, saved).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,8 +83,7 @@ impl TkVecUtils<Tk> for Vec<Tk> {
|
||||
}
|
||||
}
|
||||
fn debug_tokens(&self) {
|
||||
for token in self {
|
||||
}
|
||||
for token in self {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
36
src/main.rs
36
src/main.rs
@@ -22,10 +22,10 @@ use std::os::fd::BorrowedFd;
|
||||
use std::process::ExitCode;
|
||||
use std::sync::atomic::Ordering;
|
||||
|
||||
use nix::errno::Errno;
|
||||
use nix::libc::STDIN_FILENO;
|
||||
use nix::poll::{PollFd, PollFlags, PollTimeout, poll};
|
||||
use nix::unistd::read;
|
||||
use nix::errno::Errno;
|
||||
|
||||
use crate::builtin::trap::TrapTarget;
|
||||
use crate::libsh::error::{ShErr, ShErrKind, ShResult};
|
||||
@@ -87,7 +87,8 @@ fn main() -> ExitCode {
|
||||
};
|
||||
|
||||
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}");
|
||||
}
|
||||
|
||||
@@ -99,15 +100,24 @@ fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) -> ShResult<()> {
|
||||
if !path.is_file() {
|
||||
eprintln!("fern: Failed to open input file: {}", path.display());
|
||||
QUIT_CODE.store(1, Ordering::SeqCst);
|
||||
return Err(ShErr::simple(ShErrKind::CleanExit(1), "input file not found"));
|
||||
return Err(ShErr::simple(
|
||||
ShErrKind::CleanExit(1),
|
||||
"input file not found",
|
||||
));
|
||||
}
|
||||
let Ok(input) = fs::read_to_string(path) else {
|
||||
eprintln!("fern: Failed to read input file: {}", path.display());
|
||||
QUIT_CODE.store(1, Ordering::SeqCst);
|
||||
return Err(ShErr::simple(ShErrKind::CleanExit(1), "failed to read input file"));
|
||||
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| {
|
||||
v.cur_scope_mut()
|
||||
.bpush_arg(path.to_string_lossy().to_string())
|
||||
});
|
||||
for arg in args {
|
||||
write_vars(|v| v.cur_scope_mut().bpush_arg(arg))
|
||||
}
|
||||
@@ -129,7 +139,10 @@ fn fern_interactive() -> ShResult<()> {
|
||||
Err(e) => {
|
||||
eprintln!("Failed to initialize readline: {e}");
|
||||
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",
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -173,7 +186,10 @@ fn fern_interactive() -> ShResult<()> {
|
||||
}
|
||||
|
||||
// Check if stdin has data
|
||||
if fds[0].revents().is_some_and(|r| r.contains(PollFlags::POLLIN)) {
|
||||
if fds[0]
|
||||
.revents()
|
||||
.is_some_and(|r| r.contains(PollFlags::POLLIN))
|
||||
{
|
||||
let mut buffer = [0u8; 1024];
|
||||
match read(STDIN_FILENO, &mut buffer) {
|
||||
Ok(0) => {
|
||||
@@ -225,15 +241,13 @@ fn fern_interactive() -> ShResult<()> {
|
||||
Ok(ReadlineEvent::Pending) => {
|
||||
// No complete input yet, keep polling
|
||||
}
|
||||
Err(e) => {
|
||||
match e.kind() {
|
||||
Err(e) => match e.kind() {
|
||||
ShErrKind::CleanExit(code) => {
|
||||
QUIT_CODE.store(*code, Ordering::SeqCst);
|
||||
return Ok(());
|
||||
}
|
||||
_ => eprintln!("{e}"),
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,22 +2,33 @@ use std::collections::{HashSet, VecDeque};
|
||||
|
||||
use crate::{
|
||||
builtin::{
|
||||
alias::{alias, unalias}, cd::cd, echo::echo, export::export, flowctl::flowctl, jobctl::{JobBehavior, continue_job, jobs}, pwd::pwd, read::read_builtin, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, zoltraak::zoltraak
|
||||
alias::{alias, unalias},
|
||||
cd::cd,
|
||||
echo::echo,
|
||||
export::export,
|
||||
flowctl::flowctl,
|
||||
jobctl::{JobBehavior, continue_job, jobs},
|
||||
pwd::pwd,
|
||||
read::read_builtin,
|
||||
shift::shift,
|
||||
shopt::shopt,
|
||||
source::source,
|
||||
test::double_bracket_test,
|
||||
trap::{TrapTarget, trap},
|
||||
zoltraak::zoltraak,
|
||||
},
|
||||
expand::expand_aliases,
|
||||
jobs::{ChildProc, JobStack, dispatch_job},
|
||||
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
|
||||
prelude::*,
|
||||
procio::{IoMode, IoStack},
|
||||
state::{
|
||||
self, ShFunc, VarFlags, read_logic, write_logic, write_vars
|
||||
},
|
||||
state::{self, ShFunc, VarFlags, read_logic, write_logic, write_vars},
|
||||
};
|
||||
|
||||
use super::{
|
||||
lex::{Span, Tk, TkFlags, KEYWORDS},
|
||||
AssignKind, CaseNode, CondNode, ConjunctNode, ConjunctOp, LoopKind, NdFlags, NdRule, Node,
|
||||
ParsedSrc, Redir, RedirType,
|
||||
lex::{KEYWORDS, Span, Tk, TkFlags},
|
||||
};
|
||||
|
||||
thread_local! {
|
||||
@@ -26,7 +37,6 @@ thread_local! {
|
||||
|
||||
pub struct ScopeGuard;
|
||||
|
||||
|
||||
impl ScopeGuard {
|
||||
pub fn exclusive_scope(args: Option<Vec<(String, Span)>>) -> Self {
|
||||
let argv = args.map(|a| a.into_iter().map(|(s, _)| s).collect::<Vec<_>>());
|
||||
@@ -50,7 +60,7 @@ impl Drop for ScopeGuard {
|
||||
/// such as 'VAR=value <command> <args>'
|
||||
/// or for-loop variables
|
||||
struct VarCtxGuard {
|
||||
vars: HashSet<String>
|
||||
vars: HashSet<String>,
|
||||
}
|
||||
impl VarCtxGuard {
|
||||
fn new(vars: HashSet<String>) -> Self {
|
||||
@@ -129,7 +139,8 @@ pub fn exec_input(input: String, io_stack: Option<IoStack>, interactive: bool) -
|
||||
let result = dispatcher.begin_dispatch();
|
||||
|
||||
if state::get_status() != 0
|
||||
&& let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Error)) {
|
||||
&& let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Error))
|
||||
{
|
||||
let saved_status = state::get_status();
|
||||
exec_input(trap, None, false)?;
|
||||
state::set_status(saved_status);
|
||||
@@ -188,9 +199,12 @@ impl Dispatcher {
|
||||
self.exec_builtin(node)
|
||||
} else if is_subsh(node.get_command().cloned()) {
|
||||
self.exec_subsh(node)
|
||||
} else if crate::state::read_shopts(|s| s.core.autocd) && Path::new(cmd.span.as_str()).is_dir() {
|
||||
} else if crate::state::read_shopts(|s| s.core.autocd) && Path::new(cmd.span.as_str()).is_dir()
|
||||
{
|
||||
let dir = cmd.span.as_str().to_string();
|
||||
let stack = IoStack { stack: self.io_stack.clone() };
|
||||
let stack = IoStack {
|
||||
stack: self.io_stack.clone(),
|
||||
};
|
||||
exec_input(format!("cd {dir}"), Some(stack), self.interactive)
|
||||
} else {
|
||||
self.exec_cmd(node)
|
||||
@@ -340,9 +354,7 @@ impl Dispatcher {
|
||||
unreachable!()
|
||||
};
|
||||
self.io_stack.append_to_frame(brc_grp.redirs);
|
||||
let _guard = self.io_stack
|
||||
.pop_frame()
|
||||
.redirect()?;
|
||||
let _guard = self.io_stack.pop_frame().redirect()?;
|
||||
|
||||
for node in body {
|
||||
let blame = node.get_span();
|
||||
@@ -361,9 +373,7 @@ impl Dispatcher {
|
||||
};
|
||||
|
||||
self.io_stack.append_to_frame(case_stmt.redirs);
|
||||
let _guard = self.io_stack
|
||||
.pop_frame()
|
||||
.redirect()?;
|
||||
let _guard = self.io_stack.pop_frame().redirect()?;
|
||||
|
||||
let exp_pattern = pattern.clone().expand()?;
|
||||
let pattern_raw = exp_pattern
|
||||
@@ -402,9 +412,7 @@ impl Dispatcher {
|
||||
};
|
||||
|
||||
self.io_stack.append_to_frame(loop_stmt.redirs);
|
||||
let _guard = self.io_stack
|
||||
.pop_frame()
|
||||
.redirect()?;
|
||||
let _guard = self.io_stack.pop_frame().redirect()?;
|
||||
|
||||
let CondNode { cond, body } = cond_node;
|
||||
'outer: loop {
|
||||
@@ -445,32 +453,31 @@ impl Dispatcher {
|
||||
};
|
||||
|
||||
let to_expanded_strings = |tks: Vec<Tk>| -> ShResult<Vec<String>> {
|
||||
Ok(tks.into_iter()
|
||||
Ok(
|
||||
tks
|
||||
.into_iter()
|
||||
.map(|tk| tk.expand().map(|tk| tk.get_words()))
|
||||
.collect::<ShResult<Vec<Vec<String>>>>()?
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.collect::<Vec<_>>())
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
};
|
||||
|
||||
// Expand all array variables
|
||||
let arr: Vec<String> = to_expanded_strings(arr)?;
|
||||
let vars: Vec<String> = to_expanded_strings(vars)?;
|
||||
|
||||
let mut for_guard = VarCtxGuard::new(
|
||||
vars.iter().map(|v| v.to_string()).collect()
|
||||
);
|
||||
let mut for_guard = VarCtxGuard::new(vars.iter().map(|v| v.to_string()).collect());
|
||||
|
||||
self.io_stack.append_to_frame(for_stmt.redirs);
|
||||
let _guard = self.io_stack
|
||||
.pop_frame()
|
||||
.redirect()?;
|
||||
let _guard = self.io_stack.pop_frame().redirect()?;
|
||||
|
||||
'outer: for chunk in arr.chunks(vars.len()) {
|
||||
let empty = String::new();
|
||||
let chunk_iter = vars.iter().zip(
|
||||
chunk.iter().chain(std::iter::repeat(&empty)),
|
||||
);
|
||||
let chunk_iter = vars
|
||||
.iter()
|
||||
.zip(chunk.iter().chain(std::iter::repeat(&empty)));
|
||||
|
||||
for (var, val) in chunk_iter {
|
||||
write_vars(|v| v.set_var(&var.to_string(), &val.to_string(), VarFlags::NONE));
|
||||
@@ -506,9 +513,7 @@ impl Dispatcher {
|
||||
};
|
||||
|
||||
self.io_stack.append_to_frame(if_stmt.redirs);
|
||||
let _guard = self.io_stack
|
||||
.pop_frame()
|
||||
.redirect()?;
|
||||
let _guard = self.io_stack.pop_frame().redirect()?;
|
||||
|
||||
let mut matched = false;
|
||||
for node in cond_nodes {
|
||||
@@ -562,11 +567,7 @@ impl Dispatcher {
|
||||
Ok(())
|
||||
}
|
||||
fn exec_builtin(&mut self, mut cmd: Node) -> ShResult<()> {
|
||||
let NdRule::Command {
|
||||
assignments,
|
||||
argv,
|
||||
} = &mut cmd.class
|
||||
else {
|
||||
let NdRule::Command { assignments, argv } = &mut cmd.class else {
|
||||
unreachable!()
|
||||
};
|
||||
let env_vars = self.set_assignments(mem::take(assignments), AssignBehavior::Export)?;
|
||||
@@ -646,9 +647,7 @@ impl Dispatcher {
|
||||
|
||||
let exec_args = ExecArgs::new(argv)?;
|
||||
|
||||
let _guard = self.io_stack
|
||||
.pop_frame()
|
||||
.redirect()?;
|
||||
let _guard = self.io_stack.pop_frame().redirect()?;
|
||||
|
||||
let job = self.job_stack.curr_job_mut().unwrap();
|
||||
|
||||
|
||||
@@ -324,7 +324,8 @@ impl LexStream {
|
||||
let can_be_subshell = chars.peek() == Some(&'(');
|
||||
|
||||
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);
|
||||
self.cursor = pos;
|
||||
@@ -740,7 +741,10 @@ impl Iterator for LexStream {
|
||||
}
|
||||
self.get_token(ch_idx..self.cursor, TkRule::Sep)
|
||||
}
|
||||
'#' if !self.flags.contains(LexFlags::INTERACTIVE) || crate::state::read_shopts(|s| s.core.interactive_comments) => {
|
||||
'#'
|
||||
if !self.flags.contains(LexFlags::INTERACTIVE)
|
||||
|| crate::state::read_shopts(|s| s.core.interactive_comments) =>
|
||||
{
|
||||
let ch_idx = self.cursor;
|
||||
self.cursor += 1;
|
||||
|
||||
|
||||
@@ -1335,7 +1335,12 @@ impl ParseStream {
|
||||
break;
|
||||
}
|
||||
match tk.class {
|
||||
TkRule::EOI | TkRule::Pipe | TkRule::And | TkRule::BraceGrpEnd | TkRule::Or | TkRule::Bg => break,
|
||||
TkRule::EOI
|
||||
| TkRule::Pipe
|
||||
| TkRule::And
|
||||
| TkRule::BraceGrpEnd
|
||||
| TkRule::Or
|
||||
| TkRule::Bg => break,
|
||||
TkRule::Sep => {
|
||||
node_tks.push(tk.clone());
|
||||
break;
|
||||
|
||||
@@ -19,17 +19,17 @@ pub use std::os::unix::io::{AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd,
|
||||
pub use bitflags::bitflags;
|
||||
pub use nix::{
|
||||
errno::Errno,
|
||||
fcntl::{open, OFlag},
|
||||
fcntl::{OFlag, open},
|
||||
libc::{self, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO},
|
||||
sys::{
|
||||
signal::{self, kill, killpg, pthread_sigmask, signal, SigHandler, SigSet, SigmaskHow, Signal},
|
||||
signal::{self, SigHandler, SigSet, SigmaskHow, Signal, kill, killpg, pthread_sigmask, signal},
|
||||
stat::Mode,
|
||||
termios::{self},
|
||||
wait::{waitpid, WaitPidFlag as WtFlag, WaitStatus as WtStat},
|
||||
wait::{WaitPidFlag as WtFlag, WaitStatus as WtStat, waitpid},
|
||||
},
|
||||
unistd::{
|
||||
close, dup, dup2, execvpe, fork, getpgid, getpgrp, isatty, pipe, read, setpgid, tcgetpgrp,
|
||||
tcsetpgrp, write, ForkResult, Pid,
|
||||
ForkResult, Pid, close, dup, dup2, execvpe, fork, getpgid, getpgrp, isatty, pipe, read,
|
||||
setpgid, tcgetpgrp, tcsetpgrp, write,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -4,10 +4,13 @@ use std::{
|
||||
};
|
||||
|
||||
use crate::{
|
||||
expand::Expander, libsh::{
|
||||
expand::Expander,
|
||||
libsh::{
|
||||
error::{ShErr, ShErrKind, ShResult},
|
||||
utils::RedirVecUtils,
|
||||
}, parse::{Redir, RedirType, get_redir_file}, prelude::*
|
||||
},
|
||||
parse::{Redir, RedirType, get_redir_file},
|
||||
prelude::*,
|
||||
};
|
||||
|
||||
// Credit to fish-shell for many of the implementation ideas present in this
|
||||
@@ -70,15 +73,10 @@ impl IoMode {
|
||||
}
|
||||
pub fn open_file(mut self) -> ShResult<Self> {
|
||||
if let IoMode::File { tgt_fd, path, mode } = self {
|
||||
let path_raw = path
|
||||
.as_os_str()
|
||||
.to_str()
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let path_raw = path.as_os_str().to_str().unwrap_or_default().to_string();
|
||||
|
||||
let expanded_path = Expander::from_raw(&path_raw)?
|
||||
.expand()?
|
||||
.join(" "); // should just be one string, will have to find some way to handle a return of multiple
|
||||
let expanded_path = Expander::from_raw(&path_raw)?.expand()?.join(" "); // should just be one string, will have to find some way to handle a return of
|
||||
// multiple
|
||||
|
||||
let expanded_pathbuf = PathBuf::from(expanded_path);
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
pub mod readline;
|
||||
pub mod statusline;
|
||||
|
||||
|
||||
use crate::{expand::expand_prompt, libsh::error::ShResult, prelude::*};
|
||||
|
||||
/// Initialize the line editor
|
||||
|
||||
@@ -1,20 +1,25 @@
|
||||
use std::{env, os::unix::fs::PermissionsExt, path::PathBuf, sync::Arc};
|
||||
|
||||
use crate::{builtin::BUILTINS, libsh::error::{ShErr, ShErrKind, ShResult}, parse::lex::{self, LexFlags, Tk, TkFlags}, prompt::readline::{Marker, annotate_input, annotate_input_recursive, get_insertions, markers::{self, is_marker}}, state::{read_logic, read_vars}};
|
||||
use crate::{
|
||||
builtin::BUILTINS,
|
||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||
parse::lex::{self, LexFlags, Tk, TkFlags},
|
||||
prompt::readline::{
|
||||
Marker, annotate_input, annotate_input_recursive, get_insertions,
|
||||
markers::{self, is_marker},
|
||||
},
|
||||
state::{read_logic, read_vars},
|
||||
};
|
||||
|
||||
pub enum CompCtx {
|
||||
CmdName,
|
||||
FileName
|
||||
FileName,
|
||||
}
|
||||
|
||||
pub enum CompResult {
|
||||
NoMatch,
|
||||
Single {
|
||||
result: String
|
||||
},
|
||||
Many {
|
||||
candidates: Vec<String>
|
||||
}
|
||||
Single { result: String },
|
||||
Many { candidates: Vec<String> },
|
||||
}
|
||||
|
||||
impl CompResult {
|
||||
@@ -22,7 +27,9 @@ impl CompResult {
|
||||
if candidates.is_empty() {
|
||||
Self::NoMatch
|
||||
} else if candidates.len() == 1 {
|
||||
Self::Single { result: candidates[0].clone() }
|
||||
Self::Single {
|
||||
result: candidates[0].clone(),
|
||||
}
|
||||
} else {
|
||||
Self::Many { candidates }
|
||||
}
|
||||
@@ -55,7 +62,6 @@ impl Completer {
|
||||
|
||||
pub fn get_completion_context(&self, line: &str, cursor_pos: usize) -> (Vec<Marker>, usize) {
|
||||
let annotated = annotate_input_recursive(line);
|
||||
log::debug!("Annotated input for completion context: {:?}", annotated);
|
||||
let mut ctx = vec![markers::NULL];
|
||||
let mut last_priority = 0;
|
||||
let mut ctx_start = 0;
|
||||
@@ -63,10 +69,8 @@ impl Completer {
|
||||
|
||||
for ch in annotated.chars() {
|
||||
match ch {
|
||||
_ if is_marker(ch) => {
|
||||
match ch {
|
||||
_ if is_marker(ch) => match ch {
|
||||
markers::COMMAND | markers::BUILTIN => {
|
||||
log::debug!("Found command marker at position {}", pos);
|
||||
if last_priority < 2 {
|
||||
if last_priority > 0 {
|
||||
ctx.pop();
|
||||
@@ -77,7 +81,6 @@ impl Completer {
|
||||
}
|
||||
}
|
||||
markers::VAR_SUB => {
|
||||
log::debug!("Found variable substitution marker at position {}", pos);
|
||||
if last_priority < 3 {
|
||||
if last_priority > 0 {
|
||||
ctx.pop();
|
||||
@@ -88,15 +91,13 @@ impl Completer {
|
||||
}
|
||||
}
|
||||
markers::ARG | markers::ASSIGNMENT => {
|
||||
log::debug!("Found argument/assignment marker at position {}", pos);
|
||||
if last_priority < 1 {
|
||||
ctx_start = pos;
|
||||
ctx.push(markers::ARG);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
last_priority = 0; // reset priority on normal characters
|
||||
pos += 1; // we hit a normal character, advance our position
|
||||
@@ -118,7 +119,12 @@ impl Completer {
|
||||
self.active = false;
|
||||
}
|
||||
|
||||
pub fn complete(&mut self, line: String, cursor_pos: usize, direction: i32) -> ShResult<Option<String>> {
|
||||
pub fn complete(
|
||||
&mut self,
|
||||
line: String,
|
||||
cursor_pos: usize,
|
||||
direction: i32,
|
||||
) -> ShResult<Option<String>> {
|
||||
if self.active {
|
||||
Ok(Some(self.cycle_completion(direction)))
|
||||
} else {
|
||||
@@ -160,8 +166,7 @@ impl Completer {
|
||||
|
||||
Ok(Some(self.get_completed_line()))
|
||||
}
|
||||
CompResult::NoMatch => Ok(None)
|
||||
|
||||
CompResult::NoMatch => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,19 +225,24 @@ impl Completer {
|
||||
|
||||
let selected = &self.candidates[self.selected_idx];
|
||||
let (start, end) = self.token_span;
|
||||
format!("{}{}{}", &self.original_input[..start], selected, &self.original_input[end..])
|
||||
format!(
|
||||
"{}{}{}",
|
||||
&self.original_input[..start],
|
||||
selected,
|
||||
&self.original_input[end..]
|
||||
)
|
||||
}
|
||||
|
||||
pub fn get_candidates(&mut self, line: String, cursor_pos: usize) -> ShResult<CompResult> {
|
||||
let source = Arc::new(line.clone());
|
||||
let tokens = lex::LexStream::new(source, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>()?;
|
||||
let tokens =
|
||||
lex::LexStream::new(source, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>()?;
|
||||
|
||||
let Some(mut cur_token) = tokens.into_iter().find(|tk| {
|
||||
let start = tk.span.start;
|
||||
let end = tk.span.end;
|
||||
(start..=end).contains(&cursor_pos)
|
||||
}) else {
|
||||
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);
|
||||
@@ -241,11 +251,12 @@ impl Completer {
|
||||
|
||||
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
|
||||
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();
|
||||
@@ -257,12 +268,12 @@ impl Completer {
|
||||
if ctx.last().is_some_and(|m| *m == markers::VAR_SUB) {
|
||||
let var_sub = &cur_token.as_str();
|
||||
if let Some((var_name, start, end)) = Self::extract_var_name(var_sub) {
|
||||
log::debug!("Extracted variable name for completion: {}", var_name);
|
||||
if read_vars(|v| v.get_var(&var_name)).is_empty() {
|
||||
// if we are here, we have a variable substitution that isn't complete
|
||||
// so let's try to complete it
|
||||
let ret: ShResult<CompResult> = read_vars(|v| {
|
||||
let var_matches = v.flatten_vars()
|
||||
let var_matches = v
|
||||
.flatten_vars()
|
||||
.keys()
|
||||
.filter(|k| k.starts_with(&var_name) && *k != &var_name)
|
||||
.map(|k| k.to_string())
|
||||
@@ -272,7 +283,9 @@ impl Completer {
|
||||
let name_start = cur_token.span.start + start;
|
||||
let name_end = cur_token.span.start + end;
|
||||
self.token_span = (name_start, name_end);
|
||||
cur_token.span.set_range(self.token_span.0..self.token_span.1);
|
||||
cur_token
|
||||
.span
|
||||
.set_range(self.token_span.0..self.token_span.1);
|
||||
Ok(CompResult::from_candidates(var_matches))
|
||||
} else {
|
||||
Ok(CompResult::NoMatch)
|
||||
@@ -296,20 +309,12 @@ impl Completer {
|
||||
let expanded = expanded_words.join("\\ ");
|
||||
|
||||
let mut candidates = match ctx.pop() {
|
||||
Some(markers::COMMAND) => {
|
||||
log::debug!("Completing command: {}", &expanded);
|
||||
Self::complete_command(&expanded)?
|
||||
}
|
||||
Some(markers::ARG) => {
|
||||
log::debug!("Completing filename: {}", &expanded);
|
||||
Self::complete_filename(&expanded)
|
||||
}
|
||||
Some(m) => {
|
||||
log::warn!("Unknown marker {:?} in completion context", m);
|
||||
Some(markers::COMMAND) => Self::complete_command(&expanded)?,
|
||||
Some(markers::ARG) => Self::complete_filename(&expanded),
|
||||
Some(_) => {
|
||||
return Ok(CompResult::NoMatch);
|
||||
}
|
||||
None => {
|
||||
log::warn!("No marker found in completion context");
|
||||
return Ok(CompResult::NoMatch);
|
||||
}
|
||||
};
|
||||
@@ -321,10 +326,11 @@ impl Completer {
|
||||
// /path/to/some_path/file.txt
|
||||
// and instead returns
|
||||
// $SOME_PATH/file.txt
|
||||
candidates = candidates.into_iter()
|
||||
candidates = candidates
|
||||
.into_iter()
|
||||
.map(|c| match c.strip_prefix(&expanded) {
|
||||
Some(suffix) => format!("{raw_tk}{suffix}"),
|
||||
None => c
|
||||
None => c,
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -341,16 +347,23 @@ impl Completer {
|
||||
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; };
|
||||
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; };
|
||||
let Ok(entry) = entry else {
|
||||
continue;
|
||||
};
|
||||
let Ok(meta) = entry.metadata() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let file_name = entry.file_name().to_string_lossy().to_string();
|
||||
|
||||
if meta.is_file()
|
||||
&& (meta.permissions().mode() & 0o111) != 0
|
||||
&& file_name.starts_with(start) {
|
||||
&& file_name.starts_with(start)
|
||||
{
|
||||
candidates.push(file_name);
|
||||
}
|
||||
}
|
||||
@@ -390,13 +403,7 @@ impl Completer {
|
||||
|
||||
fn complete_filename(start: &str) -> Vec<String> {
|
||||
let mut candidates = vec![];
|
||||
|
||||
// If completing after '=', only use the part after it
|
||||
let start = if let Some(eq_pos) = start.rfind('=') {
|
||||
&start[eq_pos + 1..]
|
||||
} else {
|
||||
start
|
||||
};
|
||||
let has_dotslash = start.starts_with("./");
|
||||
|
||||
// Split path into directory and filename parts
|
||||
// Use "." if start is empty (e.g., after "foo=")
|
||||
@@ -405,9 +412,13 @@ impl Completer {
|
||||
// Completing inside a directory: "src/" → dir="src/", prefix=""
|
||||
(path, "")
|
||||
} else if let Some(parent) = path.parent()
|
||||
&& !parent.as_os_str().is_empty() {
|
||||
&& !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(""))
|
||||
(
|
||||
parent.to_path_buf(),
|
||||
path.file_name().unwrap().to_str().unwrap_or(""),
|
||||
)
|
||||
} else {
|
||||
// No directory: "fil" → dir=".", prefix="fil"
|
||||
(PathBuf::from("."), start)
|
||||
@@ -435,7 +446,12 @@ impl Completer {
|
||||
full_path.push(""); // adds trailing /
|
||||
}
|
||||
|
||||
candidates.push(full_path.to_string_lossy().to_string());
|
||||
let mut path_raw = full_path.to_string_lossy().to_string();
|
||||
if path_raw.starts_with("./") && !has_dotslash {
|
||||
path_raw = path_raw.trim_start_matches("./").to_string();
|
||||
}
|
||||
|
||||
candidates.push(path_raw);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -443,3 +459,9 @@ impl Completer {
|
||||
candidates
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Completer {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
use std::{env, os::unix::fs::PermissionsExt, path::{Path, PathBuf}};
|
||||
use std::{
|
||||
env,
|
||||
os::unix::fs::PermissionsExt,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::{libsh::term::{Style, StyleSet, Styled}, prompt::readline::{annotate_input, markers}, state::{read_logic, read_shopts}};
|
||||
use crate::{
|
||||
libsh::term::{Style, StyleSet, Styled},
|
||||
prompt::readline::{annotate_input, markers},
|
||||
state::{read_logic, read_shopts},
|
||||
};
|
||||
|
||||
/// Syntax highlighter for shell input using Unicode marker-based annotation
|
||||
///
|
||||
/// The highlighter processes annotated input strings containing invisible Unicode markers
|
||||
/// (U+FDD0-U+FDEF range) that indicate syntax elements. It generates ANSI escape codes
|
||||
/// for terminal display while maintaining a style stack for proper color restoration
|
||||
/// in nested constructs (e.g., variables inside strings inside command substitutions).
|
||||
/// The highlighter processes annotated input strings containing invisible
|
||||
/// Unicode markers (U+FDD0-U+FDEF range) that indicate syntax elements. It
|
||||
/// generates ANSI escape codes for terminal display while maintaining a style
|
||||
/// stack for proper color restoration in nested constructs (e.g., variables
|
||||
/// inside strings inside command substitutions).
|
||||
pub struct Highlighter {
|
||||
input: String,
|
||||
output: String,
|
||||
@@ -45,29 +54,26 @@ impl Highlighter {
|
||||
let mut input_chars = input.chars().peekable();
|
||||
while let Some(ch) = input_chars.next() {
|
||||
match ch {
|
||||
markers::STRING_DQ_END |
|
||||
markers::STRING_SQ_END |
|
||||
markers::VAR_SUB_END |
|
||||
markers::CMD_SUB_END |
|
||||
markers::PROC_SUB_END |
|
||||
markers::SUBSH_END => self.pop_style(),
|
||||
markers::STRING_DQ_END
|
||||
| markers::STRING_SQ_END
|
||||
| markers::VAR_SUB_END
|
||||
| markers::CMD_SUB_END
|
||||
| markers::PROC_SUB_END
|
||||
| markers::SUBSH_END => self.pop_style(),
|
||||
|
||||
markers::CMD_SEP |
|
||||
markers::RESET => self.clear_styles(),
|
||||
markers::CMD_SEP | markers::RESET => self.clear_styles(),
|
||||
|
||||
|
||||
markers::STRING_DQ |
|
||||
markers::STRING_SQ |
|
||||
markers::KEYWORD => self.push_style(Style::Yellow),
|
||||
markers::STRING_DQ | markers::STRING_SQ | markers::KEYWORD => {
|
||||
self.push_style(Style::Yellow)
|
||||
}
|
||||
markers::BUILTIN => self.push_style(Style::Green),
|
||||
markers::CASE_PAT => self.push_style(Style::Blue),
|
||||
markers::ARG => self.push_style(Style::White),
|
||||
|
||||
markers::COMMENT => self.push_style(Style::BrightBlack),
|
||||
|
||||
markers::GLOB => self.push_style(Style::Blue),
|
||||
|
||||
markers::REDIRECT |
|
||||
markers::OPERATOR => self.push_style(Style::Magenta | Style::Bold),
|
||||
markers::REDIRECT | markers::OPERATOR => self.push_style(Style::Magenta | Style::Bold),
|
||||
|
||||
markers::ASSIGNMENT => {
|
||||
let mut var_name = String::new();
|
||||
@@ -92,14 +98,34 @@ impl Highlighter {
|
||||
self.pop_style();
|
||||
}
|
||||
|
||||
markers::COMMAND => {
|
||||
let mut cmd_name = String::new();
|
||||
while let Some(ch) = input_chars.peek() {
|
||||
if *ch == markers::RESET {
|
||||
markers::ARG => {
|
||||
let mut arg = String::new();
|
||||
let mut chars_clone = input_chars.clone();
|
||||
while let Some(ch) = chars_clone.next() {
|
||||
if ch == markers::RESET {
|
||||
break;
|
||||
}
|
||||
cmd_name.push(*ch);
|
||||
input_chars.next();
|
||||
arg.push(ch);
|
||||
}
|
||||
|
||||
let style = if Self::is_filename(&arg) {
|
||||
Style::White | Style::Underline
|
||||
} else {
|
||||
Style::White.into()
|
||||
};
|
||||
|
||||
self.push_style(style);
|
||||
self.last_was_reset = false;
|
||||
}
|
||||
|
||||
markers::COMMAND => {
|
||||
let mut cmd_name = String::new();
|
||||
let mut chars_clone = input_chars.clone();
|
||||
while let Some(ch) = chars_clone.next() {
|
||||
if ch == markers::RESET {
|
||||
break;
|
||||
}
|
||||
cmd_name.push(ch);
|
||||
}
|
||||
let style = if Self::is_valid(&cmd_name) {
|
||||
Style::Green.into()
|
||||
@@ -107,7 +133,6 @@ impl Highlighter {
|
||||
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 => {
|
||||
@@ -134,17 +159,19 @@ impl Highlighter {
|
||||
markers::CMD_SUB => "$(",
|
||||
markers::SUBSH => "(",
|
||||
markers::PROC_SUB => {
|
||||
if inner.starts_with("<(") { "<(" }
|
||||
else if inner.starts_with(">(") { ">(" }
|
||||
else { "<(" } // fallback
|
||||
if inner.starts_with("<(") {
|
||||
"<("
|
||||
} else if inner.starts_with(">(") {
|
||||
">("
|
||||
} else {
|
||||
"<("
|
||||
} // fallback
|
||||
}
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
let inner_content = if incomplete {
|
||||
inner
|
||||
.strip_prefix(prefix)
|
||||
.unwrap_or(&inner)
|
||||
inner.strip_prefix(prefix).unwrap_or(&inner)
|
||||
} else {
|
||||
inner
|
||||
.strip_prefix(prefix)
|
||||
@@ -198,14 +225,16 @@ impl Highlighter {
|
||||
/// Extracts the highlighted output and resets the highlighter state
|
||||
///
|
||||
/// Clears the input buffer, style stack, and returns the generated output
|
||||
/// containing ANSI escape codes. The highlighter is ready for reuse after this.
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// Checks if a command name is valid (exists in PATH, is a function, or is an alias)
|
||||
/// 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
|
||||
@@ -222,9 +251,11 @@ impl Highlighter {
|
||||
// this is a directory and autocd is enabled
|
||||
return true;
|
||||
} else {
|
||||
let Ok(meta) = cmd_path.metadata() else { return false };
|
||||
let Ok(meta) = cmd_path.metadata() else {
|
||||
return false;
|
||||
};
|
||||
// this is a file that is executable by someone
|
||||
return meta.permissions().mode() & 0o111 == 0
|
||||
return meta.permissions().mode() & 0o111 == 0;
|
||||
}
|
||||
} else {
|
||||
// they gave us a command name
|
||||
@@ -248,6 +279,49 @@ impl Highlighter {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_filename(arg: &str) -> bool {
|
||||
let path = PathBuf::from(arg);
|
||||
|
||||
if path.exists() {
|
||||
return true;
|
||||
}
|
||||
|
||||
if let Some(parent_dir) = path.parent()
|
||||
&& let Ok(entries) = parent_dir.read_dir()
|
||||
{
|
||||
let files = entries
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.file_name().to_string_lossy().to_string())
|
||||
.collect::<Vec<_>>();
|
||||
let Some(arg_filename) = PathBuf::from(arg)
|
||||
.file_name()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
for file in files {
|
||||
if file.starts_with(&arg_filename) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if let Ok(this_dir) = env::current_dir()
|
||||
&& let Ok(entries) = this_dir.read_dir()
|
||||
{
|
||||
let this_dir_files = entries
|
||||
.filter_map(|e| e.ok())
|
||||
.map(|e| e.file_name().to_string_lossy().to_string())
|
||||
.collect::<Vec<_>>();
|
||||
for file in this_dir_files {
|
||||
if file.starts_with(arg) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
false
|
||||
}
|
||||
|
||||
/// Emits a reset ANSI code to the output, with deduplication
|
||||
///
|
||||
/// Only emits the reset if the last emitted code was not already a reset,
|
||||
@@ -280,10 +354,11 @@ impl Highlighter {
|
||||
|
||||
/// 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.
|
||||
/// 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() {
|
||||
@@ -302,13 +377,15 @@ impl Highlighter {
|
||||
self.emit_reset();
|
||||
}
|
||||
|
||||
/// Simple marker-to-ANSI replacement (unused in favor of stack-based highlighting)
|
||||
/// 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
|
||||
self.input = self
|
||||
.input
|
||||
.replace([markers::RESET, markers::ARG], "\x1b[0m")
|
||||
.replace(markers::KEYWORD, "\x1b[33m")
|
||||
.replace(markers::CASE_PAT, "\x1b[34m")
|
||||
|
||||
@@ -189,8 +189,8 @@ fn read_hist_file(path: &Path) -> ShResult<Vec<HistEntry>> {
|
||||
Ok(raw.parse::<HistEntries>()?.0)
|
||||
}
|
||||
|
||||
/// Deduplicate entries, keeping only the most recent occurrence of each command.
|
||||
/// Preserves chronological order (oldest to newest).
|
||||
/// Deduplicate entries, keeping only the most recent occurrence of each
|
||||
/// command. Preserves chronological order (oldest to newest).
|
||||
fn dedupe_entries(entries: &[HistEntry]) -> Vec<HistEntry> {
|
||||
let mut seen = HashSet::new();
|
||||
// Iterate backwards (newest first), keeping first occurrence of each command
|
||||
@@ -207,7 +207,7 @@ fn dedupe_entries(entries: &[HistEntry]) -> Vec<HistEntry> {
|
||||
|
||||
pub struct History {
|
||||
path: PathBuf,
|
||||
pub pending: Option<String>,
|
||||
pub pending: Option<(String, usize)>, // command, cursor_pos
|
||||
entries: Vec<HistEntry>,
|
||||
search_mask: Vec<HistEntry>,
|
||||
no_matches: bool,
|
||||
@@ -270,14 +270,14 @@ impl History {
|
||||
self.cursor = self.search_mask.len();
|
||||
}
|
||||
|
||||
pub fn update_pending_cmd(&mut self, command: &str) {
|
||||
let cmd = command.to_string();
|
||||
pub fn update_pending_cmd(&mut self, buf: (&str, usize)) {
|
||||
let cmd = buf.0.to_string();
|
||||
let constraint = SearchConstraint {
|
||||
kind: SearchKind::Prefix,
|
||||
term: cmd.clone(),
|
||||
};
|
||||
|
||||
self.pending = Some(cmd);
|
||||
self.pending = Some((cmd, buf.1));
|
||||
self.constrain_entries(constraint);
|
||||
}
|
||||
|
||||
@@ -328,12 +328,14 @@ impl History {
|
||||
}
|
||||
|
||||
pub fn hint_entry(&self) -> Option<&HistEntry> {
|
||||
if self.no_matches { return None };
|
||||
if self.no_matches {
|
||||
return None;
|
||||
};
|
||||
self.search_mask.last()
|
||||
}
|
||||
|
||||
pub fn get_hint(&self) -> Option<String> {
|
||||
if self.at_pending() && self.pending.as_ref().is_some_and(|p| !p.is_empty()) {
|
||||
if self.at_pending() && self.pending.as_ref().is_some_and(|p| !p.0.is_empty()) {
|
||||
let entry = self.hint_entry()?;
|
||||
Some(entry.command().to_string())
|
||||
} else {
|
||||
@@ -342,9 +344,15 @@ impl History {
|
||||
}
|
||||
|
||||
pub fn scroll(&mut self, offset: isize) -> Option<&HistEntry> {
|
||||
self.cursor = self.cursor.saturating_add_signed(offset).clamp(0, self.search_mask.len());
|
||||
self.cursor = self
|
||||
.cursor
|
||||
.saturating_add_signed(offset)
|
||||
.clamp(0, self.search_mask.len());
|
||||
|
||||
log::debug!("Scrolling history by offset {offset} from cursor at index {}", self.cursor);
|
||||
log::debug!(
|
||||
"Scrolling history by offset {offset} from cursor at index {}",
|
||||
self.cursor
|
||||
);
|
||||
self.search_mask.get(self.cursor)
|
||||
}
|
||||
|
||||
@@ -378,7 +386,8 @@ impl History {
|
||||
|
||||
let last_file_entry = self
|
||||
.entries
|
||||
.iter().rfind(|ent| !ent.new)
|
||||
.iter()
|
||||
.rfind(|ent| !ent.new)
|
||||
.map(|ent| ent.command.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
|
||||
@@ -628,8 +628,9 @@ impl LineBuf {
|
||||
self.next_sentence_start_from_punctuation(pos).is_some()
|
||||
}
|
||||
|
||||
/// If position is at sentence-ending punctuation, returns the position of the next sentence start.
|
||||
/// Handles closing delimiters (`)`, `]`, `"`, `'`) after punctuation.
|
||||
/// If position is at sentence-ending punctuation, returns the position of the
|
||||
/// next sentence start. Handles closing delimiters (`)`, `]`, `"`, `'`)
|
||||
/// after punctuation.
|
||||
#[allow(clippy::collapsible_if)]
|
||||
pub fn next_sentence_start_from_punctuation(&self, pos: usize) -> Option<usize> {
|
||||
if let Some(gr) = self.read_grapheme_at(pos) {
|
||||
@@ -956,7 +957,8 @@ impl LineBuf {
|
||||
let start = start.unwrap_or(0);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1363,7 +1365,12 @@ impl LineBuf {
|
||||
}
|
||||
|
||||
/// Find the start of the next word forward
|
||||
pub fn start_of_word_forward(&mut self, mut pos: usize, word: Word, include_last_char: bool) -> usize {
|
||||
pub fn start_of_word_forward(
|
||||
&mut self,
|
||||
mut pos: usize,
|
||||
word: Word,
|
||||
include_last_char: bool,
|
||||
) -> usize {
|
||||
let default = self.grapheme_indices().len();
|
||||
let mut indices_iter = (pos..self.cursor.max).peekable();
|
||||
|
||||
@@ -1390,8 +1397,7 @@ impl LineBuf {
|
||||
let on_whitespace = is_whitespace(&cur_char);
|
||||
|
||||
if !on_whitespace {
|
||||
let Some(ws_pos) =
|
||||
indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
|
||||
let Some(ws_pos) = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
|
||||
else {
|
||||
return default;
|
||||
};
|
||||
@@ -1457,7 +1463,12 @@ impl LineBuf {
|
||||
}
|
||||
|
||||
/// Find the end of the previous word backward
|
||||
pub fn end_of_word_backward(&mut self, mut pos: usize, word: Word, include_last_char: bool) -> usize {
|
||||
pub fn end_of_word_backward(
|
||||
&mut self,
|
||||
mut pos: usize,
|
||||
word: Word,
|
||||
include_last_char: bool,
|
||||
) -> usize {
|
||||
let default = self.grapheme_indices().len();
|
||||
let mut indices_iter = (0..pos).rev().peekable();
|
||||
|
||||
@@ -1484,8 +1495,7 @@ impl LineBuf {
|
||||
let on_whitespace = is_whitespace(&cur_char);
|
||||
|
||||
if !on_whitespace {
|
||||
let Some(ws_pos) =
|
||||
indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
|
||||
let Some(ws_pos) = indices_iter.find(|i| self.grapheme_at(*i).is_some_and(is_whitespace))
|
||||
else {
|
||||
return default;
|
||||
};
|
||||
@@ -1742,11 +1752,7 @@ impl LineBuf {
|
||||
};
|
||||
pos = next_ws_pos;
|
||||
|
||||
if pos == 0 {
|
||||
pos
|
||||
} else {
|
||||
pos + 1
|
||||
}
|
||||
if pos == 0 { pos } else { pos + 1 }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2575,7 +2581,8 @@ impl LineBuf {
|
||||
}
|
||||
Verb::SwapVisualAnchor => {
|
||||
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() {
|
||||
SelectAnchor::Start => start,
|
||||
@@ -2731,8 +2738,10 @@ impl LineBuf {
|
||||
let edit_is_merging = self.undo_stack.last().is_some_and(|edit| edit.merging);
|
||||
|
||||
// Merge character inserts into one edit
|
||||
if edit_is_merging && cmd.verb.as_ref().is_none_or(|v| !v.1.is_char_insert())
|
||||
&& let Some(edit) = self.undo_stack.last_mut() {
|
||||
if edit_is_merging
|
||||
&& cmd.verb.as_ref().is_none_or(|v| !v.1.is_char_insert())
|
||||
&& let Some(edit) = self.undo_stack.last_mut()
|
||||
{
|
||||
edit.stop_merge();
|
||||
}
|
||||
|
||||
@@ -2821,8 +2830,7 @@ impl LineBuf {
|
||||
self.saved_col = None;
|
||||
}
|
||||
|
||||
if is_char_insert
|
||||
&& let Some(edit) = self.undo_stack.last_mut() {
|
||||
if is_char_insert && let Some(edit) = self.undo_stack.last_mut() {
|
||||
edit.start_merge();
|
||||
}
|
||||
|
||||
@@ -2833,7 +2841,11 @@ impl LineBuf {
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,16 +2,22 @@ use history::History;
|
||||
use keys::{KeyCode, KeyEvent, ModKeys};
|
||||
use linebuf::{LineBuf, SelectAnchor, SelectMode};
|
||||
use nix::libc::STDOUT_FILENO;
|
||||
use term::{get_win_size, KeyReader, Layout, LineWriter, PollReader, TermWriter};
|
||||
use vicmd::{CmdFlags, Motion, MotionCmd, RegisterName, To, Verb, VerbCmd, ViCmd};
|
||||
use term::{KeyReader, Layout, LineWriter, PollReader, TermWriter, get_win_size};
|
||||
use vicmd::{CmdFlags, Motion, MotionCmd, RegisterName, Verb, VerbCmd, ViCmd};
|
||||
use vimode::{CmdReplay, ModeReport, ViInsert, ViMode, ViNormal, ViReplace, ViVisual};
|
||||
|
||||
use crate::{libsh::{
|
||||
error::{ShErrKind, ShResult},
|
||||
term::{Style, Styled},
|
||||
}, parse::lex::{self, LexFlags, Tk, TkFlags, TkRule}, prompt::readline::{complete::{CompResult, Completer}, highlight::Highlighter}};
|
||||
use crate::prelude::*;
|
||||
use crate::{
|
||||
libsh::{
|
||||
error::ShResult,
|
||||
term::{Style, Styled},
|
||||
},
|
||||
parse::lex::{self, LexFlags, Tk, TkFlags, TkRule},
|
||||
prompt::readline::{complete::Completer, highlight::Highlighter},
|
||||
};
|
||||
|
||||
pub mod complete;
|
||||
pub mod highlight;
|
||||
pub mod history;
|
||||
pub mod keys;
|
||||
pub mod layout;
|
||||
@@ -20,8 +26,6 @@ pub mod register;
|
||||
pub mod term;
|
||||
pub mod vicmd;
|
||||
pub mod vimode;
|
||||
pub mod highlight;
|
||||
pub mod complete;
|
||||
|
||||
pub mod markers {
|
||||
use super::Marker;
|
||||
@@ -65,28 +69,12 @@ pub mod markers {
|
||||
STRING_DQ_END,
|
||||
STRING_SQ_END,
|
||||
SUBSH_END,
|
||||
RESET
|
||||
RESET,
|
||||
];
|
||||
pub const TOKEN_LEVEL: [Marker; 10] = [
|
||||
SUBSH,
|
||||
COMMAND,
|
||||
BUILTIN,
|
||||
ARG,
|
||||
KEYWORD,
|
||||
OPERATOR,
|
||||
REDIRECT,
|
||||
CMD_SEP,
|
||||
CASE_PAT,
|
||||
ASSIGNMENT,
|
||||
];
|
||||
pub const SUB_TOKEN: [Marker;6] = [
|
||||
VAR_SUB,
|
||||
CMD_SUB,
|
||||
PROC_SUB,
|
||||
STRING_DQ,
|
||||
STRING_SQ,
|
||||
GLOB,
|
||||
SUBSH, COMMAND, BUILTIN, ARG, KEYWORD, OPERATOR, REDIRECT, CMD_SEP, CASE_PAT, ASSIGNMENT,
|
||||
];
|
||||
pub const SUB_TOKEN: [Marker; 6] = [VAR_SUB, CMD_SUB, PROC_SUB, STRING_DQ, STRING_SQ, GLOB];
|
||||
|
||||
pub fn is_marker(c: Marker) -> bool {
|
||||
TOKEN_LEVEL.contains(&c) || SUB_TOKEN.contains(&c) || END_MARKERS.contains(&c)
|
||||
@@ -145,14 +133,14 @@ impl FernVi {
|
||||
|
||||
pub fn with_initial(mut self, initial: &str) -> Self {
|
||||
self.editor = LineBuf::new().with_initial(initial, 0);
|
||||
self.history.update_pending_cmd(self.editor.as_str());
|
||||
self
|
||||
.history
|
||||
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
|
||||
self
|
||||
}
|
||||
|
||||
/// Feed raw bytes from stdin into the reader's buffer
|
||||
pub fn feed_bytes(&mut self, bytes: &[u8]) {
|
||||
let test_input = "echo \"hello $USER\" | grep $(whoami)";
|
||||
let annotated = annotate_input(test_input);
|
||||
self.reader.feed_bytes(bytes);
|
||||
}
|
||||
|
||||
@@ -185,13 +173,14 @@ impl FernVi {
|
||||
|
||||
// Process all available keys
|
||||
while let Some(key) = self.reader.read_key()? {
|
||||
|
||||
if self.should_accept_hint(&key) {
|
||||
self.editor.accept_hint();
|
||||
if !self.history.at_pending() {
|
||||
self.history.reset_to_pending();
|
||||
}
|
||||
self.history.update_pending_cmd(self.editor.as_str());
|
||||
self
|
||||
.history
|
||||
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
|
||||
self.needs_redraw = true;
|
||||
continue;
|
||||
}
|
||||
@@ -205,9 +194,14 @@ impl FernVi {
|
||||
let cursor_pos = self.editor.cursor_byte_pos();
|
||||
|
||||
match self.completer.complete(line, cursor_pos, direction)? {
|
||||
Some(mut line) => {
|
||||
Some(line) => {
|
||||
let span_start = self.completer.token_span.0;
|
||||
let new_cursor = span_start + self.completer.selected_candidate().map(|c| c.len()).unwrap_or_default();
|
||||
let new_cursor = span_start
|
||||
+ self
|
||||
.completer
|
||||
.selected_candidate()
|
||||
.map(|c| c.len())
|
||||
.unwrap_or_default();
|
||||
|
||||
self.editor.set_buffer(line);
|
||||
self.editor.cursor.set(new_cursor);
|
||||
@@ -215,16 +209,18 @@ impl FernVi {
|
||||
if !self.history.at_pending() {
|
||||
self.history.reset_to_pending();
|
||||
}
|
||||
self.history.update_pending_cmd(self.editor.as_str());
|
||||
self
|
||||
.history
|
||||
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
|
||||
let hint = self.history.get_hint();
|
||||
self.editor.set_hint(hint);
|
||||
}
|
||||
None => {
|
||||
match crate::state::read_shopts(|s| s.core.bell_style) {
|
||||
crate::shopt::FernBellStyle::Audible => { self.writer.flush_write("\x07")?; }
|
||||
None => match crate::state::read_shopts(|s| s.core.bell_style) {
|
||||
crate::shopt::FernBellStyle::Audible => {
|
||||
self.writer.flush_write("\x07")?;
|
||||
}
|
||||
crate::shopt::FernBellStyle::Visible | crate::shopt::FernBellStyle::Disable => {}
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
self.needs_redraw = true;
|
||||
@@ -252,8 +248,7 @@ impl FernVi {
|
||||
self.writer.flush_write("\n")?;
|
||||
let buf = self.editor.take_buf();
|
||||
// Save command to history if auto_hist is enabled
|
||||
if crate::state::read_shopts(|s| s.core.auto_hist)
|
||||
&& !buf.is_empty() {
|
||||
if crate::state::read_shopts(|s| s.core.auto_hist) && !buf.is_empty() {
|
||||
self.history.push(buf.clone());
|
||||
if let Err(e) = self.history.save() {
|
||||
eprintln!("Failed to save history: {e}");
|
||||
@@ -278,7 +273,9 @@ impl FernVi {
|
||||
let after = self.editor.as_str();
|
||||
|
||||
if before != after {
|
||||
self.history.update_pending_cmd(self.editor.as_str());
|
||||
self
|
||||
.history
|
||||
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
|
||||
}
|
||||
|
||||
let hint = self.history.get_hint();
|
||||
@@ -311,12 +308,8 @@ impl FernVi {
|
||||
let count = &cmd.motion().unwrap().0;
|
||||
let motion = &cmd.motion().unwrap().1;
|
||||
let count = match motion {
|
||||
Motion::LineUpCharwise => {
|
||||
-(*count as isize)
|
||||
}
|
||||
Motion::LineDownCharwise => {
|
||||
*count as isize
|
||||
}
|
||||
Motion::LineUpCharwise => -(*count as isize),
|
||||
Motion::LineDownCharwise => *count as isize,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
let entry = self.history.scroll(count);
|
||||
@@ -326,12 +319,13 @@ impl FernVi {
|
||||
let pending = self.editor.take_buf();
|
||||
self.editor.set_buffer(entry.command().to_string());
|
||||
if self.history.pending.is_none() {
|
||||
self.history.pending = Some(pending);
|
||||
self.history.pending = Some((pending, self.editor.cursor.get()));
|
||||
}
|
||||
self.editor.set_hint(None);
|
||||
} else if let Some(pending) = self.history.pending.take() {
|
||||
log::info!("Setting buffer to pending command: {}", &pending);
|
||||
self.editor.set_buffer(pending);
|
||||
log::info!("Setting buffer to pending command: {}", &pending.0);
|
||||
self.editor.set_buffer(pending.0);
|
||||
self.editor.cursor.set(pending.1);
|
||||
self.editor.set_hint(None);
|
||||
}
|
||||
}
|
||||
@@ -385,9 +379,7 @@ impl FernVi {
|
||||
self.writer.clear_rows(layout)?;
|
||||
}
|
||||
|
||||
self
|
||||
.writer
|
||||
.redraw(&self.prompt, &line, &new_layout)?;
|
||||
self.writer.redraw(&self.prompt, &line, &new_layout)?;
|
||||
|
||||
self.writer.flush_write(&self.mode.cursor_style())?;
|
||||
|
||||
@@ -553,8 +545,8 @@ impl FernVi {
|
||||
|
||||
/// Annotates shell input with invisible Unicode markers for syntax highlighting
|
||||
///
|
||||
/// Takes raw shell input and inserts non-character markers (U+FDD0-U+FDEF range)
|
||||
/// around syntax elements. These markers indicate:
|
||||
/// Takes raw shell input and inserts non-character markers (U+FDD0-U+FDEF
|
||||
/// range) around syntax elements. These markers indicate:
|
||||
/// - Token-level context (commands, arguments, operators, keywords)
|
||||
/// - Sub-token constructs (strings, variables, command substitutions, globs)
|
||||
///
|
||||
@@ -597,9 +589,7 @@ pub fn annotate_input_recursive(input: &str) -> String {
|
||||
|
||||
while let Some((pos, ch)) = chars.next() {
|
||||
match ch {
|
||||
markers::CMD_SUB |
|
||||
markers::SUBSH |
|
||||
markers::PROC_SUB => {
|
||||
markers::CMD_SUB | markers::SUBSH | markers::PROC_SUB => {
|
||||
let mut body = String::new();
|
||||
let span_start = pos + ch.len_utf8();
|
||||
let mut span_end = span_start;
|
||||
@@ -607,7 +597,7 @@ pub fn annotate_input_recursive(input: &str) -> String {
|
||||
markers::CMD_SUB => markers::CMD_SUB_END,
|
||||
markers::SUBSH => markers::SUBSH_END,
|
||||
markers::PROC_SUB => markers::PROC_SUB_END,
|
||||
_ => unreachable!()
|
||||
_ => unreachable!(),
|
||||
};
|
||||
while let Some((sub_pos, sub_ch)) = chars.next() {
|
||||
match sub_ch {
|
||||
@@ -619,19 +609,17 @@ pub fn annotate_input_recursive(input: &str) -> String {
|
||||
}
|
||||
}
|
||||
let prefix = match ch {
|
||||
markers::PROC_SUB => {
|
||||
match chars.peek().map(|(_, c)| *c) {
|
||||
markers::PROC_SUB => match chars.peek().map(|(_, c)| *c) {
|
||||
Some('>') => ">(",
|
||||
Some('<') => "<(",
|
||||
_ => {
|
||||
log::error!("Unexpected character after PROC_SUB marker: expected '>' or '<'");
|
||||
"<("
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
markers::CMD_SUB => "$(",
|
||||
markers::SUBSH => "(",
|
||||
_ => unreachable!()
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
body = body.trim_start_matches(prefix).to_string();
|
||||
@@ -667,8 +655,8 @@ pub fn get_insertions(input: &str) -> Vec<(usize, Marker)> {
|
||||
/// Maps token class to its corresponding marker character
|
||||
///
|
||||
/// Returns the appropriate Unicode marker for token-level syntax elements.
|
||||
/// Token-level markers are derived directly from the lexer's token classification
|
||||
/// and represent complete tokens (operators, separators, etc.).
|
||||
/// Token-level markers are derived directly from the lexer's token
|
||||
/// classification and represent complete tokens (operators, separators, etc.).
|
||||
///
|
||||
/// Returns `None` for:
|
||||
/// - String tokens (which need sub-token scanning for variables, quotes, etc.)
|
||||
@@ -676,22 +664,16 @@ pub fn get_insertions(input: &str) -> Vec<(usize, Marker)> {
|
||||
/// - Unimplemented features (comments, brace groups)
|
||||
pub fn marker_for(class: &TkRule) -> Option<Marker> {
|
||||
match class {
|
||||
TkRule::Pipe |
|
||||
TkRule::ErrPipe |
|
||||
TkRule::And |
|
||||
TkRule::Or |
|
||||
TkRule::Bg => Some(markers::OPERATOR),
|
||||
TkRule::Pipe | TkRule::ErrPipe | TkRule::And | TkRule::Or | TkRule::Bg => {
|
||||
Some(markers::OPERATOR)
|
||||
}
|
||||
TkRule::Sep => Some(markers::CMD_SEP),
|
||||
TkRule::Redir => Some(markers::REDIRECT),
|
||||
TkRule::CasePattern => Some(markers::CASE_PAT),
|
||||
TkRule::BraceGrpStart => todo!(),
|
||||
TkRule::BraceGrpEnd => todo!(),
|
||||
TkRule::Comment => todo!(),
|
||||
TkRule::Expanded { exp: _ } |
|
||||
TkRule::EOI |
|
||||
TkRule::SOI |
|
||||
TkRule::Null |
|
||||
TkRule::Str => None,
|
||||
TkRule::Expanded { exp: _ } | TkRule::EOI | TkRule::SOI | TkRule::Null | TkRule::Str => None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -702,23 +684,22 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
|
||||
// - END markers last (inserted last, ends up leftmost)
|
||||
// Result: [END][TOGGLE][RESET]
|
||||
let sort_insertions = |insertions: &mut Vec<(usize, Marker)>| {
|
||||
insertions.sort_by(|a, b| {
|
||||
match b.0.cmp(&a.0) {
|
||||
insertions.sort_by(|a, b| match b.0.cmp(&a.0) {
|
||||
std::cmp::Ordering::Equal => {
|
||||
let priority = |m: Marker| -> u8 {
|
||||
match m {
|
||||
markers::RESET => 0,
|
||||
markers::VAR_SUB |
|
||||
markers::VAR_SUB_END |
|
||||
markers::CMD_SUB |
|
||||
markers::CMD_SUB_END |
|
||||
markers::PROC_SUB |
|
||||
markers::PROC_SUB_END |
|
||||
markers::STRING_DQ |
|
||||
markers::STRING_DQ_END |
|
||||
markers::STRING_SQ |
|
||||
markers::STRING_SQ_END |
|
||||
markers::SUBSH_END => 2,
|
||||
markers::VAR_SUB
|
||||
| markers::VAR_SUB_END
|
||||
| markers::CMD_SUB
|
||||
| markers::CMD_SUB_END
|
||||
| markers::PROC_SUB
|
||||
| markers::PROC_SUB_END
|
||||
| markers::STRING_DQ
|
||||
| markers::STRING_DQ_END
|
||||
| markers::STRING_SQ
|
||||
| markers::STRING_SQ_END
|
||||
| markers::SUBSH_END => 2,
|
||||
markers::ARG => 3,
|
||||
_ => 1,
|
||||
}
|
||||
@@ -726,7 +707,6 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
|
||||
priority(a.1).cmp(&priority(b.1))
|
||||
}
|
||||
other => other,
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -738,17 +718,17 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
|
||||
let priority = |m: Marker| -> u8 {
|
||||
match m {
|
||||
markers::RESET => 0,
|
||||
markers::VAR_SUB |
|
||||
markers::VAR_SUB_END |
|
||||
markers::CMD_SUB |
|
||||
markers::CMD_SUB_END |
|
||||
markers::PROC_SUB |
|
||||
markers::PROC_SUB_END |
|
||||
markers::STRING_DQ |
|
||||
markers::STRING_DQ_END |
|
||||
markers::STRING_SQ |
|
||||
markers::STRING_SQ_END |
|
||||
markers::SUBSH_END => 2,
|
||||
markers::VAR_SUB
|
||||
| markers::VAR_SUB_END
|
||||
| markers::CMD_SUB
|
||||
| markers::CMD_SUB_END
|
||||
| markers::PROC_SUB
|
||||
| markers::PROC_SUB_END
|
||||
| markers::STRING_DQ
|
||||
| markers::STRING_DQ_END
|
||||
| markers::STRING_SQ
|
||||
| markers::STRING_SQ_END
|
||||
| markers::SUBSH_END => 2,
|
||||
markers::ARG => 3, // Lowest priority - processed first, overridden by sub-tokens
|
||||
_ => 1,
|
||||
}
|
||||
@@ -769,9 +749,9 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
|
||||
|
||||
let mut insertions: Vec<(usize, Marker)> = vec![];
|
||||
|
||||
|
||||
if token.class != TkRule::Str
|
||||
&& let Some(marker) = marker_for(&token.class) {
|
||||
&& let Some(marker) = marker_for(&token.class)
|
||||
{
|
||||
insertions.push((token.span.end, markers::RESET));
|
||||
insertions.push((token.span.start, marker));
|
||||
return insertions;
|
||||
@@ -784,11 +764,8 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
|
||||
return insertions;
|
||||
}
|
||||
|
||||
|
||||
let token_raw = token.span.as_str();
|
||||
let mut token_chars = token_raw
|
||||
.char_indices()
|
||||
.peekable();
|
||||
let mut token_chars = token_raw.char_indices().peekable();
|
||||
|
||||
let span_start = token.span.start;
|
||||
|
||||
@@ -863,7 +840,8 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
|
||||
|| *br_ch == '+'
|
||||
|| *br_ch == '='
|
||||
|| *br_ch == '/' // parameter expansion symbols
|
||||
|| *br_ch == '?' {
|
||||
|| *br_ch == '?'
|
||||
{
|
||||
token_chars.next();
|
||||
} else if *br_ch == '}' {
|
||||
token_chars.next(); // consume the closing brace
|
||||
@@ -910,7 +888,8 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
|
||||
'<' | '>' if !in_dub_qt && !in_sng_qt && cmd_sub_depth == 0 && proc_sub_depth == 0 => {
|
||||
token_chars.next();
|
||||
if let Some((_, proc_sub_ch)) = token_chars.peek()
|
||||
&& *proc_sub_ch == '(' {
|
||||
&& *proc_sub_ch == '('
|
||||
{
|
||||
proc_sub_depth += 1;
|
||||
token_chars.next(); // consume the paren
|
||||
if proc_sub_depth == 1 {
|
||||
|
||||
@@ -17,11 +17,15 @@ use unicode_segmentation::UnicodeSegmentation;
|
||||
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
|
||||
use vte::{Parser, Perform};
|
||||
|
||||
use crate::{prelude::*, procio::borrow_fd, state::{read_meta, write_meta}};
|
||||
use crate::{
|
||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||
prompt::readline::keys::{KeyCode, ModKeys},
|
||||
};
|
||||
use crate::{
|
||||
prelude::*,
|
||||
procio::borrow_fd,
|
||||
state::{read_meta, write_meta},
|
||||
};
|
||||
|
||||
use super::{keys::KeyEvent, linebuf::LineBuf};
|
||||
|
||||
@@ -242,9 +246,7 @@ impl Read for TermBuffer {
|
||||
let result = nix::unistd::read(self.tty, buf);
|
||||
match result {
|
||||
Ok(n) => Ok(n),
|
||||
Err(Errno::EINTR) => {
|
||||
Err(Errno::EINTR.into())
|
||||
}
|
||||
Err(Errno::EINTR) => Err(Errno::EINTR.into()),
|
||||
Err(e) => Err(std::io::Error::from_raw_os_error(e as i32)),
|
||||
}
|
||||
}
|
||||
@@ -281,14 +283,18 @@ impl RawModeGuard {
|
||||
}
|
||||
|
||||
pub fn with_cooked_mode<F, R>(f: F) -> R
|
||||
where F: FnOnce() -> R {
|
||||
where
|
||||
F: FnOnce() -> R,
|
||||
{
|
||||
let raw = tcgetattr(borrow_fd(STDIN_FILENO)).expect("Failed to get terminal attributes");
|
||||
let mut cooked = raw.clone();
|
||||
cooked.local_flags |= termios::LocalFlags::ICANON | termios::LocalFlags::ECHO;
|
||||
cooked.input_flags |= termios::InputFlags::ICRNL;
|
||||
tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &cooked).expect("Failed to set cooked mode");
|
||||
tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &cooked)
|
||||
.expect("Failed to set cooked mode");
|
||||
let res = f();
|
||||
tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &raw).expect("Failed to restore raw mode");
|
||||
tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &raw)
|
||||
.expect("Failed to restore raw mode");
|
||||
res
|
||||
}
|
||||
}
|
||||
@@ -333,9 +339,15 @@ impl KeyCollector {
|
||||
// CSI modifiers: param = 1 + (shift) + (alt*2) + (ctrl*4) + (meta*8)
|
||||
let bits = param.saturating_sub(1);
|
||||
let mut mods = ModKeys::empty();
|
||||
if bits & 1 != 0 { mods |= ModKeys::SHIFT; }
|
||||
if bits & 2 != 0 { mods |= ModKeys::ALT; }
|
||||
if bits & 4 != 0 { mods |= ModKeys::CTRL; }
|
||||
if bits & 1 != 0 {
|
||||
mods |= ModKeys::SHIFT;
|
||||
}
|
||||
if bits & 2 != 0 {
|
||||
mods |= ModKeys::ALT;
|
||||
}
|
||||
if bits & 4 != 0 {
|
||||
mods |= ModKeys::CTRL;
|
||||
}
|
||||
mods
|
||||
}
|
||||
}
|
||||
@@ -374,46 +386,72 @@ impl Perform for KeyCollector {
|
||||
self.push(event);
|
||||
}
|
||||
|
||||
fn csi_dispatch(&mut self, params: &vte::Params, intermediates: &[u8], _ignore: bool, action: char) {
|
||||
let params: Vec<u16> = params.iter()
|
||||
fn csi_dispatch(
|
||||
&mut self,
|
||||
params: &vte::Params,
|
||||
intermediates: &[u8],
|
||||
_ignore: bool,
|
||||
action: char,
|
||||
) {
|
||||
let params: Vec<u16> = params
|
||||
.iter()
|
||||
.map(|p| p.first().copied().unwrap_or(0))
|
||||
.collect();
|
||||
|
||||
let event = match (intermediates, action) {
|
||||
// Arrow keys: CSI A/B/C/D or CSI 1;mod A/B/C/D
|
||||
([], 'A') => {
|
||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
||||
let mods = params
|
||||
.get(1)
|
||||
.map(|&m| Self::parse_modifiers(m))
|
||||
.unwrap_or(ModKeys::empty());
|
||||
KeyEvent(KeyCode::Up, mods)
|
||||
}
|
||||
([], 'B') => {
|
||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
||||
let mods = params
|
||||
.get(1)
|
||||
.map(|&m| Self::parse_modifiers(m))
|
||||
.unwrap_or(ModKeys::empty());
|
||||
KeyEvent(KeyCode::Down, mods)
|
||||
}
|
||||
([], 'C') => {
|
||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
||||
let mods = params
|
||||
.get(1)
|
||||
.map(|&m| Self::parse_modifiers(m))
|
||||
.unwrap_or(ModKeys::empty());
|
||||
KeyEvent(KeyCode::Right, mods)
|
||||
}
|
||||
([], 'D') => {
|
||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
||||
let mods = params
|
||||
.get(1)
|
||||
.map(|&m| Self::parse_modifiers(m))
|
||||
.unwrap_or(ModKeys::empty());
|
||||
KeyEvent(KeyCode::Left, mods)
|
||||
}
|
||||
// Home/End: CSI H/F or CSI 1;mod H/F
|
||||
([], 'H') => {
|
||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
||||
let mods = params
|
||||
.get(1)
|
||||
.map(|&m| Self::parse_modifiers(m))
|
||||
.unwrap_or(ModKeys::empty());
|
||||
KeyEvent(KeyCode::Home, mods)
|
||||
}
|
||||
([], 'F') => {
|
||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
||||
let mods = params
|
||||
.get(1)
|
||||
.map(|&m| Self::parse_modifiers(m))
|
||||
.unwrap_or(ModKeys::empty());
|
||||
KeyEvent(KeyCode::End, mods)
|
||||
}
|
||||
// Shift+Tab: CSI Z
|
||||
([], 'Z') => {
|
||||
KeyEvent(KeyCode::Tab, ModKeys::SHIFT)
|
||||
}
|
||||
([], 'Z') => KeyEvent(KeyCode::Tab, ModKeys::SHIFT),
|
||||
// Special keys with tilde: CSI num ~ or CSI num;mod ~
|
||||
([], '~') => {
|
||||
let key_num = params.first().copied().unwrap_or(0);
|
||||
let mods = params.get(1).map(|&m| Self::parse_modifiers(m)).unwrap_or(ModKeys::empty());
|
||||
let mods = params
|
||||
.get(1)
|
||||
.map(|&m| Self::parse_modifiers(m))
|
||||
.unwrap_or(ModKeys::empty());
|
||||
let key = match key_num {
|
||||
1 | 7 => KeyCode::Home,
|
||||
2 => KeyCode::Insert,
|
||||
@@ -473,7 +511,9 @@ impl PollReader {
|
||||
pub fn feed_bytes(&mut self, bytes: &[u8]) {
|
||||
if bytes == [b'\x1b'] {
|
||||
// Single escape byte - user pressed ESC key
|
||||
self.collector.push(KeyEvent(KeyCode::Esc, ModKeys::empty()));
|
||||
self
|
||||
.collector
|
||||
.push(KeyEvent(KeyCode::Esc, ModKeys::empty()));
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -161,8 +161,10 @@ impl ViCmd {
|
||||
}
|
||||
/// If a ViCmd has a linewise motion, but no verb, we change it to charwise
|
||||
pub fn alter_line_motion_if_no_verb(&mut self) {
|
||||
if self.is_line_motion() && self.verb.is_none()
|
||||
&& let Some(motion) = self.motion.as_mut() {
|
||||
if self.is_line_motion()
|
||||
&& self.verb.is_none()
|
||||
&& let Some(motion) = self.motion.as_mut()
|
||||
{
|
||||
match motion.1 {
|
||||
Motion::LineUp => motion.1 = Motion::LineUpCharwise,
|
||||
Motion::LineDown => motion.1 = Motion::LineDownCharwise,
|
||||
|
||||
@@ -315,7 +315,7 @@ impl ViNormal {
|
||||
return match obj {
|
||||
TextObj::Sentence(_) | TextObj::Paragraph(_) => CmdState::Complete,
|
||||
_ => CmdState::Invalid,
|
||||
}
|
||||
};
|
||||
}
|
||||
Some(_) => return CmdState::Complete,
|
||||
None => return CmdState::Pending,
|
||||
@@ -410,7 +410,7 @@ impl ViNormal {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'~' => {
|
||||
chars_clone.next();
|
||||
@@ -445,7 +445,7 @@ impl ViNormal {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'x' => {
|
||||
return Some(ViCmd {
|
||||
@@ -454,7 +454,7 @@ impl ViNormal {
|
||||
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'X' => {
|
||||
return Some(ViCmd {
|
||||
@@ -463,7 +463,7 @@ impl ViNormal {
|
||||
motion: Some(MotionCmd(1, Motion::BackwardChar)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
's' => {
|
||||
return Some(ViCmd {
|
||||
@@ -472,7 +472,7 @@ impl ViNormal {
|
||||
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'S' => {
|
||||
return Some(ViCmd {
|
||||
@@ -481,7 +481,7 @@ impl ViNormal {
|
||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'p' => {
|
||||
chars = chars_clone;
|
||||
@@ -516,7 +516,7 @@ impl ViNormal {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'~' => {
|
||||
return Some(ViCmd {
|
||||
@@ -525,7 +525,7 @@ impl ViNormal {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'u' => {
|
||||
return Some(ViCmd {
|
||||
@@ -534,7 +534,7 @@ impl ViNormal {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'v' => {
|
||||
return Some(ViCmd {
|
||||
@@ -543,7 +543,7 @@ impl ViNormal {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'V' => {
|
||||
return Some(ViCmd {
|
||||
@@ -552,7 +552,7 @@ impl ViNormal {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'o' => {
|
||||
return Some(ViCmd {
|
||||
@@ -561,7 +561,7 @@ impl ViNormal {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'O' => {
|
||||
return Some(ViCmd {
|
||||
@@ -570,7 +570,7 @@ impl ViNormal {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'a' => {
|
||||
return Some(ViCmd {
|
||||
@@ -579,7 +579,7 @@ impl ViNormal {
|
||||
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'A' => {
|
||||
return Some(ViCmd {
|
||||
@@ -588,7 +588,7 @@ impl ViNormal {
|
||||
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'i' => {
|
||||
return Some(ViCmd {
|
||||
@@ -597,7 +597,7 @@ impl ViNormal {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'I' => {
|
||||
return Some(ViCmd {
|
||||
@@ -606,7 +606,7 @@ impl ViNormal {
|
||||
motion: Some(MotionCmd(1, Motion::BeginningOfFirstWord)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'J' => {
|
||||
return Some(ViCmd {
|
||||
@@ -615,7 +615,7 @@ impl ViNormal {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'y' => {
|
||||
chars = chars_clone;
|
||||
@@ -636,7 +636,7 @@ impl ViNormal {
|
||||
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'D' => {
|
||||
return Some(ViCmd {
|
||||
@@ -645,7 +645,7 @@ impl ViNormal {
|
||||
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'C' => {
|
||||
return Some(ViCmd {
|
||||
@@ -654,7 +654,7 @@ impl ViNormal {
|
||||
motion: Some(MotionCmd(1, Motion::EndOfLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: self.flags(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'=' => {
|
||||
chars = chars_clone;
|
||||
@@ -684,7 +684,7 @@ impl ViNormal {
|
||||
| ('~', Some(VerbCmd(_, Verb::ToggleCaseRange)))
|
||||
| ('>', Some(VerbCmd(_, Verb::Indent)))
|
||||
| ('<', Some(VerbCmd(_, Verb::Dedent))) => {
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::WholeLine))
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::WholeLine));
|
||||
}
|
||||
('W', Some(VerbCmd(_, Verb::Change))) => {
|
||||
// Same with 'W'
|
||||
@@ -994,8 +994,7 @@ impl ViNormal {
|
||||
}
|
||||
};
|
||||
|
||||
if chars.peek().is_some() {
|
||||
}
|
||||
if chars.peek().is_some() {}
|
||||
|
||||
let verb_ref = verb.as_ref().map(|v| &v.1);
|
||||
let motion_ref = motion.as_ref().map(|m| &m.1);
|
||||
@@ -1185,7 +1184,7 @@ impl ViVisual {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'?' => {
|
||||
return Some(ViCmd {
|
||||
@@ -1194,7 +1193,7 @@ impl ViVisual {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
_ => break 'verb_parse None,
|
||||
}
|
||||
@@ -1209,7 +1208,7 @@ impl ViVisual {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'x' => {
|
||||
chars = chars_clone;
|
||||
@@ -1222,7 +1221,7 @@ impl ViVisual {
|
||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'Y' => {
|
||||
return Some(ViCmd {
|
||||
@@ -1231,7 +1230,7 @@ impl ViVisual {
|
||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'D' => {
|
||||
return Some(ViCmd {
|
||||
@@ -1240,7 +1239,7 @@ impl ViVisual {
|
||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'R' | 'C' => {
|
||||
return Some(ViCmd {
|
||||
@@ -1249,7 +1248,7 @@ impl ViVisual {
|
||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'>' => {
|
||||
return Some(ViCmd {
|
||||
@@ -1258,7 +1257,7 @@ impl ViVisual {
|
||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'<' => {
|
||||
return Some(ViCmd {
|
||||
@@ -1267,7 +1266,7 @@ impl ViVisual {
|
||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'=' => {
|
||||
return Some(ViCmd {
|
||||
@@ -1276,7 +1275,7 @@ impl ViVisual {
|
||||
motion: Some(MotionCmd(1, Motion::WholeLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'p' | 'P' => {
|
||||
chars = chars_clone;
|
||||
@@ -1299,7 +1298,7 @@ impl ViVisual {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'u' => {
|
||||
return Some(ViCmd {
|
||||
@@ -1308,7 +1307,7 @@ impl ViVisual {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'U' => {
|
||||
return Some(ViCmd {
|
||||
@@ -1317,7 +1316,7 @@ impl ViVisual {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'O' | 'o' => {
|
||||
return Some(ViCmd {
|
||||
@@ -1326,7 +1325,7 @@ impl ViVisual {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'A' => {
|
||||
return Some(ViCmd {
|
||||
@@ -1335,7 +1334,7 @@ impl ViVisual {
|
||||
motion: Some(MotionCmd(1, Motion::ForwardChar)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'I' => {
|
||||
return Some(ViCmd {
|
||||
@@ -1344,7 +1343,7 @@ impl ViVisual {
|
||||
motion: Some(MotionCmd(1, Motion::BeginningOfLine)),
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'J' => {
|
||||
return Some(ViCmd {
|
||||
@@ -1353,7 +1352,7 @@ impl ViVisual {
|
||||
motion: None,
|
||||
raw_seq: self.take_cmd(),
|
||||
flags: CmdFlags::empty(),
|
||||
})
|
||||
});
|
||||
}
|
||||
'y' => {
|
||||
chars = chars_clone;
|
||||
@@ -1395,7 +1394,7 @@ impl ViVisual {
|
||||
| ('=', Some(VerbCmd(_, Verb::Equalize)))
|
||||
| ('>', Some(VerbCmd(_, Verb::Indent)))
|
||||
| ('<', Some(VerbCmd(_, Verb::Dedent))) => {
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::WholeLine))
|
||||
break 'motion_parse Some(MotionCmd(count, Motion::WholeLine));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
@@ -1652,8 +1651,7 @@ impl ViVisual {
|
||||
}
|
||||
};
|
||||
|
||||
if chars.peek().is_some() {
|
||||
}
|
||||
if chars.peek().is_some() {}
|
||||
|
||||
let verb_ref = verb.as_ref().map(|v| &v.1);
|
||||
let motion_ref = motion.as_ref().map(|m| &m.1);
|
||||
|
||||
10
src/shopt.rs
10
src/shopt.rs
@@ -117,7 +117,7 @@ impl ShOpts {
|
||||
Note::new("'shopt' takes arguments separated by periods to denote namespaces")
|
||||
.with_sub_notes(vec!["Example: 'shopt core.autocd=true'"]),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -263,7 +263,7 @@ impl ShOptCore {
|
||||
"max_recurse_depth",
|
||||
]),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -445,7 +445,9 @@ impl ShOptPrompt {
|
||||
ShErrKind::SyntaxErr,
|
||||
format!("shopt: Unexpected 'prompt' option '{opt}'"),
|
||||
)
|
||||
.with_note(Note::new("options can be accessed like 'prompt.option_name'"))
|
||||
.with_note(Note::new(
|
||||
"options can be accessed like 'prompt.option_name'",
|
||||
))
|
||||
.with_note(
|
||||
Note::new("'prompt' contains the following options").with_sub_notes(vec![
|
||||
"trunc_prompt_path",
|
||||
@@ -456,7 +458,7 @@ impl ShOptPrompt {
|
||||
"custom",
|
||||
]),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -3,7 +3,12 @@ use std::sync::atomic::{AtomicBool, AtomicI32, AtomicU64, Ordering};
|
||||
use nix::sys::signal::{SaFlags, SigAction, sigaction};
|
||||
|
||||
use crate::{
|
||||
builtin::trap::TrapTarget, jobs::{JobCmdFlags, JobID, take_term}, libsh::error::{ShErr, ShErrKind, ShResult}, parse::execute::exec_input, prelude::*, state::{read_jobs, read_logic, write_jobs, write_meta}
|
||||
builtin::trap::TrapTarget,
|
||||
jobs::{JobCmdFlags, JobID, take_term},
|
||||
libsh::error::{ShErr, ShErrKind, ShResult},
|
||||
parse::execute::exec_input,
|
||||
prelude::*,
|
||||
state::{read_jobs, read_logic, write_jobs, write_meta},
|
||||
};
|
||||
|
||||
static SIGNALS: AtomicU64 = AtomicU64::new(0);
|
||||
@@ -98,7 +103,6 @@ pub fn sig_setup() {
|
||||
|
||||
let action = SigAction::new(SigHandler::Handler(handle_signal), flags, SigSet::empty());
|
||||
|
||||
|
||||
let ignore = SigAction::new(SigHandler::SigIgn, flags, SigSet::empty());
|
||||
|
||||
unsafe {
|
||||
@@ -269,8 +273,8 @@ pub fn child_exited(pid: Pid, status: WtStat) -> ShResult<()> {
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
&& is_finished {
|
||||
}) && is_finished
|
||||
{
|
||||
if is_fg {
|
||||
take_term()?;
|
||||
} else {
|
||||
|
||||
75
src/state.rs
75
src/state.rs
@@ -1,14 +1,25 @@
|
||||
use std::{
|
||||
cell::RefCell, collections::{HashMap, VecDeque}, fmt::Display, ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref}, str::FromStr, time::Duration
|
||||
cell::RefCell,
|
||||
collections::{HashMap, VecDeque},
|
||||
fmt::Display,
|
||||
ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, Deref},
|
||||
str::FromStr,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use nix::unistd::{gethostname, getppid, User};
|
||||
use nix::unistd::{User, gethostname, getppid};
|
||||
|
||||
use crate::{
|
||||
builtin::trap::TrapTarget, exec_input, jobs::JobTab, libsh::{
|
||||
builtin::trap::TrapTarget,
|
||||
exec_input,
|
||||
jobs::JobTab,
|
||||
libsh::{
|
||||
error::{ShErr, ShErrKind, ShResult},
|
||||
utils::VecDequeExt,
|
||||
}, parse::{ConjunctNode, NdRule, Node, ParsedSrc}, prelude::*, shopt::ShOpts
|
||||
},
|
||||
parse::{ConjunctNode, NdRule, Node, ParsedSrc},
|
||||
prelude::*,
|
||||
shopt::ShOpts,
|
||||
};
|
||||
|
||||
pub struct Fern {
|
||||
@@ -49,7 +60,7 @@ pub enum ShellParam {
|
||||
Pos(usize),
|
||||
AllArgs,
|
||||
AllArgsStr,
|
||||
ArgCount
|
||||
ArgCount,
|
||||
}
|
||||
|
||||
impl ShellParam {
|
||||
@@ -116,8 +127,12 @@ impl ScopeStack {
|
||||
pub fn new() -> Self {
|
||||
let mut new = Self::default();
|
||||
new.scopes.push(VarTab::new());
|
||||
let shell_name = std::env::args().next().unwrap_or_else(|| "fern".to_string());
|
||||
new.global_params.insert(ShellParam::ShellName.to_string(), shell_name);
|
||||
let shell_name = std::env::args()
|
||||
.next()
|
||||
.unwrap_or_else(|| "fern".to_string());
|
||||
new
|
||||
.global_params
|
||||
.insert(ShellParam::ShellName.to_string(), shell_name);
|
||||
new
|
||||
}
|
||||
pub fn descend(&mut self, argv: Option<Vec<String>>) {
|
||||
@@ -208,7 +223,9 @@ impl ScopeStack {
|
||||
std::env::var(var_name).unwrap_or_default()
|
||||
}
|
||||
pub fn get_param(&self, param: ShellParam) -> String {
|
||||
if param.is_global() && let Some(val) = self.global_params.get(¶m.to_string()) {
|
||||
if param.is_global()
|
||||
&& let Some(val) = self.global_params.get(¶m.to_string())
|
||||
{
|
||||
return val.clone();
|
||||
}
|
||||
for scope in self.scopes.iter().rev() {
|
||||
@@ -224,16 +241,12 @@ impl ScopeStack {
|
||||
/// Therefore, these are global state and we use the global scope
|
||||
pub fn set_param(&mut self, param: ShellParam, val: &str) {
|
||||
match param {
|
||||
ShellParam::ShPid |
|
||||
ShellParam::Status |
|
||||
ShellParam::LastJob |
|
||||
ShellParam::ShellName => {
|
||||
self.global_params.insert(param.to_string(), val.to_string());
|
||||
ShellParam::ShPid | ShellParam::Status | ShellParam::LastJob | ShellParam::ShellName => {
|
||||
self
|
||||
.global_params
|
||||
.insert(param.to_string(), val.to_string());
|
||||
}
|
||||
ShellParam::Pos(_) |
|
||||
ShellParam::AllArgs |
|
||||
ShellParam::AllArgsStr |
|
||||
ShellParam::ArgCount => {
|
||||
ShellParam::Pos(_) | ShellParam::AllArgs | ShellParam::AllArgsStr | ShellParam::ArgCount => {
|
||||
if let Some(scope) = self.scopes.first_mut() {
|
||||
scope.set_param(param, val);
|
||||
}
|
||||
@@ -446,10 +459,7 @@ pub struct Var {
|
||||
|
||||
impl Var {
|
||||
pub fn new(kind: VarKind, flags: VarFlags) -> Self {
|
||||
Self {
|
||||
flags,
|
||||
kind
|
||||
}
|
||||
Self { flags, kind }
|
||||
}
|
||||
pub fn kind(&self) -> &VarKind {
|
||||
&self.kind
|
||||
@@ -575,7 +585,10 @@ impl VarTab {
|
||||
self.bpush_arg(env::current_exe().unwrap().to_str().unwrap().to_string());
|
||||
}
|
||||
fn update_arg_params(&mut self) {
|
||||
self.set_param(ShellParam::AllArgs, &self.sh_argv.clone().to_vec()[1..].join(" "));
|
||||
self.set_param(
|
||||
ShellParam::AllArgs,
|
||||
&self.sh_argv.clone().to_vec()[1..].join(" "),
|
||||
);
|
||||
self.set_param(ShellParam::ArgCount, &(self.sh_argv.len() - 1).to_string());
|
||||
}
|
||||
/// Push an arg to the front of the arg deque
|
||||
@@ -664,20 +677,16 @@ impl VarTab {
|
||||
}
|
||||
pub fn get_param(&self, param: ShellParam) -> String {
|
||||
match param {
|
||||
ShellParam::Pos(n) => {
|
||||
self
|
||||
ShellParam::Pos(n) => self
|
||||
.sh_argv()
|
||||
.get(n)
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
ShellParam::Status => {
|
||||
self
|
||||
.unwrap_or_default(),
|
||||
ShellParam::Status => self
|
||||
.params
|
||||
.get(&ShellParam::Status)
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or("0".into())
|
||||
}
|
||||
.unwrap_or("0".into()),
|
||||
_ => self
|
||||
.params
|
||||
.get(¶m)
|
||||
@@ -695,7 +704,7 @@ pub struct MetaTab {
|
||||
runtime_stop: Option<Instant>,
|
||||
|
||||
// pending system messages
|
||||
system_msg: Vec<String>
|
||||
system_msg: Vec<String>,
|
||||
}
|
||||
|
||||
impl MetaTab {
|
||||
@@ -788,7 +797,9 @@ pub fn get_shopt(path: &str) -> String {
|
||||
}
|
||||
|
||||
pub fn get_status() -> i32 {
|
||||
read_vars(|v| v.get_param(ShellParam::Status)).parse::<i32>().unwrap()
|
||||
read_vars(|v| v.get_param(ShellParam::Status))
|
||||
.parse::<i32>()
|
||||
.unwrap()
|
||||
}
|
||||
#[track_caller]
|
||||
pub fn set_status(code: i32) {
|
||||
|
||||
@@ -6,7 +6,7 @@ use tempfile::TempDir;
|
||||
|
||||
use crate::prompt::readline::complete::Completer;
|
||||
use crate::prompt::readline::markers;
|
||||
use crate::state::{write_logic, write_vars, VarFlags};
|
||||
use crate::state::{VarFlags, write_logic, write_vars};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -77,8 +77,9 @@ fn complete_command_builtin() {
|
||||
assert!(completer.candidates.iter().any(|c| c == "export"));
|
||||
}
|
||||
|
||||
// NOTE: Disabled - ShFunc constructor requires parsed AST which is complex to set up in tests
|
||||
// TODO: Re-enable once we have a helper to create test functions
|
||||
// NOTE: Disabled - ShFunc constructor requires parsed AST which is complex to
|
||||
// set up in tests TODO: Re-enable once we have a helper to create test
|
||||
// functions
|
||||
/*
|
||||
#[test]
|
||||
fn complete_command_function() {
|
||||
@@ -191,7 +192,12 @@ fn complete_filename_with_slash() {
|
||||
|
||||
// Should complete files in subdir/
|
||||
if result.is_some() {
|
||||
assert!(completer.candidates.iter().any(|c| c.contains("nested.txt")));
|
||||
assert!(
|
||||
completer
|
||||
.candidates
|
||||
.iter()
|
||||
.any(|c| c.contains("nested.txt"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -322,11 +328,17 @@ fn context_detection_command_position() {
|
||||
|
||||
// At the beginning - command context
|
||||
let (ctx, _) = completer.get_completion_context("ech", 3);
|
||||
assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context at start");
|
||||
assert!(
|
||||
ctx.last() == Some(&markers::COMMAND),
|
||||
"Should be in command context at start"
|
||||
);
|
||||
|
||||
// After whitespace - still command if no command yet
|
||||
let (ctx, _) = completer.get_completion_context(" ech", 5);
|
||||
assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context after whitespace");
|
||||
assert!(
|
||||
ctx.last() == Some(&markers::COMMAND),
|
||||
"Should be in command context after whitespace"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -335,10 +347,16 @@ fn context_detection_argument_position() {
|
||||
|
||||
// After a complete command - argument context
|
||||
let (ctx, _) = completer.get_completion_context("echo hello", 10);
|
||||
assert!(ctx.last() != Some(&markers::COMMAND), "Should be in argument context after command");
|
||||
assert!(
|
||||
ctx.last() != Some(&markers::COMMAND),
|
||||
"Should be in argument context after command"
|
||||
);
|
||||
|
||||
let (ctx, _) = completer.get_completion_context("ls -la /tmp", 11);
|
||||
assert!(ctx.last() != Some(&markers::COMMAND), "Should be in argument context");
|
||||
assert!(
|
||||
ctx.last() != Some(&markers::COMMAND),
|
||||
"Should be in argument context"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -347,11 +365,17 @@ fn context_detection_nested_command_sub() {
|
||||
|
||||
// Inside $() - should be command context
|
||||
let (ctx, _) = completer.get_completion_context("echo \"$(ech", 11);
|
||||
assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context inside $()");
|
||||
assert!(
|
||||
ctx.last() == Some(&markers::COMMAND),
|
||||
"Should be in command context inside $()"
|
||||
);
|
||||
|
||||
// After command in $() - argument context
|
||||
let (ctx, _) = completer.get_completion_context("echo \"$(echo hell", 17);
|
||||
assert!(ctx.last() != Some(&markers::COMMAND), "Should be in argument context inside $()");
|
||||
assert!(
|
||||
ctx.last() != Some(&markers::COMMAND),
|
||||
"Should be in argument context inside $()"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -360,7 +384,10 @@ fn context_detection_pipe() {
|
||||
|
||||
// After pipe - command context
|
||||
let (ctx, _) = completer.get_completion_context("ls | gre", 8);
|
||||
assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context after pipe");
|
||||
assert!(
|
||||
ctx.last() == Some(&markers::COMMAND),
|
||||
"Should be in command context after pipe"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -369,11 +396,17 @@ fn context_detection_command_sep() {
|
||||
|
||||
// After semicolon - command context
|
||||
let (ctx, _) = completer.get_completion_context("echo foo; l", 11);
|
||||
assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context after semicolon");
|
||||
assert!(
|
||||
ctx.last() == Some(&markers::COMMAND),
|
||||
"Should be in command context after semicolon"
|
||||
);
|
||||
|
||||
// After && - command context
|
||||
let (ctx, _) = completer.get_completion_context("true && l", 9);
|
||||
assert!(ctx.last() == Some(&markers::COMMAND), "Should be in command context after &&");
|
||||
assert!(
|
||||
ctx.last() == Some(&markers::COMMAND),
|
||||
"Should be in command context after &&"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -382,11 +415,19 @@ fn context_detection_variable_substitution() {
|
||||
|
||||
// $VAR at argument position - VAR_SUB should take priority over ARG
|
||||
let (ctx, _) = completer.get_completion_context("echo $HOM", 9);
|
||||
assert_eq!(ctx.last(), Some(&markers::VAR_SUB), "Should be in var_sub context for $HOM");
|
||||
assert_eq!(
|
||||
ctx.last(),
|
||||
Some(&markers::VAR_SUB),
|
||||
"Should be in var_sub context for $HOM"
|
||||
);
|
||||
|
||||
// $VAR at command position - VAR_SUB should take priority over COMMAND
|
||||
let (ctx, _) = completer.get_completion_context("$HOM", 4);
|
||||
assert_eq!(ctx.last(), Some(&markers::VAR_SUB), "Should be in var_sub context for bare $HOM");
|
||||
assert_eq!(
|
||||
ctx.last(),
|
||||
Some(&markers::VAR_SUB),
|
||||
"Should be in var_sub context for bare $HOM"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -395,7 +436,11 @@ fn context_detection_variable_in_double_quotes() {
|
||||
|
||||
// $VAR inside double quotes
|
||||
let (ctx, _) = completer.get_completion_context("echo \"$HOM", 10);
|
||||
assert_eq!(ctx.last(), Some(&markers::VAR_SUB), "Should be in var_sub context inside double quotes");
|
||||
assert_eq!(
|
||||
ctx.last(),
|
||||
Some(&markers::VAR_SUB),
|
||||
"Should be in var_sub context inside double quotes"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -404,7 +449,11 @@ fn context_detection_stack_base_is_null() {
|
||||
|
||||
// Empty input - only NULL on the stack
|
||||
let (ctx, _) = completer.get_completion_context("", 0);
|
||||
assert_eq!(ctx, vec![markers::NULL], "Empty input should only have NULL marker");
|
||||
assert_eq!(
|
||||
ctx,
|
||||
vec![markers::NULL],
|
||||
"Empty input should only have NULL marker"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -431,11 +480,19 @@ fn context_detection_priority_ordering() {
|
||||
// COMMAND (priority 2) should override ARG (priority 1)
|
||||
// After a pipe, the next token is a command even though it looks like an arg
|
||||
let (ctx, _) = completer.get_completion_context("echo foo | gr", 13);
|
||||
assert_eq!(ctx.last(), Some(&markers::COMMAND), "COMMAND should win over ARG after pipe");
|
||||
assert_eq!(
|
||||
ctx.last(),
|
||||
Some(&markers::COMMAND),
|
||||
"COMMAND should win over ARG after pipe"
|
||||
);
|
||||
|
||||
// VAR_SUB (priority 3) should override COMMAND (priority 2)
|
||||
let (ctx, _) = completer.get_completion_context("$PA", 3);
|
||||
assert_eq!(ctx.last(), Some(&markers::VAR_SUB), "VAR_SUB should win over COMMAND");
|
||||
assert_eq!(
|
||||
ctx.last(),
|
||||
Some(&markers::VAR_SUB),
|
||||
"VAR_SUB should win over COMMAND"
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -647,7 +704,12 @@ fn complete_special_characters_in_filename() {
|
||||
|
||||
if result.is_some() {
|
||||
// Should handle special chars in filenames
|
||||
assert!(completer.candidates.iter().any(|c| c.contains("file-with-dash") || c.contains("file_with_underscore")));
|
||||
assert!(
|
||||
completer
|
||||
.candidates
|
||||
.iter()
|
||||
.any(|c| c.contains("file-with-dash") || c.contains("file_with_underscore"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use crate::expand::{perform_param_expansion, DUB_QUOTE, VAR_SUB};
|
||||
use crate::expand::{DUB_QUOTE, VAR_SUB, perform_param_expansion};
|
||||
use crate::state::VarFlags;
|
||||
|
||||
use super::*;
|
||||
@@ -295,7 +295,10 @@ fn param_expansion_replacesuffix() {
|
||||
fn dquote_escape_dollar() {
|
||||
// "\$foo" should strip backslash, produce literal $foo (no expansion)
|
||||
let result = unescape_str(r#""\$foo""#);
|
||||
assert!(!result.contains(VAR_SUB), "Escaped $ should not become VAR_SUB");
|
||||
assert!(
|
||||
!result.contains(VAR_SUB),
|
||||
"Escaped $ should not become VAR_SUB"
|
||||
);
|
||||
assert!(result.contains('$'), "Literal $ should be preserved");
|
||||
assert!(!result.contains('\\'), "Backslash should be stripped");
|
||||
}
|
||||
@@ -304,47 +307,54 @@ fn dquote_escape_dollar() {
|
||||
fn dquote_escape_backslash() {
|
||||
// "\\" in double quotes should produce a single backslash
|
||||
let result = unescape_str(r#""\\""#);
|
||||
let inner: String = result.chars()
|
||||
.filter(|&c| c != DUB_QUOTE)
|
||||
.collect();
|
||||
assert_eq!(inner, "\\", "Double backslash should produce single backslash");
|
||||
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
|
||||
assert_eq!(
|
||||
inner, "\\",
|
||||
"Double backslash should produce single backslash"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dquote_escape_quote() {
|
||||
// "\"" should produce a literal double quote
|
||||
let result = unescape_str(r#""\"""#);
|
||||
let inner: String = result.chars()
|
||||
.filter(|&c| c != DUB_QUOTE)
|
||||
.collect();
|
||||
assert!(inner.contains('"'), "Escaped quote should produce literal quote");
|
||||
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
|
||||
assert!(
|
||||
inner.contains('"'),
|
||||
"Escaped quote should produce literal quote"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dquote_escape_backtick() {
|
||||
// "\`" should strip backslash, produce literal backtick
|
||||
let result = unescape_str(r#""\`""#);
|
||||
let inner: String = result.chars()
|
||||
.filter(|&c| c != DUB_QUOTE)
|
||||
.collect();
|
||||
assert_eq!(inner, "`", "Escaped backtick should produce literal backtick");
|
||||
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
|
||||
assert_eq!(
|
||||
inner, "`",
|
||||
"Escaped backtick should produce literal backtick"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dquote_escape_nonspecial_preserves_backslash() {
|
||||
// "\a" inside double quotes should preserve the backslash (a is not special)
|
||||
let result = unescape_str(r#""\a""#);
|
||||
let inner: String = result.chars()
|
||||
.filter(|&c| c != DUB_QUOTE)
|
||||
.collect();
|
||||
assert_eq!(inner, "\\a", "Backslash before non-special char should be preserved");
|
||||
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
|
||||
assert_eq!(
|
||||
inner, "\\a",
|
||||
"Backslash before non-special char should be preserved"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dquote_unescaped_dollar_expands() {
|
||||
// "$foo" inside double quotes should produce VAR_SUB (expansion marker)
|
||||
let result = unescape_str(r#""$foo""#);
|
||||
assert!(result.contains(VAR_SUB), "Unescaped $ should become VAR_SUB");
|
||||
assert!(
|
||||
result.contains(VAR_SUB),
|
||||
"Unescaped $ should become VAR_SUB"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -354,9 +364,7 @@ fn dquote_mixed_escapes() {
|
||||
assert!(!result.contains(VAR_SUB), "Escaped $ should not expand");
|
||||
assert!(result.contains('$'), "Literal $ should be in output");
|
||||
// Should have exactly one backslash (from \\)
|
||||
let inner: String = result.chars()
|
||||
.filter(|&c| c != DUB_QUOTE)
|
||||
.collect();
|
||||
let inner: String = result.chars().filter(|&c| c != DUB_QUOTE).collect();
|
||||
let backslash_count = inner.chars().filter(|&c| c == '\\').count();
|
||||
assert_eq!(backslash_count, 1, "\\\\ should produce one backslash");
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::prompt::readline::{
|
||||
annotate_input, annotate_input_recursive, markers,
|
||||
highlight::Highlighter,
|
||||
annotate_input, annotate_input_recursive, highlight::Highlighter, markers,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
@@ -17,7 +16,10 @@ fn find_marker(annotated: &str, marker: char) -> Option<usize> {
|
||||
|
||||
/// Helper to check if markers appear in the correct order
|
||||
fn marker_before(annotated: &str, first: char, second: char) -> bool {
|
||||
if let (Some(pos1), Some(pos2)) = (find_marker(annotated, first), find_marker(annotated, second)) {
|
||||
if let (Some(pos1), Some(pos2)) = (
|
||||
find_marker(annotated, first),
|
||||
find_marker(annotated, second),
|
||||
) {
|
||||
pos1 < pos2
|
||||
} else {
|
||||
false
|
||||
@@ -51,7 +53,8 @@ fn annotate_builtin_command() {
|
||||
// Should mark "export" as 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
|
||||
// separately)
|
||||
assert!(has_marker(&annotated, markers::ASSIGNMENT) || has_marker(&annotated, markers::ARG));
|
||||
}
|
||||
|
||||
@@ -149,7 +152,11 @@ fn annotate_variable_in_string() {
|
||||
assert!(has_marker(&annotated, markers::VAR_SUB));
|
||||
|
||||
// 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]
|
||||
@@ -239,7 +246,10 @@ fn annotate_recursive_nested_command_sub() {
|
||||
|
||||
// Should have multiple CMD_SUB markers (nested)
|
||||
let cmd_sub_count = annotated.chars().filter(|&c| c == markers::CMD_SUB).count();
|
||||
assert!(cmd_sub_count >= 2, "Should have at least 2 CMD_SUB markers for nested substitutions");
|
||||
assert!(
|
||||
cmd_sub_count >= 2,
|
||||
"Should have at least 2 CMD_SUB markers for nested substitutions"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -251,7 +261,10 @@ fn annotate_recursive_command_sub_with_args() {
|
||||
// Just check that we have command-type markers
|
||||
let builtin_count = annotated.chars().filter(|&c| c == markers::BUILTIN).count();
|
||||
let command_count = annotated.chars().filter(|&c| c == markers::COMMAND).count();
|
||||
assert!(builtin_count + command_count >= 2, "Expected at least 2 command markers (BUILTIN or COMMAND)");
|
||||
assert!(
|
||||
builtin_count + command_count >= 2,
|
||||
"Expected at least 2 command markers (BUILTIN or COMMAND)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -297,7 +310,10 @@ fn annotate_recursive_deeply_nested() {
|
||||
let annotated = annotate_input_recursive(input);
|
||||
|
||||
// Should have multiple STRING_DQ and CMD_SUB markers
|
||||
let string_count = annotated.chars().filter(|&c| c == markers::STRING_DQ).count();
|
||||
let string_count = annotated
|
||||
.chars()
|
||||
.filter(|&c| c == markers::STRING_DQ)
|
||||
.count();
|
||||
let cmd_sub_count = annotated.chars().filter(|&c| c == markers::CMD_SUB).count();
|
||||
|
||||
assert!(string_count >= 2, "Should have multiple STRING_DQ markers");
|
||||
@@ -314,7 +330,11 @@ fn marker_priority_var_in_string() {
|
||||
let annotated = annotate_input(input);
|
||||
|
||||
// STRING_DQ should come before VAR_SUB (outer before inner)
|
||||
assert!(marker_before(&annotated, markers::STRING_DQ, markers::VAR_SUB));
|
||||
assert!(marker_before(
|
||||
&annotated,
|
||||
markers::STRING_DQ,
|
||||
markers::VAR_SUB
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -351,7 +371,10 @@ fn highlighter_produces_ansi_codes() {
|
||||
let output = highlighter.take();
|
||||
|
||||
// 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
|
||||
assert!(output.contains("echo"));
|
||||
@@ -401,7 +424,8 @@ fn highlighter_preserves_text_content() {
|
||||
let output = highlighter.take();
|
||||
|
||||
// Remove ANSI codes to check text content
|
||||
let text_only: String = output.chars()
|
||||
let text_only: String = output
|
||||
.chars()
|
||||
.filter(|c| !c.is_control() && *c != '\x1b')
|
||||
.collect();
|
||||
|
||||
@@ -518,7 +542,11 @@ fn annotate_special_variables() {
|
||||
|
||||
// Should mark positional parameters
|
||||
let var_count = annotated.chars().filter(|&c| c == markers::VAR_SUB).count();
|
||||
assert!(var_count >= 5, "Expected at least 5 VAR_SUB markers, found {}", var_count);
|
||||
assert!(
|
||||
var_count >= 5,
|
||||
"Expected at least 5 VAR_SUB markers, found {}",
|
||||
var_count
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -539,7 +567,10 @@ fn annotate_complex_pipeline() {
|
||||
let annotated = annotate_input(input);
|
||||
|
||||
// Should have multiple OPERATOR markers for pipes
|
||||
let operator_count = annotated.chars().filter(|&c| c == markers::OPERATOR).count();
|
||||
let operator_count = annotated
|
||||
.chars()
|
||||
.filter(|&c| c == markers::OPERATOR)
|
||||
.count();
|
||||
assert!(operator_count >= 4);
|
||||
|
||||
// Should have multiple COMMAND markers
|
||||
@@ -577,7 +608,10 @@ fn annotate_multiple_redirects() {
|
||||
let annotated = annotate_input(input);
|
||||
|
||||
// Should have multiple REDIRECT markers
|
||||
let redirect_count = annotated.chars().filter(|&c| c == markers::REDIRECT).count();
|
||||
let redirect_count = annotated
|
||||
.chars()
|
||||
.filter(|&c| c == markers::REDIRECT)
|
||||
.count();
|
||||
assert!(redirect_count >= 2);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,9 @@ use super::*;
|
||||
use crate::expand::{expand_aliases, unescape_str};
|
||||
use crate::libsh::error::{Note, ShErr, ShErrKind};
|
||||
use crate::parse::{
|
||||
NdRule, Node, ParseStream,
|
||||
lex::{LexFlags, LexStream, Tk, TkRule},
|
||||
node_operation, NdRule, Node, ParseStream,
|
||||
node_operation,
|
||||
};
|
||||
use crate::state::{write_logic, write_vars};
|
||||
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
use crate::{
|
||||
libsh::{error::ShErr, term::{Style, Styled}},
|
||||
libsh::{
|
||||
error::ShErr,
|
||||
term::{Style, Styled},
|
||||
},
|
||||
prompt::readline::{
|
||||
FernVi,
|
||||
history::History,
|
||||
keys::{KeyCode, KeyEvent, ModKeys},
|
||||
linebuf::LineBuf,
|
||||
term::{raw_mode, KeyReader, LineWriter},
|
||||
term::{KeyReader, LineWriter, raw_mode},
|
||||
vimode::{ViInsert, ViMode, ViNormal},
|
||||
FernVi,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -173,8 +176,9 @@ impl LineWriter for TestWriter {
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: FernVi structure has changed significantly and readline() method no longer exists
|
||||
// These test helpers are disabled until they can be properly updated
|
||||
// NOTE: FernVi structure has changed significantly and readline() method no
|
||||
// longer exists These test helpers are disabled until they can be properly
|
||||
// updated
|
||||
/*
|
||||
impl FernVi {
|
||||
pub fn new_test(prompt: Option<String>, input: &str, initial: &str) -> Self {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::parse::{
|
||||
NdRule, Node, ParseStream, Redir, RedirType,
|
||||
lex::{LexFlags, LexStream},
|
||||
Node, NdRule, ParseStream, RedirType, Redir,
|
||||
};
|
||||
use crate::procio::{IoFrame, IoMode, IoStack};
|
||||
|
||||
@@ -16,9 +16,7 @@ fn parse_command(input: &str) -> Node {
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut nodes = ParseStream::new(tokens)
|
||||
.flatten()
|
||||
.collect::<Vec<_>>();
|
||||
let mut nodes = ParseStream::new(tokens).flatten().collect::<Vec<_>>();
|
||||
|
||||
assert_eq!(nodes.len(), 1, "Expected exactly one node");
|
||||
let top_node = nodes.remove(0);
|
||||
@@ -27,24 +25,41 @@ fn parse_command(input: &str) -> Node {
|
||||
// Structure is typically: Conjunction -> Pipeline -> Command
|
||||
match top_node.class {
|
||||
NdRule::Conjunction { elements } => {
|
||||
let first_element = elements.into_iter().next().expect("Expected at least one conjunction element");
|
||||
let first_element = elements
|
||||
.into_iter()
|
||||
.next()
|
||||
.expect("Expected at least one conjunction element");
|
||||
match first_element.cmd.class {
|
||||
NdRule::Pipeline { cmds, .. } => {
|
||||
let mut commands = cmds;
|
||||
assert_eq!(commands.len(), 1, "Expected exactly one command in pipeline");
|
||||
assert_eq!(
|
||||
commands.len(),
|
||||
1,
|
||||
"Expected exactly one command in pipeline"
|
||||
);
|
||||
commands.remove(0)
|
||||
}
|
||||
NdRule::Command { .. } => *first_element.cmd,
|
||||
_ => panic!("Expected Command or Pipeline node, got {:?}", first_element.cmd.class),
|
||||
_ => panic!(
|
||||
"Expected Command or Pipeline node, got {:?}",
|
||||
first_element.cmd.class
|
||||
),
|
||||
}
|
||||
}
|
||||
NdRule::Pipeline { cmds, .. } => {
|
||||
let mut commands = cmds;
|
||||
assert_eq!(commands.len(), 1, "Expected exactly one command in pipeline");
|
||||
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),
|
||||
_ => panic!(
|
||||
"Expected Conjunction, Pipeline, or Command node, got {:?}",
|
||||
top_node.class
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,7 +114,13 @@ fn parse_stderr_to_stdout() {
|
||||
assert_eq!(node.redirs.len(), 1);
|
||||
let redir = &node.redirs[0];
|
||||
|
||||
assert!(matches!(redir.io_mode, IoMode::Fd { tgt_fd: 2, src_fd: 1 }));
|
||||
assert!(matches!(
|
||||
redir.io_mode,
|
||||
IoMode::Fd {
|
||||
tgt_fd: 2,
|
||||
src_fd: 1
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -109,7 +130,13 @@ fn parse_stdout_to_stderr() {
|
||||
assert_eq!(node.redirs.len(), 1);
|
||||
let redir = &node.redirs[0];
|
||||
|
||||
assert!(matches!(redir.io_mode, IoMode::Fd { tgt_fd: 1, src_fd: 2 }));
|
||||
assert!(matches!(
|
||||
redir.io_mode,
|
||||
IoMode::Fd {
|
||||
tgt_fd: 1,
|
||||
src_fd: 2
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -120,15 +147,24 @@ fn parse_multiple_redirects() {
|
||||
|
||||
// Input redirect
|
||||
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
|
||||
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
|
||||
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]
|
||||
@@ -149,7 +185,13 @@ fn parse_custom_fd_dup() {
|
||||
assert_eq!(node.redirs.len(), 1);
|
||||
let redir = &node.redirs[0];
|
||||
|
||||
assert!(matches!(redir.io_mode, IoMode::Fd { tgt_fd: 3, src_fd: 4 }));
|
||||
assert!(matches!(
|
||||
redir.io_mode,
|
||||
IoMode::Fd {
|
||||
tgt_fd: 3,
|
||||
src_fd: 4
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -187,11 +229,20 @@ fn parse_redirect_order_preserved() {
|
||||
assert_eq!(node.redirs.len(), 2);
|
||||
|
||||
// First redirect: 2>&1
|
||||
assert!(matches!(node.redirs[0].io_mode, IoMode::Fd { tgt_fd: 2, src_fd: 1 }));
|
||||
assert!(matches!(
|
||||
node.redirs[0].io_mode,
|
||||
IoMode::Fd {
|
||||
tgt_fd: 2,
|
||||
src_fd: 1
|
||||
}
|
||||
));
|
||||
|
||||
// Second redirect: > file.txt
|
||||
assert!(matches!(node.redirs[1].class, RedirType::Output));
|
||||
assert!(matches!(node.redirs[1].io_mode, IoMode::File { tgt_fd: 1, .. }));
|
||||
assert!(matches!(
|
||||
node.redirs[1].io_mode,
|
||||
IoMode::File { tgt_fd: 1, .. }
|
||||
));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
@@ -241,10 +292,7 @@ fn iostack_never_empties() {
|
||||
fn iostack_push_to_frame() {
|
||||
let mut stack = IoStack::new();
|
||||
|
||||
let redir = crate::parse::Redir::new(
|
||||
IoMode::fd(1, 2),
|
||||
RedirType::Output,
|
||||
);
|
||||
let redir = crate::parse::Redir::new(IoMode::fd(1, 2), RedirType::Output);
|
||||
|
||||
stack.push_to_frame(redir);
|
||||
assert_eq!(stack.curr_frame().len(), 1);
|
||||
@@ -299,7 +347,10 @@ fn iostack_flatten() {
|
||||
|
||||
// Push new frame with redir
|
||||
let mut frame2 = IoFrame::new();
|
||||
frame2.push(crate::parse::Redir::new(IoMode::fd(2, 1), RedirType::Output));
|
||||
frame2.push(crate::parse::Redir::new(
|
||||
IoMode::fd(2, 1),
|
||||
RedirType::Output,
|
||||
));
|
||||
stack.push_frame(frame2);
|
||||
|
||||
// Push third frame with redir
|
||||
|
||||
@@ -9,7 +9,9 @@ fn scopestack_new() {
|
||||
let stack = ScopeStack::new();
|
||||
|
||||
// Should start with one global scope
|
||||
assert!(stack.var_exists("PATH") || !stack.var_exists("PATH")); // Just check it doesn't panic
|
||||
assert!(stack.var_exists("PATH") || !stack.var_exists("PATH")); // Just check
|
||||
// it doesn't
|
||||
// panic
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -59,7 +61,11 @@ fn scopestack_variable_shadowing() {
|
||||
stack.ascend();
|
||||
|
||||
// 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]
|
||||
@@ -147,9 +153,13 @@ fn scopestack_descend_with_args() {
|
||||
|
||||
// In local scope, positional params come from the VarTab created during descend
|
||||
// VarTab::new() initializes with process args, then our args are appended
|
||||
// So we check that SOME positional parameter exists (implementation detail may vary)
|
||||
// So we check that SOME positional parameter exists (implementation detail may
|
||||
// vary)
|
||||
let local_param = stack.get_param(ShellParam::Pos(1));
|
||||
assert!(!local_param.is_empty(), "Should have positional parameters in local scope");
|
||||
assert!(
|
||||
!local_param.is_empty(),
|
||||
"Should have positional parameters in local scope"
|
||||
);
|
||||
|
||||
// Ascend back
|
||||
stack.ascend();
|
||||
@@ -239,13 +249,19 @@ fn scopestack_var_exists() {
|
||||
assert!(stack.var_exists("EXISTS"));
|
||||
|
||||
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);
|
||||
assert!(stack.var_exists("LOCAL"));
|
||||
|
||||
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]
|
||||
@@ -333,11 +349,14 @@ fn logtab_multiple_aliases() {
|
||||
assert_eq!(logtab.aliases().len(), 3);
|
||||
assert_eq!(logtab.get_alias("ll"), Some("ls -la".to_string()));
|
||||
assert_eq!(logtab.get_alias("la"), Some("ls -A".to_string()));
|
||||
assert_eq!(logtab.get_alias("grep"), Some("grep --color=auto".to_string()));
|
||||
assert_eq!(
|
||||
logtab.get_alias("grep"),
|
||||
Some("grep --color=auto".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
// Note: Function tests are limited because ShFunc requires complex setup (parsed AST)
|
||||
// We'll test the basic storage/retrieval mechanics
|
||||
// Note: Function tests are limited because ShFunc requires complex setup
|
||||
// (parsed AST) We'll test the basic storage/retrieval mechanics
|
||||
|
||||
#[test]
|
||||
fn logtab_funcs_empty_initially() {
|
||||
@@ -427,7 +446,10 @@ fn vartab_positional_params() {
|
||||
// Now sh_argv should be: [exe_path, test_arg1, test_arg2]
|
||||
// Pos(0) = exe_path, Pos(1) = test_arg1, Pos(2) = test_arg2
|
||||
let final_len = vartab.sh_argv().len();
|
||||
assert!(final_len > initial_len || final_len >= 1, "Should have arguments");
|
||||
assert!(
|
||||
final_len > initial_len || final_len >= 1,
|
||||
"Should have arguments"
|
||||
);
|
||||
|
||||
// Just verify we can retrieve the last args we pushed
|
||||
let last_idx = final_len - 1;
|
||||
@@ -517,13 +539,34 @@ fn shellparam_is_global() {
|
||||
|
||||
#[test]
|
||||
fn shellparam_from_str() {
|
||||
assert!(matches!("?".parse::<ShellParam>().unwrap(), ShellParam::Status));
|
||||
assert!(matches!("$".parse::<ShellParam>().unwrap(), ShellParam::ShPid));
|
||||
assert!(matches!("!".parse::<ShellParam>().unwrap(), ShellParam::LastJob));
|
||||
assert!(matches!("0".parse::<ShellParam>().unwrap(), ShellParam::ShellName));
|
||||
assert!(matches!("@".parse::<ShellParam>().unwrap(), ShellParam::AllArgs));
|
||||
assert!(matches!("*".parse::<ShellParam>().unwrap(), ShellParam::AllArgsStr));
|
||||
assert!(matches!("#".parse::<ShellParam>().unwrap(), ShellParam::ArgCount));
|
||||
assert!(matches!(
|
||||
"?".parse::<ShellParam>().unwrap(),
|
||||
ShellParam::Status
|
||||
));
|
||||
assert!(matches!(
|
||||
"$".parse::<ShellParam>().unwrap(),
|
||||
ShellParam::ShPid
|
||||
));
|
||||
assert!(matches!(
|
||||
"!".parse::<ShellParam>().unwrap(),
|
||||
ShellParam::LastJob
|
||||
));
|
||||
assert!(matches!(
|
||||
"0".parse::<ShellParam>().unwrap(),
|
||||
ShellParam::ShellName
|
||||
));
|
||||
assert!(matches!(
|
||||
"@".parse::<ShellParam>().unwrap(),
|
||||
ShellParam::AllArgs
|
||||
));
|
||||
assert!(matches!(
|
||||
"*".parse::<ShellParam>().unwrap(),
|
||||
ShellParam::AllArgsStr
|
||||
));
|
||||
assert!(matches!(
|
||||
"#".parse::<ShellParam>().unwrap(),
|
||||
ShellParam::ArgCount
|
||||
));
|
||||
|
||||
match "1".parse::<ShellParam>().unwrap() {
|
||||
ShellParam::Pos(n) => assert_eq!(n, 1),
|
||||
|
||||
Reference in New Issue
Block a user