rustfmt'd the codebase

This commit is contained in:
2026-03-04 19:52:29 -05:00
parent 79cb34246b
commit 0e3f2afe99
51 changed files with 4926 additions and 4131 deletions

View File

@@ -18,7 +18,9 @@ pub fn alias(node: Node) -> ShResult<()> {
}; };
let mut argv = prepare_argv(argv)?; let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); } if !argv.is_empty() {
argv.remove(0);
}
if argv.is_empty() { if argv.is_empty() {
// Display the environment variables // Display the environment variables
@@ -37,11 +39,19 @@ pub fn alias(node: Node) -> ShResult<()> {
} else { } else {
for (arg, span) in argv { for (arg, span) in argv {
if arg == "command" || arg == "builtin" { if arg == "command" || arg == "builtin" {
return Err(ShErr::at(ShErrKind::ExecFail, span, format!("alias: Cannot assign alias to reserved name '{arg}'"))); return Err(ShErr::at(
ShErrKind::ExecFail,
span,
format!("alias: Cannot assign alias to reserved name '{arg}'"),
));
} }
let Some((name, body)) = arg.split_once('=') else { let Some((name, body)) = arg.split_once('=') else {
return Err(ShErr::at(ShErrKind::SyntaxErr, span, "alias: Expected an assignment in alias args")); return Err(ShErr::at(
ShErrKind::SyntaxErr,
span,
"alias: Expected an assignment in alias args",
));
}; };
write_logic(|l| l.insert_alias(name, body, span.clone())); write_logic(|l| l.insert_alias(name, body, span.clone()));
} }
@@ -60,7 +70,9 @@ pub fn unalias(node: Node) -> ShResult<()> {
}; };
let mut argv = prepare_argv(argv)?; let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); } if !argv.is_empty() {
argv.remove(0);
}
if argv.is_empty() { if argv.is_empty() {
// Display the environment variables // Display the environment variables
@@ -79,7 +91,11 @@ pub fn unalias(node: Node) -> ShResult<()> {
} else { } else {
for (arg, span) in argv { for (arg, span) in argv {
if read_logic(|l| l.get_alias(&arg)).is_none() { if read_logic(|l| l.get_alias(&arg)).is_none() {
return Err(ShErr::at(ShErrKind::SyntaxErr, span, format!("unalias: alias '{}' not found",arg.fg(next_color())))); return Err(ShErr::at(
ShErrKind::SyntaxErr,
span,
format!("unalias: alias '{}' not found", arg.fg(next_color())),
));
}; };
write_logic(|l| l.remove_alias(&arg)) write_logic(|l| l.remove_alias(&arg))
} }

View File

@@ -1,44 +1,52 @@
use std::collections::VecDeque; use std::collections::VecDeque;
use crate::{ use crate::{
getopt::{Opt, OptSpec, get_opts_from_tokens}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node, execute::prepare_argv}, prelude::*, procio::borrow_fd, state::{self, VarFlags, VarKind, write_vars} getopt::{Opt, OptSpec, get_opts_from_tokens},
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
parse::{NdRule, Node, execute::prepare_argv},
prelude::*,
procio::borrow_fd,
state::{self, VarFlags, VarKind, write_vars},
}; };
fn arr_op_optspec() -> Vec<OptSpec> { fn arr_op_optspec() -> Vec<OptSpec> {
vec![ vec![
OptSpec { OptSpec {
opt: Opt::Short('c'), opt: Opt::Short('c'),
takes_arg: true takes_arg: true,
}, },
OptSpec { OptSpec {
opt: Opt::Short('r'), opt: Opt::Short('r'),
takes_arg: false takes_arg: false,
}, },
OptSpec { OptSpec {
opt: Opt::Short('v'), opt: Opt::Short('v'),
takes_arg: true takes_arg: true,
} },
] ]
} }
pub struct ArrOpOpts { pub struct ArrOpOpts {
count: usize, count: usize,
reverse: bool, reverse: bool,
var: Option<String>, var: Option<String>,
} }
impl Default for ArrOpOpts { impl Default for ArrOpOpts {
fn default() -> Self { fn default() -> Self {
Self { Self {
count: 1, count: 1,
reverse: false, reverse: false,
var: None, var: None,
} }
} }
} }
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
enum End { Front, Back } enum End {
Front,
Back,
}
fn arr_pop_inner(node: Node, end: End) -> ShResult<()> { fn arr_pop_inner(node: Node, end: End) -> ShResult<()> {
let NdRule::Command { let NdRule::Command {
@@ -49,40 +57,42 @@ fn arr_pop_inner(node: Node, end: End) -> ShResult<()> {
unreachable!() unreachable!()
}; };
let (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?; let (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?;
let arr_op_opts = get_arr_op_opts(opts)?; let arr_op_opts = get_arr_op_opts(opts)?;
let mut argv = prepare_argv(argv)?; let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); } if !argv.is_empty() {
argv.remove(0);
}
let stdout = borrow_fd(STDOUT_FILENO); let stdout = borrow_fd(STDOUT_FILENO);
let mut status = 0; let mut status = 0;
for (arg,_) in argv { for (arg, _) in argv {
for _ in 0..arr_op_opts.count { for _ in 0..arr_op_opts.count {
let pop = |arr: &mut std::collections::VecDeque<String>| match end { let pop = |arr: &mut std::collections::VecDeque<String>| match end {
End::Front => arr.pop_front(), End::Front => arr.pop_front(),
End::Back => arr.pop_back(), End::Back => arr.pop_back(),
}; };
let Some(popped) = write_vars(|v| v.get_arr_mut(&arg).ok().and_then(pop)) else { let Some(popped) = write_vars(|v| v.get_arr_mut(&arg).ok().and_then(pop)) else {
status = 1; status = 1;
break; break;
}; };
status = 0; status = 0;
if let Some(ref var) = arr_op_opts.var { if let Some(ref var) = arr_op_opts.var {
write_vars(|v| v.set_var(var, VarKind::Str(popped), VarFlags::NONE))?; write_vars(|v| v.set_var(var, VarKind::Str(popped), VarFlags::NONE))?;
} else { } else {
write(stdout, popped.as_bytes())?; write(stdout, popped.as_bytes())?;
write(stdout, b"\n")?; write(stdout, b"\n")?;
} }
} }
} }
state::set_status(status); state::set_status(status);
Ok(()) Ok(())
} }
fn arr_push_inner(node: Node, end: End) -> ShResult<()> { fn arr_push_inner(node: Node, end: End) -> ShResult<()> {
let blame = node.get_span().clone(); let blame = node.get_span().clone();
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
argv, argv,
@@ -91,49 +101,60 @@ fn arr_push_inner(node: Node, end: End) -> ShResult<()> {
unreachable!() unreachable!()
}; };
let (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?; let (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?;
let _arr_op_opts = get_arr_op_opts(opts)?; let _arr_op_opts = get_arr_op_opts(opts)?;
let mut argv = prepare_argv(argv)?; let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); } if !argv.is_empty() {
argv.remove(0);
}
let mut argv = argv.into_iter(); let mut argv = argv.into_iter();
let Some((name, _)) = argv.next() else { let Some((name, _)) = argv.next() else {
return Err(ShErr::at(ShErrKind::ExecFail, blame, "push: missing array name".to_string())); return Err(ShErr::at(
}; ShErrKind::ExecFail,
blame,
"push: missing array name".to_string(),
));
};
for (val, span) in argv { for (val, span) in argv {
let push_val = val.clone(); let push_val = val.clone();
write_vars(|v| { write_vars(|v| {
if let Ok(arr) = v.get_arr_mut(&name) { if let Ok(arr) = v.get_arr_mut(&name) {
match end { match end {
End::Front => arr.push_front(push_val), End::Front => arr.push_front(push_val),
End::Back => arr.push_back(push_val), End::Back => arr.push_back(push_val),
} }
Ok(()) Ok(())
} else { } else {
v.set_var(&name, VarKind::Arr(VecDeque::from([push_val])), VarFlags::NONE) v.set_var(
} &name,
}).blame(span)?; VarKind::Arr(VecDeque::from([push_val])),
} VarFlags::NONE,
)
}
})
.blame(span)?;
}
state::set_status(0); state::set_status(0);
Ok(()) Ok(())
} }
pub fn arr_pop(node: Node) -> ShResult<()> { pub fn arr_pop(node: Node) -> ShResult<()> {
arr_pop_inner(node, End::Back) arr_pop_inner(node, End::Back)
} }
pub fn arr_fpop(node: Node) -> ShResult<()> { pub fn arr_fpop(node: Node) -> ShResult<()> {
arr_pop_inner(node, End::Front) arr_pop_inner(node, End::Front)
} }
pub fn arr_push(node: Node) -> ShResult<()> { pub fn arr_push(node: Node) -> ShResult<()> {
arr_push_inner(node, End::Back) arr_push_inner(node, End::Back)
} }
pub fn arr_fpush(node: Node) -> ShResult<()> { pub fn arr_fpush(node: Node) -> ShResult<()> {
arr_push_inner(node, End::Front) arr_push_inner(node, End::Front)
} }
pub fn arr_rotate(node: Node) -> ShResult<()> { pub fn arr_rotate(node: Node) -> ShResult<()> {
@@ -145,52 +166,63 @@ pub fn arr_rotate(node: Node) -> ShResult<()> {
unreachable!() unreachable!()
}; };
let (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?; let (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?;
let arr_op_opts = get_arr_op_opts(opts)?; let arr_op_opts = get_arr_op_opts(opts)?;
let mut argv = prepare_argv(argv)?; let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); } if !argv.is_empty() {
argv.remove(0);
}
for (arg, _) in argv { for (arg, _) in argv {
write_vars(|v| -> ShResult<()> { write_vars(|v| -> ShResult<()> {
let arr = v.get_arr_mut(&arg)?; let arr = v.get_arr_mut(&arg)?;
if arr_op_opts.reverse { if arr_op_opts.reverse {
arr.rotate_right(arr_op_opts.count.min(arr.len())); arr.rotate_right(arr_op_opts.count.min(arr.len()));
} else { } else {
arr.rotate_left(arr_op_opts.count.min(arr.len())); arr.rotate_left(arr_op_opts.count.min(arr.len()));
} }
Ok(()) Ok(())
})?; })?;
} }
state::set_status(0); state::set_status(0);
Ok(()) Ok(())
} }
pub fn get_arr_op_opts(opts: Vec<Opt>) -> ShResult<ArrOpOpts> { pub fn get_arr_op_opts(opts: Vec<Opt>) -> ShResult<ArrOpOpts> {
let mut arr_op_opts = ArrOpOpts::default(); let mut arr_op_opts = ArrOpOpts::default();
for opt in opts { for opt in opts {
match opt { match opt {
Opt::ShortWithArg('c', count) => { Opt::ShortWithArg('c', count) => {
arr_op_opts.count = count.parse::<usize>().map_err(|_| { arr_op_opts.count = count
ShErr::simple(ShErrKind::ParseErr, format!("invalid count: {}", count)) .parse::<usize>()
})?; .map_err(|_| ShErr::simple(ShErrKind::ParseErr, format!("invalid count: {}", count)))?;
} }
Opt::Short('c') => { Opt::Short('c') => {
return Err(ShErr::simple(ShErrKind::ParseErr, "missing count for -c".to_string())); return Err(ShErr::simple(
} ShErrKind::ParseErr,
Opt::Short('r') => { "missing count for -c".to_string(),
arr_op_opts.reverse = true; ));
} }
Opt::ShortWithArg('v', var) => { Opt::Short('r') => {
arr_op_opts.var = Some(var); arr_op_opts.reverse = true;
} }
Opt::Short('v') => { Opt::ShortWithArg('v', var) => {
return Err(ShErr::simple(ShErrKind::ParseErr, "missing variable name for -v".to_string())); arr_op_opts.var = Some(var);
} }
_ => { Opt::Short('v') => {
return Err(ShErr::simple(ShErrKind::ParseErr, format!("invalid option: {}", opt))); return Err(ShErr::simple(
} ShErrKind::ParseErr,
} "missing variable name for -v".to_string(),
} ));
Ok(arr_op_opts) }
_ => {
return Err(ShErr::simple(
ShErrKind::ParseErr,
format!("invalid option: {}", opt),
));
}
}
}
Ok(arr_op_opts)
} }

View File

@@ -1,48 +1,56 @@
use regex::Regex; use regex::Regex;
use crate::{ use crate::{
getopt::{Opt, OptSpec, get_opts_from_tokens}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node, execute::prepare_argv}, state::{self, AutoCmd, AutoCmdKind, write_logic} getopt::{Opt, OptSpec, get_opts_from_tokens},
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
parse::{NdRule, Node, execute::prepare_argv},
state::{self, AutoCmd, AutoCmdKind, write_logic},
}; };
pub struct AutoCmdOpts { pub struct AutoCmdOpts {
pattern: Option<Regex>, pattern: Option<Regex>,
clear: bool clear: bool,
} }
fn autocmd_optspec() -> [OptSpec;2] { fn autocmd_optspec() -> [OptSpec; 2] {
[ [
OptSpec { OptSpec {
opt: Opt::Short('p'), opt: Opt::Short('p'),
takes_arg: true takes_arg: true,
}, },
OptSpec { OptSpec {
opt: Opt::Short('c'), opt: Opt::Short('c'),
takes_arg: false takes_arg: false,
} },
] ]
} }
pub fn get_autocmd_opts(opts: &[Opt]) -> ShResult<AutoCmdOpts> { pub fn get_autocmd_opts(opts: &[Opt]) -> ShResult<AutoCmdOpts> {
let mut autocmd_opts = AutoCmdOpts { let mut autocmd_opts = AutoCmdOpts {
pattern: None, pattern: None,
clear: false clear: false,
}; };
let mut opts = opts.iter(); let mut opts = opts.iter();
while let Some(arg) = opts.next() { while let Some(arg) = opts.next() {
match arg { match arg {
Opt::ShortWithArg('p', arg) => { Opt::ShortWithArg('p', arg) => {
autocmd_opts.pattern = Some(Regex::new(arg).map_err(|e| ShErr::simple(ShErrKind::ExecFail, format!("invalid regex for -p: {}", e)))?); autocmd_opts.pattern = Some(Regex::new(arg).map_err(|e| {
} ShErr::simple(ShErrKind::ExecFail, format!("invalid regex for -p: {}", e))
Opt::Short('c') => { })?);
autocmd_opts.clear = true; }
} Opt::Short('c') => {
_ => { autocmd_opts.clear = true;
return Err(ShErr::simple(ShErrKind::ExecFail, format!("unexpected option: {}", arg))); }
} _ => {
} return Err(ShErr::simple(
} ShErrKind::ExecFail,
format!("unexpected option: {}", arg),
));
}
}
}
Ok(autocmd_opts) Ok(autocmd_opts)
} }
pub fn autocmd(node: Node) -> ShResult<()> { pub fn autocmd(node: Node) -> ShResult<()> {
@@ -55,36 +63,50 @@ pub fn autocmd(node: Node) -> ShResult<()> {
unreachable!() unreachable!()
}; };
let (argv,opts) = get_opts_from_tokens(argv, &autocmd_optspec()).promote_err(span.clone())?; let (argv, opts) = get_opts_from_tokens(argv, &autocmd_optspec()).promote_err(span.clone())?;
let autocmd_opts = get_autocmd_opts(&opts).promote_err(span.clone())?; let autocmd_opts = get_autocmd_opts(&opts).promote_err(span.clone())?;
let mut argv = prepare_argv(argv)?; let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); } if !argv.is_empty() {
let mut args = argv.iter(); argv.remove(0);
}
let mut args = argv.iter();
let Some(autocmd_kind) = args.next() else { let Some(autocmd_kind) = args.next() else {
return Err(ShErr::at(ShErrKind::ExecFail, span, "expected an autocmd kind".to_string())); return Err(ShErr::at(
}; ShErrKind::ExecFail,
span,
"expected an autocmd kind".to_string(),
));
};
let Ok(autocmd_kind) = autocmd_kind.0.parse::<AutoCmdKind>() else { let Ok(autocmd_kind) = autocmd_kind.0.parse::<AutoCmdKind>() else {
return Err(ShErr::at(ShErrKind::ExecFail, autocmd_kind.1.clone(), format!("invalid autocmd kind: {}", autocmd_kind.0))); return Err(ShErr::at(
}; ShErrKind::ExecFail,
autocmd_kind.1.clone(),
format!("invalid autocmd kind: {}", autocmd_kind.0),
));
};
if autocmd_opts.clear { if autocmd_opts.clear {
write_logic(|l| l.clear_autocmds(autocmd_kind)); write_logic(|l| l.clear_autocmds(autocmd_kind));
state::set_status(0); state::set_status(0);
return Ok(()); return Ok(());
} }
let Some(autocmd_cmd) = args.next() else { let Some(autocmd_cmd) = args.next() else {
return Err(ShErr::at(ShErrKind::ExecFail, span, "expected an autocmd command".to_string())); return Err(ShErr::at(
}; ShErrKind::ExecFail,
span,
"expected an autocmd command".to_string(),
));
};
let autocmd = AutoCmd { let autocmd = AutoCmd {
pattern: autocmd_opts.pattern, pattern: autocmd_opts.pattern,
command: autocmd_cmd.0.clone(), command: autocmd_cmd.0.clone(),
}; };
write_logic(|l| l.insert_autocmd(autocmd_kind, autocmd)); write_logic(|l| l.insert_autocmd(autocmd_kind, autocmd));
state::set_status(0); state::set_status(0);
Ok(()) Ok(())

View File

@@ -17,40 +17,58 @@ pub fn cd(node: Node) -> ShResult<()> {
else { else {
unreachable!() unreachable!()
}; };
let cd_span = argv.first().unwrap().span.clone(); let cd_span = argv.first().unwrap().span.clone();
let mut argv = prepare_argv(argv)?; let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); } if !argv.is_empty() {
argv.remove(0);
}
let (new_dir,arg_span) = if let Some((arg, span)) = argv.into_iter().next() { let (new_dir, arg_span) = if let Some((arg, span)) = argv.into_iter().next() {
(PathBuf::from(arg),Some(span)) (PathBuf::from(arg), Some(span))
} else { } else {
(PathBuf::from(env::var("HOME").unwrap()),None) (PathBuf::from(env::var("HOME").unwrap()), None)
}; };
if !new_dir.exists() { if !new_dir.exists() {
let mut err = ShErr::new( let mut err = ShErr::new(ShErrKind::ExecFail, span.clone())
ShErrKind::ExecFail, .labeled(cd_span.clone(), "Failed to change directory");
span.clone(), if let Some(span) = arg_span {
).labeled(cd_span.clone(), "Failed to change directory"); err = err.labeled(
if let Some(span) = arg_span { span,
err = err.labeled(span, format!("No such file or directory '{}'", new_dir.display().fg(next_color()))); format!(
} "No such file or directory '{}'",
return Err(err); new_dir.display().fg(next_color())
),
);
}
return Err(err);
} }
if !new_dir.is_dir() { if !new_dir.is_dir() {
return Err(ShErr::new(ShErrKind::ExecFail, span.clone()) return Err(ShErr::new(ShErrKind::ExecFail, span.clone()).labeled(
.labeled(cd_span.clone(), format!("cd: Not a directory '{}'", new_dir.display().fg(next_color())))); cd_span.clone(),
format!(
"cd: Not a directory '{}'",
new_dir.display().fg(next_color())
),
));
} }
if let Err(e) = state::change_dir(new_dir) { if let Err(e) = state::change_dir(new_dir) {
return Err(ShErr::new(ShErrKind::ExecFail, span.clone()) return Err(ShErr::new(ShErrKind::ExecFail, span.clone()).labeled(
.labeled(cd_span.clone(), format!("cd: Failed to change directory: '{}'", e.fg(Color::Red)))); cd_span.clone(),
format!("cd: Failed to change directory: '{}'", e.fg(Color::Red)),
));
} }
let new_dir = env::current_dir().map_err(|e| { let new_dir = env::current_dir().map_err(|e| {
ShErr::new(ShErrKind::ExecFail, span.clone()) ShErr::new(ShErrKind::ExecFail, span.clone()).labeled(
.labeled(cd_span.clone(), format!("cd: Failed to get current directory: '{}'", e.fg(Color::Red))) cd_span.clone(),
format!(
"cd: Failed to get current directory: '{}'",
e.fg(Color::Red)
),
)
})?; })?;
unsafe { env::set_var("PWD", new_dir) }; unsafe { env::set_var("PWD", new_dir) };

View File

@@ -167,7 +167,9 @@ pub fn complete_builtin(node: Node) -> ShResult<()> {
let (argv, opts) = get_opts_from_tokens(argv, &COMP_OPTS)?; let (argv, opts) = get_opts_from_tokens(argv, &COMP_OPTS)?;
let comp_opts = get_comp_opts(opts)?; let comp_opts = get_comp_opts(opts)?;
let mut argv = prepare_argv(argv)?; let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); } if !argv.is_empty() {
argv.remove(0);
}
if comp_opts.flags.contains(CompFlags::PRINT) { if comp_opts.flags.contains(CompFlags::PRINT) {
if argv.is_empty() { if argv.is_empty() {
@@ -204,7 +206,11 @@ pub fn complete_builtin(node: Node) -> ShResult<()> {
if argv.is_empty() { if argv.is_empty() {
state::set_status(1); state::set_status(1);
return Err(ShErr::at(ShErrKind::ExecFail, blame, "complete: no command specified")); return Err(ShErr::at(
ShErrKind::ExecFail,
blame,
"complete: no command specified",
));
} }
let comp_spec = BashCompSpec::from_comp_opts(comp_opts).with_source(src); let comp_spec = BashCompSpec::from_comp_opts(comp_opts).with_source(src);
@@ -279,12 +285,16 @@ pub fn get_comp_opts(opts: Vec<Opt>) -> ShResult<CompOpts> {
"space" => comp_opts.opt_flags |= CompOptFlags::SPACE, "space" => comp_opts.opt_flags |= CompOptFlags::SPACE,
_ => { _ => {
let span: crate::parse::lex::Span = Default::default(); let span: crate::parse::lex::Span = Default::default();
return Err(ShErr::at(ShErrKind::InvalidOpt, span, format!("complete: invalid option: {}", opt_flag))); return Err(ShErr::at(
ShErrKind::InvalidOpt,
span,
format!("complete: invalid option: {}", opt_flag),
));
} }
}, },
Opt::Short('a') => comp_opts.flags |= CompFlags::ALIAS, Opt::Short('a') => comp_opts.flags |= CompFlags::ALIAS,
Opt::Short('S') => comp_opts.flags |= CompFlags::SIGNALS, Opt::Short('S') => comp_opts.flags |= CompFlags::SIGNALS,
Opt::Short('r') => comp_opts.flags |= CompFlags::REMOVE, Opt::Short('r') => comp_opts.flags |= CompFlags::REMOVE,
Opt::Short('j') => comp_opts.flags |= CompFlags::JOBS, Opt::Short('j') => comp_opts.flags |= CompFlags::JOBS,
Opt::Short('p') => comp_opts.flags |= CompFlags::PRINT, Opt::Short('p') => comp_opts.flags |= CompFlags::PRINT,

View File

@@ -47,18 +47,26 @@ fn print_dirs() -> ShResult<()> {
fn change_directory(target: &PathBuf, blame: Span) -> ShResult<()> { fn change_directory(target: &PathBuf, blame: Span) -> ShResult<()> {
if !target.is_dir() { if !target.is_dir() {
return Err( return Err(ShErr::at(
ShErr::at(ShErrKind::ExecFail, blame, format!("not a directory: '{}'", target.display().fg(next_color()))) ShErrKind::ExecFail,
); blame,
format!("not a directory: '{}'", target.display().fg(next_color())),
));
} }
if let Err(e) = state::change_dir(target) { if let Err(e) = state::change_dir(target) {
return Err( return Err(ShErr::at(
ShErr::at(ShErrKind::ExecFail, blame, format!("Failed to change directory: '{}'", e.fg(Color::Red))) ShErrKind::ExecFail,
); blame,
format!("Failed to change directory: '{}'", e.fg(Color::Red)),
));
} }
let new_dir = env::current_dir().map_err(|e| { let new_dir = env::current_dir().map_err(|e| {
ShErr::at(ShErrKind::ExecFail, blame, format!("Failed to get current directory: '{}'", e.fg(Color::Red))) ShErr::at(
ShErrKind::ExecFail,
blame,
format!("Failed to get current directory: '{}'", e.fg(Color::Red)),
)
})?; })?;
unsafe { env::set_var("PWD", new_dir) }; unsafe { env::set_var("PWD", new_dir) };
Ok(()) Ok(())
@@ -74,24 +82,32 @@ fn parse_stack_idx(arg: &str, blame: Span, cmd: &str) -> ShResult<StackIdx> {
}; };
if digits.is_empty() { if digits.is_empty() {
return Err( return Err(ShErr::at(
ShErr::at(ShErrKind::ExecFail, blame, format!( ShErrKind::ExecFail,
blame,
format!(
"{cmd}: missing index after '{}'", "{cmd}: missing index after '{}'",
if from_top { "+" } else { "-" } if from_top { "+" } else { "-" }
)) ),
); ));
} }
for ch in digits.chars() { for ch in digits.chars() {
if !ch.is_ascii_digit() { if !ch.is_ascii_digit() {
return Err( return Err(ShErr::at(
ShErr::at(ShErrKind::ExecFail, blame, format!("{cmd}: invalid argument: '{}'",arg.fg(next_color()))) ShErrKind::ExecFail,
); blame,
format!("{cmd}: invalid argument: '{}'", arg.fg(next_color())),
));
} }
} }
let n = digits.parse::<usize>().map_err(|e| { let n = digits.parse::<usize>().map_err(|e| {
ShErr::at(ShErrKind::ExecFail, blame, format!("{cmd}: invalid index: '{}'",e.fg(next_color()))) ShErr::at(
ShErrKind::ExecFail,
blame,
format!("{cmd}: invalid index: '{}'", e.fg(next_color())),
)
})?; })?;
if from_top { if from_top {
@@ -112,7 +128,9 @@ pub fn pushd(node: Node) -> ShResult<()> {
}; };
let mut argv = prepare_argv(argv)?; let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); } if !argv.is_empty() {
argv.remove(0);
}
let mut dir = None; let mut dir = None;
let mut rotate_idx = None; let mut rotate_idx = None;
@@ -126,20 +144,29 @@ pub fn pushd(node: Node) -> ShResult<()> {
} else if arg == "-n" { } else if arg == "-n" {
no_cd = true; no_cd = true;
} else if arg.starts_with('-') { } else if arg.starts_with('-') {
return Err( return Err(ShErr::at(
ShErr::at(ShErrKind::ExecFail, blame, format!("pushd: invalid option: '{}'", arg.fg(next_color()))) ShErrKind::ExecFail,
); blame,
format!("pushd: invalid option: '{}'", arg.fg(next_color())),
));
} else { } else {
if dir.is_some() { if dir.is_some() {
return Err( return Err(ShErr::at(
ShErr::at(ShErrKind::ExecFail, blame, "pushd: too many arguments") ShErrKind::ExecFail,
); blame,
"pushd: too many arguments",
));
} }
let target = PathBuf::from(&arg); let target = PathBuf::from(&arg);
if !target.is_dir() { if !target.is_dir() {
return Err( return Err(ShErr::at(
ShErr::at(ShErrKind::ExecFail, blame, format!("pushd: not a directory: '{}'", target.display().fg(next_color()))) ShErrKind::ExecFail,
); blame,
format!(
"pushd: not a directory: '{}'",
target.display().fg(next_color())
),
));
} }
dir = Some(target); dir = Some(target);
} }
@@ -193,7 +220,9 @@ pub fn popd(node: Node) -> ShResult<()> {
}; };
let mut argv = prepare_argv(argv)?; let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); } if !argv.is_empty() {
argv.remove(0);
}
let mut remove_idx = None; let mut remove_idx = None;
let mut no_cd = false; let mut no_cd = false;
@@ -206,9 +235,11 @@ pub fn popd(node: Node) -> ShResult<()> {
} else if arg == "-n" { } else if arg == "-n" {
no_cd = true; no_cd = true;
} else if arg.starts_with('-') { } else if arg.starts_with('-') {
return Err( return Err(ShErr::at(
ShErr::at(ShErrKind::ExecFail, blame, format!("popd: invalid option: '{}'", arg.fg(next_color()))) ShErrKind::ExecFail,
); blame,
format!("popd: invalid option: '{}'", arg.fg(next_color())),
));
} }
} }
@@ -221,9 +252,11 @@ pub fn popd(node: Node) -> ShResult<()> {
if let Some(dir) = dir { if let Some(dir) = dir {
change_directory(&dir, blame.clone())?; change_directory(&dir, blame.clone())?;
} else { } else {
return Err( return Err(ShErr::at(
ShErr::at(ShErrKind::ExecFail, blame, "popd: directory stack empty") ShErrKind::ExecFail,
); blame,
"popd: directory stack empty",
));
} }
} }
} }
@@ -233,9 +266,11 @@ pub fn popd(node: Node) -> ShResult<()> {
let dirs = m.dirs_mut(); let dirs = m.dirs_mut();
let idx = n - 1; let idx = n - 1;
if idx >= dirs.len() { if idx >= dirs.len() {
return Err( return Err(ShErr::at(
ShErr::at(ShErrKind::ExecFail, blame.clone(), format!("popd: directory index out of range: +{n}")) ShErrKind::ExecFail,
); blame.clone(),
format!("popd: directory index out of range: +{n}"),
));
} }
dirs.remove(idx); dirs.remove(idx);
Ok(()) Ok(())
@@ -245,7 +280,11 @@ pub fn popd(node: Node) -> ShResult<()> {
write_meta(|m| -> ShResult<()> { write_meta(|m| -> ShResult<()> {
let dirs = m.dirs_mut(); let dirs = m.dirs_mut();
let actual = dirs.len().checked_sub(n + 1).ok_or_else(|| { let actual = dirs.len().checked_sub(n + 1).ok_or_else(|| {
ShErr::at(ShErrKind::ExecFail, blame.clone(), format!("popd: directory index out of range: -{n}")) ShErr::at(
ShErrKind::ExecFail,
blame.clone(),
format!("popd: directory index out of range: -{n}"),
)
})?; })?;
dirs.remove(actual); dirs.remove(actual);
Ok(()) Ok(())
@@ -265,9 +304,11 @@ pub fn popd(node: Node) -> ShResult<()> {
change_directory(&dir, blame.clone())?; change_directory(&dir, blame.clone())?;
print_dirs()?; print_dirs()?;
} else { } else {
return Err( return Err(ShErr::at(
ShErr::at(ShErrKind::ExecFail, blame, "popd: directory stack empty") ShErrKind::ExecFail,
); blame,
"popd: directory stack empty",
));
} }
} }
@@ -285,7 +326,9 @@ pub fn dirs(node: Node) -> ShResult<()> {
}; };
let mut argv = prepare_argv(argv)?; let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); } if !argv.is_empty() {
argv.remove(0);
}
let mut abbreviate_home = true; let mut abbreviate_home = true;
let mut one_per_line = false; let mut one_per_line = false;
@@ -306,14 +349,18 @@ pub fn dirs(node: Node) -> ShResult<()> {
target_idx = Some(parse_stack_idx(&arg, blame.clone(), "dirs")?); target_idx = Some(parse_stack_idx(&arg, blame.clone(), "dirs")?);
} }
_ if arg.starts_with('-') => { _ if arg.starts_with('-') => {
return Err( return Err(ShErr::at(
ShErr::at(ShErrKind::ExecFail, blame, format!("dirs: invalid option: '{}'", arg.fg(next_color()))) ShErrKind::ExecFail,
); blame,
format!("dirs: invalid option: '{}'", arg.fg(next_color())),
));
} }
_ => { _ => {
return Err( return Err(ShErr::at(
ShErr::at(ShErrKind::ExecFail, blame, format!("dirs: unexpected argument: '{}'", arg.fg(next_color()))) ShErrKind::ExecFail,
); blame,
format!("dirs: unexpected argument: '{}'", arg.fg(next_color())),
));
} }
} }
} }
@@ -358,15 +405,17 @@ pub fn dirs(node: Node) -> ShResult<()> {
if let Some(dir) = target { if let Some(dir) = target {
dirs = vec![dir.clone()]; dirs = vec![dir.clone()];
} else { } else {
return Err( return Err(ShErr::at(
ShErr::at(ShErrKind::ExecFail, blame, format!( ShErrKind::ExecFail,
blame,
format!(
"dirs: directory index out of range: {}", "dirs: directory index out of range: {}",
match idx { match idx {
StackIdx::FromTop(n) => format!("+{n}"), StackIdx::FromTop(n) => format!("+{n}"),
StackIdx::FromBottom(n) => format!("-{n}"), StackIdx::FromBottom(n) => format!("-{n}"),
} }
)) ),
); ));
} }
} }

View File

@@ -50,7 +50,9 @@ pub fn echo(node: Node) -> ShResult<()> {
let (argv, opts) = get_opts_from_tokens(argv, &ECHO_OPTS)?; let (argv, opts) = get_opts_from_tokens(argv, &ECHO_OPTS)?;
let flags = get_echo_flags(opts).blame(blame)?; let flags = get_echo_flags(opts).blame(blame)?;
let mut argv = prepare_argv(argv)?; let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); } if !argv.is_empty() {
argv.remove(0);
}
let output_channel = if flags.contains(EchoFlags::USE_STDERR) { let output_channel = if flags.contains(EchoFlags::USE_STDERR) {
borrow_fd(STDERR_FILENO) borrow_fd(STDERR_FILENO)

View File

@@ -1,6 +1,9 @@
use crate::{ use crate::{
libsh::error::ShResult, libsh::error::ShResult,
parse::{NdRule, Node, execute::{exec_input, prepare_argv}}, parse::{
NdRule, Node,
execute::{exec_input, prepare_argv},
},
state, state,
}; };
@@ -14,7 +17,9 @@ pub fn eval(node: Node) -> ShResult<()> {
}; };
let mut expanded_argv = prepare_argv(argv)?; let mut expanded_argv = prepare_argv(argv)?;
if !expanded_argv.is_empty() { expanded_argv.remove(0); } if !expanded_argv.is_empty() {
expanded_argv.remove(0);
}
if expanded_argv.is_empty() { if expanded_argv.is_empty() {
state::set_status(0); state::set_status(0);

View File

@@ -2,7 +2,10 @@ use nix::{errno::Errno, unistd::execvpe};
use crate::{ use crate::{
libsh::error::{ShErr, ShErrKind, ShResult}, libsh::error::{ShErr, ShErrKind, ShResult},
parse::{NdRule, Node, execute::{ExecArgs, prepare_argv}}, parse::{
NdRule, Node,
execute::{ExecArgs, prepare_argv},
},
state, state,
}; };
@@ -16,7 +19,9 @@ pub fn exec_builtin(node: Node) -> ShResult<()> {
}; };
let mut expanded_argv = prepare_argv(argv)?; let mut expanded_argv = prepare_argv(argv)?;
if !expanded_argv.is_empty() { expanded_argv.remove(0); } if !expanded_argv.is_empty() {
expanded_argv.remove(0);
}
if expanded_argv.is_empty() { if expanded_argv.is_empty() {
state::set_status(0); state::set_status(0);
@@ -34,9 +39,9 @@ pub fn exec_builtin(node: Node) -> ShResult<()> {
let cmd_str = cmd.to_str().unwrap().to_string(); let cmd_str = cmd.to_str().unwrap().to_string();
match e { match e {
Errno::ENOENT => Err( Errno::ENOENT => Err(
ShErr::new(ShErrKind::NotFound, span.clone()) ShErr::new(ShErrKind::NotFound, span.clone())
.labeled(span, format!("exec: command not found: {}", cmd_str)) .labeled(span, format!("exec: command not found: {}", cmd_str)),
), ),
_ => Err(ShErr::at(ShErrKind::Errno(e), span, format!("{e}"))), _ => Err(ShErr::at(ShErrKind::Errno(e), span, format!("{e}"))),
} }
} }

View File

@@ -21,7 +21,11 @@ pub fn flowctl(node: Node, kind: ShErrKind) -> ShResult<()> {
let (arg, span) = argv.into_iter().next().unwrap(); let (arg, span) = argv.into_iter().next().unwrap();
let Ok(status) = arg.parse::<i32>() else { let Ok(status) = arg.parse::<i32>() else {
return Err(ShErr::at(ShErrKind::SyntaxErr, span, format!("{cmd}: Expected a number"))); return Err(ShErr::at(
ShErrKind::SyntaxErr,
span,
format!("{cmd}: Expected a number"),
));
}; };
code = status; code = status;

View File

@@ -3,229 +3,251 @@ use std::str::FromStr;
use ariadne::Fmt; use ariadne::Fmt;
use crate::{ use crate::{
getopt::{Opt, OptSpec}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color}, parse::{NdRule, Node, execute::prepare_argv, lex::Span}, state::{self, VarFlags, VarKind, read_meta, read_vars, write_meta, write_vars} getopt::{Opt, OptSpec},
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color},
parse::{NdRule, Node, execute::prepare_argv, lex::Span},
state::{self, VarFlags, VarKind, read_meta, read_vars, write_meta, write_vars},
}; };
enum OptMatch { enum OptMatch {
NoMatch, NoMatch,
IsMatch, IsMatch,
WantsArg WantsArg,
} }
struct GetOptsSpec { struct GetOptsSpec {
silent_err: bool, silent_err: bool,
opt_specs: Vec<OptSpec> opt_specs: Vec<OptSpec>,
} }
impl GetOptsSpec { impl GetOptsSpec {
pub fn matches(&self, ch: char) -> OptMatch { pub fn matches(&self, ch: char) -> OptMatch {
for spec in &self.opt_specs { for spec in &self.opt_specs {
let OptSpec { opt, takes_arg } = spec; let OptSpec { opt, takes_arg } = spec;
match opt { match opt {
Opt::Short(opt_ch) if ch == *opt_ch => { Opt::Short(opt_ch) if ch == *opt_ch => {
if *takes_arg { if *takes_arg {
return OptMatch::WantsArg return OptMatch::WantsArg;
} else { } else {
return OptMatch::IsMatch return OptMatch::IsMatch;
} }
} }
_ => { continue } _ => continue,
} }
} }
OptMatch::NoMatch OptMatch::NoMatch
} }
} }
impl FromStr for GetOptsSpec { impl FromStr for GetOptsSpec {
type Err = ShErr; type Err = ShErr;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut s = s; let mut s = s;
let mut opt_specs = vec![]; let mut opt_specs = vec![];
let mut silent_err = false; let mut silent_err = false;
if s.starts_with(':') { if s.starts_with(':') {
silent_err = true; silent_err = true;
s = &s[1..]; s = &s[1..];
} }
let mut chars = s.chars().peekable(); let mut chars = s.chars().peekable();
while let Some(ch) = chars.peek() { while let Some(ch) = chars.peek() {
match ch { match ch {
ch if ch.is_alphanumeric() => { ch if ch.is_alphanumeric() => {
let opt = Opt::Short(*ch); let opt = Opt::Short(*ch);
chars.next(); chars.next();
let takes_arg = chars.peek() == Some(&':'); let takes_arg = chars.peek() == Some(&':');
if takes_arg { if takes_arg {
chars.next(); chars.next();
} }
opt_specs.push(OptSpec { opt, takes_arg }) opt_specs.push(OptSpec { opt, takes_arg })
} }
_ => return Err(ShErr::simple( _ => {
ShErrKind::ParseErr, return Err(ShErr::simple(
format!("unexpected character '{}'", ch.fg(next_color())) ShErrKind::ParseErr,
)), format!("unexpected character '{}'", ch.fg(next_color())),
} ));
} }
}
}
Ok(GetOptsSpec { silent_err, opt_specs }) Ok(GetOptsSpec {
} silent_err,
opt_specs,
})
}
} }
fn advance_optind(opt_index: usize, amount: usize) -> ShResult<()> { fn advance_optind(opt_index: usize, amount: usize) -> ShResult<()> {
write_vars(|v| v.set_var("OPTIND", VarKind::Str((opt_index + amount).to_string()), VarFlags::NONE)) write_vars(|v| {
v.set_var(
"OPTIND",
VarKind::Str((opt_index + amount).to_string()),
VarFlags::NONE,
)
})
} }
fn getopts_inner(opts_spec: &GetOptsSpec, opt_var: &str, argv: &[String], blame: Span) -> ShResult<()> { fn getopts_inner(
let opt_index = read_vars(|v| v.get_var("OPTIND").parse::<usize>().unwrap_or(1)); opts_spec: &GetOptsSpec,
// OPTIND is 1-based opt_var: &str,
let arr_idx = opt_index.saturating_sub(1); argv: &[String],
blame: Span,
) -> ShResult<()> {
let opt_index = read_vars(|v| v.get_var("OPTIND").parse::<usize>().unwrap_or(1));
// OPTIND is 1-based
let arr_idx = opt_index.saturating_sub(1);
let Some(arg) = argv.get(arr_idx) else { let Some(arg) = argv.get(arr_idx) else {
state::set_status(1); state::set_status(1);
return Ok(()) return Ok(());
}; };
// "--" stops option processing // "--" stops option processing
if arg.as_str() == "--" { if arg.as_str() == "--" {
advance_optind(opt_index, 1)?; advance_optind(opt_index, 1)?;
write_meta(|m| m.reset_getopts_char_offset()); write_meta(|m| m.reset_getopts_char_offset());
state::set_status(1); state::set_status(1);
return Ok(()) return Ok(());
} }
// Not an option — done // Not an option — done
let Some(opt_str) = arg.strip_prefix('-') else { let Some(opt_str) = arg.strip_prefix('-') else {
state::set_status(1); state::set_status(1);
return Ok(()); return Ok(());
}; };
// Bare "-" is not an option // Bare "-" is not an option
if opt_str.is_empty() { if opt_str.is_empty() {
state::set_status(1); state::set_status(1);
return Ok(()); return Ok(());
} }
let char_idx = read_meta(|m| m.getopts_char_offset()); let char_idx = read_meta(|m| m.getopts_char_offset());
let Some(ch) = opt_str.chars().nth(char_idx) else { let Some(ch) = opt_str.chars().nth(char_idx) else {
// Ran out of chars in this arg (shouldn't normally happen), // Ran out of chars in this arg (shouldn't normally happen),
// advance to next arg and signal done for this call // advance to next arg and signal done for this call
write_meta(|m| m.reset_getopts_char_offset()); write_meta(|m| m.reset_getopts_char_offset());
advance_optind(opt_index, 1)?; advance_optind(opt_index, 1)?;
state::set_status(1); state::set_status(1);
return Ok(()); return Ok(());
}; };
let last_char_in_arg = char_idx >= opt_str.len() - 1; let last_char_in_arg = char_idx >= opt_str.len() - 1;
// Advance past this character: either move to next char in this // Advance past this character: either move to next char in this
// arg, or reset offset and bump OPTIND to the next arg. // arg, or reset offset and bump OPTIND to the next arg.
let advance_one_char = |last: bool| -> ShResult<()> { let advance_one_char = |last: bool| -> ShResult<()> {
if last { if last {
write_meta(|m| m.reset_getopts_char_offset()); write_meta(|m| m.reset_getopts_char_offset());
advance_optind(opt_index, 1)?; advance_optind(opt_index, 1)?;
} else { } else {
write_meta(|m| m.inc_getopts_char_offset()); write_meta(|m| m.inc_getopts_char_offset());
} }
Ok(()) Ok(())
}; };
match opts_spec.matches(ch) { match opts_spec.matches(ch) {
OptMatch::NoMatch => { OptMatch::NoMatch => {
advance_one_char(last_char_in_arg)?; advance_one_char(last_char_in_arg)?;
if opts_spec.silent_err { if opts_spec.silent_err {
write_vars(|v| v.set_var(opt_var, VarKind::Str("?".into()), VarFlags::NONE))?; write_vars(|v| v.set_var(opt_var, VarKind::Str("?".into()), VarFlags::NONE))?;
write_vars(|v| v.set_var("OPTARG", VarKind::Str(ch.to_string()), VarFlags::NONE))?; write_vars(|v| v.set_var("OPTARG", VarKind::Str(ch.to_string()), VarFlags::NONE))?;
} else { } else {
write_vars(|v| v.set_var(opt_var, VarKind::Str("?".into()), VarFlags::NONE))?; write_vars(|v| v.set_var(opt_var, VarKind::Str("?".into()), VarFlags::NONE))?;
ShErr::at( ShErr::at(
ShErrKind::ExecFail, ShErrKind::ExecFail,
blame.clone(), blame.clone(),
format!("illegal option '-{}'", ch.fg(next_color())) format!("illegal option '-{}'", ch.fg(next_color())),
).print_error(); )
} .print_error();
state::set_status(0); }
} state::set_status(0);
OptMatch::IsMatch => { }
advance_one_char(last_char_in_arg)?; OptMatch::IsMatch => {
write_vars(|v| v.set_var(opt_var, VarKind::Str(ch.to_string()), VarFlags::NONE))?; advance_one_char(last_char_in_arg)?;
state::set_status(0); write_vars(|v| v.set_var(opt_var, VarKind::Str(ch.to_string()), VarFlags::NONE))?;
} state::set_status(0);
OptMatch::WantsArg => { }
write_meta(|m| m.reset_getopts_char_offset()); OptMatch::WantsArg => {
write_meta(|m| m.reset_getopts_char_offset());
if !last_char_in_arg { if !last_char_in_arg {
// Remaining chars in this arg are the argument: -bVALUE // Remaining chars in this arg are the argument: -bVALUE
let optarg: String = opt_str.chars().skip(char_idx + 1).collect(); let optarg: String = opt_str.chars().skip(char_idx + 1).collect();
write_vars(|v| v.set_var("OPTARG", VarKind::Str(optarg), VarFlags::NONE))?; write_vars(|v| v.set_var("OPTARG", VarKind::Str(optarg), VarFlags::NONE))?;
advance_optind(opt_index, 1)?; advance_optind(opt_index, 1)?;
} else if let Some(next_arg) = argv.get(arr_idx + 1) { } else if let Some(next_arg) = argv.get(arr_idx + 1) {
// Next arg is the argument // Next arg is the argument
write_vars(|v| v.set_var("OPTARG", VarKind::Str(next_arg.clone()), VarFlags::NONE))?; write_vars(|v| v.set_var("OPTARG", VarKind::Str(next_arg.clone()), VarFlags::NONE))?;
// Skip both the option arg and its value // Skip both the option arg and its value
advance_optind(opt_index, 2)?; advance_optind(opt_index, 2)?;
} else { } else {
// Missing required argument // Missing required argument
if opts_spec.silent_err { if opts_spec.silent_err {
write_vars(|v| v.set_var(opt_var, VarKind::Str(":".into()), VarFlags::NONE))?; write_vars(|v| v.set_var(opt_var, VarKind::Str(":".into()), VarFlags::NONE))?;
write_vars(|v| v.set_var("OPTARG", VarKind::Str(ch.to_string()), VarFlags::NONE))?; write_vars(|v| v.set_var("OPTARG", VarKind::Str(ch.to_string()), VarFlags::NONE))?;
} else { } else {
write_vars(|v| v.set_var(opt_var, VarKind::Str("?".into()), VarFlags::NONE))?; write_vars(|v| v.set_var(opt_var, VarKind::Str("?".into()), VarFlags::NONE))?;
ShErr::at( ShErr::at(
ShErrKind::ExecFail, ShErrKind::ExecFail,
blame.clone(), blame.clone(),
format!("option '-{}' requires an argument", ch.fg(next_color())) format!("option '-{}' requires an argument", ch.fg(next_color())),
).print_error(); )
} .print_error();
advance_optind(opt_index, 1)?; }
state::set_status(0); advance_optind(opt_index, 1)?;
return Ok(()); state::set_status(0);
} return Ok(());
}
write_vars(|v| v.set_var(opt_var, VarKind::Str(ch.to_string()), VarFlags::NONE))?; write_vars(|v| v.set_var(opt_var, VarKind::Str(ch.to_string()), VarFlags::NONE))?;
state::set_status(0); state::set_status(0);
} }
} }
Ok(()) Ok(())
} }
pub fn getopts(node: Node) -> ShResult<()> { pub fn getopts(node: Node) -> ShResult<()> {
let span = node.get_span().clone(); let span = node.get_span().clone();
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
argv, argv,
} = node.class } = node.class
else { else {
unreachable!() unreachable!()
}; };
let mut argv = prepare_argv(argv)?; let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); } if !argv.is_empty() {
let mut args = argv.into_iter(); argv.remove(0);
}
let mut args = argv.into_iter();
let Some(arg_string) = args.next() else { let Some(arg_string) = args.next() else {
return Err(ShErr::at( return Err(ShErr::at(
ShErrKind::ExecFail, ShErrKind::ExecFail,
span, span,
"getopts: missing option spec" "getopts: missing option spec",
)) ));
}; };
let Some(opt_var) = args.next() else { let Some(opt_var) = args.next() else {
return Err(ShErr::at( return Err(ShErr::at(
ShErrKind::ExecFail, ShErrKind::ExecFail,
span, span,
"getopts: missing variable name" "getopts: missing variable name",
)) ));
}; };
let opts_spec = GetOptsSpec::from_str(&arg_string.0) let opts_spec = GetOptsSpec::from_str(&arg_string.0).promote_err(arg_string.1.clone())?;
.promote_err(arg_string.1.clone())?;
let explicit_args: Vec<String> = args.map(|s| s.0).collect(); let explicit_args: Vec<String> = args.map(|s| s.0).collect();
if !explicit_args.is_empty() { if !explicit_args.is_empty() {
getopts_inner(&opts_spec, &opt_var.0, &explicit_args, span) getopts_inner(&opts_spec, &opt_var.0, &explicit_args, span)
} else { } else {
let pos_params: Vec<String> = read_vars(|v| v.sh_argv().iter().skip(1).cloned().collect()); let pos_params: Vec<String> = read_vars(|v| v.sh_argv().iter().skip(1).cloned().collect());
getopts_inner(&opts_spec, &opt_var.0, &pos_params, span) getopts_inner(&opts_spec, &opt_var.0, &pos_params, span)
} }
} }

View File

@@ -2,7 +2,12 @@ use std::{env, os::unix::fs::PermissionsExt, path::Path};
use ariadne::{Fmt, Span}; use ariadne::{Fmt, Span};
use crate::{builtin::BUILTINS, libsh::error::{ShErr, ShErrKind, ShResult, next_color}, parse::{NdRule, Node, execute::prepare_argv, lex::KEYWORDS}, state::{self, ShAlias, ShFunc, read_logic}}; use crate::{
builtin::BUILTINS,
libsh::error::{ShErr, ShErrKind, ShResult, next_color},
parse::{NdRule, Node, execute::prepare_argv, lex::KEYWORDS},
state::{self, ShAlias, ShFunc, read_logic},
};
pub fn type_builtin(node: Node) -> ShResult<()> { pub fn type_builtin(node: Node) -> ShResult<()> {
let NdRule::Command { let NdRule::Command {
@@ -14,59 +19,75 @@ pub fn type_builtin(node: Node) -> ShResult<()> {
}; };
let mut argv = prepare_argv(argv)?; let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); } if !argv.is_empty() {
argv.remove(0);
}
/* /*
* we have to check in the same order that the dispatcher checks this * we have to check in the same order that the dispatcher checks this
* 1. function * 1. function
* 2. builtin * 2. builtin
* 3. command * 3. command
*/ */
'outer: for (arg,span) in argv { 'outer: for (arg, span) in argv {
if let Some(func) = read_logic(|v| v.get_func(&arg)) { if let Some(func) = read_logic(|v| v.get_func(&arg)) {
let ShFunc { body: _, source } = func; let ShFunc { body: _, source } = func;
let (line, col) = source.line_and_col(); let (line, col) = source.line_and_col();
let name = source.source().name(); let name = source.source().name();
println!("{arg} is a function defined at {name}:{}:{}", line + 1, col + 1); println!(
} else if let Some(alias) = read_logic(|v| v.get_alias(&arg)) { "{arg} is a function defined at {name}:{}:{}",
let ShAlias { body, source } = alias; line + 1,
let (line, col) = source.line_and_col(); col + 1
let name = source.source().name(); );
println!("{arg} is an alias for '{body}' defined at {name}:{}:{}", line + 1, col + 1); } else if let Some(alias) = read_logic(|v| v.get_alias(&arg)) {
} else if BUILTINS.contains(&arg.as_str()) { let ShAlias { body, source } = alias;
println!("{arg} is a shell builtin"); let (line, col) = source.line_and_col();
} else if KEYWORDS.contains(&arg.as_str()) { let name = source.source().name();
println!("{arg} is a shell keyword"); println!(
} else { "{arg} is an alias for '{body}' defined at {name}:{}:{}",
let path = env::var("PATH").unwrap_or_default(); line + 1,
let paths = path.split(':') col + 1
.map(Path::new) );
.collect::<Vec<_>>(); } else if BUILTINS.contains(&arg.as_str()) {
println!("{arg} is a shell builtin");
} else if KEYWORDS.contains(&arg.as_str()) {
println!("{arg} is a shell keyword");
} else {
let path = env::var("PATH").unwrap_or_default();
let paths = path.split(':').map(Path::new).collect::<Vec<_>>();
for path in paths { for path in paths {
if let Ok(entries) = path.read_dir() { if let Ok(entries) = path.read_dir() {
for entry in entries.flatten() { for entry in entries.flatten() {
let Ok(meta) = std::fs::metadata(entry.path()) else { let Ok(meta) = std::fs::metadata(entry.path()) else {
continue; continue;
}; };
let is_exec = meta.permissions().mode() & 0o111 != 0; let is_exec = meta.permissions().mode() & 0o111 != 0;
if meta.is_file() if meta.is_file()
&& is_exec && is_exec
&& let Some(name) = entry.file_name().to_str() && let Some(name) = entry.file_name().to_str()
&& name == arg { && name == arg
println!("{arg} is {}", entry.path().display()); {
continue 'outer; println!("{arg} is {}", entry.path().display());
} continue 'outer;
} }
} }
} }
}
state::set_status(1); state::set_status(1);
return Err(ShErr::at(ShErrKind::NotFound, span, format!("'{}' is not a command, function, or alias", arg.fg(next_color())))); return Err(ShErr::at(
} ShErrKind::NotFound,
} span,
format!(
"'{}' is not a command, function, or alias",
arg.fg(next_color())
),
));
}
}
state::set_status(0); state::set_status(0);
Ok(()) Ok(())

View File

@@ -16,8 +16,8 @@ pub enum JobBehavior {
pub fn continue_job(node: Node, behavior: JobBehavior) -> ShResult<()> { pub fn continue_job(node: Node, behavior: JobBehavior) -> ShResult<()> {
let blame = node.get_span().clone(); let blame = node.get_span().clone();
let cmd_tk = node.get_command(); let cmd_tk = node.get_command();
let cmd_span = cmd_tk.unwrap().span.clone(); let cmd_span = cmd_tk.unwrap().span.clone();
let cmd = match behavior { let cmd = match behavior {
JobBehavior::Foregound => "fg", JobBehavior::Foregound => "fg",
JobBehavior::Background => "bg", JobBehavior::Background => "bg",
@@ -31,11 +31,17 @@ pub fn continue_job(node: Node, behavior: JobBehavior) -> ShResult<()> {
}; };
let mut argv = prepare_argv(argv)?; let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); } if !argv.is_empty() {
argv.remove(0);
}
let mut argv = argv.into_iter(); let mut argv = argv.into_iter();
if read_jobs(|j| j.get_fg().is_some()) { if read_jobs(|j| j.get_fg().is_some()) {
return Err(ShErr::at(ShErrKind::InternalErr, cmd_span, format!("Somehow called '{}' with an existing foreground job", cmd))); return Err(ShErr::at(
ShErrKind::InternalErr,
cmd_span,
format!("Somehow called '{}' with an existing foreground job", cmd),
));
} }
let curr_job_id = if let Some(id) = read_jobs(|j| j.curr_job()) { let curr_job_id = if let Some(id) = read_jobs(|j| j.curr_job()) {
@@ -55,7 +61,11 @@ pub fn continue_job(node: Node, behavior: JobBehavior) -> ShResult<()> {
if query_result.is_some() { if query_result.is_some() {
Ok(j.remove_job(id.clone()).unwrap()) Ok(j.remove_job(id.clone()).unwrap())
} else { } else {
Err(ShErr::at(ShErrKind::ExecFail, blame.clone(), format!("Job id `{}' not found", tabid))) Err(ShErr::at(
ShErrKind::ExecFail,
blame.clone(),
format!("Job id `{}' not found", tabid),
))
} }
})?; })?;
@@ -82,12 +92,16 @@ fn parse_job_id(arg: &str, blame: Span) -> ShResult<usize> {
if arg.starts_with('%') { if arg.starts_with('%') {
let arg = arg.strip_prefix('%').unwrap(); let arg = arg.strip_prefix('%').unwrap();
if arg.chars().all(|ch| ch.is_ascii_digit()) { if arg.chars().all(|ch| ch.is_ascii_digit()) {
let num = arg.parse::<usize>().unwrap_or_default(); let num = arg.parse::<usize>().unwrap_or_default();
if num == 0 { if num == 0 {
Err(ShErr::at(ShErrKind::SyntaxErr, blame, format!("Invalid job id: {}", arg.fg(next_color())))) Err(ShErr::at(
} else { ShErrKind::SyntaxErr,
Ok(num.saturating_sub(1)) blame,
} format!("Invalid job id: {}", arg.fg(next_color())),
))
} else {
Ok(num.saturating_sub(1))
}
} else { } else {
let result = write_jobs(|j| { let result = write_jobs(|j| {
let query_result = j.query(JobID::Command(arg.into())); let query_result = j.query(JobID::Command(arg.into()));
@@ -95,7 +109,11 @@ fn parse_job_id(arg: &str, blame: Span) -> ShResult<usize> {
}); });
match result { match result {
Some(id) => Ok(id), Some(id) => Ok(id),
None => Err(ShErr::at(ShErrKind::InternalErr, blame, "Found a job but no table id in parse_job_id()")), None => Err(ShErr::at(
ShErrKind::InternalErr,
blame,
"Found a job but no table id in parse_job_id()",
)),
} }
} }
} else if arg.chars().all(|ch| ch.is_ascii_digit()) { } else if arg.chars().all(|ch| ch.is_ascii_digit()) {
@@ -115,10 +133,18 @@ fn parse_job_id(arg: &str, blame: Span) -> ShResult<usize> {
match result { match result {
Some(id) => Ok(id), Some(id) => Ok(id),
None => Err(ShErr::at(ShErrKind::InternalErr, blame, "Found a job but no table id in parse_job_id()")), None => Err(ShErr::at(
ShErrKind::InternalErr,
blame,
"Found a job but no table id in parse_job_id()",
)),
} }
} else { } else {
Err(ShErr::at(ShErrKind::SyntaxErr, blame, format!("Invalid arg: {}", arg.fg(next_color())))) Err(ShErr::at(
ShErrKind::SyntaxErr,
blame,
format!("Invalid arg: {}", arg.fg(next_color())),
))
} }
} }
@@ -132,13 +158,19 @@ pub fn jobs(node: Node) -> ShResult<()> {
}; };
let mut argv = prepare_argv(argv)?; let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); } if !argv.is_empty() {
argv.remove(0);
}
let mut flags = JobCmdFlags::empty(); let mut flags = JobCmdFlags::empty();
for (arg, span) in argv { for (arg, span) in argv {
let mut chars = arg.chars().peekable(); let mut chars = arg.chars().peekable();
if chars.peek().is_none_or(|ch| *ch != '-') { if chars.peek().is_none_or(|ch| *ch != '-') {
return Err(ShErr::at(ShErrKind::SyntaxErr, span, "Invalid flag in jobs call")); return Err(ShErr::at(
ShErrKind::SyntaxErr,
span,
"Invalid flag in jobs call",
));
} }
chars.next(); chars.next();
for ch in chars { for ch in chars {
@@ -149,7 +181,11 @@ pub fn jobs(node: Node) -> ShResult<()> {
'r' => JobCmdFlags::RUNNING, 'r' => JobCmdFlags::RUNNING,
's' => JobCmdFlags::STOPPED, 's' => JobCmdFlags::STOPPED,
_ => { _ => {
return Err(ShErr::at(ShErrKind::SyntaxErr, span, "Invalid flag in jobs call")); return Err(ShErr::at(
ShErrKind::SyntaxErr,
span,
"Invalid flag in jobs call",
));
} }
}; };
flags |= flag flags |= flag
@@ -162,41 +198,44 @@ pub fn jobs(node: Node) -> ShResult<()> {
} }
pub fn wait(node: Node) -> ShResult<()> { pub fn wait(node: Node) -> ShResult<()> {
let blame = node.get_span().clone(); let blame = node.get_span().clone();
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
argv, argv,
} = node.class } = node.class
else { else {
unreachable!() unreachable!()
}; };
let mut argv = prepare_argv(argv)?; let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); } if !argv.is_empty() {
if read_jobs(|j| j.curr_job().is_none()) { argv.remove(0);
state::set_status(0); }
return Err(ShErr::at(ShErrKind::ExecFail, blame, "wait: No jobs found")); if read_jobs(|j| j.curr_job().is_none()) {
} state::set_status(0);
let argv = argv.into_iter() return Err(ShErr::at(ShErrKind::ExecFail, blame, "wait: No jobs found"));
.map(|arg| { }
if arg.0.as_str().chars().all(|ch| ch.is_ascii_digit()) { let argv = argv
Ok(JobID::Pid(Pid::from_raw(arg.0.parse::<i32>().unwrap()))) .into_iter()
} else { .map(|arg| {
Ok(JobID::TableID(parse_job_id(&arg.0, arg.1)?)) if arg.0.as_str().chars().all(|ch| ch.is_ascii_digit()) {
} Ok(JobID::Pid(Pid::from_raw(arg.0.parse::<i32>().unwrap())))
}) } else {
.collect::<ShResult<Vec<JobID>>>()?; Ok(JobID::TableID(parse_job_id(&arg.0, arg.1)?))
}
})
.collect::<ShResult<Vec<JobID>>>()?;
if argv.is_empty() { if argv.is_empty() {
write_jobs(|j| j.wait_all_bg())?; write_jobs(|j| j.wait_all_bg())?;
} else { } else {
for arg in argv { for arg in argv {
wait_bg(arg)?; wait_bg(arg)?;
} }
} }
// don't set status here, the status of the waited-on job should be the status of the wait builtin // don't set status here, the status of the waited-on job should be the status of the wait builtin
Ok(()) Ok(())
} }
pub fn disown(node: Node) -> ShResult<()> { pub fn disown(node: Node) -> ShResult<()> {
@@ -210,13 +249,19 @@ pub fn disown(node: Node) -> ShResult<()> {
}; };
let mut argv = prepare_argv(argv)?; let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); } if !argv.is_empty() {
argv.remove(0);
}
let mut argv = argv.into_iter(); let mut argv = argv.into_iter();
let curr_job_id = if let Some(id) = read_jobs(|j| j.curr_job()) { let curr_job_id = if let Some(id) = read_jobs(|j| j.curr_job()) {
id id
} else { } else {
return Err(ShErr::at(ShErrKind::ExecFail, blame, "disown: No jobs to disown")); return Err(ShErr::at(
ShErrKind::ExecFail,
blame,
"disown: No jobs to disown",
));
}; };
let mut tabid = curr_job_id; let mut tabid = curr_job_id;

View File

@@ -1,112 +1,130 @@
use crate::{ use crate::{
expand::expand_keymap, getopt::{Opt, OptSpec, get_opts_from_tokens}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node, execute::prepare_argv}, prelude::*, readline::keys::KeyEvent, state::{self, write_logic} expand::expand_keymap,
getopt::{Opt, OptSpec, get_opts_from_tokens},
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
parse::{NdRule, Node, execute::prepare_argv},
prelude::*,
readline::keys::KeyEvent,
state::{self, write_logic},
}; };
bitflags! { bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct KeyMapFlags: u32 { pub struct KeyMapFlags: u32 {
const NORMAL = 0b0000001; const NORMAL = 0b0000001;
const INSERT = 0b0000010; const INSERT = 0b0000010;
const VISUAL = 0b0000100; const VISUAL = 0b0000100;
const EX = 0b0001000; const EX = 0b0001000;
const OP_PENDING = 0b0010000; const OP_PENDING = 0b0010000;
const REPLACE = 0b0100000; const REPLACE = 0b0100000;
const VERBATIM = 0b1000000; const VERBATIM = 0b1000000;
} }
} }
pub struct KeyMapOpts { pub struct KeyMapOpts {
remove: Option<String>, remove: Option<String>,
flags: KeyMapFlags, flags: KeyMapFlags,
} }
impl KeyMapOpts { impl KeyMapOpts {
pub fn from_opts(opts: &[Opt]) -> ShResult<Self> { pub fn from_opts(opts: &[Opt]) -> ShResult<Self> {
let mut flags = KeyMapFlags::empty(); let mut flags = KeyMapFlags::empty();
let mut remove = None; let mut remove = None;
for opt in opts { for opt in opts {
match opt { match opt {
Opt::Short('n') => flags |= KeyMapFlags::NORMAL, Opt::Short('n') => flags |= KeyMapFlags::NORMAL,
Opt::Short('i') => flags |= KeyMapFlags::INSERT, Opt::Short('i') => flags |= KeyMapFlags::INSERT,
Opt::Short('v') => flags |= KeyMapFlags::VISUAL, Opt::Short('v') => flags |= KeyMapFlags::VISUAL,
Opt::Short('x') => flags |= KeyMapFlags::EX, Opt::Short('x') => flags |= KeyMapFlags::EX,
Opt::Short('o') => flags |= KeyMapFlags::OP_PENDING, Opt::Short('o') => flags |= KeyMapFlags::OP_PENDING,
Opt::Short('r') => flags |= KeyMapFlags::REPLACE, Opt::Short('r') => flags |= KeyMapFlags::REPLACE,
Opt::LongWithArg(name, arg) if name == "remove" => { Opt::LongWithArg(name, arg) if name == "remove" => {
if remove.is_some() { if remove.is_some() {
return Err(ShErr::simple(ShErrKind::ExecFail, "Duplicate --remove option for keymap".to_string())); return Err(ShErr::simple(
} ShErrKind::ExecFail,
remove = Some(arg.clone()); "Duplicate --remove option for keymap".to_string(),
}, ));
_ => return Err(ShErr::simple(ShErrKind::ExecFail, format!("Invalid option for keymap: {:?}", opt))), }
} remove = Some(arg.clone());
} }
if flags.is_empty() { _ => {
return Err(ShErr::simple(ShErrKind::ExecFail, "At least one mode option must be specified for keymap".to_string()).with_note("Use -n for normal mode, -i for insert mode, -v for visual mode, -x for ex mode, and -o for operator-pending mode".to_string())); return Err(ShErr::simple(
} ShErrKind::ExecFail,
Ok(Self { remove, flags }) format!("Invalid option for keymap: {:?}", opt),
} ));
pub fn keymap_opts() -> [OptSpec;6] { }
[ }
OptSpec { }
opt: Opt::Short('n'), // normal mode if flags.is_empty() {
takes_arg: false return Err(ShErr::simple(ShErrKind::ExecFail, "At least one mode option must be specified for keymap".to_string()).with_note("Use -n for normal mode, -i for insert mode, -v for visual mode, -x for ex mode, and -o for operator-pending mode".to_string()));
}, }
OptSpec { Ok(Self { remove, flags })
opt: Opt::Short('i'), // insert mode }
takes_arg: false pub fn keymap_opts() -> [OptSpec; 6] {
}, [
OptSpec { OptSpec {
opt: Opt::Short('v'), // visual mode opt: Opt::Short('n'), // normal mode
takes_arg: false takes_arg: false,
}, },
OptSpec { OptSpec {
opt: Opt::Short('x'), // ex mode opt: Opt::Short('i'), // insert mode
takes_arg: false takes_arg: false,
}, },
OptSpec { OptSpec {
opt: Opt::Short('o'), // operator-pending mode opt: Opt::Short('v'), // visual mode
takes_arg: false takes_arg: false,
}, },
OptSpec { OptSpec {
opt: Opt::Short('r'), // replace mode opt: Opt::Short('x'), // ex mode
takes_arg: false takes_arg: false,
}, },
] OptSpec {
} opt: Opt::Short('o'), // operator-pending mode
takes_arg: false,
},
OptSpec {
opt: Opt::Short('r'), // replace mode
takes_arg: false,
},
]
}
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeyMapMatch { pub enum KeyMapMatch {
NoMatch, NoMatch,
IsPrefix, IsPrefix,
IsExact IsExact,
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct KeyMap { pub struct KeyMap {
pub flags: KeyMapFlags, pub flags: KeyMapFlags,
pub keys: String, pub keys: String,
pub action: String pub action: String,
} }
impl KeyMap { impl KeyMap {
pub fn keys_expanded(&self) -> Vec<KeyEvent> { pub fn keys_expanded(&self) -> Vec<KeyEvent> {
expand_keymap(&self.keys) expand_keymap(&self.keys)
} }
pub fn action_expanded(&self) -> Vec<KeyEvent> { pub fn action_expanded(&self) -> Vec<KeyEvent> {
expand_keymap(&self.action) expand_keymap(&self.action)
} }
pub fn compare(&self, other: &[KeyEvent]) -> KeyMapMatch { pub fn compare(&self, other: &[KeyEvent]) -> KeyMapMatch {
log::debug!("Comparing keymap keys {:?} with input {:?}", self.keys_expanded(), other); log::debug!(
let ours = self.keys_expanded(); "Comparing keymap keys {:?} with input {:?}",
if other == ours { self.keys_expanded(),
KeyMapMatch::IsExact other
} else if ours.starts_with(other) { );
KeyMapMatch::IsPrefix let ours = self.keys_expanded();
} else { if other == ours {
KeyMapMatch::NoMatch KeyMapMatch::IsExact
} } else if ours.starts_with(other) {
} KeyMapMatch::IsPrefix
} else {
KeyMapMatch::NoMatch
}
}
} }
pub fn keymap(node: Node) -> ShResult<()> { pub fn keymap(node: Node) -> ShResult<()> {
@@ -119,32 +137,42 @@ pub fn keymap(node: Node) -> ShResult<()> {
unreachable!() unreachable!()
}; };
let (argv, opts) = get_opts_from_tokens(argv, &KeyMapOpts::keymap_opts())?; let (argv, opts) = get_opts_from_tokens(argv, &KeyMapOpts::keymap_opts())?;
let opts = KeyMapOpts::from_opts(&opts).promote_err(span.clone())?; let opts = KeyMapOpts::from_opts(&opts).promote_err(span.clone())?;
if let Some(to_rm) = opts.remove { if let Some(to_rm) = opts.remove {
write_logic(|l| l.remove_keymap(&to_rm)); write_logic(|l| l.remove_keymap(&to_rm));
state::set_status(0); state::set_status(0);
return Ok(()); return Ok(());
} }
let mut argv = prepare_argv(argv)?; let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); } if !argv.is_empty() {
argv.remove(0);
}
let Some((keys,_)) = argv.first() else { let Some((keys, _)) = argv.first() else {
return Err(ShErr::at(ShErrKind::ExecFail, span, "missing keys argument".to_string())); return Err(ShErr::at(
}; ShErrKind::ExecFail,
span,
"missing keys argument".to_string(),
));
};
let Some((action,_)) = argv.get(1) else { let Some((action, _)) = argv.get(1) else {
return Err(ShErr::at(ShErrKind::ExecFail, span, "missing action argument".to_string())); return Err(ShErr::at(
}; ShErrKind::ExecFail,
span,
"missing action argument".to_string(),
));
};
let keymap = KeyMap { let keymap = KeyMap {
flags: opts.flags, flags: opts.flags,
keys: keys.clone(), keys: keys.clone(),
action: action.clone(), action: action.clone(),
}; };
write_logic(|l| l.insert_keymap(keymap)); write_logic(|l| l.insert_keymap(keymap));
state::set_status(0); state::set_status(0);
Ok(()) Ok(())

View File

@@ -5,240 +5,239 @@ use nix::{libc::STDOUT_FILENO, unistd::write};
use serde_json::{Map, Value}; use serde_json::{Map, Value};
use crate::{ use crate::{
expand::expand_cmd_sub, getopt::{Opt, OptSpec, get_opts_from_tokens}, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node, lex::{split_tk, split_tk_at}}, procio::borrow_fd, state::{self, read_vars, write_vars} expand::expand_cmd_sub,
getopt::{Opt, OptSpec, get_opts_from_tokens},
libsh::error::{ShErr, ShErrKind, ShResult},
parse::{
NdRule, Node,
lex::{split_tk, split_tk_at},
},
procio::borrow_fd,
state::{self, read_vars, write_vars},
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum MapNode { pub enum MapNode {
DynamicLeaf(String), // eval'd on access DynamicLeaf(String), // eval'd on access
StaticLeaf(String), // static value StaticLeaf(String), // static value
Array(Vec<MapNode>), Array(Vec<MapNode>),
Branch(HashMap<String, MapNode>), Branch(HashMap<String, MapNode>),
} }
impl Default for MapNode { impl Default for MapNode {
fn default() -> Self { fn default() -> Self {
Self::Branch(HashMap::new()) Self::Branch(HashMap::new())
} }
} }
impl From<MapNode> for serde_json::Value { impl From<MapNode> for serde_json::Value {
fn from(val: MapNode) -> Self { fn from(val: MapNode) -> Self {
match val { match val {
MapNode::Branch(map) => { MapNode::Branch(map) => {
let val_map = map.into_iter() let val_map = map
.map(|(k,v)| { .into_iter()
(k,v.into()) .map(|(k, v)| (k, v.into()))
}) .collect::<Map<String, Value>>();
.collect::<Map<String,Value>>();
Value::Object(val_map) Value::Object(val_map)
} }
MapNode::Array(nodes) => { MapNode::Array(nodes) => {
let arr = nodes let arr = nodes.into_iter().map(|node| node.into()).collect();
.into_iter() Value::Array(arr)
.map(|node| node.into()) }
.collect(); MapNode::StaticLeaf(leaf) | MapNode::DynamicLeaf(leaf) => Value::String(leaf),
Value::Array(arr) }
} }
MapNode::StaticLeaf(leaf) | MapNode::DynamicLeaf(leaf) => {
Value::String(leaf)
}
}
}
} }
impl From<Value> for MapNode { impl From<Value> for MapNode {
fn from(value: Value) -> Self { fn from(value: Value) -> Self {
match value { match value {
Value::Object(map) => { Value::Object(map) => {
let node_map = map.into_iter() let node_map = map
.map(|(k,v)| { .into_iter()
(k, v.into()) .map(|(k, v)| (k, v.into()))
}) .collect::<HashMap<String, MapNode>>();
.collect::<HashMap<String, MapNode>>();
MapNode::Branch(node_map) MapNode::Branch(node_map)
} }
Value::Array(arr) => { Value::Array(arr) => {
let nodes = arr let nodes = arr.into_iter().map(|v| v.into()).collect();
.into_iter() MapNode::Array(nodes)
.map(|v| v.into()) }
.collect(); Value::String(s) => MapNode::StaticLeaf(s),
MapNode::Array(nodes) v => MapNode::StaticLeaf(v.to_string()),
} }
Value::String(s) => MapNode::StaticLeaf(s), }
v => MapNode::StaticLeaf(v.to_string())
}
}
} }
impl MapNode { impl MapNode {
fn get(&self, path: &[String]) -> Option<&MapNode> { fn get(&self, path: &[String]) -> Option<&MapNode> {
match path { match path {
[] => Some(self), [] => Some(self),
[key, rest @ ..] => match self { [key, rest @ ..] => match self {
MapNode::StaticLeaf(_) | MapNode::DynamicLeaf(_) => None, MapNode::StaticLeaf(_) | MapNode::DynamicLeaf(_) => None,
MapNode::Array(map_nodes) => { MapNode::Array(map_nodes) => {
let idx: usize = key.parse().ok()?; let idx: usize = key.parse().ok()?;
map_nodes.get(idx)?.get(rest) map_nodes.get(idx)?.get(rest)
} }
MapNode::Branch(map) => map.get(key)?.get(rest) MapNode::Branch(map) => map.get(key)?.get(rest),
} },
} }
} }
fn set(&mut self, path: &[String], value: MapNode) { fn set(&mut self, path: &[String], value: MapNode) {
match path { match path {
[] => *self = value, [] => *self = value,
[key, rest @ ..] => { [key, rest @ ..] => {
if matches!(self, MapNode::StaticLeaf(_) | MapNode::DynamicLeaf(_)) { if matches!(self, MapNode::StaticLeaf(_) | MapNode::DynamicLeaf(_)) {
// promote leaf to branch if we still have path left to traverse // promote leaf to branch if we still have path left to traverse
*self = Self::default(); *self = Self::default();
} }
match self { match self {
MapNode::Branch(map) => { MapNode::Branch(map) => {
let child = map let child = map.entry(key.to_string()).or_insert_with(Self::default);
.entry(key.to_string()) child.set(rest, value);
.or_insert_with(Self::default); }
child.set(rest, value); MapNode::Array(map_nodes) => {
} let idx: usize = key.parse().expect("expected array index");
MapNode::Array(map_nodes) => { if idx >= map_nodes.len() {
let idx: usize = key.parse().expect("expected array index"); map_nodes.resize(idx + 1, Self::default());
if idx >= map_nodes.len() { }
map_nodes.resize(idx + 1, Self::default()); map_nodes[idx].set(rest, value);
} }
map_nodes[idx].set(rest, value); _ => unreachable!(),
} }
_ => unreachable!() }
} }
} }
}
}
fn remove(&mut self, path: &[String]) -> Option<MapNode> { fn remove(&mut self, path: &[String]) -> Option<MapNode> {
match path { match path {
[] => None, [] => None,
[key] => match self { [key] => match self {
MapNode::Branch(map) => map.remove(key), MapNode::Branch(map) => map.remove(key),
MapNode::Array(nodes) => { MapNode::Array(nodes) => {
let idx: usize = key.parse().ok()?; let idx: usize = key.parse().ok()?;
if idx >= nodes.len() { if idx >= nodes.len() {
return None; return None;
} }
Some(nodes.remove(idx)) Some(nodes.remove(idx))
} }
_ => None _ => None,
} },
[key, rest @ ..] => match self { [key, rest @ ..] => match self {
MapNode::Branch(map) => map.get_mut(key)?.remove(rest), MapNode::Branch(map) => map.get_mut(key)?.remove(rest),
MapNode::Array(nodes) => { MapNode::Array(nodes) => {
let idx: usize = key.parse().ok()?; let idx: usize = key.parse().ok()?;
if idx >= nodes.len() { if idx >= nodes.len() {
return None; return None;
} }
nodes[idx].remove(rest) nodes[idx].remove(rest)
} }
_ => None _ => None,
} },
} }
} }
fn keys(&self) -> Vec<String> { fn keys(&self) -> Vec<String> {
match self { match self {
MapNode::Branch(map) => map.keys().map(|k| k.to_string()).collect(), MapNode::Branch(map) => map.keys().map(|k| k.to_string()).collect(),
MapNode::Array(nodes) => nodes.iter().filter_map(|n| n.display(false, false).ok()).collect(), MapNode::Array(nodes) => nodes
MapNode::StaticLeaf(_) | MapNode::DynamicLeaf(_) => vec![], .iter()
} .filter_map(|n| n.display(false, false).ok())
} .collect(),
MapNode::StaticLeaf(_) | MapNode::DynamicLeaf(_) => vec![],
}
}
fn display(&self, json: bool, pretty: bool) -> ShResult<String> { fn display(&self, json: bool, pretty: bool) -> ShResult<String> {
if json || matches!(self, MapNode::Branch(_)) { if json || matches!(self, MapNode::Branch(_)) {
let val: Value = self.clone().into(); let val: Value = self.clone().into();
if pretty { if pretty {
match serde_json::to_string_pretty(&val) { match serde_json::to_string_pretty(&val) {
Ok(s) => Ok(s), Ok(s) => Ok(s),
Err(e) => Err(ShErr::simple( Err(e) => Err(ShErr::simple(
ShErrKind::InternalErr, ShErrKind::InternalErr,
format!("failed to serialize map: {e}") format!("failed to serialize map: {e}"),
)) )),
} }
} else { } else {
match serde_json::to_string(&val) { match serde_json::to_string(&val) {
Ok(s) => Ok(s), Ok(s) => Ok(s),
Err(e) => Err(ShErr::simple( Err(e) => Err(ShErr::simple(
ShErrKind::InternalErr, ShErrKind::InternalErr,
format!("failed to serialize map: {e}") format!("failed to serialize map: {e}"),
)) )),
} }
} }
} else { } else {
match self { match self {
MapNode::StaticLeaf(leaf) => Ok(leaf.clone()), MapNode::StaticLeaf(leaf) => Ok(leaf.clone()),
MapNode::DynamicLeaf(cmd) => expand_cmd_sub(cmd), MapNode::DynamicLeaf(cmd) => expand_cmd_sub(cmd),
MapNode::Array(nodes) => { MapNode::Array(nodes) => {
let mut s = String::new(); let mut s = String::new();
for node in nodes { for node in nodes {
let display = node.display(json, pretty)?; let display = node.display(json, pretty)?;
if matches!(node, MapNode::Branch(_)) { if matches!(node, MapNode::Branch(_)) {
s.push_str(&format!("'{}'", display)); s.push_str(&format!("'{}'", display));
} else { } else {
s.push_str(&node.display(json, pretty)?); s.push_str(&node.display(json, pretty)?);
} }
s.push('\n'); s.push('\n');
} }
Ok(s.trim_end_matches('\n').to_string()) Ok(s.trim_end_matches('\n').to_string())
} }
_ => unreachable!() _ => unreachable!(),
} }
} }
} }
} }
fn map_opts_spec() -> [OptSpec; 6] { fn map_opts_spec() -> [OptSpec; 6] {
[ [
OptSpec { OptSpec {
opt: Opt::Short('r'), opt: Opt::Short('r'),
takes_arg: false takes_arg: false,
}, },
OptSpec { OptSpec {
opt: Opt::Short('j'), opt: Opt::Short('j'),
takes_arg: false takes_arg: false,
}, },
OptSpec { OptSpec {
opt: Opt::Short('k'), opt: Opt::Short('k'),
takes_arg: false takes_arg: false,
}, },
OptSpec { OptSpec {
opt: Opt::Long("pretty".into()), opt: Opt::Long("pretty".into()),
takes_arg: false takes_arg: false,
}, },
OptSpec { OptSpec {
opt: Opt::Short('F'), opt: Opt::Short('F'),
takes_arg: false takes_arg: false,
}, },
OptSpec { OptSpec {
opt: Opt::Short('l'), opt: Opt::Short('l'),
takes_arg: false takes_arg: false,
}, },
] ]
} }
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct MapOpts { pub struct MapOpts {
flags: MapFlags, flags: MapFlags,
} }
bitflags! { bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct MapFlags: u32 { pub struct MapFlags: u32 {
const REMOVE = 0b000001; const REMOVE = 0b000001;
const KEYS = 0b000010; const KEYS = 0b000010;
const JSON = 0b000100; const JSON = 0b000100;
const LOCAL = 0b001000; const LOCAL = 0b001000;
const PRETTY = 0b010000; const PRETTY = 0b010000;
const FUNC = 0b100000; const FUNC = 0b100000;
} }
} }
pub fn map(node: Node) -> ShResult<()> { pub fn map(node: Node) -> ShResult<()> {
@@ -250,136 +249,140 @@ pub fn map(node: Node) -> ShResult<()> {
unreachable!() unreachable!()
}; };
let (mut argv, opts) = get_opts_from_tokens(argv, &map_opts_spec())?; let (mut argv, opts) = get_opts_from_tokens(argv, &map_opts_spec())?;
let map_opts = get_map_opts(opts); let map_opts = get_map_opts(opts);
if !argv.is_empty() { if !argv.is_empty() {
argv.remove(0); // remove "map" command from argv argv.remove(0); // remove "map" command from argv
} }
for arg in argv { for arg in argv {
if let Some((lhs,rhs)) = split_tk_at(&arg, "=") { if let Some((lhs, rhs)) = split_tk_at(&arg, "=") {
let path = split_tk(&lhs, ".") let path = split_tk(&lhs, ".")
.into_iter() .into_iter()
.map(|s| s.expand().map(|exp| exp.get_words().join(" "))) .map(|s| s.expand().map(|exp| exp.get_words().join(" ")))
.collect::<ShResult<Vec<String>>>()?; .collect::<ShResult<Vec<String>>>()?;
let Some(name) = path.first() else { let Some(name) = path.first() else {
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::InternalErr, ShErrKind::InternalErr,
format!("invalid map path: {}", lhs.as_str()) format!("invalid map path: {}", lhs.as_str()),
)); ));
}; };
let is_json = map_opts.flags.contains(MapFlags::JSON); let is_json = map_opts.flags.contains(MapFlags::JSON);
let is_func = map_opts.flags.contains(MapFlags::FUNC); let is_func = map_opts.flags.contains(MapFlags::FUNC);
let make_leaf = |s: String| { let make_leaf = |s: String| {
if is_func { MapNode::DynamicLeaf(s) } else { MapNode::StaticLeaf(s) } if is_func {
}; MapNode::DynamicLeaf(s)
let expanded = rhs.expand()?.get_words().join(" "); } else {
let found = write_vars(|v| -> ShResult<bool> { MapNode::StaticLeaf(s)
if let Some(map) = v.get_map_mut(name) { }
if is_json { };
if let Ok(parsed) = serde_json::from_str::<Value>(expanded.as_str()) { let expanded = rhs.expand()?.get_words().join(" ");
map.set(&path[1..], parsed.into()); let found = write_vars(|v| -> ShResult<bool> {
} else { if let Some(map) = v.get_map_mut(name) {
map.set(&path[1..], make_leaf(expanded.clone())); if is_json {
} if let Ok(parsed) = serde_json::from_str::<Value>(expanded.as_str()) {
} else { map.set(&path[1..], parsed.into());
map.set(&path[1..], make_leaf(expanded.clone())); } else {
} map.set(&path[1..], make_leaf(expanded.clone()));
Ok(true) }
} else { } else {
Ok(false) map.set(&path[1..], make_leaf(expanded.clone()));
} }
}); Ok(true)
} else {
Ok(false)
}
});
if !found? { if !found? {
let mut new = MapNode::default(); let mut new = MapNode::default();
if is_json /*&& let Ok(parsed) = serde_json::from_str::<Value>(rhs.as_str()) */{ if is_json
let parsed = serde_json::from_str::<Value>(expanded.as_str()).unwrap(); /*&& let Ok(parsed) = serde_json::from_str::<Value>(rhs.as_str()) */
let node: MapNode = parsed.into(); {
new.set(&path[1..], node); let parsed = serde_json::from_str::<Value>(expanded.as_str()).unwrap();
} else { let node: MapNode = parsed.into();
new.set(&path[1..], make_leaf(expanded)); new.set(&path[1..], node);
} } else {
write_vars(|v| v.set_map(name, new, map_opts.flags.contains(MapFlags::LOCAL))); new.set(&path[1..], make_leaf(expanded));
} }
} else { write_vars(|v| v.set_map(name, new, map_opts.flags.contains(MapFlags::LOCAL)));
let expanded = arg.expand()?.get_words().join(" "); }
let path: Vec<String> = expanded.split('.').map(|s| s.to_string()).collect(); } else {
let Some(name) = path.first() else { let expanded = arg.expand()?.get_words().join(" ");
return Err(ShErr::simple( let path: Vec<String> = expanded.split('.').map(|s| s.to_string()).collect();
ShErrKind::InternalErr, let Some(name) = path.first() else {
format!("invalid map path: {}", expanded) return Err(ShErr::simple(
)); ShErrKind::InternalErr,
}; format!("invalid map path: {}", expanded),
));
};
if map_opts.flags.contains(MapFlags::REMOVE) { if map_opts.flags.contains(MapFlags::REMOVE) {
write_vars(|v| { write_vars(|v| {
if path.len() == 1 { if path.len() == 1 {
v.remove_map(name); v.remove_map(name);
} else { } else {
let Some(map) = v.get_map_mut(name) else { let Some(map) = v.get_map_mut(name) else {
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::ExecFail, ShErrKind::ExecFail,
format!("map not found: {}", name) format!("map not found: {}", name),
)); ));
}; };
map.remove(&path[1..]); map.remove(&path[1..]);
} }
Ok(()) Ok(())
})?; })?;
continue; continue;
} }
let json = map_opts.flags.contains(MapFlags::JSON); let json = map_opts.flags.contains(MapFlags::JSON);
let pretty = map_opts.flags.contains(MapFlags::PRETTY); let pretty = map_opts.flags.contains(MapFlags::PRETTY);
let keys = map_opts.flags.contains(MapFlags::KEYS); let keys = map_opts.flags.contains(MapFlags::KEYS);
let has_map = read_vars(|v| v.get_map(name).is_some()); let has_map = read_vars(|v| v.get_map(name).is_some());
if !has_map { if !has_map {
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::ExecFail, ShErrKind::ExecFail,
format!("map not found: {}", name) format!("map not found: {}", name),
)); ));
} }
let Some(node) = read_vars(|v| { let Some(node) = read_vars(|v| v.get_map(name).and_then(|map| map.get(&path[1..]).cloned()))
v.get_map(name) else {
.and_then(|map| map.get(&path[1..]).cloned()) state::set_status(1);
}) else { continue;
state::set_status(1); };
continue; let output = if keys {
}; node.keys().join(" ")
let output = if keys { } else {
node.keys().join(" ") node.display(json, pretty)?
} else { };
node.display(json, pretty)?
};
let stdout = borrow_fd(STDOUT_FILENO); let stdout = borrow_fd(STDOUT_FILENO);
write(stdout, output.as_bytes())?; write(stdout, output.as_bytes())?;
write(stdout, b"\n")?; write(stdout, b"\n")?;
} }
} }
state::set_status(0); state::set_status(0);
Ok(()) Ok(())
} }
pub fn get_map_opts(opts: Vec<Opt>) -> MapOpts { pub fn get_map_opts(opts: Vec<Opt>) -> MapOpts {
let mut map_opts = MapOpts { let mut map_opts = MapOpts {
flags: MapFlags::empty() flags: MapFlags::empty(),
}; };
for opt in opts { for opt in opts {
match opt { match opt {
Opt::Short('r') => map_opts.flags |= MapFlags::REMOVE, Opt::Short('r') => map_opts.flags |= MapFlags::REMOVE,
Opt::Short('j') => map_opts.flags |= MapFlags::JSON, Opt::Short('j') => map_opts.flags |= MapFlags::JSON,
Opt::Short('k') => map_opts.flags |= MapFlags::KEYS, Opt::Short('k') => map_opts.flags |= MapFlags::KEYS,
Opt::Short('l') => map_opts.flags |= MapFlags::LOCAL, Opt::Short('l') => map_opts.flags |= MapFlags::LOCAL,
Opt::Long(ref s) if s == "pretty" => map_opts.flags |= MapFlags::PRETTY, Opt::Long(ref s) if s == "pretty" => map_opts.flags |= MapFlags::PRETTY,
Opt::Short('F') => map_opts.flags |= MapFlags::FUNC, Opt::Short('F') => map_opts.flags |= MapFlags::FUNC,
_ => unreachable!() _ => unreachable!(),
} }
} }
map_opts map_opts
} }

View File

@@ -1,9 +1,8 @@
use crate::{ use crate::{libsh::error::ShResult, state};
libsh::error::ShResult,
state,
};
pub mod alias; pub mod alias;
pub mod arrops;
pub mod autocmd;
pub mod cd; pub mod cd;
pub mod complete; pub mod complete;
pub mod dirstack; pub mod dirstack;
@@ -11,7 +10,11 @@ pub mod echo;
pub mod eval; pub mod eval;
pub mod exec; pub mod exec;
pub mod flowctl; pub mod flowctl;
pub mod getopts;
pub mod intro;
pub mod jobctl; pub mod jobctl;
pub mod keymap;
pub mod map;
pub mod pwd; pub mod pwd;
pub mod read; pub mod read;
pub mod shift; pub mod shift;
@@ -21,19 +24,13 @@ pub mod test; // [[ ]] thing
pub mod trap; pub mod trap;
pub mod varcmds; pub mod varcmds;
pub mod zoltraak; pub mod zoltraak;
pub mod map;
pub mod arrops;
pub mod intro;
pub mod getopts;
pub mod keymap;
pub mod autocmd;
pub const BUILTINS: [&str; 47] = [ pub const BUILTINS: [&str; 47] = [
"echo", "cd", "read", "export", "local", "pwd", "source", "shift", "jobs", "fg", "bg", "disown", "echo", "cd", "read", "export", "local", "pwd", "source", "shift", "jobs", "fg", "bg", "disown",
"alias", "unalias", "return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "alias", "unalias", "return", "break", "continue", "exit", "zoltraak", "shopt", "builtin",
"command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly", "command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly",
"unset", "complete", "compgen", "map", "pop", "fpop", "push", "fpush", "rotate", "wait", "type", "unset", "complete", "compgen", "map", "pop", "fpop", "push", "fpush", "rotate", "wait", "type",
"getopts", "keymap", "read_key", "autocmd" "getopts", "keymap", "read_key", "autocmd",
]; ];
pub fn true_builtin() -> ShResult<()> { pub fn true_builtin() -> ShResult<()> {

View File

@@ -6,7 +6,16 @@ use nix::{
}; };
use crate::{ use crate::{
expand::expand_keymap, getopt::{Opt, OptSpec, get_opts_from_tokens}, libsh::{error::{ShErr, ShErrKind, ShResult, ShResultExt}, sys::TTY_FILENO}, parse::{NdRule, Node, execute::prepare_argv}, procio::borrow_fd, readline::term::{KeyReader, PollReader, RawModeGuard}, state::{self, VarFlags, VarKind, read_vars, write_vars} expand::expand_keymap,
getopt::{Opt, OptSpec, get_opts_from_tokens},
libsh::{
error::{ShErr, ShErrKind, ShResult, ShResultExt},
sys::TTY_FILENO,
},
parse::{NdRule, Node, execute::prepare_argv},
procio::borrow_fd,
readline::term::{KeyReader, PollReader, RawModeGuard},
state::{self, VarFlags, VarKind, read_vars, write_vars},
}; };
pub const READ_OPTS: [OptSpec; 7] = [ pub const READ_OPTS: [OptSpec; 7] = [
@@ -40,19 +49,19 @@ pub const READ_OPTS: [OptSpec; 7] = [
}, // read until delimiter }, // read until delimiter
]; ];
pub const READ_KEY_OPTS: [OptSpec;3] = [ pub const READ_KEY_OPTS: [OptSpec; 3] = [
OptSpec { OptSpec {
opt: Opt::Short('v'), // var name opt: Opt::Short('v'), // var name
takes_arg: true takes_arg: true,
}, },
OptSpec { OptSpec {
opt: Opt::Short('w'), // char whitelist opt: Opt::Short('w'), // char whitelist
takes_arg: true takes_arg: true,
}, },
OptSpec { OptSpec {
opt: Opt::Short('b'), // char blacklist opt: Opt::Short('b'), // char blacklist
takes_arg: true takes_arg: true,
} },
]; ];
bitflags! { bitflags! {
@@ -84,7 +93,9 @@ pub fn read_builtin(node: Node) -> ShResult<()> {
let (argv, opts) = get_opts_from_tokens(argv, &READ_OPTS)?; let (argv, opts) = get_opts_from_tokens(argv, &READ_OPTS)?;
let read_opts = get_read_flags(opts).blame(blame.clone())?; let read_opts = get_read_flags(opts).blame(blame.clone())?;
let mut argv = prepare_argv(argv)?; let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); } if !argv.is_empty() {
argv.remove(0);
}
if let Some(prompt) = read_opts.prompt { if let Some(prompt) = read_opts.prompt {
write(borrow_fd(STDOUT_FILENO), prompt.as_bytes())?; write(borrow_fd(STDOUT_FILENO), prompt.as_bytes())?;
@@ -257,96 +268,98 @@ pub fn get_read_flags(opts: Vec<Opt>) -> ShResult<ReadOpts> {
} }
pub struct ReadKeyOpts { pub struct ReadKeyOpts {
var_name: Option<String>, var_name: Option<String>,
char_whitelist: Option<String>, char_whitelist: Option<String>,
char_blacklist: Option<String> char_blacklist: Option<String>,
} }
pub fn read_key(node: Node) -> ShResult<()> { pub fn read_key(node: Node) -> ShResult<()> {
let blame = node.get_span().clone(); let blame = node.get_span().clone();
let NdRule::Command { argv, .. } = node.class else { unreachable!() }; let NdRule::Command { argv, .. } = node.class else {
unreachable!()
};
if !isatty(*TTY_FILENO)? { if !isatty(*TTY_FILENO)? {
state::set_status(1); state::set_status(1);
return Ok(()); return Ok(());
} }
let (_, opts) = get_opts_from_tokens(argv, &READ_KEY_OPTS).blame(blame.clone())?; let (_, opts) = get_opts_from_tokens(argv, &READ_KEY_OPTS).blame(blame.clone())?;
let read_key_opts = get_read_key_opts(opts).blame(blame.clone())?; let read_key_opts = get_read_key_opts(opts).blame(blame.clone())?;
let key = { let key = {
let _raw = crate::readline::term::raw_mode(); let _raw = crate::readline::term::raw_mode();
let mut buf = [0u8; 16]; let mut buf = [0u8; 16];
match read(*TTY_FILENO, &mut buf) { match read(*TTY_FILENO, &mut buf) {
Ok(0) => { Ok(0) => {
state::set_status(1); state::set_status(1);
return Ok(()); return Ok(());
} }
Ok(n) => { Ok(n) => {
let mut reader = PollReader::new(); let mut reader = PollReader::new();
reader.feed_bytes(&buf[..n], false); reader.feed_bytes(&buf[..n], false);
let Some(key) = reader.read_key()? else { let Some(key) = reader.read_key()? else {
state::set_status(1); state::set_status(1);
return Ok(()); return Ok(());
}; };
key key
}, }
Err(Errno::EINTR) => { Err(Errno::EINTR) => {
state::set_status(130); state::set_status(130);
return Ok(()); return Ok(());
} }
Err(e) => return Err(ShErr::simple(ShErrKind::ExecFail, format!("read_key: {e}"))), Err(e) => return Err(ShErr::simple(ShErrKind::ExecFail, format!("read_key: {e}"))),
} }
}; };
let vim_seq = key.as_vim_seq()?; let vim_seq = key.as_vim_seq()?;
if let Some(wl) = read_key_opts.char_whitelist { if let Some(wl) = read_key_opts.char_whitelist {
let allowed = expand_keymap(&wl); let allowed = expand_keymap(&wl);
if !allowed.contains(&key) { if !allowed.contains(&key) {
state::set_status(1); state::set_status(1);
return Ok(()); return Ok(());
} }
} }
if let Some(bl) = read_key_opts.char_blacklist { if let Some(bl) = read_key_opts.char_blacklist {
let disallowed = expand_keymap(&bl); let disallowed = expand_keymap(&bl);
if disallowed.contains(&key) { if disallowed.contains(&key) {
state::set_status(1); state::set_status(1);
return Ok(()); return Ok(());
} }
} }
if let Some(var) = read_key_opts.var_name { if let Some(var) = read_key_opts.var_name {
write_vars(|v| v.set_var(&var, VarKind::Str(vim_seq), VarFlags::NONE))?; write_vars(|v| v.set_var(&var, VarKind::Str(vim_seq), VarFlags::NONE))?;
} else { } else {
write(borrow_fd(STDOUT_FILENO), vim_seq.as_bytes())?; write(borrow_fd(STDOUT_FILENO), vim_seq.as_bytes())?;
} }
state::set_status(0); state::set_status(0);
Ok(()) Ok(())
} }
pub fn get_read_key_opts(opts: Vec<Opt>) -> ShResult<ReadKeyOpts> { pub fn get_read_key_opts(opts: Vec<Opt>) -> ShResult<ReadKeyOpts> {
let mut read_key_opts = ReadKeyOpts { let mut read_key_opts = ReadKeyOpts {
var_name: None, var_name: None,
char_whitelist: None, char_whitelist: None,
char_blacklist: None char_blacklist: None,
}; };
for opt in opts { for opt in opts {
match opt { match opt {
Opt::ShortWithArg('v', var_name) => read_key_opts.var_name = Some(var_name), Opt::ShortWithArg('v', var_name) => read_key_opts.var_name = Some(var_name),
Opt::ShortWithArg('w', char_whitelist) => read_key_opts.char_whitelist = Some(char_whitelist), Opt::ShortWithArg('w', char_whitelist) => read_key_opts.char_whitelist = Some(char_whitelist),
Opt::ShortWithArg('b', char_blacklist) => read_key_opts.char_blacklist = Some(char_blacklist), Opt::ShortWithArg('b', char_blacklist) => read_key_opts.char_blacklist = Some(char_blacklist),
_ => { _ => {
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::ExecFail, ShErrKind::ExecFail,
format!("read_key: Unexpected flag '{opt}'") format!("read_key: Unexpected flag '{opt}'"),
)); ));
} }
} }
} }
Ok(read_key_opts) Ok(read_key_opts)
} }

View File

@@ -14,12 +14,18 @@ pub fn shift(node: Node) -> ShResult<()> {
}; };
let mut argv = prepare_argv(argv)?; let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); } if !argv.is_empty() {
argv.remove(0);
}
let mut argv = argv.into_iter(); let mut argv = argv.into_iter();
if let Some((arg, span)) = argv.next() { if let Some((arg, span)) = argv.next() {
let Ok(count) = arg.parse::<usize>() else { let Ok(count) = arg.parse::<usize>() else {
return Err(ShErr::at(ShErrKind::ExecFail, span, "Expected a number in shift args")); return Err(ShErr::at(
ShErrKind::ExecFail,
span,
"Expected a number in shift args",
));
}; };
for _ in 0..count { for _ in 0..count {
write_vars(|v| v.cur_scope_mut().fpop_arg()); write_vars(|v| v.cur_scope_mut().fpop_arg());

View File

@@ -16,7 +16,9 @@ pub fn shopt(node: Node) -> ShResult<()> {
}; };
let mut argv = prepare_argv(argv)?; let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); } if !argv.is_empty() {
argv.remove(0);
}
if argv.is_empty() { if argv.is_empty() {
let mut output = write_shopts(|s| s.display_opts())?; let mut output = write_shopts(|s| s.display_opts())?;

View File

@@ -15,15 +15,25 @@ pub fn source(node: Node) -> ShResult<()> {
}; };
let mut argv = prepare_argv(argv)?; let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); } if !argv.is_empty() {
argv.remove(0);
}
for (arg, span) in argv { for (arg, span) in argv {
let path = PathBuf::from(arg); let path = PathBuf::from(arg);
if !path.exists() { if !path.exists() {
return Err(ShErr::at(ShErrKind::ExecFail, span, format!("source: File '{}' not found", path.display()))); return Err(ShErr::at(
ShErrKind::ExecFail,
span,
format!("source: File '{}' not found", path.display()),
));
} }
if !path.is_file() { if !path.is_file() {
return Err(ShErr::at(ShErrKind::ExecFail, span, format!("source: Given path '{}' is not a file", path.display()))); return Err(ShErr::at(
ShErrKind::ExecFail,
span,
format!("source: Given path '{}' is not a file", path.display()),
));
} }
source_file(path)?; source_file(path)?;
} }

View File

@@ -61,10 +61,7 @@ impl FromStr for UnaryOp {
"-t" => Ok(Self::Terminal), "-t" => Ok(Self::Terminal),
"-n" => Ok(Self::NonNull), "-n" => Ok(Self::NonNull),
"-z" => Ok(Self::Null), "-z" => Ok(Self::Null),
_ => Err(ShErr::simple( _ => Err(ShErr::simple(ShErrKind::SyntaxErr, "Invalid test operator")),
ShErrKind::SyntaxErr,
"Invalid test operator",
)),
} }
} }
} }
@@ -97,10 +94,7 @@ impl FromStr for TestOp {
"-ge" => Ok(Self::IntGe), "-ge" => Ok(Self::IntGe),
"-le" => Ok(Self::IntLe), "-le" => Ok(Self::IntLe),
_ if TEST_UNARY_OPS.contains(&s) => Ok(Self::Unary(s.parse::<UnaryOp>()?)), _ if TEST_UNARY_OPS.contains(&s) => Ok(Self::Unary(s.parse::<UnaryOp>()?)),
_ => Err(ShErr::simple( _ => Err(ShErr::simple(ShErrKind::SyntaxErr, "Invalid test operator")),
ShErrKind::SyntaxErr,
"Invalid test operator",
)),
} }
} }
} }
@@ -138,7 +132,11 @@ pub fn double_bracket_test(node: Node) -> ShResult<bool> {
let operand = operand.expand()?.get_words().join(" "); let operand = operand.expand()?.get_words().join(" ");
conjunct_op = conjunct; conjunct_op = conjunct;
let TestOp::Unary(op) = TestOp::from_str(operator.as_str())? else { let TestOp::Unary(op) = TestOp::from_str(operator.as_str())? else {
return Err(ShErr::at(ShErrKind::SyntaxErr, err_span, "Invalid unary operator")); return Err(ShErr::at(
ShErrKind::SyntaxErr,
err_span,
"Invalid unary operator",
));
}; };
match op { match op {
UnaryOp::Exists => { UnaryOp::Exists => {
@@ -241,7 +239,11 @@ pub fn double_bracket_test(node: Node) -> ShResult<bool> {
let test_op = operator.as_str().parse::<TestOp>()?; let test_op = operator.as_str().parse::<TestOp>()?;
match test_op { match test_op {
TestOp::Unary(_) => { TestOp::Unary(_) => {
return Err(ShErr::at(ShErrKind::SyntaxErr, err_span, "Expected a binary operator in this test call; found a unary operator")); return Err(ShErr::at(
ShErrKind::SyntaxErr,
err_span,
"Expected a binary operator in this test call; found a unary operator",
));
} }
TestOp::StringEq => { TestOp::StringEq => {
let pattern = crate::expand::glob_to_regex(rhs.trim(), true); let pattern = crate::expand::glob_to_regex(rhs.trim(), true);
@@ -257,7 +259,11 @@ pub fn double_bracket_test(node: Node) -> ShResult<bool> {
| TestOp::IntGe | TestOp::IntGe
| TestOp::IntLe | TestOp::IntLe
| TestOp::IntEq => { | TestOp::IntEq => {
let err = ShErr::at(ShErrKind::SyntaxErr, err_span.clone(), format!("Expected an integer with '{}' operator", operator)); let err = ShErr::at(
ShErrKind::SyntaxErr,
err_span.clone(),
format!("Expected an integer with '{}' operator", operator),
);
let Ok(lhs) = lhs.trim().parse::<i32>() else { let Ok(lhs) = lhs.trim().parse::<i32>() else {
return Err(err); return Err(err);
}; };

View File

@@ -122,7 +122,9 @@ pub fn trap(node: Node) -> ShResult<()> {
}; };
let mut argv = prepare_argv(argv)?; let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); } if !argv.is_empty() {
argv.remove(0);
}
if argv.is_empty() { if argv.is_empty() {
let stdout = borrow_fd(STDOUT_FILENO); let stdout = borrow_fd(STDOUT_FILENO);

View File

@@ -16,7 +16,11 @@ pub fn readonly(node: Node) -> ShResult<()> {
}; };
// Remove "readonly" from argv // Remove "readonly" from argv
let argv = if !argv.is_empty() { &argv[1..] } else { &argv[..] }; let argv = if !argv.is_empty() {
&argv[1..]
} else {
&argv[..]
};
if argv.is_empty() { if argv.is_empty() {
// Display the local variables // Display the local variables
@@ -67,15 +71,25 @@ pub fn unset(node: Node) -> ShResult<()> {
}; };
let mut argv = prepare_argv(argv)?; let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); } if !argv.is_empty() {
argv.remove(0);
}
if argv.is_empty() { if argv.is_empty() {
return Err(ShErr::at(ShErrKind::SyntaxErr, blame, "unset: Expected at least one argument")); return Err(ShErr::at(
ShErrKind::SyntaxErr,
blame,
"unset: Expected at least one argument",
));
} }
for (arg, span) in argv { for (arg, span) in argv {
if !read_vars(|v| v.var_exists(&arg)) { if !read_vars(|v| v.var_exists(&arg)) {
return Err(ShErr::at(ShErrKind::ExecFail, span, format!("unset: No such variable '{arg}'"))); return Err(ShErr::at(
ShErrKind::ExecFail,
span,
format!("unset: No such variable '{arg}'"),
));
} }
write_vars(|v| v.unset_var(&arg))?; write_vars(|v| v.unset_var(&arg))?;
} }
@@ -94,7 +108,11 @@ pub fn export(node: Node) -> ShResult<()> {
}; };
// Remove "export" from argv // Remove "export" from argv
let argv = if !argv.is_empty() { &argv[1..] } else { &argv[..] }; let argv = if !argv.is_empty() {
&argv[1..]
} else {
&argv[..]
};
if argv.is_empty() { if argv.is_empty() {
// Display the environment variables // Display the environment variables
@@ -137,7 +155,11 @@ pub fn local(node: Node) -> ShResult<()> {
}; };
// Remove "local" from argv // Remove "local" from argv
let argv = if !argv.is_empty() { &argv[1..] } else { &argv[..] }; let argv = if !argv.is_empty() {
&argv[1..]
} else {
&argv[..]
};
if argv.is_empty() { if argv.is_empty() {
// Display the local variables // Display the local variables

View File

@@ -104,7 +104,9 @@ pub fn zoltraak(node: Node) -> ShResult<()> {
} }
let mut argv = prepare_argv(argv)?; let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); } if !argv.is_empty() {
argv.remove(0);
}
for (arg, span) in argv { for (arg, span) in argv {
if &arg == "/" && !flags.contains(ZoltFlags::NO_PRESERVE_ROOT) { if &arg == "/" && !flags.contains(ZoltFlags::NO_PRESERVE_ROOT) {
@@ -113,9 +115,7 @@ pub fn zoltraak(node: Node) -> ShResult<()> {
ShErrKind::ExecFail, ShErrKind::ExecFail,
"zoltraak: Attempted to destroy root directory '/'", "zoltraak: Attempted to destroy root directory '/'",
) )
.with_note( .with_note("If you really want to do this, you can use the --no-preserve-root flag"),
"If you really want to do this, you can use the --no-preserve-root flag"
),
); );
} }
annihilate(&arg, flags).blame(span)? annihilate(&arg, flags).blame(span)?
@@ -176,9 +176,7 @@ fn annihilate(path: &str, flags: ZoltFlags) -> ShResult<()> {
ShErrKind::ExecFail, ShErrKind::ExecFail,
format!("zoltraak: '{path}' is a directory"), format!("zoltraak: '{path}' is a directory"),
) )
.with_note( .with_note("Use the '-r' flag to recursively shred directories"),
"Use the '-r' flag to recursively shred directories"
),
); );
} }
} }

View File

@@ -10,13 +10,14 @@ use crate::libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color};
use crate::parse::execute::exec_input; use crate::parse::execute::exec_input;
use crate::parse::lex::{LexFlags, LexStream, QuoteState, Tk, TkFlags, TkRule, is_hard_sep}; use crate::parse::lex::{LexFlags, LexStream, QuoteState, Tk, TkFlags, TkRule, is_hard_sep};
use crate::parse::{Redir, RedirType}; use crate::parse::{Redir, RedirType};
use crate::prelude::*;
use crate::procio::{IoBuf, IoFrame, IoMode, IoStack}; use crate::procio::{IoBuf, IoFrame, IoMode, IoStack};
use crate::readline::keys::{KeyCode, KeyEvent, ModKeys}; use crate::readline::keys::{KeyCode, KeyEvent, ModKeys};
use crate::readline::markers; use crate::readline::markers;
use crate::state::{ use crate::state::{
self, ArrIndex, LogTab, VarFlags, VarKind, read_jobs, read_logic, read_shopts, read_vars, write_jobs, write_meta, write_vars self, ArrIndex, LogTab, VarFlags, VarKind, read_jobs, read_logic, read_shopts, read_vars,
write_jobs, write_meta, write_vars,
}; };
use crate::prelude::*;
const PARAMETERS: [char; 7] = ['@', '*', '#', '$', '?', '!', '0']; const PARAMETERS: [char; 7] = ['@', '*', '#', '$', '?', '!', '0'];
@@ -25,9 +26,7 @@ impl Tk {
pub fn expand(self) -> ShResult<Self> { pub fn expand(self) -> ShResult<Self> {
let flags = self.flags; let flags = self.flags;
let span = self.span.clone(); let span = self.span.clone();
let exp = Expander::new(self)? let exp = Expander::new(self)?.expand().promote_err(span.clone())?;
.expand()
.promote_err(span.clone())?;
let class = TkRule::Expanded { exp }; let class = TkRule::Expanded { exp };
Ok(Self { class, span, flags }) Ok(Self { class, span, flags })
} }
@@ -251,14 +250,14 @@ fn get_brace_parts(word: &str) -> Option<(String, String, String)> {
prefix.push(next); prefix.push(next);
} }
} }
'\'' => { '\'' => {
qt_state.toggle_single(); qt_state.toggle_single();
prefix.push(ch); prefix.push(ch);
} }
'"' => { '"' => {
qt_state.toggle_double(); qt_state.toggle_double();
prefix.push(ch); prefix.push(ch);
} }
'{' if qt_state.outside() => { '{' if qt_state.outside() => {
break; break;
} }
@@ -279,14 +278,14 @@ fn get_brace_parts(word: &str) -> Option<(String, String, String)> {
inner.push(next); inner.push(next);
} }
} }
'\'' => { '\'' => {
qt_state.toggle_single(); qt_state.toggle_single();
inner.push(ch); inner.push(ch);
} }
'"' => { '"' => {
qt_state.toggle_double(); qt_state.toggle_double();
inner.push(ch); inner.push(ch);
} }
'{' if qt_state.outside() => { '{' if qt_state.outside() => {
depth += 1; depth += 1;
inner.push(ch); inner.push(ch);
@@ -330,14 +329,14 @@ fn split_brace_inner(inner: &str) -> Vec<String> {
current.push(next); current.push(next);
} }
} }
'\'' => { '\'' => {
qt_state.toggle_single(); qt_state.toggle_single();
current.push(ch); current.push(ch);
} }
'"' => { '"' => {
qt_state.toggle_double(); qt_state.toggle_double();
current.push(ch); current.push(ch);
} }
'{' if qt_state.outside() => { '{' if qt_state.outside() => {
depth += 1; depth += 1;
current.push(ch); current.push(ch);
@@ -534,11 +533,9 @@ pub fn expand_var(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
let arg_sep = markers::ARG_SEP.to_string(); let arg_sep = markers::ARG_SEP.to_string();
read_vars(|v| v.get_arr_elems(&var_name))?.join(&arg_sep) read_vars(|v| v.get_arr_elems(&var_name))?.join(&arg_sep)
} }
ArrIndex::ArgCount => { ArrIndex::ArgCount => read_vars(|v| v.get_arr_elems(&var_name))
read_vars(|v| v.get_arr_elems(&var_name)) .map(|elems| elems.len().to_string())
.map(|elems| elems.len().to_string()) .unwrap_or_else(|_| "0".to_string()),
.unwrap_or_else(|_| "0".to_string())
}
ArrIndex::AllJoined => { ArrIndex::AllJoined => {
let ifs = read_vars(|v| v.try_get_var("IFS")) let ifs = read_vars(|v| v.try_get_var("IFS"))
.unwrap_or_else(|| " \t\n".to_string()) .unwrap_or_else(|| " \t\n".to_string())
@@ -653,7 +650,7 @@ enum ArithTk {
Op(ArithOp), Op(ArithOp),
LParen, LParen,
RParen, RParen,
Var(String) Var(String),
} }
impl ArithTk { impl ArithTk {
@@ -695,21 +692,21 @@ impl ArithTk {
tokens.push(Self::RParen); tokens.push(Self::RParen);
chars.next(); chars.next();
} }
_ if ch.is_alphabetic() || ch == '_' => { _ if ch.is_alphabetic() || ch == '_' => {
chars.next(); chars.next();
let mut var_name = ch.to_string(); let mut var_name = ch.to_string();
while let Some(ch) = chars.peek() { while let Some(ch) = chars.peek() {
match ch { match ch {
_ if ch.is_alphabetic() || *ch == '_' => { _ if ch.is_alphabetic() || *ch == '_' => {
var_name.push(*ch); var_name.push(*ch);
chars.next(); chars.next();
} }
_ => break _ => break,
} }
} }
tokens.push(Self::Var(var_name)); tokens.push(Self::Var(var_name));
} }
_ => { _ => {
return Ok(None); return Ok(None);
} }
@@ -753,22 +750,28 @@ impl ArithTk {
} }
} }
} }
ArithTk::Var(var) => { ArithTk::Var(var) => {
let Some(val) = read_vars(|v| v.try_get_var(&var)) else { let Some(val) = read_vars(|v| v.try_get_var(&var)) else {
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::NotFound, ShErrKind::NotFound,
format!("Undefined variable in arithmetic expression: '{}'",var.fg(next_color())), format!(
)); "Undefined variable in arithmetic expression: '{}'",
}; var.fg(next_color())
let Ok(num) = val.parse::<f64>() else { ),
return Err(ShErr::simple( ));
ShErrKind::ParseErr, };
format!("Variable '{}' does not contain a number", var.fg(next_color())), let Ok(num) = val.parse::<f64>() else {
)); return Err(ShErr::simple(
}; ShErrKind::ParseErr,
format!(
"Variable '{}' does not contain a number",
var.fg(next_color())
),
));
};
output.push(ArithTk::Num(num)); output.push(ArithTk::Num(num));
} }
} }
} }
@@ -785,14 +788,14 @@ impl ArithTk {
match token { match token {
ArithTk::Num(n) => stack.push(n), ArithTk::Num(n) => stack.push(n),
ArithTk::Op(op) => { ArithTk::Op(op) => {
let rhs = stack.pop().ok_or(ShErr::simple( let rhs = stack.pop().ok_or(ShErr::simple(
ShErrKind::ParseErr, ShErrKind::ParseErr,
"Missing right-hand operand", "Missing right-hand operand",
))?; ))?;
let lhs = stack.pop().ok_or(ShErr::simple( let lhs = stack.pop().ok_or(ShErr::simple(
ShErrKind::ParseErr, ShErrKind::ParseErr,
"Missing left-hand operand", "Missing left-hand operand",
))?; ))?;
let result = match op { let result = match op {
ArithOp::Add => lhs + rhs, ArithOp::Add => lhs + rhs,
ArithOp::Sub => lhs - rhs, ArithOp::Sub => lhs - rhs,
@@ -803,19 +806,19 @@ impl ArithTk {
stack.push(result); stack.push(result);
} }
_ => { _ => {
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::ParseErr, ShErrKind::ParseErr,
"Unexpected token during evaluation", "Unexpected token during evaluation",
)); ));
} }
} }
} }
if stack.len() != 1 { if stack.len() != 1 {
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::ParseErr, ShErrKind::ParseErr,
"Invalid arithmetic expression", "Invalid arithmetic expression",
)); ));
} }
Ok(stack[0]) Ok(stack[0])
@@ -840,10 +843,10 @@ impl FromStr for ArithOp {
'*' => Ok(Self::Mul), '*' => Ok(Self::Mul),
'/' => Ok(Self::Div), '/' => Ok(Self::Div),
'%' => Ok(Self::Mod), '%' => Ok(Self::Mod),
_ => Err(ShErr::simple( _ => Err(ShErr::simple(
ShErrKind::ParseErr, ShErrKind::ParseErr,
"Invalid arithmetic operator", "Invalid arithmetic operator",
)), )),
} }
} }
} }
@@ -853,8 +856,8 @@ pub fn expand_arithmetic(raw: &str) -> ShResult<Option<String>> {
let unescaped = unescape_math(body); let unescaped = unescape_math(body);
let expanded = expand_raw(&mut unescaped.chars().peekable())?; let expanded = expand_raw(&mut unescaped.chars().peekable())?;
let Some(tokens) = ArithTk::tokenize(&expanded)? else { let Some(tokens) = ArithTk::tokenize(&expanded)? else {
return Ok(None); return Ok(None);
}; };
let rpn = ArithTk::to_rpn(tokens)?; let rpn = ArithTk::to_rpn(tokens)?;
let result = ArithTk::eval_rpn(rpn)?; let result = ArithTk::eval_rpn(rpn)?;
Ok(Some(result.to_string())) Ok(Some(result.to_string()))
@@ -894,7 +897,12 @@ pub fn expand_proc_sub(raw: &str, is_input: bool) -> ShResult<String> {
let mut io_stack = IoStack::new(); let mut io_stack = IoStack::new();
io_stack.push_frame(io_frame); io_stack.push_frame(io_frame);
if let Err(e) = exec_input(raw.to_string(), Some(io_stack), false, Some("process_sub".into())) { if let Err(e) = exec_input(
raw.to_string(),
Some(io_stack),
false,
Some("process_sub".into()),
) {
e.print_error(); e.print_error();
exit(1); exit(1);
} }
@@ -925,11 +933,16 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult<String> {
match unsafe { fork()? } { match unsafe { fork()? } {
ForkResult::Child => { ForkResult::Child => {
io_stack.push_frame(cmd_sub_io_frame); io_stack.push_frame(cmd_sub_io_frame);
if let Err(e) = exec_input(raw.to_string(), Some(io_stack), false, Some("command_sub".into())) { if let Err(e) = exec_input(
raw.to_string(),
Some(io_stack),
false,
Some("command_sub".into()),
) {
e.print_error(); e.print_error();
unsafe { libc::_exit(1) }; unsafe { libc::_exit(1) };
} }
let status = state::get_status(); let status = state::get_status();
unsafe { libc::_exit(status) }; unsafe { libc::_exit(status) };
} }
ForkResult::Parent { child } => { ForkResult::Parent { child } => {
@@ -956,9 +969,9 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult<String> {
match status { match status {
WtStat::Exited(_, code) => { WtStat::Exited(_, code) => {
state::set_status(code); state::set_status(code);
Ok(io_buf.as_str()?.trim_end().to_string()) Ok(io_buf.as_str()?.trim_end().to_string())
}, }
_ => Err(ShErr::simple(ShErrKind::InternalErr, "Command sub failed")), _ => Err(ShErr::simple(ShErrKind::InternalErr, "Command sub failed")),
} }
} }
@@ -1062,76 +1075,76 @@ pub fn unescape_str(raw: &str) -> String {
} }
} }
} }
'$' => { '$' => {
log::debug!("Found ANSI-C quoting"); log::debug!("Found ANSI-C quoting");
chars.next(); chars.next();
while let Some(q_ch) = chars.next() { while let Some(q_ch) = chars.next() {
match q_ch { match q_ch {
'\'' => { '\'' => {
break; break;
} }
'\\' => { '\\' => {
if let Some(esc) = chars.next() { if let Some(esc) = chars.next() {
match esc { match esc {
'n' => result.push('\n'), 'n' => result.push('\n'),
't' => result.push('\t'), 't' => result.push('\t'),
'r' => result.push('\r'), 'r' => result.push('\r'),
'\'' => result.push('\''), '\'' => result.push('\''),
'\\' => result.push('\\'), '\\' => result.push('\\'),
'a' => result.push('\x07'), 'a' => result.push('\x07'),
'b' => result.push('\x08'), 'b' => result.push('\x08'),
'e' | 'E' => result.push('\x1b'), 'e' | 'E' => result.push('\x1b'),
'v' => result.push('\x0b'), 'v' => result.push('\x0b'),
'x' => { 'x' => {
let mut hex = String::new(); let mut hex = String::new();
if let Some(h1) = chars.next() { if let Some(h1) = chars.next() {
hex.push(h1); hex.push(h1);
} else { } else {
result.push_str("\\x"); result.push_str("\\x");
continue; continue;
} }
if let Some(h2) = chars.next() { if let Some(h2) = chars.next() {
hex.push(h2); hex.push(h2);
} else { } else {
result.push_str(&format!("\\x{hex}")); result.push_str(&format!("\\x{hex}"));
continue; continue;
} }
if let Ok(byte) = u8::from_str_radix(&hex, 16) { if let Ok(byte) = u8::from_str_radix(&hex, 16) {
result.push(byte as char); result.push(byte as char);
} else { } else {
result.push_str(&format!("\\x{hex}")); result.push_str(&format!("\\x{hex}"));
continue; continue;
} }
} }
'o' => { 'o' => {
let mut oct = String::new(); let mut oct = String::new();
for _ in 0..3 { for _ in 0..3 {
if let Some(o) = chars.peek() { if let Some(o) = chars.peek() {
if o.is_digit(8) { if o.is_digit(8) {
oct.push(*o); oct.push(*o);
chars.next(); chars.next();
} else { } else {
break; break;
} }
} else { } else {
break; break;
} }
} }
if let Ok(byte) = u8::from_str_radix(&oct, 8) { if let Ok(byte) = u8::from_str_radix(&oct, 8) {
result.push(byte as char); result.push(byte as char);
} else { } else {
result.push_str(&format!("\\o{oct}")); result.push_str(&format!("\\o{oct}"));
continue; continue;
} }
} }
_ => result.push(esc), _ => result.push(esc),
} }
} }
} }
_ => result.push(q_ch), _ => result.push(q_ch),
} }
} }
} }
'"' => { '"' => {
result.push(markers::DUB_QUOTE); result.push(markers::DUB_QUOTE);
break; break;
@@ -1144,14 +1157,14 @@ pub fn unescape_str(raw: &str) -> String {
result.push(markers::SNG_QUOTE); result.push(markers::SNG_QUOTE);
while let Some(q_ch) = chars.next() { while let Some(q_ch) = chars.next() {
match q_ch { match q_ch {
'\\' => { '\\' => {
if chars.peek() == Some(&'\'') { if chars.peek() == Some(&'\'') {
result.push('\''); result.push('\'');
chars.next(); chars.next();
} else { } else {
result.push('\\'); result.push('\\');
} }
} }
'\'' => { '\'' => {
result.push(markers::SNG_QUOTE); result.push(markers::SNG_QUOTE);
break; break;
@@ -1219,7 +1232,7 @@ pub fn unescape_str(raw: &str) -> String {
} }
} }
'$' if chars.peek() == Some(&'\'') => { '$' if chars.peek() == Some(&'\'') => {
log::debug!("Found ANSI-C quoting"); log::debug!("Found ANSI-C quoting");
chars.next(); chars.next();
result.push(markers::SNG_QUOTE); result.push(markers::SNG_QUOTE);
while let Some(q_ch) = chars.next() { while let Some(q_ch) = chars.next() {
@@ -1387,13 +1400,13 @@ impl FromStr for ParamExp {
use ParamExp::*; use ParamExp::*;
let parse_err = || { let parse_err = || {
Err(ShErr::simple( Err(ShErr::simple(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
"Invalid parameter expansion", "Invalid parameter expansion",
) ) ))
}; };
log::debug!("Parsing parameter expansion: '{:?}'", s); log::debug!("Parsing parameter expansion: '{:?}'", s);
// Handle indirect var expansion: ${!var} // Handle indirect var expansion: ${!var}
if let Some(var) = s.strip_prefix('!') { if let Some(var) = s.strip_prefix('!') {
@@ -1410,7 +1423,7 @@ impl FromStr for ParamExp {
return Ok(RemShortestPrefix(rest.to_string())); return Ok(RemShortestPrefix(rest.to_string()));
} }
if let Some(rest) = s.strip_prefix("%%") { if let Some(rest) = s.strip_prefix("%%") {
log::debug!("Matched longest suffix pattern: '{}'", rest); log::debug!("Matched longest suffix pattern: '{}'", rest);
return Ok(RemLongestSuffix(rest.to_string())); return Ok(RemLongestSuffix(rest.to_string()));
} else if let Some(rest) = s.strip_prefix('%') { } else if let Some(rest) = s.strip_prefix('%') {
return Ok(RemShortestSuffix(rest.to_string())); return Ok(RemShortestSuffix(rest.to_string()));
@@ -1476,11 +1489,11 @@ impl FromStr for ParamExp {
pub fn parse_pos_len(s: &str) -> Option<(usize, Option<usize>)> { pub fn parse_pos_len(s: &str) -> Option<(usize, Option<usize>)> {
let raw = s.strip_prefix(':')?; let raw = s.strip_prefix(':')?;
if let Some((start, len)) = raw.split_once(':') { if let Some((start, len)) = raw.split_once(':') {
let start = expand_raw(&mut start.chars().peekable()).unwrap_or_else(|_| start.to_string()); let start = expand_raw(&mut start.chars().peekable()).unwrap_or_else(|_| start.to_string());
let len = expand_raw(&mut len.chars().peekable()).unwrap_or_else(|_| len.to_string()); let len = expand_raw(&mut len.chars().peekable()).unwrap_or_else(|_| len.to_string());
Some((start.parse::<usize>().ok()?, len.parse::<usize>().ok())) Some((start.parse::<usize>().ok()?, len.parse::<usize>().ok()))
} else { } else {
let raw = expand_raw(&mut raw.chars().peekable()).unwrap_or_else(|_| raw.to_string()); let raw = expand_raw(&mut raw.chars().peekable()).unwrap_or_else(|_| raw.to_string());
Some((raw.parse::<usize>().ok()?, None)) Some((raw.parse::<usize>().ok()?, None))
} }
} }
@@ -1554,10 +1567,7 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
Some(val) => Ok(val), Some(val) => Ok(val),
None => { None => {
let expanded = expand_raw(&mut err.chars().peekable())?; let expanded = expand_raw(&mut err.chars().peekable())?;
Err(ShErr::simple( Err(ShErr::simple(ShErrKind::ExecFail, expanded))
ShErrKind::ExecFail,
expanded,
))
} }
} }
} }
@@ -1565,10 +1575,7 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
Some(val) => Ok(val), Some(val) => Ok(val),
None => { None => {
let expanded = expand_raw(&mut err.chars().peekable())?; let expanded = expand_raw(&mut err.chars().peekable())?;
Err(ShErr::simple( Err(ShErr::simple(ShErrKind::ExecFail, expanded))
ShErrKind::ExecFail,
expanded,
))
} }
}, },
ParamExp::Substr(pos) => { ParamExp::Substr(pos) => {
@@ -1630,7 +1637,8 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
ParamExp::RemLongestSuffix(suffix) => { ParamExp::RemLongestSuffix(suffix) => {
let value = vars.get_var(&var_name); let value = vars.get_var(&var_name);
let unescaped = unescape_str(&suffix); let unescaped = unescape_str(&suffix);
let expanded_suffix = expand_raw(&mut unescaped.chars().peekable()).unwrap_or(suffix.clone()); let expanded_suffix =
expand_raw(&mut unescaped.chars().peekable()).unwrap_or(suffix.clone());
let pattern = Pattern::new(&expanded_suffix).unwrap(); let pattern = Pattern::new(&expanded_suffix).unwrap();
for i in 0..=value.len() { for i in 0..=value.len() {
let sliced = &value[i..]; let sliced = &value[i..];
@@ -2299,82 +2307,82 @@ pub fn expand_aliases(
} }
pub fn expand_keymap(s: &str) -> Vec<KeyEvent> { pub fn expand_keymap(s: &str) -> Vec<KeyEvent> {
let mut keys = Vec::new(); let mut keys = Vec::new();
let mut chars = s.chars().collect::<VecDeque<char>>(); let mut chars = s.chars().collect::<VecDeque<char>>();
while let Some(ch) = chars.pop_front() { while let Some(ch) = chars.pop_front() {
match ch { match ch {
'\\' => { '\\' => {
if let Some(next_ch) = chars.pop_front() { if let Some(next_ch) = chars.pop_front() {
keys.push(KeyEvent(KeyCode::Char(next_ch), ModKeys::NONE)); keys.push(KeyEvent(KeyCode::Char(next_ch), ModKeys::NONE));
} }
} }
'<' => { '<' => {
let mut alias = String::new(); let mut alias = String::new();
while let Some(a_ch) = chars.pop_front() { while let Some(a_ch) = chars.pop_front() {
match a_ch { match a_ch {
'\\' => { '\\' => {
if let Some(esc_ch) = chars.pop_front() { if let Some(esc_ch) = chars.pop_front() {
alias.push(esc_ch); alias.push(esc_ch);
} }
} }
'>' => { '>' => {
if alias.eq_ignore_ascii_case("leader") { if alias.eq_ignore_ascii_case("leader") {
let mut leader = read_shopts(|o| o.prompt.leader.clone()); let mut leader = read_shopts(|o| o.prompt.leader.clone());
if leader == "\\" { if leader == "\\" {
leader.push('\\'); leader.push('\\');
} }
keys.extend(expand_keymap(&leader)); keys.extend(expand_keymap(&leader));
} else if let Some(key) = parse_key_alias(&alias) { } else if let Some(key) = parse_key_alias(&alias) {
keys.push(key); keys.push(key);
} }
break; break;
} }
_ => alias.push(a_ch), _ => alias.push(a_ch),
} }
} }
} }
_ => { _ => {
keys.push(KeyEvent(KeyCode::Char(ch), ModKeys::NONE)); keys.push(KeyEvent(KeyCode::Char(ch), ModKeys::NONE));
} }
} }
} }
keys keys
} }
pub fn parse_key_alias(alias: &str) -> Option<KeyEvent> { pub fn parse_key_alias(alias: &str) -> Option<KeyEvent> {
let parts: Vec<&str> = alias.split('-').collect(); let parts: Vec<&str> = alias.split('-').collect();
let (mods_parts, key_name) = parts.split_at(parts.len() - 1); let (mods_parts, key_name) = parts.split_at(parts.len() - 1);
let mut mods = ModKeys::NONE; let mut mods = ModKeys::NONE;
for m in mods_parts { for m in mods_parts {
match m.to_uppercase().as_str() { match m.to_uppercase().as_str() {
"C" => mods |= ModKeys::CTRL, "C" => mods |= ModKeys::CTRL,
"A" | "M" => mods |= ModKeys::ALT, "A" | "M" => mods |= ModKeys::ALT,
"S" => mods |= ModKeys::SHIFT, "S" => mods |= ModKeys::SHIFT,
_ => return None, _ => return None,
} }
} }
let key = match *key_name.first()? { let key = match *key_name.first()? {
"CR" => KeyCode::Char('\r'), "CR" => KeyCode::Char('\r'),
"ENTER" | "RETURN" => KeyCode::Enter, "ENTER" | "RETURN" => KeyCode::Enter,
"ESC" | "ESCAPE" => KeyCode::Esc, "ESC" | "ESCAPE" => KeyCode::Esc,
"TAB" => KeyCode::Tab, "TAB" => KeyCode::Tab,
"BS" | "BACKSPACE" => KeyCode::Backspace, "BS" | "BACKSPACE" => KeyCode::Backspace,
"DEL" | "DELETE" => KeyCode::Delete, "DEL" | "DELETE" => KeyCode::Delete,
"INS" | "INSERT" => KeyCode::Insert, "INS" | "INSERT" => KeyCode::Insert,
"SPACE" => KeyCode::Char(' '), "SPACE" => KeyCode::Char(' '),
"UP" => KeyCode::Up, "UP" => KeyCode::Up,
"DOWN" => KeyCode::Down, "DOWN" => KeyCode::Down,
"LEFT" => KeyCode::Left, "LEFT" => KeyCode::Left,
"RIGHT" => KeyCode::Right, "RIGHT" => KeyCode::Right,
"HOME" => KeyCode::Home, "HOME" => KeyCode::Home,
"END" => KeyCode::End, "END" => KeyCode::End,
"PGUP" | "PAGEUP" => KeyCode::PageUp, "PGUP" | "PAGEUP" => KeyCode::PageUp,
"PGDN" | "PAGEDOWN" => KeyCode::PageDown, "PGDN" | "PAGEDOWN" => KeyCode::PageDown,
k if k.len() == 1 => KeyCode::Char(k.chars().next().unwrap()), k if k.len() == 1 => KeyCode::Char(k.chars().next().unwrap()),
_ => return None _ => return None,
}; };
Some(KeyEvent(key, mods)) Some(KeyEvent(key, mods))
} }

View File

@@ -59,18 +59,12 @@ impl fmt::Display for DisplayWaitStatus {
} }
pub fn code_from_status(stat: &WtStat) -> Option<i32> { pub fn code_from_status(stat: &WtStat) -> Option<i32> {
match stat { match stat {
WtStat::Exited(_, exit_code) => { WtStat::Exited(_, exit_code) => Some(*exit_code),
Some(*exit_code) WtStat::Stopped(_, sig) => Some(SIG_EXIT_OFFSET + *sig as i32),
} WtStat::Signaled(_, sig, _) => Some(SIG_EXIT_OFFSET + *sig as i32),
WtStat::Stopped(_, sig) => { _ => None,
Some(SIG_EXIT_OFFSET + *sig as i32) }
}
WtStat::Signaled(_, sig, _) => {
Some(SIG_EXIT_OFFSET + *sig as i32)
}
_ => { None }
}
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@@ -186,7 +180,12 @@ impl JobTab {
} }
pub fn curr_job(&self) -> Option<usize> { pub fn curr_job(&self) -> Option<usize> {
// Find the most recent valid job (order can have stale entries) // Find the most recent valid job (order can have stale entries)
self.order.iter().rev().find(|&&id| self.jobs.get(id).is_some_and(|slot| slot.is_some())).copied() self
.order
.iter()
.rev()
.find(|&&id| self.jobs.get(id).is_some_and(|slot| slot.is_some()))
.copied()
} }
pub fn prev_job(&self) -> Option<usize> { pub fn prev_job(&self) -> Option<usize> {
// Find the second most recent valid job // Find the second most recent valid job
@@ -233,7 +232,7 @@ impl JobTab {
self.next_open_pos() self.next_open_pos()
}; };
job.set_tabid(tab_pos); job.set_tabid(tab_pos);
let last_pid = job.children().last().map(|c| c.pid()); let last_pid = job.children().last().map(|c| c.pid());
self.order.push(tab_pos); self.order.push(tab_pos);
if !silent { if !silent {
write( write(
@@ -247,9 +246,9 @@ impl JobTab {
self.jobs[tab_pos] = Some(job); self.jobs[tab_pos] = Some(job);
} }
if let Some(pid) = last_pid { if let Some(pid) = last_pid {
write_vars(|v| v.set_param(ShellParam::LastJob, &pid.to_string())) write_vars(|v| v.set_param(ShellParam::LastJob, &pid.to_string()))
} }
Ok(tab_pos) Ok(tab_pos)
} }
@@ -281,25 +280,23 @@ impl JobTab {
}), }),
} }
} }
pub fn update_by_id(&mut self, id: JobID, stat: WtStat) -> ShResult<()> { pub fn update_by_id(&mut self, id: JobID, stat: WtStat) -> ShResult<()> {
let Some(job) = self.query_mut(id.clone()) else { let Some(job) = self.query_mut(id.clone()) else {
return Ok(()) return Ok(());
}; };
match id { match id {
JobID::Pid(pid) => { JobID::Pid(pid) => {
let Some(child) = job.children_mut().iter_mut().find(|c| c.pid() == pid) else { let Some(child) = job.children_mut().iter_mut().find(|c| c.pid() == pid) else {
return Ok(()) return Ok(());
}; };
child.set_stat(stat); child.set_stat(stat);
} }
JobID::Pgid(_) | JobID::Pgid(_) | JobID::TableID(_) | JobID::Command(_) => {
JobID::TableID(_) | job.set_stats(stat);
JobID::Command(_) => { }
job.set_stats(stat); }
} Ok(())
} }
Ok(())
}
pub fn query_mut(&mut self, identifier: JobID) -> Option<&mut Job> { pub fn query_mut(&mut self, identifier: JobID) -> Option<&mut Job> {
match identifier { match identifier {
// Match by process group ID // Match by process group ID
@@ -355,17 +352,17 @@ impl JobTab {
} }
Ok(()) Ok(())
} }
pub fn wait_all_bg(&mut self) -> ShResult<()> { pub fn wait_all_bg(&mut self) -> ShResult<()> {
disable_reaping(); disable_reaping();
defer! { defer! {
enable_reaping(); enable_reaping();
} }
for job in self.jobs.iter_mut() { for job in self.jobs.iter_mut() {
let Some(job) = job else { continue }; let Some(job) = job else { continue };
job.wait_pgrp()?; job.wait_pgrp()?;
} }
Ok(()) Ok(())
} }
pub fn remove_job(&mut self, id: JobID) -> Option<Job> { pub fn remove_job(&mut self, id: JobID) -> Option<Job> {
let tabid = self.query(id).map(|job| job.tabid().unwrap()); let tabid = self.query(id).map(|job| job.tabid().unwrap());
if let Some(tabid) = tabid { if let Some(tabid) = tabid {
@@ -611,12 +608,11 @@ impl Job {
pub fn children_mut(&mut self) -> &mut Vec<ChildProc> { pub fn children_mut(&mut self) -> &mut Vec<ChildProc> {
&mut self.children &mut self.children
} }
pub fn is_done(&self) -> bool { pub fn is_done(&self) -> bool {
self self.children.iter().all(|chld| {
.children chld.exited() || chld.stat() == WtStat::Signaled(chld.pid(), Signal::SIGHUP, true)
.iter() })
.all(|chld| chld.exited() || chld.stat() == WtStat::Signaled(chld.pid(), Signal::SIGHUP, true)) }
}
pub fn killpg(&mut self, sig: Signal) -> ShResult<()> { pub fn killpg(&mut self, sig: Signal) -> ShResult<()> {
let stat = match sig { let stat = match sig {
Signal::SIGTSTP => WtStat::Stopped(self.pgid, Signal::SIGTSTP), Signal::SIGTSTP => WtStat::Stopped(self.pgid, Signal::SIGTSTP),
@@ -711,8 +707,8 @@ impl Job {
let padding = " ".repeat(padding_count); let padding = " ".repeat(padding_count);
let mut output = String::new(); let mut output = String::new();
let id_box = format!("[{}]{}", id + 1, symbol); let id_box = format!("[{}]{}", id + 1, symbol);
output.push_str(&format!("{id_box}\t")); output.push_str(&format!("{id_box}\t"));
for (i, cmd) in self.get_cmds().iter().enumerate() { for (i, cmd) in self.get_cmds().iter().enumerate() {
let pid = if pids || init { let pid = if pids || init {
let mut pid = self.get_pids().get(i).unwrap().to_string(); let mut pid = self.get_pids().get(i).unwrap().to_string();
@@ -735,12 +731,12 @@ impl Job {
}, },
_ => stat_line.styled(Style::Cyan), _ => stat_line.styled(Style::Cyan),
}; };
if i != 0 { if i != 0 {
let padding = " ".repeat(id_box.len() - 1); let padding = " ".repeat(id_box.len() - 1);
stat_line = format!("{padding}{}", stat_line); stat_line = format!("{padding}{}", stat_line);
} }
if i != self.get_cmds().len() - 1 { if i != self.get_cmds().len() - 1 {
stat_line.push_str(" |"); stat_line.push_str(" |");
} }
let stat_final = if long { let stat_final = if long {
@@ -767,61 +763,64 @@ pub fn term_ctlr() -> Pid {
/// Calls attach_tty() on the shell's process group to retake control of the /// Calls attach_tty() on the shell's process group to retake control of the
/// terminal /// terminal
pub fn take_term() -> ShResult<()> { pub fn take_term() -> ShResult<()> {
// take the terminal back // take the terminal back
attach_tty(getpgrp())?; attach_tty(getpgrp())?;
// send SIGWINCH to tell readline to update its window size in case it changed while we were in the background // send SIGWINCH to tell readline to update its window size in case it changed while we were in the background
killpg(getpgrp(), Signal::SIGWINCH)?; killpg(getpgrp(), Signal::SIGWINCH)?;
Ok(()) Ok(())
} }
pub fn wait_bg(id: JobID) -> ShResult<()> { pub fn wait_bg(id: JobID) -> ShResult<()> {
disable_reaping(); disable_reaping();
defer! { defer! {
enable_reaping(); enable_reaping();
}; };
match id { match id {
JobID::Pid(pid) => { JobID::Pid(pid) => {
let stat = loop { let stat = loop {
match waitpid(pid, None) { match waitpid(pid, None) {
Ok(stat) => break stat, Ok(stat) => break stat,
Err(Errno::EINTR) => continue, // Retry on signal interruption Err(Errno::EINTR) => continue, // Retry on signal interruption
Err(Errno::ECHILD) => return Ok(()), // No such child, treat as already reaped Err(Errno::ECHILD) => return Ok(()), // No such child, treat as already reaped
Err(e) => return Err(e.into()), Err(e) => return Err(e.into()),
} }
}; };
write_jobs(|j| j.update_by_id(id, stat))?; write_jobs(|j| j.update_by_id(id, stat))?;
set_status(code_from_status(&stat).unwrap_or(0)); set_status(code_from_status(&stat).unwrap_or(0));
} }
_ => { _ => {
let Some(mut job) = write_jobs(|j| j.remove_job(id.clone())) else { let Some(mut job) = write_jobs(|j| j.remove_job(id.clone())) else {
return Err(ShErr::simple(ShErrKind::ExecFail, format!("wait: No such job with id {:?}", id))); return Err(ShErr::simple(
}; ShErrKind::ExecFail,
let statuses = job.wait_pgrp()?; format!("wait: No such job with id {:?}", id),
let mut was_stopped = false; ));
let mut code = 0; };
for status in statuses { let statuses = job.wait_pgrp()?;
code = code_from_status(&status).unwrap_or(0); let mut was_stopped = false;
match status { let mut code = 0;
WtStat::Stopped(_, _) => { for status in statuses {
was_stopped = true; code = code_from_status(&status).unwrap_or(0);
} match status {
WtStat::Signaled(_, sig, _) => { WtStat::Stopped(_, _) => {
if sig == Signal::SIGTSTP { was_stopped = true;
was_stopped = true; }
} WtStat::Signaled(_, sig, _) => {
} if sig == Signal::SIGTSTP {
_ => { /* Do nothing */ } was_stopped = true;
} }
} }
_ => { /* Do nothing */ }
}
}
if was_stopped { if was_stopped {
write_jobs(|j| j.insert_job(job, false))?; write_jobs(|j| j.insert_job(job, false))?;
} }
set_status(code); set_status(code);
} }
} }
Ok(()) Ok(())
} }
/// Waits on the current foreground job and updates the shell's last status code /// Waits on the current foreground job and updates the shell's last status code
@@ -835,12 +834,12 @@ pub fn wait_fg(job: Job, interactive: bool) -> ShResult<()> {
attach_tty(job.pgid())?; attach_tty(job.pgid())?;
} }
disable_reaping(); disable_reaping();
defer! { defer! {
enable_reaping(); enable_reaping();
} }
let statuses = write_jobs(|j| j.new_fg(job))?; let statuses = write_jobs(|j| j.new_fg(job))?;
for status in statuses { for status in statuses {
code = code_from_status(&status).unwrap_or(0); code = code_from_status(&status).unwrap_or(0);
match status { match status {
WtStat::Stopped(_, _) => { WtStat::Stopped(_, _) => {
was_stopped = true; was_stopped = true;

View File

@@ -1,9 +1,9 @@
use std::cell::RefCell;
use std::collections::{HashMap, VecDeque};
use std::fmt::Display;
use ariadne::Color; use ariadne::Color;
use ariadne::{Report, ReportKind}; use ariadne::{Report, ReportKind};
use rand::TryRng; use rand::TryRng;
use std::cell::RefCell;
use std::collections::{HashMap, VecDeque};
use std::fmt::Display;
use crate::procio::RedirGuard; use crate::procio::RedirGuard;
use crate::{ use crate::{
@@ -15,96 +15,96 @@ use crate::{
pub type ShResult<T> = Result<T, ShErr>; pub type ShResult<T> = Result<T, ShErr>;
pub struct ColorRng { pub struct ColorRng {
last_color: Option<Color>, last_color: Option<Color>,
} }
impl ColorRng { impl ColorRng {
fn get_colors() -> &'static [Color] { fn get_colors() -> &'static [Color] {
&[ &[
Color::Red, Color::Red,
Color::Cyan, Color::Cyan,
Color::Blue, Color::Blue,
Color::Green, Color::Green,
Color::Yellow, Color::Yellow,
Color::Magenta, Color::Magenta,
Color::Fixed(208), // orange Color::Fixed(208), // orange
Color::Fixed(39), // deep sky blue Color::Fixed(39), // deep sky blue
Color::Fixed(170), // orchid / magenta-pink Color::Fixed(170), // orchid / magenta-pink
Color::Fixed(76), // chartreuse Color::Fixed(76), // chartreuse
Color::Fixed(51), // aqua Color::Fixed(51), // aqua
Color::Fixed(226), // bright yellow Color::Fixed(226), // bright yellow
Color::Fixed(99), // slate blue Color::Fixed(99), // slate blue
Color::Fixed(214), // light orange Color::Fixed(214), // light orange
Color::Fixed(48), // spring green Color::Fixed(48), // spring green
Color::Fixed(201), // hot pink Color::Fixed(201), // hot pink
Color::Fixed(81), // steel blue Color::Fixed(81), // steel blue
Color::Fixed(220), // gold Color::Fixed(220), // gold
Color::Fixed(105), // medium purple Color::Fixed(105), // medium purple
] ]
} }
pub fn last_color(&mut self) -> Color { pub fn last_color(&mut self) -> Color {
if let Some(color) = self.last_color.take() { if let Some(color) = self.last_color.take() {
color color
} else { } else {
let color = self.next().unwrap_or(Color::White); let color = self.next().unwrap_or(Color::White);
self.last_color = Some(color); self.last_color = Some(color);
color color
} }
} }
} }
impl Iterator for ColorRng { impl Iterator for ColorRng {
type Item = Color; type Item = Color;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let colors = Self::get_colors(); let colors = Self::get_colors();
let idx = rand::rngs::SysRng.try_next_u32().ok()? as usize % colors.len(); let idx = rand::rngs::SysRng.try_next_u32().ok()? as usize % colors.len();
Some(colors[idx]) Some(colors[idx])
} }
} }
thread_local! { thread_local! {
static COLOR_RNG: RefCell<ColorRng> = const { RefCell::new(ColorRng { last_color: None }) }; static COLOR_RNG: RefCell<ColorRng> = const { RefCell::new(ColorRng { last_color: None }) };
} }
pub fn next_color() -> Color { pub fn next_color() -> Color {
COLOR_RNG.with(|rng| { COLOR_RNG.with(|rng| {
let color = rng.borrow_mut().next().unwrap(); let color = rng.borrow_mut().next().unwrap();
rng.borrow_mut().last_color = Some(color); rng.borrow_mut().last_color = Some(color);
color color
}) })
} }
pub fn last_color() -> Color { pub fn last_color() -> Color {
COLOR_RNG.with(|rng| rng.borrow_mut().last_color()) COLOR_RNG.with(|rng| rng.borrow_mut().last_color())
} }
pub fn clear_color() { pub fn clear_color() {
COLOR_RNG.with(|rng| rng.borrow_mut().last_color = None); COLOR_RNG.with(|rng| rng.borrow_mut().last_color = None);
} }
pub trait ShResultExt { pub trait ShResultExt {
fn blame(self, span: Span) -> Self; fn blame(self, span: Span) -> Self;
fn try_blame(self, span: Span) -> Self; fn try_blame(self, span: Span) -> Self;
fn promote_err(self, span: Span) -> Self; fn promote_err(self, span: Span) -> Self;
fn is_flow_control(&self) -> bool; fn is_flow_control(&self) -> bool;
} }
impl<T> ShResultExt for Result<T, ShErr> { impl<T> ShResultExt for Result<T, ShErr> {
/// Blame a span for an error /// Blame a span for an error
fn blame(self, new_span: Span) -> Self { fn blame(self, new_span: Span) -> Self {
self.map_err(|e| e.blame(new_span)) self.map_err(|e| e.blame(new_span))
} }
/// Blame a span if no blame has been assigned yet /// Blame a span if no blame has been assigned yet
fn try_blame(self, new_span: Span) -> Self { fn try_blame(self, new_span: Span) -> Self {
self.map_err(|e| e.try_blame(new_span)) self.map_err(|e| e.try_blame(new_span))
}
fn promote_err(self, span: Span) -> Self {
self.map_err(|e| e.promote(span))
}
fn is_flow_control(&self) -> bool {
self.as_ref().is_err_and(|e| e.is_flow_control())
} }
fn promote_err(self, span: Span) -> Self {
self.map_err(|e| e.promote(span))
}
fn is_flow_control(&self) -> bool {
self.as_ref().is_err_and(|e| e.is_flow_control())
}
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
@@ -163,160 +163,256 @@ impl Display for Note {
#[derive(Debug)] #[derive(Debug)]
pub struct ShErr { pub struct ShErr {
kind: ShErrKind, kind: ShErrKind,
src_span: Option<Span>, src_span: Option<Span>,
labels: Vec<ariadne::Label<Span>>, labels: Vec<ariadne::Label<Span>>,
sources: Vec<SpanSource>, sources: Vec<SpanSource>,
notes: Vec<String>, notes: Vec<String>,
/// If we propagate through a redirect boundary, we take ownership of /// If we propagate through a redirect boundary, we take ownership of
/// the RedirGuard(s) so that redirections stay alive until the error /// the RedirGuard(s) so that redirections stay alive until the error
/// is printed. Multiple guards can accumulate as the error bubbles /// is printed. Multiple guards can accumulate as the error bubbles
/// through nested redirect scopes. /// through nested redirect scopes.
io_guards: Vec<RedirGuard> io_guards: Vec<RedirGuard>,
} }
impl ShErr { impl ShErr {
pub fn new(kind: ShErrKind, span: Span) -> Self { pub fn new(kind: ShErrKind, span: Span) -> Self {
Self { kind, src_span: Some(span), labels: vec![], sources: vec![], notes: vec![], io_guards: vec![] } Self {
} kind,
pub fn simple(kind: ShErrKind, msg: impl Into<String>) -> Self { src_span: Some(span),
Self { kind, src_span: None, labels: vec![], sources: vec![], notes: vec![msg.into()], io_guards: vec![] } labels: vec![],
} sources: vec![],
pub fn is_flow_control(&self) -> bool { notes: vec![],
self.kind.is_flow_control() io_guards: vec![],
} }
pub fn promote(mut self, span: Span) -> Self { }
if self.notes.is_empty() { pub fn simple(kind: ShErrKind, msg: impl Into<String>) -> Self {
return self Self {
} kind,
let first = self.notes[0].clone(); src_span: None,
if self.notes.len() > 1 { labels: vec![],
self.notes = self.notes[1..].to_vec(); sources: vec![],
} notes: vec![msg.into()],
io_guards: vec![],
}
}
pub fn is_flow_control(&self) -> bool {
self.kind.is_flow_control()
}
pub fn promote(mut self, span: Span) -> Self {
if self.notes.is_empty() {
return self;
}
let first = self.notes[0].clone();
if self.notes.len() > 1 {
self.notes = self.notes[1..].to_vec();
}
self.labeled(span, first) self.labeled(span, first)
} }
pub fn with_redirs(mut self, guard: RedirGuard) -> Self { pub fn with_redirs(mut self, guard: RedirGuard) -> Self {
self.io_guards.push(guard); self.io_guards.push(guard);
self self
} }
pub fn at(kind: ShErrKind, span: Span, msg: impl Into<String>) -> Self { pub fn at(kind: ShErrKind, span: Span, msg: impl Into<String>) -> Self {
let color = last_color(); // use last_color to ensure the same color is used for the label and the message given let color = last_color(); // use last_color to ensure the same color is used for the label and the message given
let src = span.span_source().clone(); let src = span.span_source().clone();
let msg: String = msg.into(); let msg: String = msg.into();
Self::new(kind, span.clone()) Self::new(kind, span.clone()).with_label(
.with_label(src, ariadne::Label::new(span).with_color(color).with_message(msg)) src,
} ariadne::Label::new(span)
pub fn labeled(self, span: Span, msg: impl Into<String>) -> Self { .with_color(color)
let color = last_color(); .with_message(msg),
let src = span.span_source().clone(); )
let msg: String = msg.into(); }
self.with_label(src, ariadne::Label::new(span).with_color(color).with_message(msg)) pub fn labeled(self, span: Span, msg: impl Into<String>) -> Self {
} let color = last_color();
pub fn blame(self, span: Span) -> Self { let src = span.span_source().clone();
let ShErr { kind, src_span: _, labels, sources, notes, io_guards } = self; let msg: String = msg.into();
Self { kind, src_span: Some(span), labels, sources, notes, io_guards } self.with_label(
} src,
pub fn try_blame(self, span: Span) -> Self { ariadne::Label::new(span)
match self { .with_color(color)
ShErr { kind, src_span: None, labels, sources, notes, io_guards } => Self { kind, src_span: Some(span), labels, sources, notes, io_guards }, .with_message(msg),
_ => self )
} }
} pub fn blame(self, span: Span) -> Self {
pub fn kind(&self) -> &ShErrKind { let ShErr {
&self.kind kind,
} src_span: _,
pub fn rename(mut self, name: impl Into<String>) -> Self { labels,
if let Some(span) = self.src_span.as_mut() { sources,
span.rename(name.into()); notes,
} io_guards,
self } = self;
} Self {
pub fn with_label(self, source: SpanSource, label: ariadne::Label<Span>) -> Self { kind,
let ShErr { kind, src_span, mut labels, mut sources, notes, io_guards } = self; src_span: Some(span),
sources.push(source); labels,
labels.push(label); sources,
Self { kind, src_span, labels, sources, notes, io_guards } notes,
} io_guards,
pub fn with_context(self, ctx: VecDeque<(SpanSource, ariadne::Label<Span>)>) -> Self { }
let ShErr { kind, src_span, mut labels, mut sources, notes, io_guards } = self; }
for (src, label) in ctx { pub fn try_blame(self, span: Span) -> Self {
sources.push(src); match self {
labels.push(label); ShErr {
} kind,
Self { kind, src_span, labels, sources, notes, io_guards } src_span: None,
} labels,
pub fn with_note(self, note: impl Into<String>) -> Self { sources,
let ShErr { kind, src_span, labels, sources, mut notes, io_guards } = self; notes,
notes.push(note.into()); io_guards,
Self { kind, src_span, labels, sources, notes, io_guards } } => Self {
} kind,
pub fn build_report(&self) -> Option<Report<'_, Span>> { src_span: Some(span),
let span = self.src_span.as_ref()?; labels,
let mut report = Report::build(ReportKind::Error, span.clone()) sources,
.with_config(ariadne::Config::default().with_color(true)); notes,
let msg = if self.notes.is_empty() { io_guards,
self.kind.to_string() },
} else { _ => self,
format!("{} - {}", self.kind, self.notes.first().unwrap()) }
}; }
report = report.with_message(msg); pub fn kind(&self) -> &ShErrKind {
&self.kind
}
pub fn rename(mut self, name: impl Into<String>) -> Self {
if let Some(span) = self.src_span.as_mut() {
span.rename(name.into());
}
self
}
pub fn with_label(self, source: SpanSource, label: ariadne::Label<Span>) -> Self {
let ShErr {
kind,
src_span,
mut labels,
mut sources,
notes,
io_guards,
} = self;
sources.push(source);
labels.push(label);
Self {
kind,
src_span,
labels,
sources,
notes,
io_guards,
}
}
pub fn with_context(self, ctx: VecDeque<(SpanSource, ariadne::Label<Span>)>) -> Self {
let ShErr {
kind,
src_span,
mut labels,
mut sources,
notes,
io_guards,
} = self;
for (src, label) in ctx {
sources.push(src);
labels.push(label);
}
Self {
kind,
src_span,
labels,
sources,
notes,
io_guards,
}
}
pub fn with_note(self, note: impl Into<String>) -> Self {
let ShErr {
kind,
src_span,
labels,
sources,
mut notes,
io_guards,
} = self;
notes.push(note.into());
Self {
kind,
src_span,
labels,
sources,
notes,
io_guards,
}
}
pub fn build_report(&self) -> Option<Report<'_, Span>> {
let span = self.src_span.as_ref()?;
let mut report = Report::build(ReportKind::Error, span.clone())
.with_config(ariadne::Config::default().with_color(true));
let msg = if self.notes.is_empty() {
self.kind.to_string()
} else {
format!("{} - {}", self.kind, self.notes.first().unwrap())
};
report = report.with_message(msg);
for label in self.labels.clone() { for label in self.labels.clone() {
report = report.with_label(label); report = report.with_label(label);
} }
for note in &self.notes { for note in &self.notes {
report = report.with_note(note); report = report.with_note(note);
} }
Some(report.finish()) Some(report.finish())
} }
fn collect_sources(&self) -> HashMap<SpanSource, String> { fn collect_sources(&self) -> HashMap<SpanSource, String> {
let mut source_map = HashMap::new(); let mut source_map = HashMap::new();
if let Some(span) = &self.src_span { if let Some(span) = &self.src_span {
let src = span.span_source().clone(); let src = span.span_source().clone();
source_map.entry(src.clone()) source_map
.or_insert_with(|| src.content().to_string()); .entry(src.clone())
} .or_insert_with(|| src.content().to_string());
for src in &self.sources { }
source_map.entry(src.clone()) for src in &self.sources {
.or_insert_with(|| src.content().to_string()); source_map
} .entry(src.clone())
source_map .or_insert_with(|| src.content().to_string());
} }
pub fn print_error(&self) { source_map
let default = || { }
eprintln!("\n{}", self.kind); pub fn print_error(&self) {
for note in &self.notes { let default = || {
eprintln!("note: {note}"); eprintln!("\n{}", self.kind);
} for note in &self.notes {
}; eprintln!("note: {note}");
let Some(report) = self.build_report() else { }
return default(); };
}; let Some(report) = self.build_report() else {
return default();
};
let sources = self.collect_sources(); let sources = self.collect_sources();
let cache = ariadne::FnCache::new(move |src: &SpanSource| { let cache = ariadne::FnCache::new(move |src: &SpanSource| {
sources.get(src) sources
.cloned() .get(src)
.ok_or_else(|| format!("Failed to fetch source '{}'", src.name())) .cloned()
}); .ok_or_else(|| format!("Failed to fetch source '{}'", src.name()))
eprintln!(); });
if report.eprint(cache).is_err() { eprintln!();
default(); if report.eprint(cache).is_err() {
} default();
} }
}
} }
impl Display for ShErr { impl Display for ShErr {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
if self.notes.is_empty() { if self.notes.is_empty() {
write!(f, "{}", self.kind) write!(f, "{}", self.kind)
} else { } else {
write!(f, "{} - {}", self.kind, self.notes.first().unwrap()) write!(f, "{} - {}", self.kind, self.notes.first().unwrap())
} }
} }
} }
impl From<std::io::Error> for ShErr { impl From<std::io::Error> for ShErr {
@@ -350,7 +446,7 @@ pub enum ShErrKind {
ResourceLimitExceeded, ResourceLimitExceeded,
BadPermission, BadPermission,
Errno(Errno), Errno(Errno),
NotFound, NotFound,
ReadlineErr, ReadlineErr,
ExCommand, ExCommand,
@@ -364,15 +460,16 @@ pub enum ShErrKind {
} }
impl ShErrKind { impl ShErrKind {
pub fn is_flow_control(&self) -> bool { pub fn is_flow_control(&self) -> bool {
matches!(self, matches!(
Self::CleanExit(_) | self,
Self::FuncReturn(_) | Self::CleanExit(_)
Self::LoopContinue(_) | | Self::FuncReturn(_)
Self::LoopBreak(_) | | Self::LoopContinue(_)
Self::ClearReadline | Self::LoopBreak(_)
) | Self::ClearReadline
} )
}
} }
impl Display for ShErrKind { impl Display for ShErrKind {

View File

@@ -144,7 +144,8 @@ impl RawModeGuard {
F: FnOnce() -> R, F: FnOnce() -> R,
{ {
let current = tcgetattr(borrow_fd(*TTY_FILENO)).expect("Failed to get terminal attributes"); let current = tcgetattr(borrow_fd(*TTY_FILENO)).expect("Failed to get terminal attributes");
let orig = ORIG_TERMIOS.with(|cell| cell.borrow().clone()) let orig = ORIG_TERMIOS
.with(|cell| cell.borrow().clone())
.expect("with_cooked_mode called before raw_mode()"); .expect("with_cooked_mode called before raw_mode()");
tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &orig) tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &orig)
.expect("Failed to restore cooked mode"); .expect("Failed to restore cooked mode");

View File

@@ -25,8 +25,8 @@ pub trait TkVecUtils<Tk> {
} }
pub trait AutoCmdVecUtils { pub trait AutoCmdVecUtils {
fn exec(&self); fn exec(&self);
fn exec_with(&self, pattern: &str); fn exec_with(&self, pattern: &str);
} }
pub trait RedirVecUtils<Redir> { pub trait RedirVecUtils<Redir> {
@@ -37,36 +37,44 @@ pub trait RedirVecUtils<Redir> {
} }
pub trait NodeVecUtils<Node> { pub trait NodeVecUtils<Node> {
fn get_span(&self) -> Option<Span>; fn get_span(&self) -> Option<Span>;
} }
impl AutoCmdVecUtils for Vec<AutoCmd> { impl AutoCmdVecUtils for Vec<AutoCmd> {
fn exec(&self) { fn exec(&self) {
let saved_status = crate::state::get_status(); let saved_status = crate::state::get_status();
for cmd in self { for cmd in self {
let AutoCmd { pattern: _, command } = cmd; let AutoCmd {
if let Err(e) = exec_input(command.clone(), None, false, Some("autocmd".into())) { pattern: _,
e.print_error(); command,
} } = cmd;
} if let Err(e) = exec_input(command.clone(), None, false, Some("autocmd".into())) {
crate::state::set_status(saved_status); e.print_error();
} }
fn exec_with(&self, other_pattern: &str) { }
let saved_status = crate::state::get_status(); crate::state::set_status(saved_status);
for cmd in self { }
let AutoCmd { pattern, command } = cmd; fn exec_with(&self, other_pattern: &str) {
if let Some(pat) = pattern let saved_status = crate::state::get_status();
&& !pat.is_match(other_pattern) { for cmd in self {
log::trace!("autocmd pattern '{}' did not match '{}', skipping", pat, other_pattern); let AutoCmd { pattern, command } = cmd;
continue; if let Some(pat) = pattern
} && !pat.is_match(other_pattern)
{
log::trace!(
"autocmd pattern '{}' did not match '{}', skipping",
pat,
other_pattern
);
continue;
}
if let Err(e) = exec_input(command.clone(), None, false, Some("autocmd".into())) { if let Err(e) = exec_input(command.clone(), None, false, Some("autocmd".into())) {
e.print_error(); e.print_error();
} }
} }
crate::state::set_status(saved_status); crate::state::set_status(saved_status);
} }
} }
impl<T> VecDequeExt<T> for VecDeque<T> { impl<T> VecDequeExt<T> for VecDeque<T> {
@@ -118,9 +126,12 @@ impl CharDequeUtils for VecDeque<char> {
impl TkVecUtils<Tk> for Vec<Tk> { impl TkVecUtils<Tk> for Vec<Tk> {
fn get_span(&self) -> Option<Span> { fn get_span(&self) -> Option<Span> {
if let Some(first_tk) = self.first() { if let Some(first_tk) = self.first() {
self self.last().map(|last_tk| {
.last() Span::new(
.map(|last_tk| Span::new(first_tk.span.range().start..last_tk.span.range().end, first_tk.source())) first_tk.span.range().start..last_tk.span.range().end,
first_tk.source(),
)
})
} else { } else {
None None
} }
@@ -170,13 +181,17 @@ impl RedirVecUtils<Redir> for Vec<Redir> {
impl NodeVecUtils<Node> for Vec<Node> { impl NodeVecUtils<Node> for Vec<Node> {
fn get_span(&self) -> Option<Span> { fn get_span(&self) -> Option<Span> {
if let Some(first_nd) = self.first() if let Some(first_nd) = self.first()
&& let Some(last_nd) = self.last() { && let Some(last_nd) = self.last()
let first_start = first_nd.get_span().range().start; {
let last_end = last_nd.get_span().range().end; let first_start = first_nd.get_span().range().start;
if first_start <= last_end { let last_end = last_nd.get_span().range().end;
return Some(Span::new(first_start..last_end, first_nd.get_span().source().content())); if first_start <= last_end {
} return Some(Span::new(
first_start..last_end,
first_nd.get_span().source().content(),
));
}
} }
None None
} }
} }

View File

@@ -2,7 +2,7 @@
clippy::derivable_impls, clippy::derivable_impls,
clippy::tabs_in_doc_comments, clippy::tabs_in_doc_comments,
clippy::while_let_on_iterator, clippy::while_let_on_iterator,
clippy::result_large_err clippy::result_large_err
)] )]
pub mod builtin; pub mod builtin;
pub mod expand; pub mod expand;
@@ -34,7 +34,6 @@ use crate::libsh::sys::TTY_FILENO;
use crate::libsh::utils::AutoCmdVecUtils; use crate::libsh::utils::AutoCmdVecUtils;
use crate::parse::execute::exec_input; use crate::parse::execute::exec_input;
use crate::prelude::*; use crate::prelude::*;
use crate::procio::IoMode;
use crate::readline::term::{LineWriter, RawModeGuard, raw_mode}; use crate::readline::term::{LineWriter, RawModeGuard, raw_mode};
use crate::readline::{Prompt, ReadlineEvent, ShedVi}; use crate::readline::{Prompt, ReadlineEvent, ShedVi};
use crate::signal::{GOT_SIGWINCH, JOB_DONE, QUIT_CODE, check_signals, sig_setup, signals_pending}; use crate::signal::{GOT_SIGWINCH, JOB_DONE, QUIT_CODE, check_signals, sig_setup, signals_pending};
@@ -86,20 +85,22 @@ fn setup_panic_handler() {
} }
}); });
let data_dir = env::var("XDG_DATA_HOME").unwrap_or_else(|_| { let data_dir = env::var("XDG_DATA_HOME").unwrap_or_else(|_| {
let home = env::var("HOME").unwrap(); let home = env::var("HOME").unwrap();
format!("{home}/.local/share") format!("{home}/.local/share")
}); });
let log_dir = Path::new(&data_dir).join("shed").join("log"); let log_dir = Path::new(&data_dir).join("shed").join("log");
std::fs::create_dir_all(&log_dir).unwrap(); std::fs::create_dir_all(&log_dir).unwrap();
let log_file_path = log_dir.join("panic.log"); let log_file_path = log_dir.join("panic.log");
let mut log_file = parse::get_redir_file(parse::RedirType::Output, log_file_path).unwrap(); let mut log_file = parse::get_redir_file(parse::RedirType::Output, log_file_path).unwrap();
let panic_info_raw = info.to_string(); let panic_info_raw = info.to_string();
log_file.write_all(panic_info_raw.as_bytes()).unwrap(); log_file.write_all(panic_info_raw.as_bytes()).unwrap();
let backtrace = std::backtrace::Backtrace::force_capture(); let backtrace = std::backtrace::Backtrace::force_capture();
log_file.write_all(format!("\nBacktrace:\n{:?}", backtrace).as_bytes()).unwrap(); log_file
.write_all(format!("\nBacktrace:\n{:?}", backtrace).as_bytes())
.unwrap();
default_panic_hook(info); default_panic_hook(info);
})); }));
@@ -134,16 +135,17 @@ fn main() -> ExitCode {
} else { } else {
shed_interactive(args) shed_interactive(args)
} { } {
e.print_error(); e.print_error();
}; };
if let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Exit)) if let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Exit))
&& let Err(e) = exec_input(trap, None, false, Some("trap".into())) { && let Err(e) = exec_input(trap, None, false, Some("trap".into()))
e.print_error(); {
e.print_error();
} }
let on_exit_autocmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnExit)); let on_exit_autocmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnExit));
on_exit_autocmds.exec(); on_exit_autocmds.exec();
write_jobs(|j| j.hang_up()); write_jobs(|j| j.hang_up());
ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8) ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8)
@@ -151,7 +153,7 @@ fn main() -> ExitCode {
fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) -> ShResult<()> { fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) -> ShResult<()> {
let path = path.as_ref(); let path = path.as_ref();
let path_raw = path.to_string_lossy().to_string(); let path_raw = path.to_string_lossy().to_string();
if !path.is_file() { if !path.is_file() {
eprintln!("shed: Failed to open input file: {}", path.display()); eprintln!("shed: Failed to open input file: {}", path.display());
QUIT_CODE.store(1, Ordering::SeqCst); QUIT_CODE.store(1, Ordering::SeqCst);
@@ -207,7 +209,7 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
m.try_rehash_commands(); m.try_rehash_commands();
m.try_rehash_cwd_listing(); m.try_rehash_cwd_listing();
}); });
error::clear_color(); error::clear_color();
// Handle any pending signals // Handle any pending signals
while signals_pending() { while signals_pending() {
@@ -267,15 +269,26 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
// Timeout — resolve pending keymap ambiguity // Timeout — resolve pending keymap ambiguity
if !readline.pending_keymap.is_empty() if !readline.pending_keymap.is_empty()
&& fds[0].revents().is_none_or(|r| !r.contains(PollFlags::POLLIN)) && fds[0]
.revents()
.is_none_or(|r| !r.contains(PollFlags::POLLIN))
{ {
log::debug!("[keymap timeout] resolving pending={:?}", readline.pending_keymap); log::debug!(
"[keymap timeout] resolving pending={:?}",
readline.pending_keymap
);
let keymap_flags = readline.curr_keymap_flags(); let keymap_flags = readline.curr_keymap_flags();
let matches = read_logic(|l| l.keymaps_filtered(keymap_flags, &readline.pending_keymap)); let matches = read_logic(|l| l.keymaps_filtered(keymap_flags, &readline.pending_keymap));
// If there's an exact match, fire it; otherwise flush as normal keys // If there's an exact match, fire it; otherwise flush as normal keys
let exact = matches.iter().find(|km| km.compare(&readline.pending_keymap) == KeyMapMatch::IsExact); let exact = matches
.iter()
.find(|km| km.compare(&readline.pending_keymap) == KeyMapMatch::IsExact);
if let Some(km) = exact { if let Some(km) = exact {
log::debug!("[keymap timeout] firing exact match: {:?} -> {:?}", km.keys, km.action); log::debug!(
"[keymap timeout] firing exact match: {:?} -> {:?}",
km.keys,
km.action
);
let action = km.action_expanded(); let action = km.action_expanded();
readline.pending_keymap.clear(); readline.pending_keymap.clear();
for key in action { for key in action {
@@ -284,7 +297,9 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
ReadlineEvent::Line(input) => { ReadlineEvent::Line(input) => {
let start = Instant::now(); let start = Instant::now();
write_meta(|m| m.start_timer()); write_meta(|m| m.start_timer());
if let Err(e) = RawModeGuard::with_cooked_mode(|| exec_input(input, None, true, Some("<stdin>".into()))) { if let Err(e) = RawModeGuard::with_cooked_mode(|| {
exec_input(input, None, true, Some("<stdin>".into()))
}) {
match e.kind() { match e.kind() {
ShErrKind::CleanExit(code) => { ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst); QUIT_CODE.store(*code, Ordering::SeqCst);
@@ -310,7 +325,10 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
} }
} }
} else { } else {
log::debug!("[keymap timeout] no exact match, flushing {} keys as normal input", readline.pending_keymap.len()); log::debug!(
"[keymap timeout] no exact match, flushing {} keys as normal input",
readline.pending_keymap.len()
);
let buffered = std::mem::take(&mut readline.pending_keymap); let buffered = std::mem::take(&mut readline.pending_keymap);
for key in buffered { for key in buffered {
if let Some(event) = readline.handle_key(key)? { if let Some(event) = readline.handle_key(key)? {
@@ -318,7 +336,9 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
ReadlineEvent::Line(input) => { ReadlineEvent::Line(input) => {
let start = Instant::now(); let start = Instant::now();
write_meta(|m| m.start_timer()); write_meta(|m| m.start_timer());
if let Err(e) = RawModeGuard::with_cooked_mode(|| exec_input(input, None, true, Some("<stdin>".into()))) { if let Err(e) = RawModeGuard::with_cooked_mode(|| {
exec_input(input, None, true, Some("<stdin>".into()))
}) {
match e.kind() { match e.kind() {
ShErrKind::CleanExit(code) => { ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst); QUIT_CODE.store(*code, Ordering::SeqCst);
@@ -376,14 +396,16 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
// Process any available input // Process any available input
match readline.process_input() { match readline.process_input() {
Ok(ReadlineEvent::Line(input)) => { Ok(ReadlineEvent::Line(input)) => {
let pre_exec = read_logic(|l| l.get_autocmds(AutoCmdKind::PreCmd)); let pre_exec = read_logic(|l| l.get_autocmds(AutoCmdKind::PreCmd));
let post_exec = read_logic(|l| l.get_autocmds(AutoCmdKind::PostCmd)); let post_exec = read_logic(|l| l.get_autocmds(AutoCmdKind::PostCmd));
pre_exec.exec_with(&input); pre_exec.exec_with(&input);
let start = Instant::now(); let start = Instant::now();
write_meta(|m| m.start_timer()); write_meta(|m| m.start_timer());
if let Err(e) = RawModeGuard::with_cooked_mode(|| exec_input(input.clone(), None, true, Some("<stdin>".into()))) { if let Err(e) = RawModeGuard::with_cooked_mode(|| {
exec_input(input.clone(), None, true, Some("<stdin>".into()))
}) {
match e.kind() { match e.kind() {
ShErrKind::CleanExit(code) => { ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst); QUIT_CODE.store(*code, Ordering::SeqCst);
@@ -396,10 +418,10 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
log::info!("Command executed in {:.2?}", command_run_time); log::info!("Command executed in {:.2?}", command_run_time);
write_meta(|m| m.stop_timer()); write_meta(|m| m.stop_timer());
post_exec.exec_with(&input); post_exec.exec_with(&input);
readline.fix_column()?; readline.fix_column()?;
readline.writer.flush_write("\n\r")?; readline.writer.flush_write("\n\r")?;
// Reset for next command with fresh prompt // Reset for next command with fresh prompt
readline.reset(true)?; readline.reset(true)?;

View File

@@ -1,15 +1,37 @@
use std::{ use std::{
cell::Cell, collections::{HashSet, VecDeque}, os::unix::fs::PermissionsExt cell::Cell,
collections::{HashSet, VecDeque},
os::unix::fs::PermissionsExt,
}; };
use ariadne::Fmt; use ariadne::Fmt;
use crate::{ use crate::{
builtin::{ builtin::{
alias::{alias, unalias}, arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate}, autocmd::autocmd, cd::cd, complete::{compgen_builtin, complete_builtin}, dirstack::{dirs, popd, pushd}, echo::echo, eval, exec, flowctl::flowctl, getopts::getopts, intro, jobctl::{self, JobBehavior, continue_job, disown, jobs}, keymap, map, pwd::pwd, read::{self, read_builtin}, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, varcmds::{export, local, readonly, unset}, zoltraak::zoltraak alias::{alias, unalias},
arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate},
autocmd::autocmd,
cd::cd,
complete::{compgen_builtin, complete_builtin},
dirstack::{dirs, popd, pushd},
echo::echo,
eval, exec,
flowctl::flowctl,
getopts::getopts,
intro,
jobctl::{self, JobBehavior, continue_job, disown, jobs},
keymap, map,
pwd::pwd,
read::{self, read_builtin},
shift::shift,
shopt::shopt,
source::source,
test::double_bracket_test,
trap::{TrapTarget, trap},
varcmds::{export, local, readonly, unset},
zoltraak::zoltraak,
}, },
expand::{Expander, expand_aliases, expand_case_pattern, expand_raw, glob_to_regex}, expand::{expand_aliases, expand_case_pattern, glob_to_regex},
jobs::{ChildProc, JobStack, attach_tty, dispatch_job}, jobs::{ChildProc, JobStack, attach_tty, dispatch_job},
libsh::{ libsh::{
error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color}, error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color},
@@ -19,7 +41,7 @@ use crate::{
prelude::*, prelude::*,
procio::{IoMode, IoStack}, procio::{IoMode, IoStack},
state::{ state::{
self, ShFunc, VarFlags, VarKind, read_logic, read_shopts, write_jobs, write_logic, write_vars self, ShFunc, VarFlags, VarKind, read_logic, read_shopts, write_jobs, write_logic, write_vars,
}, },
}; };
@@ -110,7 +132,12 @@ impl ExecArgs {
} }
} }
pub fn exec_input(input: String, io_stack: Option<IoStack>, interactive: bool, source_name: Option<String>) -> ShResult<()> { pub fn exec_input(
input: String,
io_stack: Option<IoStack>,
interactive: bool,
source_name: Option<String>,
) -> ShResult<()> {
let log_tab = read_logic(|l| l.clone()); let log_tab = read_logic(|l| l.clone());
let input = expand_aliases(input, HashSet::new(), &log_tab); let input = expand_aliases(input, HashSet::new(), &log_tab);
let lex_flags = if interactive { let lex_flags = if interactive {
@@ -118,8 +145,10 @@ pub fn exec_input(input: String, io_stack: Option<IoStack>, interactive: bool, s
} else { } else {
super::lex::LexFlags::empty() super::lex::LexFlags::empty()
}; };
let source_name = source_name.unwrap_or("<stdin>".into()); let source_name = source_name.unwrap_or("<stdin>".into());
let mut parser = ParsedSrc::new(Arc::new(input)).with_lex_flags(lex_flags).with_name(source_name.clone()); let mut parser = ParsedSrc::new(Arc::new(input))
.with_lex_flags(lex_flags)
.with_name(source_name.clone());
if let Err(errors) = parser.parse_src() { if let Err(errors) = parser.parse_src() {
for error in errors { for error in errors {
error.print_error(); error.print_error();
@@ -149,7 +178,7 @@ pub fn exec_input(input: String, io_stack: Option<IoStack>, interactive: bool, s
pub struct Dispatcher { pub struct Dispatcher {
nodes: VecDeque<Node>, nodes: VecDeque<Node>,
interactive: bool, interactive: bool,
source_name: String, source_name: String,
pub io_stack: IoStack, pub io_stack: IoStack,
pub job_stack: JobStack, pub job_stack: JobStack,
fg_job: bool, fg_job: bool,
@@ -161,7 +190,7 @@ impl Dispatcher {
Self { Self {
nodes, nodes,
interactive, interactive,
source_name, source_name,
io_stack: IoStack::new(), io_stack: IoStack::new(),
job_stack: JobStack::new(), job_stack: JobStack::new(),
fg_job: true, fg_job: true,
@@ -208,7 +237,12 @@ impl Dispatcher {
let stack = IoStack { let stack = IoStack {
stack: self.io_stack.clone(), stack: self.io_stack.clone(),
}; };
exec_input(format!("cd {dir}"), Some(stack), self.interactive, Some(self.source_name.clone())) exec_input(
format!("cd {dir}"),
Some(stack),
self.interactive,
Some(self.source_name.clone()),
)
} else { } else {
self.exec_cmd(node) self.exec_cmd(node)
} }
@@ -274,16 +308,16 @@ impl Dispatcher {
return Ok(()); return Ok(());
} }
let func = ShFunc::new(func_parser,blame); let func = ShFunc::new(func_parser, blame);
write_logic(|l| l.insert_func(name, func)); // Store the AST write_logic(|l| l.insert_func(name, func)); // Store the AST
Ok(()) Ok(())
} }
fn exec_subsh(&mut self, subsh: Node) -> ShResult<()> { fn exec_subsh(&mut self, subsh: Node) -> ShResult<()> {
let blame = subsh.get_span().clone(); let blame = subsh.get_span().clone();
let NdRule::Command { assignments, argv } = subsh.class else { let NdRule::Command { assignments, argv } = subsh.class else {
unreachable!() unreachable!()
}; };
let name = self.source_name.clone(); let name = self.source_name.clone();
self.run_fork("anonymous_subshell", |s| { self.run_fork("anonymous_subshell", |s| {
if let Err(e) = s.set_assignments(assignments, AssignBehavior::Export) { if let Err(e) = s.set_assignments(assignments, AssignBehavior::Export) {
@@ -309,8 +343,11 @@ impl Dispatcher {
} }
fn exec_func(&mut self, func: Node) -> ShResult<()> { fn exec_func(&mut self, func: Node) -> ShResult<()> {
let mut blame = func.get_span().clone(); let mut blame = func.get_span().clone();
let func_name = func.get_command().unwrap().to_string(); let func_name = func.get_command().unwrap().to_string();
let func_ctx = func.get_context(format!("in call to function '{}'",func_name.fg(next_color()))); let func_ctx = func.get_context(format!(
"in call to function '{}'",
func_name.fg(next_color())
));
let NdRule::Command { let NdRule::Command {
assignments, assignments,
mut argv, mut argv,
@@ -340,12 +377,12 @@ impl Dispatcher {
self.io_stack.append_to_frame(func.redirs); self.io_stack.append_to_frame(func.redirs);
blame.rename(func_name.clone()); blame.rename(func_name.clone());
let argv = prepare_argv(argv).try_blame(blame.clone())?; let argv = prepare_argv(argv).try_blame(blame.clone())?;
let result = if let Some(ref mut func_body) = read_logic(|l| l.get_func(&func_name)) { let result = if let Some(ref mut func_body) = read_logic(|l| l.get_func(&func_name)) {
let _guard = scope_guard(Some(argv)); let _guard = scope_guard(Some(argv));
func_body.body_mut().propagate_context(func_ctx); func_body.body_mut().propagate_context(func_ctx);
func_body.body_mut().flags = func.flags; func_body.body_mut().flags = func.flags;
if let Err(e) = self.exec_brc_grp(func_body.body().clone()) { if let Err(e) = self.exec_brc_grp(func_body.body().clone()) {
@@ -354,7 +391,7 @@ impl Dispatcher {
state::set_status(*code); state::set_status(*code);
Ok(()) Ok(())
} }
_ => Err(e) _ => Err(e),
} }
} else { } else {
Ok(()) Ok(())
@@ -423,12 +460,17 @@ impl Dispatcher {
'outer: for block in case_blocks { 'outer: for block in case_blocks {
let CaseNode { pattern, body } = block; let CaseNode { pattern, body } = block;
let block_pattern_raw = pattern.span.as_str().strip_suffix(')').unwrap_or(pattern.span.as_str()).trim(); let block_pattern_raw = pattern
.span
.as_str()
.strip_suffix(')')
.unwrap_or(pattern.span.as_str())
.trim();
// Split at '|' to allow for multiple patterns like `foo|bar)` // Split at '|' to allow for multiple patterns like `foo|bar)`
let block_patterns = block_pattern_raw.split('|'); let block_patterns = block_pattern_raw.split('|');
for pattern in block_patterns { for pattern in block_patterns {
let pattern_exp = expand_case_pattern(pattern)?; let pattern_exp = expand_case_pattern(pattern)?;
let pattern_regex = glob_to_regex(&pattern_exp, false); let pattern_regex = glob_to_regex(&pattern_exp, false);
if pattern_regex.is_match(&pattern_raw) { if pattern_regex.is_match(&pattern_raw) {
for node in &body { for node in &body {
@@ -450,7 +492,9 @@ impl Dispatcher {
} }
}) })
} else { } else {
case_logic(self).try_blame(blame).map_err(|e| e.with_redirs(guard)) case_logic(self)
.try_blame(blame)
.map_err(|e| e.with_redirs(guard))
} }
} }
fn exec_loop(&mut self, loop_stmt: Node) -> ShResult<()> { fn exec_loop(&mut self, loop_stmt: Node) -> ShResult<()> {
@@ -513,7 +557,9 @@ impl Dispatcher {
} }
}) })
} else { } else {
loop_logic(self).try_blame(blame).map_err(|e| e.with_redirs(guard)) loop_logic(self)
.try_blame(blame)
.map_err(|e| e.with_redirs(guard))
} }
} }
fn exec_for(&mut self, for_stmt: Node) -> ShResult<()> { fn exec_for(&mut self, for_stmt: Node) -> ShResult<()> {
@@ -591,7 +637,9 @@ impl Dispatcher {
} }
}) })
} else { } else {
for_logic(self).try_blame(blame).map_err(|e| e.with_redirs(guard)) for_logic(self)
.try_blame(blame)
.map_err(|e| e.with_redirs(guard))
} }
} }
fn exec_if(&mut self, if_stmt: Node) -> ShResult<()> { fn exec_if(&mut self, if_stmt: Node) -> ShResult<()> {
@@ -648,7 +696,9 @@ impl Dispatcher {
} }
}) })
} else { } else {
if_logic(self).try_blame(blame).map_err(|e| e.with_redirs(guard)) if_logic(self)
.try_blame(blame)
.map_err(|e| e.with_redirs(guard))
} }
} }
fn exec_pipeline(&mut self, pipeline: Node) -> ShResult<()> { fn exec_pipeline(&mut self, pipeline: Node) -> ShResult<()> {
@@ -692,11 +742,14 @@ impl Dispatcher {
// SIGTTOU when they try to modify terminal attributes. // SIGTTOU when they try to modify terminal attributes.
// Only for interactive (top-level) pipelines — command substitution // Only for interactive (top-level) pipelines — command substitution
// and other non-interactive contexts must not steal the terminal. // and other non-interactive contexts must not steal the terminal.
if !tty_attached && !is_bg && self.interactive if !tty_attached
&& let Some(pgid) = self.job_stack.curr_job_mut().unwrap().pgid() { && !is_bg
attach_tty(pgid).ok(); && self.interactive
tty_attached = true; && let Some(pgid) = self.job_stack.curr_job_mut().unwrap().pgid()
} {
attach_tty(pgid).ok();
tty_attached = true;
}
} }
let job = self.job_stack.finalize_job().unwrap(); let job = self.job_stack.finalize_job().unwrap();
dispatch_job(job, is_bg, self.interactive)?; dispatch_job(job, is_bg, self.interactive)?;
@@ -732,7 +785,7 @@ impl Dispatcher {
} }
fn dispatch_builtin(&mut self, mut cmd: Node) -> ShResult<()> { fn dispatch_builtin(&mut self, mut cmd: Node) -> ShResult<()> {
let cmd_raw = cmd.get_command().unwrap().to_string(); let cmd_raw = cmd.get_command().unwrap().to_string();
let context = cmd.context.clone(); let context = cmd.context.clone();
let NdRule::Command { assignments, argv } = &mut cmd.class else { let NdRule::Command { assignments, argv } = &mut cmd.class else {
unreachable!() unreachable!()
}; };
@@ -815,18 +868,18 @@ impl Dispatcher {
"unset" => unset(cmd), "unset" => unset(cmd),
"complete" => complete_builtin(cmd), "complete" => complete_builtin(cmd),
"compgen" => compgen_builtin(cmd), "compgen" => compgen_builtin(cmd),
"map" => map::map(cmd), "map" => map::map(cmd),
"pop" => arr_pop(cmd), "pop" => arr_pop(cmd),
"fpop" => arr_fpop(cmd), "fpop" => arr_fpop(cmd),
"push" => arr_push(cmd), "push" => arr_push(cmd),
"fpush" => arr_fpush(cmd), "fpush" => arr_fpush(cmd),
"rotate" => arr_rotate(cmd), "rotate" => arr_rotate(cmd),
"wait" => jobctl::wait(cmd), "wait" => jobctl::wait(cmd),
"type" => intro::type_builtin(cmd), "type" => intro::type_builtin(cmd),
"getopts" => getopts(cmd), "getopts" => getopts(cmd),
"keymap" => keymap::keymap(cmd), "keymap" => keymap::keymap(cmd),
"read_key" => read::read_key(cmd), "read_key" => read::read_key(cmd),
"autocmd" => autocmd(cmd), "autocmd" => autocmd(cmd),
"true" | ":" => { "true" | ":" => {
state::set_status(0); state::set_status(0);
Ok(()) Ok(())
@@ -838,17 +891,17 @@ impl Dispatcher {
_ => unimplemented!("Have not yet added support for builtin '{}'", cmd_raw), _ => unimplemented!("Have not yet added support for builtin '{}'", cmd_raw),
}; };
if let Err(e) = result { if let Err(e) = result {
if !e.is_flow_control() { if !e.is_flow_control() {
state::set_status(1); state::set_status(1);
} }
Err(e.with_context(context).with_redirs(redir_guard)) Err(e.with_context(context).with_redirs(redir_guard))
} else { } else {
Ok(()) Ok(())
} }
} }
fn exec_cmd(&mut self, cmd: Node) -> ShResult<()> { fn exec_cmd(&mut self, cmd: Node) -> ShResult<()> {
let blame = cmd.get_span().clone(); let blame = cmd.get_span().clone();
let context = cmd.context.clone(); let context = cmd.context.clone();
let NdRule::Command { assignments, argv } = cmd.class else { let NdRule::Command { assignments, argv } = cmd.class else {
unreachable!() unreachable!()
@@ -887,7 +940,7 @@ impl Dispatcher {
// For foreground jobs, take the terminal BEFORE resetting // For foreground jobs, take the terminal BEFORE resetting
// signals. SIGTTOU is still SIG_IGN (inherited from the shell), // signals. SIGTTOU is still SIG_IGN (inherited from the shell),
// so tcsetpgrp won't stop us. This prevents a race // so tcsetpgrp won't stop us. This prevents a race
// where the child exec's and tries to read stdin before the // where the child exec's and tries to read stdin before the
// parent has called tcsetpgrp — which would deliver SIGTTIN // parent has called tcsetpgrp — which would deliver SIGTTIN
// (now SIG_DFL after reset_signals) and stop the child. // (now SIG_DFL after reset_signals) and stop the child.
@@ -918,14 +971,14 @@ impl Dispatcher {
match e { match e {
Errno::ENOENT => { Errno::ENOENT => {
ShErr::new(ShErrKind::NotFound, span.clone()) ShErr::new(ShErrKind::NotFound, span.clone())
.labeled(span, format!("{cmd_str}: command not found")) .labeled(span, format!("{cmd_str}: command not found"))
.with_context(context) .with_context(context)
.print_error(); .print_error();
} }
_ => { _ => {
ShErr::at(ShErrKind::Errno(e), span, format!("{e}")) ShErr::at(ShErrKind::Errno(e), span, format!("{e}"))
.with_context(context) .with_context(context)
.print_error(); .print_error();
} }
} }
exit(e as i32) exit(e as i32)

View File

@@ -25,98 +25,101 @@ pub const KEYWORDS: [&str; 16] = [
pub const OPENERS: [&str; 6] = ["if", "while", "until", "for", "select", "case"]; pub const OPENERS: [&str; 6] = ["if", "while", "until", "for", "select", "case"];
/// Used to track whether the lexer is currently inside a quote, and if so, which type /// Used to track whether the lexer is currently inside a quote, and if so, which type
#[derive(Default,Debug)] #[derive(Default, Debug)]
pub enum QuoteState { pub enum QuoteState {
#[default] #[default]
Outside, Outside,
Single, Single,
Double Double,
} }
impl QuoteState { impl QuoteState {
pub fn outside(&self) -> bool { pub fn outside(&self) -> bool {
matches!(self, QuoteState::Outside) matches!(self, QuoteState::Outside)
} }
pub fn in_single(&self) -> bool { pub fn in_single(&self) -> bool {
matches!(self, QuoteState::Single) matches!(self, QuoteState::Single)
} }
pub fn in_double(&self) -> bool { pub fn in_double(&self) -> bool {
matches!(self, QuoteState::Double) matches!(self, QuoteState::Double)
} }
pub fn in_quote(&self) -> bool { pub fn in_quote(&self) -> bool {
!self.outside() !self.outside()
} }
/// Toggles whether we are in a double quote. If self = QuoteState::Single, this does nothing, since double quotes inside single quotes are just literal characters /// Toggles whether we are in a double quote. If self = QuoteState::Single, this does nothing, since double quotes inside single quotes are just literal characters
pub fn toggle_double(&mut self) { pub fn toggle_double(&mut self) {
match self { match self {
QuoteState::Outside => *self = QuoteState::Double, QuoteState::Outside => *self = QuoteState::Double,
QuoteState::Double => *self = QuoteState::Outside, QuoteState::Double => *self = QuoteState::Outside,
_ => {} _ => {}
} }
} }
/// Toggles whether we are in a single quote. If self == QuoteState::Double, this does nothing, since single quotes are not interpreted inside double quotes /// Toggles whether we are in a single quote. If self == QuoteState::Double, this does nothing, since single quotes are not interpreted inside double quotes
pub fn toggle_single(&mut self) { pub fn toggle_single(&mut self) {
match self { match self {
QuoteState::Outside => *self = QuoteState::Single, QuoteState::Outside => *self = QuoteState::Single,
QuoteState::Single => *self = QuoteState::Outside, QuoteState::Single => *self = QuoteState::Outside,
_ => {} _ => {}
} }
} }
} }
#[derive(Clone, PartialEq, Default, Debug, Eq, Hash)] #[derive(Clone, PartialEq, Default, Debug, Eq, Hash)]
pub struct SpanSource { pub struct SpanSource {
name: String, name: String,
content: Arc<String> content: Arc<String>,
} }
impl SpanSource { impl SpanSource {
pub fn name(&self) -> &str { pub fn name(&self) -> &str {
&self.name &self.name
} }
pub fn content(&self) -> Arc<String> { pub fn content(&self) -> Arc<String> {
self.content.clone() self.content.clone()
} }
pub fn rename(&mut self, name: String) { pub fn rename(&mut self, name: String) {
self.name = name; self.name = name;
} }
} }
impl Display for SpanSource { impl Display for SpanSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name) write!(f, "{}", self.name)
} }
} }
/// Span::new(10..20) /// Span::new(10..20)
#[derive(Clone, PartialEq, Default, Debug)] #[derive(Clone, PartialEq, Default, Debug)]
pub struct Span { pub struct Span {
range: Range<usize>, range: Range<usize>,
source: SpanSource source: SpanSource,
} }
impl Span { impl Span {
/// New `Span`. Wraps a range and a string slice that it refers to. /// New `Span`. Wraps a range and a string slice that it refers to.
pub fn new(range: Range<usize>, source: Arc<String>) -> Self { pub fn new(range: Range<usize>, source: Arc<String>) -> Self {
let source = SpanSource { name: "<stdin>".into(), content: source }; let source = SpanSource {
name: "<stdin>".into(),
content: source,
};
Span { range, source } Span { range, source }
} }
pub fn from_span_source(range: Range<usize>, source: SpanSource) -> Self { pub fn from_span_source(range: Range<usize>, source: SpanSource) -> Self {
Span { range, source } Span { range, source }
} }
pub fn rename(&mut self, name: String) { pub fn rename(&mut self, name: String) {
self.source.name = name; self.source.name = name;
} }
pub fn with_name(mut self, name: String) -> Self { pub fn with_name(mut self, name: String) -> Self {
self.source.name = name; self.source.name = name;
self self
} }
pub fn line_and_col(&self) -> (usize,usize) { pub fn line_and_col(&self) -> (usize, usize) {
let content = self.source.content(); let content = self.source.content();
let source = ariadne::Source::from(content.as_str()); let source = ariadne::Source::from(content.as_str());
let (_, line, col) = source.get_byte_line(self.range.start).unwrap(); let (_, line, col) = source.get_byte_line(self.range.start).unwrap();
(line, col) (line, col)
} }
/// Slice the source string at the wrapped range /// Slice the source string at the wrapped range
pub fn as_str(&self) -> &str { pub fn as_str(&self) -> &str {
&self.source.content[self.range().start..self.range().end] &self.source.content[self.range().start..self.range().end]
@@ -138,19 +141,19 @@ impl Span {
} }
impl ariadne::Span for Span { impl ariadne::Span for Span {
type SourceId = SpanSource; type SourceId = SpanSource;
fn source(&self) -> &Self::SourceId { fn source(&self) -> &Self::SourceId {
&self.source &self.source
} }
fn start(&self) -> usize { fn start(&self) -> usize {
self.range.start self.range.start
} }
fn end(&self) -> usize { fn end(&self) -> usize {
self.range.end self.range.end
} }
} }
/// Allows simple access to the underlying range wrapped by the span /// Allows simple access to the underlying range wrapped by the span
@@ -243,7 +246,7 @@ bitflags! {
pub struct LexStream { pub struct LexStream {
source: Arc<String>, source: Arc<String>,
pub cursor: usize, pub cursor: usize,
pub name: String, pub name: String,
quote_state: QuoteState, quote_state: QuoteState,
brc_grp_depth: usize, brc_grp_depth: usize,
brc_grp_start: Option<usize>, brc_grp_start: Option<usize>,
@@ -273,23 +276,23 @@ bitflags! {
} }
pub fn clean_input(input: &str) -> String { pub fn clean_input(input: &str) -> String {
let mut chars = input.chars().peekable(); let mut chars = input.chars().peekable();
let mut output = String::new(); let mut output = String::new();
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
match ch { match ch {
'\\' if chars.peek() == Some(&'\n') => { '\\' if chars.peek() == Some(&'\n') => {
chars.next(); chars.next();
} }
'\r' => { '\r' => {
if chars.peek() == Some(&'\n') { if chars.peek() == Some(&'\n') {
chars.next(); chars.next();
} }
output.push('\n'); output.push('\n');
} }
_ => output.push(ch), _ => output.push(ch),
} }
} }
output output
} }
impl LexStream { impl LexStream {
@@ -298,7 +301,7 @@ impl LexStream {
Self { Self {
flags, flags,
source, source,
name: "<stdin>".into(), name: "<stdin>".into(),
cursor: 0, cursor: 0,
quote_state: QuoteState::default(), quote_state: QuoteState::default(),
brc_grp_depth: 0, brc_grp_depth: 0,
@@ -327,10 +330,10 @@ impl LexStream {
}; };
self.source.get(start..end) self.source.get(start..end)
} }
pub fn with_name(mut self, name: String) -> Self { pub fn with_name(mut self, name: String) -> Self {
self.name = name; self.name = name;
self self
} }
pub fn slice_from_cursor(&self) -> Option<&str> { pub fn slice_from_cursor(&self) -> Option<&str> {
self.slice(self.cursor..) self.slice(self.cursor..)
} }
@@ -475,11 +478,11 @@ impl LexStream {
pos += ch.len_utf8(); pos += ch.len_utf8();
} }
} }
'\'' => { '\'' => {
pos += 1; pos += 1;
self.quote_state.toggle_single(); self.quote_state.toggle_single();
} }
_ if self.quote_state.in_single() => pos += ch.len_utf8(), _ if self.quote_state.in_single() => pos += ch.len_utf8(),
'$' if chars.peek() == Some(&'(') => { '$' if chars.peek() == Some(&'(') => {
pos += 2; pos += 2;
chars.next(); chars.next();
@@ -543,11 +546,11 @@ impl LexStream {
} }
} }
} }
'"' => { '"' => {
pos += 1; pos += 1;
self.quote_state.toggle_double(); self.quote_state.toggle_double();
} }
_ if self.quote_state.in_double() => pos += ch.len_utf8(), _ if self.quote_state.in_double() => pos += ch.len_utf8(),
'<' if chars.peek() == Some(&'(') => { '<' if chars.peek() == Some(&'(') => {
pos += 2; pos += 2;
chars.next(); chars.next();
@@ -770,7 +773,7 @@ impl LexStream {
} }
pub fn get_token(&self, range: Range<usize>, class: TkRule) -> Tk { pub fn get_token(&self, range: Range<usize>, class: TkRule) -> Tk {
let mut span = Span::new(range, self.source.clone()); let mut span = Span::new(range, self.source.clone());
span.rename(self.name.clone()); span.rename(self.name.clone());
Tk::new(class, span) Tk::new(class, span)
} }
} }
@@ -845,15 +848,15 @@ impl Iterator for LexStream {
self.set_next_is_cmd(true); self.set_next_is_cmd(true);
while let Some(ch) = get_char(&self.source, self.cursor) { while let Some(ch) = get_char(&self.source, self.cursor) {
match ch { match ch {
'\\' if get_char(&self.source, self.cursor + 1) == Some('\n') => { '\\' if get_char(&self.source, self.cursor + 1) == Some('\n') => {
self.cursor = (self.cursor + 2).min(self.source.len()); self.cursor = (self.cursor + 2).min(self.source.len());
} }
_ if is_hard_sep(ch) => { _ if is_hard_sep(ch) => {
self.cursor += 1; self.cursor += 1;
} }
_ => break, _ => break,
} }
} }
self.get_token(ch_idx..self.cursor, TkRule::Sep) self.get_token(ch_idx..self.cursor, TkRule::Sep)
} }
@@ -974,84 +977,101 @@ pub fn ends_with_unescaped(slice: &str, pat: &str) -> bool {
/// Splits a string by a pattern, but only if the pattern is not escaped by a backslash /// Splits a string by a pattern, but only if the pattern is not escaped by a backslash
/// and not in quotes. /// and not in quotes.
pub fn split_all_unescaped(slice: &str, pat: &str) -> Vec<String> { pub fn split_all_unescaped(slice: &str, pat: &str) -> Vec<String> {
let mut cursor = 0; let mut cursor = 0;
let mut splits = vec![]; let mut splits = vec![];
while let Some(split) = split_at_unescaped(&slice[cursor..], pat) { while let Some(split) = split_at_unescaped(&slice[cursor..], pat) {
cursor += split.0.len() + pat.len(); cursor += split.0.len() + pat.len();
splits.push(split.0); splits.push(split.0);
} }
if let Some(remaining) = slice.get(cursor..) { if let Some(remaining) = slice.get(cursor..) {
splits.push(remaining.to_string()); splits.push(remaining.to_string());
} }
splits splits
} }
/// Splits a string at the first occurrence of a pattern, but only if the pattern is not escaped by a backslash /// Splits a string at the first occurrence of a pattern, but only if the pattern is not escaped by a backslash
/// and not in quotes. Returns None if the pattern is not found or only found escaped. /// and not in quotes. Returns None if the pattern is not found or only found escaped.
pub fn split_at_unescaped(slice: &str, pat: &str) -> Option<(String,String)> { pub fn split_at_unescaped(slice: &str, pat: &str) -> Option<(String, String)> {
let mut chars = slice.char_indices().peekable(); let mut chars = slice.char_indices().peekable();
let mut qt_state = QuoteState::default(); let mut qt_state = QuoteState::default();
while let Some((i, ch)) = chars.next() { while let Some((i, ch)) = chars.next() {
match ch { match ch {
'\\' => { chars.next(); continue; } '\\' => {
'\'' => qt_state.toggle_single(), chars.next();
'"' => qt_state.toggle_double(), continue;
_ if qt_state.in_quote() => continue, }
_ => {} '\'' => qt_state.toggle_single(),
} '"' => qt_state.toggle_double(),
_ if qt_state.in_quote() => continue,
_ => {}
}
if slice[i..].starts_with(pat) { if slice[i..].starts_with(pat) {
let before = slice[..i].to_string(); let before = slice[..i].to_string();
let after = slice[i + pat.len()..].to_string(); let after = slice[i + pat.len()..].to_string();
return Some((before, after)); return Some((before, after));
} }
} }
None
None
} }
pub fn split_tk(tk: &Tk, pat: &str) -> Vec<Tk> { pub fn split_tk(tk: &Tk, pat: &str) -> Vec<Tk> {
let slice = tk.as_str(); let slice = tk.as_str();
let mut cursor = 0; let mut cursor = 0;
let mut splits = vec![]; let mut splits = vec![];
while let Some(split) = split_at_unescaped(&slice[cursor..], pat) { while let Some(split) = split_at_unescaped(&slice[cursor..], pat) {
let before_span = Span::new(tk.span.range().start + cursor..tk.span.range().start + cursor + split.0.len(), tk.source().clone()); let before_span = Span::new(
splits.push(Tk::new(tk.class.clone(), before_span)); tk.span.range().start + cursor..tk.span.range().start + cursor + split.0.len(),
cursor += split.0.len() + pat.len(); tk.source().clone(),
} );
if slice.get(cursor..).is_some_and(|s| !s.is_empty()) { splits.push(Tk::new(tk.class.clone(), before_span));
let remaining_span = Span::new(tk.span.range().start + cursor..tk.span.range().end, tk.source().clone()); cursor += split.0.len() + pat.len();
splits.push(Tk::new(tk.class.clone(), remaining_span)); }
} if slice.get(cursor..).is_some_and(|s| !s.is_empty()) {
splits let remaining_span = Span::new(
tk.span.range().start + cursor..tk.span.range().end,
tk.source().clone(),
);
splits.push(Tk::new(tk.class.clone(), remaining_span));
}
splits
} }
pub fn split_tk_at(tk: &Tk, pat: &str) -> Option<(Tk, Tk)> { pub fn split_tk_at(tk: &Tk, pat: &str) -> Option<(Tk, Tk)> {
let slice = tk.as_str(); let slice = tk.as_str();
let mut chars = slice.char_indices().peekable(); let mut chars = slice.char_indices().peekable();
let mut qt_state = QuoteState::default(); let mut qt_state = QuoteState::default();
while let Some((i, ch)) = chars.next() { while let Some((i, ch)) = chars.next() {
match ch { match ch {
'\\' => { chars.next(); continue; } '\\' => {
'\'' => qt_state.toggle_single(), chars.next();
'"' => qt_state.toggle_double(), continue;
_ if qt_state.in_quote() => continue, }
_ => {} '\'' => qt_state.toggle_single(),
} '"' => qt_state.toggle_double(),
_ if qt_state.in_quote() => continue,
_ => {}
}
if slice[i..].starts_with(pat) { if slice[i..].starts_with(pat) {
let before_span = Span::new(tk.span.range().start..tk.span.range().start + i, tk.source().clone()); let before_span = Span::new(
let after_span = Span::new(tk.span.range().start + i + pat.len()..tk.span.range().end, tk.source().clone()); tk.span.range().start..tk.span.range().start + i,
let before_tk = Tk::new(tk.class.clone(), before_span); tk.source().clone(),
let after_tk = Tk::new(tk.class.clone(), after_span); );
return Some((before_tk, after_tk)); let after_span = Span::new(
} tk.span.range().start + i + pat.len()..tk.span.range().end,
} tk.source().clone(),
);
let before_tk = Tk::new(tk.class.clone(), before_span);
let after_tk = Tk::new(tk.class.clone(), after_span);
return Some((before_tk, after_tk));
}
}
None None
} }
pub fn pos_is_escaped(slice: &str, pos: usize) -> bool { pub fn pos_is_escaped(slice: &str, pos: usize) -> bool {
@@ -1083,7 +1103,7 @@ pub fn lookahead(pat: &str, mut chars: Chars) -> Option<usize> {
pub fn case_pat_lookahead(mut chars: Peekable<Chars>) -> Option<usize> { pub fn case_pat_lookahead(mut chars: Peekable<Chars>) -> Option<usize> {
let mut pos = 0; let mut pos = 0;
let mut qt_state = QuoteState::default(); let mut qt_state = QuoteState::default();
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
pos += ch.len_utf8(); pos += ch.len_utf8();
match ch { match ch {
@@ -1108,12 +1128,12 @@ pub fn case_pat_lookahead(mut chars: Peekable<Chars>) -> Option<usize> {
} }
} }
} }
'\'' => { '\'' => {
qt_state.toggle_single(); qt_state.toggle_single();
} }
'"' => { '"' => {
qt_state.toggle_double(); qt_state.toggle_double();
} }
')' if qt_state.outside() => return Some(pos), ')' if qt_state.outside() => return Some(pos),
'(' if qt_state.outside() => return None, '(' if qt_state.outside() => return None,
_ => { /* continue */ } _ => { /* continue */ }

View File

@@ -9,7 +9,10 @@ use crate::{
libsh::{ libsh::{
error::{ShErr, ShErrKind, ShResult, last_color, next_color}, error::{ShErr, ShErrKind, ShResult, last_color, next_color},
utils::{NodeVecUtils, TkVecUtils}, utils::{NodeVecUtils, TkVecUtils},
}, parse::lex::clean_input, prelude::*, procio::IoMode },
parse::lex::clean_input,
prelude::*,
procio::IoMode,
}; };
pub mod execute; pub mod execute;
@@ -42,7 +45,7 @@ macro_rules! try_match {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ParsedSrc { pub struct ParsedSrc {
pub src: Arc<String>, pub src: Arc<String>,
pub name: String, pub name: String,
pub ast: Ast, pub ast: Ast,
pub lex_flags: LexFlags, pub lex_flags: LexFlags,
pub context: LabelCtx, pub context: LabelCtx,
@@ -57,16 +60,16 @@ impl ParsedSrc {
}; };
Self { Self {
src, src,
name: "<stdin>".into(), name: "<stdin>".into(),
ast: Ast::new(vec![]), ast: Ast::new(vec![]),
lex_flags: LexFlags::empty(), lex_flags: LexFlags::empty(),
context: VecDeque::new(), context: VecDeque::new(),
} }
} }
pub fn with_name(mut self, name: String) -> Self { pub fn with_name(mut self, name: String) -> Self {
self.name = name; self.name = name;
self self
} }
pub fn with_lex_flags(mut self, flags: LexFlags) -> Self { pub fn with_lex_flags(mut self, flags: LexFlags) -> Self {
self.lex_flags = flags; self.lex_flags = flags;
self self
@@ -77,7 +80,8 @@ impl ParsedSrc {
} }
pub fn parse_src(&mut self) -> Result<(), Vec<ShErr>> { pub fn parse_src(&mut self) -> Result<(), Vec<ShErr>> {
let mut tokens = vec![]; let mut tokens = vec![];
for lex_result in LexStream::new(self.src.clone(), self.lex_flags).with_name(self.name.clone()) { for lex_result in LexStream::new(self.src.clone(), self.lex_flags).with_name(self.name.clone())
{
match lex_result { match lex_result {
Ok(token) => tokens.push(token), Ok(token) => tokens.push(token),
Err(error) => return Err(vec![error]), Err(error) => return Err(vec![error]),
@@ -128,7 +132,7 @@ pub struct Node {
pub flags: NdFlags, pub flags: NdFlags,
pub redirs: Vec<Redir>, pub redirs: Vec<Redir>,
pub tokens: Vec<Tk>, pub tokens: Vec<Tk>,
pub context: LabelCtx, pub context: LabelCtx,
} }
impl Node { impl Node {
@@ -143,108 +147,108 @@ impl Node {
None None
} }
} }
pub fn get_context(&self, msg: String) -> (SpanSource, Label<Span>) { pub fn get_context(&self, msg: String) -> (SpanSource, Label<Span>) {
let color = last_color(); let color = last_color();
let span = self.get_span().clone(); let span = self.get_span().clone();
( (
span.clone().source().clone(), span.clone().source().clone(),
Label::new(span).with_color(color).with_message(msg) Label::new(span).with_color(color).with_message(msg),
) )
} }
fn walk_tree<F: Fn(&mut Node)>(&mut self, f: &F) { fn walk_tree<F: Fn(&mut Node)>(&mut self, f: &F) {
f(self); f(self);
match self.class { match self.class {
NdRule::IfNode { NdRule::IfNode {
ref mut cond_nodes, ref mut cond_nodes,
ref mut else_block, ref mut else_block,
} => { } => {
for node in cond_nodes { for node in cond_nodes {
let CondNode { cond, body } = node; let CondNode { cond, body } = node;
cond.walk_tree(f); cond.walk_tree(f);
for body_node in body { for body_node in body {
body_node.walk_tree(f); body_node.walk_tree(f);
} }
} }
for else_node in else_block { for else_node in else_block {
else_node.walk_tree(f); else_node.walk_tree(f);
} }
} }
NdRule::LoopNode { NdRule::LoopNode {
kind: _, kind: _,
ref mut cond_node, ref mut cond_node,
} => { } => {
let CondNode { cond, body } = cond_node; let CondNode { cond, body } = cond_node;
cond.walk_tree(f); cond.walk_tree(f);
for body_node in body { for body_node in body {
body_node.walk_tree(f); body_node.walk_tree(f);
} }
} }
NdRule::ForNode { NdRule::ForNode {
vars: _, vars: _,
arr: _, arr: _,
ref mut body, ref mut body,
} => { } => {
for body_node in body { for body_node in body {
body_node.walk_tree(f); body_node.walk_tree(f);
} }
} }
NdRule::CaseNode { NdRule::CaseNode {
pattern: _, pattern: _,
ref mut case_blocks, ref mut case_blocks,
} => { } => {
for block in case_blocks { for block in case_blocks {
let CaseNode { pattern: _, body } = block; let CaseNode { pattern: _, body } = block;
for body_node in body { for body_node in body {
body_node.walk_tree(f); body_node.walk_tree(f);
} }
} }
} }
NdRule::Command { NdRule::Command {
ref mut assignments, ref mut assignments,
argv: _, argv: _,
} => { } => {
for assign_node in assignments { for assign_node in assignments {
assign_node.walk_tree(f); assign_node.walk_tree(f);
} }
} }
NdRule::Pipeline { NdRule::Pipeline {
ref mut cmds, ref mut cmds,
pipe_err: _, pipe_err: _,
} => { } => {
for cmd_node in cmds { for cmd_node in cmds {
cmd_node.walk_tree(f); cmd_node.walk_tree(f);
} }
} }
NdRule::Conjunction { ref mut elements } => { NdRule::Conjunction { ref mut elements } => {
for node in elements.iter_mut() { for node in elements.iter_mut() {
let ConjunctNode { cmd, operator: _ } = node; let ConjunctNode { cmd, operator: _ } = node;
cmd.walk_tree(f); cmd.walk_tree(f);
} }
} }
NdRule::Assignment { NdRule::Assignment {
kind: _, kind: _,
var: _, var: _,
val: _, val: _,
} => (), // No nodes to check } => (), // No nodes to check
NdRule::BraceGrp { ref mut body } => { NdRule::BraceGrp { ref mut body } => {
for body_node in body { for body_node in body {
body_node.walk_tree(f); body_node.walk_tree(f);
} }
} }
NdRule::FuncDef { NdRule::FuncDef {
name: _, name: _,
ref mut body, ref mut body,
} => { } => {
body.walk_tree(f); body.walk_tree(f);
} }
NdRule::Test { cases: _ } => (), NdRule::Test { cases: _ } => (),
} }
} }
pub fn propagate_context(&mut self, ctx: (SpanSource, Label<Span>)) { pub fn propagate_context(&mut self, ctx: (SpanSource, Label<Span>)) {
self.walk_tree(&|nd| nd.context.push_back(ctx.clone())); self.walk_tree(&|nd| nd.context.push_back(ctx.clone()));
} }
pub fn get_span(&self) -> Span { pub fn get_span(&self) -> Span {
let Some(first_tk) = self.tokens.first() else { let Some(first_tk) = self.tokens.first() else {
unreachable!() unreachable!()
@@ -662,20 +666,23 @@ pub enum NdRule {
pub struct ParseStream { pub struct ParseStream {
pub tokens: Vec<Tk>, pub tokens: Vec<Tk>,
pub context: LabelCtx pub context: LabelCtx,
} }
impl Debug for ParseStream { impl Debug for ParseStream {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ParseStream") f.debug_struct("ParseStream")
.field("tokens", &self.tokens) .field("tokens", &self.tokens)
.finish() .finish()
} }
} }
impl ParseStream { impl ParseStream {
pub fn new(tokens: Vec<Tk>) -> Self { pub fn new(tokens: Vec<Tk>) -> Self {
Self { tokens, context: VecDeque::new() } Self {
tokens,
context: VecDeque::new(),
}
} }
pub fn with_context(tokens: Vec<Tk>, context: LabelCtx) -> Self { pub fn with_context(tokens: Vec<Tk>, context: LabelCtx) -> Self {
Self { tokens, context } Self { tokens, context }
@@ -831,39 +838,42 @@ impl ParseStream {
let name_tk = self.next_tk().unwrap(); let name_tk = self.next_tk().unwrap();
node_tks.push(name_tk.clone()); node_tks.push(name_tk.clone());
let name = name_tk.clone(); let name = name_tk.clone();
let name_raw = name.to_string(); let name_raw = name.to_string();
let mut src = name_tk.span.span_source().clone(); let mut src = name_tk.span.span_source().clone();
src.rename(name_raw.clone()); src.rename(name_raw.clone());
let color = next_color(); let color = next_color();
// Push a placeholder context so child nodes inherit it // Push a placeholder context so child nodes inherit it
self.context.push_back(( self.context.push_back((
src.clone(), src.clone(),
Label::new(name_tk.span.clone().with_name(name_raw.clone())) Label::new(name_tk.span.clone().with_name(name_raw.clone()))
.with_message(format!("in function '{}' defined here", name_raw.clone().fg(color))) .with_message(format!(
.with_color(color), "in function '{}' defined here",
)); name_raw.clone().fg(color)
))
.with_color(color),
));
let Some(brc_grp) = self.parse_brc_grp(true /* from_func_def */)? else { let Some(brc_grp) = self.parse_brc_grp(true /* from_func_def */)? else {
self.context.pop_back(); self.context.pop_back();
return Err(parse_err_full( return Err(parse_err_full(
"Expected a brace group after function name", "Expected a brace group after function name",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone() self.context.clone(),
)); ));
}; };
body = Box::new(brc_grp); body = Box::new(brc_grp);
// Replace placeholder with full-span label // Replace placeholder with full-span label
self.context.pop_back(); self.context.pop_back();
let node = Node { let node = Node {
class: NdRule::FuncDef { name, body }, class: NdRule::FuncDef { name, body },
flags: NdFlags::empty(), flags: NdFlags::empty(),
redirs: vec![], redirs: vec![],
tokens: node_tks, tokens: node_tks,
context: self.context.clone() context: self.context.clone(),
}; };
self.context.pop_back(); self.context.pop_back();
Ok(Some(node)) Ok(Some(node))
} }
fn panic_mode(&mut self, node_tks: &mut Vec<Tk>) { fn panic_mode(&mut self, node_tks: &mut Vec<Tk>) {
@@ -893,7 +903,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Malformed test call", "Malformed test call",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone() self.context.clone(),
)); ));
} else { } else {
break; break;
@@ -920,7 +930,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Invalid placement for logical operator in test", "Invalid placement for logical operator in test",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone() self.context.clone(),
)); ));
} }
let op = match tk.class { let op = match tk.class {
@@ -936,7 +946,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Invalid placement for logical operator in test", "Invalid placement for logical operator in test",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone() self.context.clone(),
)); ));
} }
} }
@@ -982,7 +992,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Expected a closing brace for this brace group", "Expected a closing brace for this brace group",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone() self.context.clone(),
)); ));
} }
} }
@@ -1049,11 +1059,9 @@ impl ParseStream {
let pat_err = parse_err_full( let pat_err = parse_err_full(
"Expected a pattern after 'case' keyword", "Expected a pattern after 'case' keyword",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone() self.context.clone(),
) )
.with_note( .with_note("Patterns can be raw text, or anything that gets substituted with raw text");
"Patterns can be raw text, or anything that gets substituted with raw text"
);
let Some(pat_tk) = self.next_tk() else { let Some(pat_tk) = self.next_tk() else {
self.panic_mode(&mut node_tks); self.panic_mode(&mut node_tks);
@@ -1073,7 +1081,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Expected 'in' after case variable name", "Expected 'in' after case variable name",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone() self.context.clone(),
)); ));
} }
node_tks.push(self.next_tk().unwrap()); node_tks.push(self.next_tk().unwrap());
@@ -1086,7 +1094,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Expected a case pattern here", "Expected a case pattern here",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone() self.context.clone(),
)); ));
} }
let case_pat_tk = self.next_tk().unwrap(); let case_pat_tk = self.next_tk().unwrap();
@@ -1123,7 +1131,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Expected 'esac' after case block", "Expected 'esac' after case block",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone() self.context.clone(),
)); ));
} }
} }
@@ -1140,12 +1148,12 @@ impl ParseStream {
}; };
Ok(Some(node)) Ok(Some(node))
} }
fn make_err(&self, span: lex::Span, label: Label<lex::Span>) -> ShErr { fn make_err(&self, span: lex::Span, label: Label<lex::Span>) -> ShErr {
let src = span.span_source().clone(); let src = span.span_source().clone();
ShErr::new(ShErrKind::ParseErr, span) ShErr::new(ShErrKind::ParseErr, span)
.with_label(src, label) .with_label(src, label)
.with_context(self.context.clone()) .with_context(self.context.clone())
} }
fn parse_if(&mut self) -> ShResult<Option<Node>> { fn parse_if(&mut self) -> ShResult<Option<Node>> {
// Needs at last one 'if-then', // Needs at last one 'if-then',
// Any number of 'elif-then', // Any number of 'elif-then',
@@ -1164,14 +1172,19 @@ impl ParseStream {
let prefix_keywrd = if cond_nodes.is_empty() { "if" } else { "elif" }; let prefix_keywrd = if cond_nodes.is_empty() { "if" } else { "elif" };
let Some(cond) = self.parse_cmd_list()? else { let Some(cond) = self.parse_cmd_list()? else {
self.panic_mode(&mut node_tks); self.panic_mode(&mut node_tks);
let span = node_tks.get_span().unwrap(); let span = node_tks.get_span().unwrap();
let color = next_color(); let color = next_color();
return Err(self.make_err(span.clone(), return Err(
Label::new(span) self.make_err(
.with_message(format!("Expected an expression after '{}'", prefix_keywrd.fg(color))) span.clone(),
.with_color(color) Label::new(span)
)); .with_message(format!(
"Expected an expression after '{}'",
prefix_keywrd.fg(color)
))
.with_color(color),
),
);
}; };
node_tks.extend(cond.tokens.clone()); node_tks.extend(cond.tokens.clone());
@@ -1180,7 +1193,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
&format!("Expected 'then' after '{prefix_keywrd}' condition"), &format!("Expected 'then' after '{prefix_keywrd}' condition"),
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone() self.context.clone(),
)); ));
} }
node_tks.push(self.next_tk().unwrap()); node_tks.push(self.next_tk().unwrap());
@@ -1196,7 +1209,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Expected an expression after 'then'", "Expected an expression after 'then'",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone() self.context.clone(),
)); ));
}; };
let cond_node = CondNode { let cond_node = CondNode {
@@ -1205,7 +1218,7 @@ impl ParseStream {
}; };
cond_nodes.push(cond_node); cond_nodes.push(cond_node);
self.catch_separator(&mut node_tks); self.catch_separator(&mut node_tks);
if !self.check_keyword("elif") || !self.next_tk_is_some() { if !self.check_keyword("elif") || !self.next_tk_is_some() {
break; break;
} else { } else {
@@ -1214,7 +1227,7 @@ impl ParseStream {
} }
} }
self.catch_separator(&mut node_tks); self.catch_separator(&mut node_tks);
if self.check_keyword("else") { if self.check_keyword("else") {
node_tks.push(self.next_tk().unwrap()); node_tks.push(self.next_tk().unwrap());
self.catch_separator(&mut node_tks); self.catch_separator(&mut node_tks);
@@ -1226,7 +1239,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Expected an expression after 'else'", "Expected an expression after 'else'",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone() self.context.clone(),
)); ));
} }
} }
@@ -1237,7 +1250,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Expected 'fi' after if statement", "Expected 'fi' after if statement",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone() self.context.clone(),
)); ));
} }
node_tks.push(self.next_tk().unwrap()); node_tks.push(self.next_tk().unwrap());
@@ -1293,7 +1306,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"This for loop is missing a variable", "This for loop is missing a variable",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone() self.context.clone(),
)); ));
} }
if arr.is_empty() { if arr.is_empty() {
@@ -1301,7 +1314,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"This for loop is missing an array", "This for loop is missing an array",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone() self.context.clone(),
)); ));
} }
if !self.check_keyword("do") || !self.next_tk_is_some() { if !self.check_keyword("do") || !self.next_tk_is_some() {
@@ -1309,7 +1322,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Missing a 'do' for this for loop", "Missing a 'do' for this for loop",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone() self.context.clone(),
)); ));
} }
node_tks.push(self.next_tk().unwrap()); node_tks.push(self.next_tk().unwrap());
@@ -1325,7 +1338,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Missing a 'done' after this for loop", "Missing a 'done' after this for loop",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone() self.context.clone(),
)); ));
} }
node_tks.push(self.next_tk().unwrap()); node_tks.push(self.next_tk().unwrap());
@@ -1366,7 +1379,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
&format!("Expected an expression after '{loop_kind}'"), // It also implements Display &format!("Expected an expression after '{loop_kind}'"), // It also implements Display
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone() self.context.clone(),
)); ));
}; };
node_tks.extend(cond.tokens.clone()); node_tks.extend(cond.tokens.clone());
@@ -1376,7 +1389,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Expected 'do' after loop condition", "Expected 'do' after loop condition",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone() self.context.clone(),
)); ));
} }
node_tks.push(self.next_tk().unwrap()); node_tks.push(self.next_tk().unwrap());
@@ -1392,7 +1405,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Expected an expression after 'do'", "Expected an expression after 'do'",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone() self.context.clone(),
)); ));
}; };
@@ -1402,7 +1415,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Expected 'done' after loop body", "Expected 'done' after loop body",
&node_tks.get_span().unwrap(), &node_tks.get_span().unwrap(),
self.context.clone() self.context.clone(),
)); ));
} }
node_tks.push(self.next_tk().unwrap()); node_tks.push(self.next_tk().unwrap());
@@ -1479,7 +1492,7 @@ impl ParseStream {
return Err(parse_err_full( return Err(parse_err_full(
"Found case pattern in command", "Found case pattern in command",
&prefix_tk.span, &prefix_tk.span,
self.context.clone() self.context.clone(),
)); ));
} }
let is_cmd = prefix_tk.flags.contains(TkFlags::IS_CMD); let is_cmd = prefix_tk.flags.contains(TkFlags::IS_CMD);
@@ -1515,14 +1528,14 @@ impl ParseStream {
// If we have assignments but no command word, // If we have assignments but no command word,
// return the assignment-only command without parsing more tokens // return the assignment-only command without parsing more tokens
self.commit(node_tks.len()); self.commit(node_tks.len());
let mut context = self.context.clone(); let mut context = self.context.clone();
let assignments_span = assignments.get_span().unwrap(); let assignments_span = assignments.get_span().unwrap();
context.push_back(( context.push_back((
assignments_span.source().clone(), assignments_span.source().clone(),
Label::new(assignments_span) Label::new(assignments_span)
.with_message("in variable assignment defined here".to_string()) .with_message("in variable assignment defined here".to_string())
.with_color(next_color()) .with_color(next_color()),
)); ));
return Ok(Some(Node { return Ok(Some(Node {
class: NdRule::Command { assignments, argv }, class: NdRule::Command { assignments, argv },
tokens: node_tks, tokens: node_tks,
@@ -1559,7 +1572,7 @@ impl ParseStream {
let path_tk = tk_iter.next(); let path_tk = tk_iter.next();
if path_tk.is_none_or(|tk| tk.class == TkRule::EOI) { if path_tk.is_none_or(|tk| tk.class == TkRule::EOI) {
self.panic_mode(&mut node_tks); self.panic_mode(&mut node_tks);
return Err(ShErr::at( return Err(ShErr::at(
ShErrKind::ParseErr, ShErrKind::ParseErr,
tk.span.clone(), tk.span.clone(),
@@ -1737,7 +1750,7 @@ fn node_is_punctuated(tokens: &[Tk]) -> bool {
} }
pub fn get_redir_file<P: AsRef<Path>>(class: RedirType, path: P) -> ShResult<File> { pub fn get_redir_file<P: AsRef<Path>>(class: RedirType, path: P) -> ShResult<File> {
let path = path.as_ref(); let path = path.as_ref();
let result = match class { let result = match class {
RedirType::Input => OpenOptions::new().read(true).open(Path::new(&path)), RedirType::Input => OpenOptions::new().read(true).open(Path::new(&path)),
RedirType::Output => OpenOptions::new() RedirType::Output => OpenOptions::new()
@@ -1745,23 +1758,22 @@ pub fn get_redir_file<P: AsRef<Path>>(class: RedirType, path: P) -> ShResult<Fil
.create(true) .create(true)
.truncate(true) .truncate(true)
.open(path), .open(path),
RedirType::Append => OpenOptions::new() RedirType::Append => OpenOptions::new().create(true).append(true).open(path),
.create(true)
.append(true)
.open(path),
_ => unimplemented!(), _ => unimplemented!(),
}; };
Ok(result?) Ok(result?)
} }
fn parse_err_full(reason: &str, blame: &Span, context: LabelCtx) -> ShErr { fn parse_err_full(reason: &str, blame: &Span, context: LabelCtx) -> ShErr {
let color = last_color(); let color = last_color();
ShErr::new(ShErrKind::ParseErr, blame.clone()) ShErr::new(ShErrKind::ParseErr, blame.clone())
.with_label( .with_label(
blame.span_source().clone(), blame.span_source().clone(),
Label::new(blame.clone()).with_message(reason).with_color(color) Label::new(blame.clone())
) .with_message(reason)
.with_context(context) .with_color(color),
)
.with_context(context)
} }
fn is_func_name(tk: Option<&Tk>) -> bool { fn is_func_name(tk: Option<&Tk>) -> bool {

File diff suppressed because it is too large Load Diff

View File

@@ -26,7 +26,7 @@ pub struct Highlighter {
style_stack: Vec<StyleSet>, style_stack: Vec<StyleSet>,
last_was_reset: bool, last_was_reset: bool,
in_selection: bool, in_selection: bool,
only_hl_visual: bool only_hl_visual: bool,
} }
impl Highlighter { impl Highlighter {
@@ -39,12 +39,12 @@ impl Highlighter {
style_stack: Vec::new(), style_stack: Vec::new(),
last_was_reset: true, // start as true so we don't emit a leading reset last_was_reset: true, // start as true so we don't emit a leading reset
in_selection: false, in_selection: false,
only_hl_visual: false only_hl_visual: false,
} }
} }
pub fn only_visual(&mut self, only_visual: bool) { pub fn only_visual(&mut self, only_visual: bool) {
self.only_hl_visual = only_visual; self.only_hl_visual = only_visual;
} }
/// Loads raw input text and annotates it with syntax markers /// Loads raw input text and annotates it with syntax markers
/// ///
@@ -66,25 +66,25 @@ impl Highlighter {
out out
} }
pub fn expand_control_chars(&mut self) { pub fn expand_control_chars(&mut self) {
let mut expanded = String::new(); let mut expanded = String::new();
let mut chars = self.input.chars().peekable(); let mut chars = self.input.chars().peekable();
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
match ch { match ch {
'\n' | '\t' | '\r' => expanded.push(ch), '\n' | '\t' | '\r' => expanded.push(ch),
c if c as u32 <= 0x1F => { c if c as u32 <= 0x1F => {
let display = (c as u8 + b'@') as char; let display = (c as u8 + b'@') as char;
expanded.push_str("\x1b[7m^"); expanded.push_str("\x1b[7m^");
expanded.push(display); expanded.push(display);
expanded.push_str("\x1b[0m"); expanded.push_str("\x1b[0m");
} }
_ => expanded.push(ch), _ => expanded.push(ch),
} }
} }
self.input = expanded; self.input = expanded;
} }
/// Processes the annotated input and generates ANSI-styled output /// Processes the annotated input and generates ANSI-styled output
/// ///
@@ -104,9 +104,9 @@ impl Highlighter {
self.reapply_style(); self.reapply_style();
self.in_selection = false; self.in_selection = false;
} }
_ if self.only_hl_visual => { _ if self.only_hl_visual => {
self.output.push(ch); self.output.push(ch);
} }
markers::STRING_DQ_END markers::STRING_DQ_END
| markers::STRING_SQ_END | markers::STRING_SQ_END
| markers::VAR_SUB_END | markers::VAR_SUB_END
@@ -471,7 +471,7 @@ impl Highlighter {
} }
impl Default for Highlighter { impl Default for Highlighter {
fn default() -> Self { fn default() -> Self {
Self::new() Self::new()
} }
} }

View File

@@ -70,10 +70,10 @@ impl HistEntry {
impl FromStr for HistEntry { impl FromStr for HistEntry {
type Err = ShErr; type Err = ShErr;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
let err = Err(ShErr::simple( let err = Err(ShErr::simple(
ShErrKind::HistoryReadErr, ShErrKind::HistoryReadErr,
format!("Bad formatting on history entry '{s}'"), format!("Bad formatting on history entry '{s}'"),
)); ));
//: 248972349;148;echo foo; echo bar //: 248972349;148;echo foo; echo bar
let Some(cleaned) = s.strip_prefix(": ") else { let Some(cleaned) = s.strip_prefix(": ") else {
@@ -132,10 +132,10 @@ impl FromStr for HistEntries {
while let Some((i, line)) = lines.next() { while let Some((i, line)) = lines.next() {
if !line.starts_with(": ") { if !line.starts_with(": ") {
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::HistoryReadErr, ShErrKind::HistoryReadErr,
format!("Bad formatting on line {i}"), format!("Bad formatting on line {i}"),
)); ));
} }
let mut chars = line.chars().peekable(); let mut chars = line.chars().peekable();
let mut feeding_lines = true; let mut feeding_lines = true;
@@ -161,10 +161,10 @@ impl FromStr for HistEntries {
} }
if feeding_lines { if feeding_lines {
let Some((_, line)) = lines.next() else { let Some((_, line)) = lines.next() else {
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::HistoryReadErr, ShErrKind::HistoryReadErr,
format!("Bad formatting on line {i}"), format!("Bad formatting on line {i}"),
)); ));
}; };
chars = line.chars().peekable(); chars = line.chars().peekable();
} }

View File

@@ -89,110 +89,112 @@ impl KeyEvent {
} }
} }
} }
pub fn as_vim_seq(&self) -> ShResult<String> { pub fn as_vim_seq(&self) -> ShResult<String> {
let mut seq = String::new(); let mut seq = String::new();
let KeyEvent(event, mods) = self; let KeyEvent(event, mods) = self;
let mut needs_angle_bracket = false; let mut needs_angle_bracket = false;
if mods.contains(ModKeys::CTRL) { if mods.contains(ModKeys::CTRL) {
seq.push_str("C-"); seq.push_str("C-");
needs_angle_bracket = true; needs_angle_bracket = true;
} }
if mods.contains(ModKeys::ALT) { if mods.contains(ModKeys::ALT) {
seq.push_str("A-"); seq.push_str("A-");
needs_angle_bracket = true; needs_angle_bracket = true;
} }
if mods.contains(ModKeys::SHIFT) { if mods.contains(ModKeys::SHIFT) {
seq.push_str("S-"); seq.push_str("S-");
needs_angle_bracket = true; needs_angle_bracket = true;
} }
match event { match event {
KeyCode::UnknownEscSeq => return Err(ShErr::simple( KeyCode::UnknownEscSeq => {
ShErrKind::ParseErr, return Err(ShErr::simple(
"Cannot convert unknown escape sequence to Vim key sequence".to_string(), ShErrKind::ParseErr,
)), "Cannot convert unknown escape sequence to Vim key sequence".to_string(),
KeyCode::Backspace => { ));
seq.push_str("BS"); }
needs_angle_bracket = true; KeyCode::Backspace => {
} seq.push_str("BS");
KeyCode::BackTab => { needs_angle_bracket = true;
seq.push_str("S-Tab"); }
needs_angle_bracket = true; KeyCode::BackTab => {
} seq.push_str("S-Tab");
KeyCode::BracketedPasteStart => todo!(), needs_angle_bracket = true;
KeyCode::BracketedPasteEnd => todo!(), }
KeyCode::Delete => { KeyCode::BracketedPasteStart => todo!(),
seq.push_str("Del"); KeyCode::BracketedPasteEnd => todo!(),
needs_angle_bracket = true; KeyCode::Delete => {
} seq.push_str("Del");
KeyCode::Down => { needs_angle_bracket = true;
seq.push_str("Down"); }
needs_angle_bracket = true; KeyCode::Down => {
} seq.push_str("Down");
KeyCode::End => { needs_angle_bracket = true;
seq.push_str("End"); }
needs_angle_bracket = true; KeyCode::End => {
} seq.push_str("End");
KeyCode::Enter => { needs_angle_bracket = true;
seq.push_str("Enter"); }
needs_angle_bracket = true; KeyCode::Enter => {
} seq.push_str("Enter");
KeyCode::Esc => { needs_angle_bracket = true;
seq.push_str("Esc"); }
needs_angle_bracket = true; KeyCode::Esc => {
} seq.push_str("Esc");
needs_angle_bracket = true;
}
KeyCode::F(f) => { KeyCode::F(f) => {
seq.push_str(&format!("F{}", f)); seq.push_str(&format!("F{}", f));
needs_angle_bracket = true; needs_angle_bracket = true;
} }
KeyCode::Home => { KeyCode::Home => {
seq.push_str("Home"); seq.push_str("Home");
needs_angle_bracket = true; needs_angle_bracket = true;
} }
KeyCode::Insert => { KeyCode::Insert => {
seq.push_str("Insert"); seq.push_str("Insert");
needs_angle_bracket = true; needs_angle_bracket = true;
} }
KeyCode::Left => { KeyCode::Left => {
seq.push_str("Left"); seq.push_str("Left");
needs_angle_bracket = true; needs_angle_bracket = true;
} }
KeyCode::Null => todo!(), KeyCode::Null => todo!(),
KeyCode::PageDown => { KeyCode::PageDown => {
seq.push_str("PgDn"); seq.push_str("PgDn");
needs_angle_bracket = true; needs_angle_bracket = true;
} }
KeyCode::PageUp => { KeyCode::PageUp => {
seq.push_str("PgUp"); seq.push_str("PgUp");
needs_angle_bracket = true; needs_angle_bracket = true;
} }
KeyCode::Right => { KeyCode::Right => {
seq.push_str("Right"); seq.push_str("Right");
needs_angle_bracket = true; needs_angle_bracket = true;
} }
KeyCode::Tab => { KeyCode::Tab => {
seq.push_str("Tab"); seq.push_str("Tab");
needs_angle_bracket = true; needs_angle_bracket = true;
} }
KeyCode::Up => { KeyCode::Up => {
seq.push_str("Up"); seq.push_str("Up");
needs_angle_bracket = true; needs_angle_bracket = true;
} }
KeyCode::Char(ch) => { KeyCode::Char(ch) => {
seq.push(*ch); seq.push(*ch);
} }
KeyCode::Grapheme(gr) => seq.push_str(gr), KeyCode::Grapheme(gr) => seq.push_str(gr),
KeyCode::Verbatim(s) => seq.push_str(s), KeyCode::Verbatim(s) => seq.push_str(s),
} }
if needs_angle_bracket { if needs_angle_bracket {
Ok(format!("<{}>", seq)) Ok(format!("<{}>", seq))
} else { } else {
Ok(seq) Ok(seq)
} }
} }
} }
#[derive(Clone, PartialEq, Eq, Debug)] #[derive(Clone, PartialEq, Eq, Debug)]
@@ -204,7 +206,7 @@ pub enum KeyCode {
BracketedPasteEnd, BracketedPasteEnd,
Char(char), Char(char),
Grapheme(Arc<str>), Grapheme(Arc<str>),
Verbatim(Arc<str>), // For sequences that should be treated as literal input, not parsed into a KeyCode Verbatim(Arc<str>), // For sequences that should be treated as literal input, not parsed into a KeyCode
Delete, Delete,
Down, Down,
End, End,

View File

@@ -1,5 +1,7 @@
use std::{ use std::{
collections::HashSet, fmt::Display, ops::{Range, RangeInclusive} collections::HashSet,
fmt::Display,
ops::{Range, RangeInclusive},
}; };
use unicode_segmentation::UnicodeSegmentation; use unicode_segmentation::UnicodeSegmentation;
@@ -11,7 +13,10 @@ use super::vicmd::{
}; };
use crate::{ use crate::{
libsh::{error::ShResult, guards::var_ctx_guard}, libsh::{error::ShResult, guards::var_ctx_guard},
parse::{execute::exec_input, lex::{LexFlags, LexStream, Tk, TkFlags, TkRule}}, parse::{
execute::exec_input,
lex::{LexFlags, LexStream, Tk, TkFlags, TkRule},
},
prelude::*, prelude::*,
readline::{ readline::{
markers, markers,
@@ -297,20 +302,20 @@ impl ClampedUsize {
pub fn sub(&mut self, value: usize) { pub fn sub(&mut self, value: usize) {
self.value = self.value.saturating_sub(value) self.value = self.value.saturating_sub(value)
} }
pub fn wrap_add(&mut self, value: usize) { pub fn wrap_add(&mut self, value: usize) {
self.value = self.ret_wrap_add(value); self.value = self.ret_wrap_add(value);
} }
pub fn wrap_sub(&mut self, value: usize) { pub fn wrap_sub(&mut self, value: usize) {
self.value = self.ret_wrap_sub(value); self.value = self.ret_wrap_sub(value);
} }
pub fn ret_wrap_add(&self, value: usize) -> usize { pub fn ret_wrap_add(&self, value: usize) -> usize {
let max = self.upper_bound(); let max = self.upper_bound();
(self.value + value) % (max + 1) (self.value + value) % (max + 1)
} }
pub fn ret_wrap_sub(&self, value: usize) -> usize { pub fn ret_wrap_sub(&self, value: usize) -> usize {
let max = self.upper_bound(); let max = self.upper_bound();
(self.value + (max + 1) - (value % (max + 1))) % (max + 1) (self.value + (max + 1) - (value % (max + 1))) % (max + 1)
} }
/// Add a value to the wrapped usize, return the result /// Add a value to the wrapped usize, return the result
/// ///
/// Returns the result instead of mutating the inner value /// Returns the result instead of mutating the inner value
@@ -352,12 +357,12 @@ pub struct LineBuf {
impl LineBuf { impl LineBuf {
pub fn new() -> Self { pub fn new() -> Self {
let mut new = Self { let mut new = Self {
grapheme_indices: Some(vec![]), // We know the buffer is empty, so this keeps us safe from unwrapping None grapheme_indices: Some(vec![]), // We know the buffer is empty, so this keeps us safe from unwrapping None
..Default::default() ..Default::default()
}; };
new.update_graphemes(); new.update_graphemes();
new new
} }
/// Only update self.grapheme_indices if it is None /// Only update self.grapheme_indices if it is None
pub fn update_graphemes_lazy(&mut self) { pub fn update_graphemes_lazy(&mut self) {
@@ -437,17 +442,17 @@ impl LineBuf {
self.cursor.set_max(indices.len()); self.cursor.set_max(indices.len());
self.grapheme_indices = Some(indices) self.grapheme_indices = Some(indices)
} }
#[track_caller] #[track_caller]
pub fn grapheme_indices(&self) -> &[usize] { pub fn grapheme_indices(&self) -> &[usize] {
if self.grapheme_indices.is_none() { if self.grapheme_indices.is_none() {
let caller = std::panic::Location::caller(); let caller = std::panic::Location::caller();
panic!( panic!(
"grapheme_indices is None. This likely means you forgot to call update_graphemes() before calling a method that relies on grapheme_indices, or you called a method that relies on grapheme_indices from another method that also relies on grapheme_indices without updating graphemes in between. Caller: {}:{}:{}", "grapheme_indices is None. This likely means you forgot to call update_graphemes() before calling a method that relies on grapheme_indices, or you called a method that relies on grapheme_indices from another method that also relies on grapheme_indices without updating graphemes in between. Caller: {}:{}:{}",
caller.file(), caller.file(),
caller.line(), caller.line(),
caller.column(), caller.column(),
); );
} }
self.grapheme_indices.as_ref().unwrap() self.grapheme_indices.as_ref().unwrap()
} }
pub fn grapheme_indices_owned(&self) -> Vec<usize> { pub fn grapheme_indices_owned(&self) -> Vec<usize> {
@@ -806,19 +811,19 @@ impl LineBuf {
} }
Some(self.line_bounds(line_no)) Some(self.line_bounds(line_no))
} }
pub fn this_word(&mut self, word: Word) -> (usize, usize) { pub fn this_word(&mut self, word: Word) -> (usize, usize) {
let start = if self.is_word_bound(self.cursor.get(), word, Direction::Backward) { let start = if self.is_word_bound(self.cursor.get(), word, Direction::Backward) {
self.cursor.get() self.cursor.get()
} else { } else {
self.start_of_word_backward(self.cursor.get(), word) self.start_of_word_backward(self.cursor.get(), word)
}; };
let end = if self.is_word_bound(self.cursor.get(), word, Direction::Forward) { let end = if self.is_word_bound(self.cursor.get(), word, Direction::Forward) {
self.cursor.get() self.cursor.get()
} else { } else {
self.end_of_word_forward(self.cursor.get(), word) self.end_of_word_forward(self.cursor.get(), word)
}; };
(start, end) (start, end)
} }
pub fn this_line_exclusive(&mut self) -> (usize, usize) { pub fn this_line_exclusive(&mut self) -> (usize, usize) {
let line_no = self.cursor_line_number(); let line_no = self.cursor_line_number();
let (start, mut end) = self.line_bounds(line_no); let (start, mut end) = self.line_bounds(line_no);
@@ -827,10 +832,10 @@ impl LineBuf {
} }
(start, end) (start, end)
} }
pub fn this_line_content(&mut self) -> Option<&str> { pub fn this_line_content(&mut self) -> Option<&str> {
let (start,end) = self.this_line_exclusive(); let (start, end) = self.this_line_exclusive();
self.slice(start..end) self.slice(start..end)
} }
pub fn this_line(&mut self) -> (usize, usize) { pub fn this_line(&mut self) -> (usize, usize) {
let line_no = self.cursor_line_number(); let line_no = self.cursor_line_number();
self.line_bounds(line_no) self.line_bounds(line_no)
@@ -940,7 +945,7 @@ impl LineBuf {
} }
pub fn is_word_bound(&mut self, pos: usize, word: Word, dir: Direction) -> bool { pub fn is_word_bound(&mut self, pos: usize, word: Word, dir: Direction) -> bool {
let clamped_pos = ClampedUsize::new(pos, self.cursor.max, true); let clamped_pos = ClampedUsize::new(pos, self.cursor.max, true);
log::debug!("clamped_pos: {}", clamped_pos.get()); log::debug!("clamped_pos: {}", clamped_pos.get());
let cur_char = self let cur_char = self
.grapheme_at(clamped_pos.get()) .grapheme_at(clamped_pos.get())
.map(|c| c.to_string()) .map(|c| c.to_string())
@@ -1011,11 +1016,11 @@ impl LineBuf {
} else { } else {
self.start_of_word_backward(self.cursor.get(), word) self.start_of_word_backward(self.cursor.get(), word)
}; };
let end = if self.is_word_bound(self.cursor.get(), word, Direction::Forward) { let end = if self.is_word_bound(self.cursor.get(), word, Direction::Forward) {
self.cursor.get() self.cursor.get()
} else { } else {
self.end_of_word_forward(self.cursor.get(), word) self.end_of_word_forward(self.cursor.get(), word)
}; };
Some((start, end)) Some((start, end))
} }
Bound::Around => { Bound::Around => {
@@ -1994,9 +1999,9 @@ impl LineBuf {
let mut level: usize = 0; let mut level: usize = 0;
if to_cursor.ends_with("\\\n") { if to_cursor.ends_with("\\\n") {
level += 1; // Line continuation, so we need to add an extra level level += 1; // Line continuation, so we need to add an extra level
} }
let input = Arc::new(to_cursor); let input = Arc::new(to_cursor);
let Ok(tokens) = LexStream::new(input, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>() let Ok(tokens) = LexStream::new(input, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>()
@@ -2004,23 +2009,23 @@ impl LineBuf {
log::error!("Failed to lex buffer for indent calculation"); log::error!("Failed to lex buffer for indent calculation");
return; return;
}; };
let mut last_keyword: Option<String> = None; let mut last_keyword: Option<String> = None;
for tk in tokens { for tk in tokens {
if tk.flags.contains(TkFlags::KEYWORD) { if tk.flags.contains(TkFlags::KEYWORD) {
match tk.as_str() { match tk.as_str() {
"in" => { "in" => {
if last_keyword.as_deref() == Some("case") { if last_keyword.as_deref() == Some("case") {
level += 1; level += 1;
} else { } else {
// 'in' is also used in for loops, but we already increment level on 'do' for those // 'in' is also used in for loops, but we already increment level on 'do' for those
// so we just skip it here // so we just skip it here
} }
} }
"then" | "do" => level += 1, "then" | "do" => level += 1,
"done" | "fi" | "esac" => level = level.saturating_sub(1), "done" | "fi" | "esac" => level = level.saturating_sub(1),
_ => { /* Continue */ } _ => { /* Continue */ }
} }
last_keyword = Some(tk.to_string()); last_keyword = Some(tk.to_string());
} else if tk.class == TkRule::BraceGrpStart { } else if tk.class == TkRule::BraceGrpStart {
level += 1; level += 1;
} else if tk.class == TkRule::BraceGrpEnd { } else if tk.class == TkRule::BraceGrpEnd {
@@ -2362,12 +2367,12 @@ impl LineBuf {
} }
MotionCmd(_count, Motion::BeginningOfBuffer) => MotionKind::On(0), MotionCmd(_count, Motion::BeginningOfBuffer) => MotionKind::On(0),
MotionCmd(_count, Motion::EndOfBuffer) => { MotionCmd(_count, Motion::EndOfBuffer) => {
if self.cursor.exclusive { if self.cursor.exclusive {
MotionKind::On(self.grapheme_indices().len().saturating_sub(1)) MotionKind::On(self.grapheme_indices().len().saturating_sub(1))
} else { } else {
MotionKind::On(self.grapheme_indices().len()) MotionKind::On(self.grapheme_indices().len())
} }
}, }
MotionCmd(_count, Motion::ToColumn) => todo!(), MotionCmd(_count, Motion::ToColumn) => todo!(),
MotionCmd(count, Motion::Range(start, end)) => { MotionCmd(count, Motion::Range(start, end)) => {
let mut final_end = end; let mut final_end = end;
@@ -2794,7 +2799,11 @@ impl LineBuf {
match content { match content {
RegisterContent::Span(ref text) => { RegisterContent::Span(ref text) => {
let insert_idx = match anchor { let insert_idx = match anchor {
Anchor::After => self.cursor.get().saturating_add(1).min(self.grapheme_indices().len()), Anchor::After => self
.cursor
.get()
.saturating_add(1)
.min(self.grapheme_indices().len()),
Anchor::Before => self.cursor.get(), Anchor::Before => self.cursor.get(),
}; };
self.insert_str_at(insert_idx, text); self.insert_str_at(insert_idx, text);
@@ -2859,34 +2868,35 @@ impl LineBuf {
Verb::InsertChar(ch) => { Verb::InsertChar(ch) => {
self.insert_at_cursor(ch); self.insert_at_cursor(ch);
self.cursor.add(1); self.cursor.add(1);
let before = self.auto_indent_level; let before = self.auto_indent_level;
if read_shopts(|o| o.prompt.auto_indent) if read_shopts(|o| o.prompt.auto_indent)
&& let Some(line_content) = self.this_line_content() { && let Some(line_content) = self.this_line_content()
match line_content.trim() { {
"esac" | "done" | "fi" | "}" => { match line_content.trim() {
self.calc_indent_level(); "esac" | "done" | "fi" | "}" => {
if self.auto_indent_level < before { self.calc_indent_level();
let delta = before - self.auto_indent_level; if self.auto_indent_level < before {
let line_start = self.start_of_line(); let delta = before - self.auto_indent_level;
for _ in 0..delta { let line_start = self.start_of_line();
if self.grapheme_at(line_start).is_some_and(|gr| gr == "\t") { for _ in 0..delta {
self.remove(line_start); if self.grapheme_at(line_start).is_some_and(|gr| gr == "\t") {
if !self.cursor_at_max() { self.remove(line_start);
self.cursor.sub(1); if !self.cursor_at_max() {
} self.cursor.sub(1);
} }
} }
} }
} }
_ => { /* nothing to see here */ } }
} _ => { /* nothing to see here */ }
} }
}
} }
Verb::Insert(string) => { Verb::Insert(string) => {
self.push_str(&string); self.push_str(&string);
let graphemes = string.graphemes(true).count(); let graphemes = string.graphemes(true).count();
log::debug!("Inserted string: {string:?}, graphemes: {graphemes}"); log::debug!("Inserted string: {string:?}, graphemes: {graphemes}");
log::debug!("buffer after insert: {:?}", self.buffer); log::debug!("buffer after insert: {:?}", self.buffer);
self.cursor.add(graphemes); self.cursor.add(graphemes);
} }
Verb::Indent => { Verb::Indent => {
@@ -3039,71 +3049,82 @@ impl LineBuf {
} }
} }
} }
Verb::IncrementNumber(n) | Verb::IncrementNumber(n) | Verb::DecrementNumber(n) => {
Verb::DecrementNumber(n) => { let inc = if matches!(verb, Verb::IncrementNumber(_)) {
let inc = if matches!(verb, Verb::IncrementNumber(_)) { n as i64 } else { -(n as i64) }; n as i64
let (s, e) = self.select_range().unwrap_or(self.this_word(Word::Normal)); } else {
let end = if self.select_range().is_some() { -(n as i64)
if e < self.grapheme_indices().len() - 1 { };
e let (s, e) = self.select_range().unwrap_or(self.this_word(Word::Normal));
} else { let end = if self.select_range().is_some() {
e + 1 if e < self.grapheme_indices().len() - 1 {
} e
} else { } else {
(e + 1).min(self.grapheme_indices().len()) e + 1
}; // inclusive → exclusive, capped at buffer len }
let word = self.slice(s..end).unwrap_or_default().to_lowercase(); } else {
(e + 1).min(self.grapheme_indices().len())
}; // inclusive → exclusive, capped at buffer len
let word = self.slice(s..end).unwrap_or_default().to_lowercase();
let byte_start = self.index_byte_pos(s); let byte_start = self.index_byte_pos(s);
let byte_end = if end >= self.grapheme_indices().len() { let byte_end = if end >= self.grapheme_indices().len() {
self.buffer.len() self.buffer.len()
} else { } else {
self.index_byte_pos(end) self.index_byte_pos(end)
}; };
if word.starts_with("0x") { if word.starts_with("0x") {
let body = word.strip_prefix("0x").unwrap(); let body = word.strip_prefix("0x").unwrap();
let width = body.len(); let width = body.len();
if let Ok(num) = i64::from_str_radix(body, 16) { if let Ok(num) = i64::from_str_radix(body, 16) {
let new_num = num + inc; let new_num = num + inc;
self.buffer.replace_range(byte_start..byte_end, &format!("0x{new_num:0>width$x}")); self
self.update_graphemes(); .buffer
self.cursor.set(s); .replace_range(byte_start..byte_end, &format!("0x{new_num:0>width$x}"));
} self.update_graphemes();
} else if word.starts_with("0b") { self.cursor.set(s);
let body = word.strip_prefix("0b").unwrap(); }
let width = body.len(); } else if word.starts_with("0b") {
if let Ok(num) = i64::from_str_radix(body, 2) { let body = word.strip_prefix("0b").unwrap();
let new_num = num + inc; let width = body.len();
self.buffer.replace_range(byte_start..byte_end, &format!("0b{new_num:0>width$b}")); if let Ok(num) = i64::from_str_radix(body, 2) {
self.update_graphemes(); let new_num = num + inc;
self.cursor.set(s); self
} .buffer
} else if word.starts_with("0o") { .replace_range(byte_start..byte_end, &format!("0b{new_num:0>width$b}"));
let body = word.strip_prefix("0o").unwrap(); self.update_graphemes();
let width = body.len(); self.cursor.set(s);
if let Ok(num) = i64::from_str_radix(body, 8) { }
let new_num = num + inc; } else if word.starts_with("0o") {
self.buffer.replace_range(byte_start..byte_end, &format!("0o{new_num:0>width$o}")); let body = word.strip_prefix("0o").unwrap();
self.update_graphemes(); let width = body.len();
self.cursor.set(s); if let Ok(num) = i64::from_str_radix(body, 8) {
} let new_num = num + inc;
} else if let Ok(num) = word.parse::<i64>() { self
let width = word.len(); .buffer
let new_num = num + inc; .replace_range(byte_start..byte_end, &format!("0o{new_num:0>width$o}"));
self.buffer.replace_range(byte_start..byte_end, &format!("{new_num:0>width$}")); self.update_graphemes();
self.update_graphemes(); self.cursor.set(s);
self.cursor.set(s); }
} } else if let Ok(num) = word.parse::<i64>() {
} let width = word.len();
let new_num = num + inc;
self
.buffer
.replace_range(byte_start..byte_end, &format!("{new_num:0>width$}"));
self.update_graphemes();
self.cursor.set(s);
}
}
Verb::Complete Verb::Complete
| Verb::ExMode | Verb::ExMode
| Verb::EndOfFile | Verb::EndOfFile
| Verb::InsertMode | Verb::InsertMode
| Verb::NormalMode | Verb::NormalMode
| Verb::VisualMode | Verb::VisualMode
| Verb::VerbatimMode | Verb::VerbatimMode
| Verb::ReplaceMode | Verb::ReplaceMode
| Verb::VisualModeLine | Verb::VisualModeLine
| Verb::VisualModeBlock | Verb::VisualModeBlock
@@ -3111,46 +3132,63 @@ impl LineBuf {
| Verb::VisualModeSelectLast => self.apply_motion(motion), // Already handled logic for these | Verb::VisualModeSelectLast => self.apply_motion(motion), // Already handled logic for these
Verb::ShellCmd(cmd) => { Verb::ShellCmd(cmd) => {
log::debug!("Executing ex-mode command from widget: {cmd}"); log::debug!("Executing ex-mode command from widget: {cmd}");
let mut vars = HashSet::new(); let mut vars = HashSet::new();
vars.insert("_BUFFER".into()); vars.insert("_BUFFER".into());
vars.insert("_CURSOR".into()); vars.insert("_CURSOR".into());
vars.insert("_ANCHOR".into()); vars.insert("_ANCHOR".into());
let _guard = var_ctx_guard(vars); let _guard = var_ctx_guard(vars);
let mut buf = self.as_str().to_string(); let mut buf = self.as_str().to_string();
let mut cursor = self.cursor.get(); let mut cursor = self.cursor.get();
let mut anchor = self.select_range().map(|r| if r.0 != cursor { r.0 } else { r.1 }).unwrap_or(cursor); let mut anchor = self
.select_range()
.map(|r| if r.0 != cursor { r.0 } else { r.1 })
.unwrap_or(cursor);
write_vars(|v| { write_vars(|v| {
v.set_var("_BUFFER", VarKind::Str(buf.clone()), VarFlags::EXPORT)?; v.set_var("_BUFFER", VarKind::Str(buf.clone()), VarFlags::EXPORT)?;
v.set_var("_CURSOR", VarKind::Str(cursor.to_string()), VarFlags::EXPORT)?; v.set_var(
v.set_var("_ANCHOR", VarKind::Str(anchor.to_string()), VarFlags::EXPORT) "_CURSOR",
})?; VarKind::Str(cursor.to_string()),
VarFlags::EXPORT,
)?;
v.set_var(
"_ANCHOR",
VarKind::Str(anchor.to_string()),
VarFlags::EXPORT,
)
})?;
RawModeGuard::with_cooked_mode(|| exec_input(cmd, None, true, Some("<ex-mode-cmd>".into())))?; RawModeGuard::with_cooked_mode(|| {
exec_input(cmd, None, true, Some("<ex-mode-cmd>".into()))
})?;
let keys = write_vars(|v| { let keys = write_vars(|v| {
buf = v.take_var("_BUFFER"); buf = v.take_var("_BUFFER");
cursor = v.take_var("_CURSOR").parse().unwrap_or(cursor); cursor = v.take_var("_CURSOR").parse().unwrap_or(cursor);
anchor = v.take_var("_ANCHOR").parse().unwrap_or(anchor); anchor = v.take_var("_ANCHOR").parse().unwrap_or(anchor);
v.take_var("_KEYS") v.take_var("_KEYS")
}); });
self.set_buffer(buf); self.set_buffer(buf);
self.update_graphemes(); self.update_graphemes();
self.cursor.set_max(self.buffer.graphemes(true).count()); self.cursor.set_max(self.buffer.graphemes(true).count());
self.cursor.set(cursor); self.cursor.set(cursor);
log::debug!("[ShellCmd] post-widget: cursor={}, anchor={}, select_range={:?}", cursor, anchor, self.select_range); log::debug!(
if anchor != cursor && self.select_range.is_some() { "[ShellCmd] post-widget: cursor={}, anchor={}, select_range={:?}",
self.select_range = Some(ordered(cursor, anchor)); cursor,
} anchor,
if !keys.is_empty() { self.select_range
log::debug!("Pending widget keys from shell command: {keys}"); );
write_meta(|m| m.set_pending_widget_keys(&keys)) if anchor != cursor && self.select_range.is_some() {
} self.select_range = Some(ordered(cursor, anchor));
}
} if !keys.is_empty() {
log::debug!("Pending widget keys from shell command: {keys}");
write_meta(|m| m.set_pending_widget_keys(&keys))
}
}
Verb::Normal(_) Verb::Normal(_)
| Verb::Read(_) | Verb::Read(_)
| Verb::Write(_) | Verb::Write(_)

View File

@@ -1,7 +1,7 @@
use std::fmt::Write;
use history::History; use history::History;
use keys::{KeyCode, KeyEvent, ModKeys}; use keys::{KeyCode, KeyEvent, ModKeys};
use linebuf::{LineBuf, SelectAnchor, SelectMode}; use linebuf::{LineBuf, SelectAnchor, SelectMode};
use std::fmt::Write;
use term::{KeyReader, Layout, LineWriter, PollReader, TermWriter, get_win_size}; use term::{KeyReader, Layout, LineWriter, PollReader, TermWriter, get_win_size};
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use vicmd::{CmdFlags, Motion, MotionCmd, RegisterName, Verb, VerbCmd, ViCmd}; use vicmd::{CmdFlags, Motion, MotionCmd, RegisterName, Verb, VerbCmd, ViCmd};
@@ -12,16 +12,21 @@ use crate::expand::expand_prompt;
use crate::libsh::sys::TTY_FILENO; use crate::libsh::sys::TTY_FILENO;
use crate::libsh::utils::AutoCmdVecUtils; use crate::libsh::utils::AutoCmdVecUtils;
use crate::parse::lex::{LexStream, QuoteState}; use crate::parse::lex::{LexStream, QuoteState};
use crate::{prelude::*, state};
use crate::readline::complete::FuzzyCompleter; use crate::readline::complete::FuzzyCompleter;
use crate::readline::term::{Pos, TermReader, calc_str_width}; use crate::readline::term::{Pos, TermReader, calc_str_width};
use crate::readline::vimode::{ViEx, ViVerbatim}; use crate::readline::vimode::{ViEx, ViVerbatim};
use crate::state::{AutoCmdKind, ShellParam, VarFlags, VarKind, read_logic, read_shopts, with_vars, write_meta, write_vars}; use crate::state::{
AutoCmdKind, ShellParam, VarFlags, VarKind, read_logic, read_shopts, write_meta, write_vars,
};
use crate::{ use crate::{
libsh::error::ShResult, libsh::error::ShResult,
parse::lex::{self, LexFlags, Tk, TkFlags, TkRule}, parse::lex::{self, LexFlags, Tk, TkFlags, TkRule},
readline::{complete::{CompResponse, Completer}, highlight::Highlighter}, readline::{
complete::{CompResponse, Completer},
highlight::Highlighter,
},
}; };
use crate::{prelude::*, state};
pub mod complete; pub mod complete;
pub mod highlight; pub mod highlight;
@@ -150,8 +155,8 @@ impl Prompt {
let Ok(ps1_raw) = env::var("PS1") else { let Ok(ps1_raw) = env::var("PS1") else {
return Self::default(); return Self::default();
}; };
// PS1 expansion may involve running commands (e.g., for \h or \W), which can modify shell state // PS1 expansion may involve running commands (e.g., for \h or \W), which can modify shell state
let saved_status = state::get_status(); let saved_status = state::get_status();
let Ok(ps1_expanded) = expand_prompt(&ps1_raw) else { let Ok(ps1_expanded) = expand_prompt(&ps1_raw) else {
return Self::default(); return Self::default();
@@ -164,8 +169,8 @@ impl Prompt {
.ok() .ok()
.flatten(); .flatten();
// Restore shell state after prompt expansion, since it may have been modified by command substitutions in the prompt // Restore shell state after prompt expansion, since it may have been modified by command substitutions in the prompt
state::set_status(saved_status); state::set_status(saved_status);
Self { Self {
ps1_expanded, ps1_expanded,
ps1_raw, ps1_raw,
@@ -208,10 +213,10 @@ impl Prompt {
if let Ok(expanded) = expand_prompt(&self.ps1_raw) { if let Ok(expanded) = expand_prompt(&self.ps1_raw) {
self.ps1_expanded = expanded; self.ps1_expanded = expanded;
} }
if let Some(psr_raw) = &self.psr_raw { if let Some(psr_raw) = &self.psr_raw
if let Ok(expanded) = expand_prompt(psr_raw) { && let Ok(expanded) = expand_prompt(psr_raw)
self.psr_expanded = Some(expanded); {
} self.psr_expanded = Some(expanded);
} }
state::set_status(saved_status); state::set_status(saved_status);
self.dirty = false; self.dirty = false;
@@ -244,12 +249,12 @@ pub struct ShedVi {
pub completer: Box<dyn Completer>, pub completer: Box<dyn Completer>,
pub mode: Box<dyn ViMode>, pub mode: Box<dyn ViMode>,
pub saved_mode: Option<Box<dyn ViMode>>, pub saved_mode: Option<Box<dyn ViMode>>,
pub pending_keymap: Vec<KeyEvent>, pub pending_keymap: Vec<KeyEvent>,
pub repeat_action: Option<CmdReplay>, pub repeat_action: Option<CmdReplay>,
pub repeat_motion: Option<MotionCmd>, pub repeat_motion: Option<MotionCmd>,
pub editor: LineBuf, pub editor: LineBuf,
pub next_is_escaped: bool, pub next_is_escaped: bool,
pub old_layout: Option<Layout>, pub old_layout: Option<Layout>,
pub history: History, pub history: History,
@@ -266,9 +271,9 @@ impl ShedVi {
completer: Box::new(FuzzyCompleter::default()), completer: Box::new(FuzzyCompleter::default()),
highlighter: Highlighter::new(), highlighter: Highlighter::new(),
mode: Box::new(ViInsert::new()), mode: Box::new(ViInsert::new()),
next_is_escaped: false, next_is_escaped: false,
saved_mode: None, saved_mode: None,
pending_keymap: Vec::new(), pending_keymap: Vec::new(),
old_layout: None, old_layout: None,
repeat_action: None, repeat_action: None,
repeat_motion: None, repeat_motion: None,
@@ -276,8 +281,14 @@ impl ShedVi {
history: History::new()?, history: History::new()?,
needs_redraw: true, needs_redraw: true,
}; };
write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(new.mode.report_mode().to_string()), VarFlags::NONE))?; write_vars(|v| {
new.prompt.refresh(); v.set_var(
"SHED_VI_MODE",
VarKind::Str(new.mode.report_mode().to_string()),
VarFlags::NONE,
)
})?;
new.prompt.refresh();
new.writer.flush_write("\n")?; // ensure we start on a new line, in case the previous command didn't end with a newline new.writer.flush_write("\n")?; // ensure we start on a new line, in case the previous command didn't end with a newline
new.print_line(false)?; new.print_line(false)?;
Ok(new) Ok(new)
@@ -293,7 +304,7 @@ impl ShedVi {
/// Feed raw bytes from stdin into the reader's buffer /// Feed raw bytes from stdin into the reader's buffer
pub fn feed_bytes(&mut self, bytes: &[u8]) { pub fn feed_bytes(&mut self, bytes: &[u8]) {
let verbatim = self.mode.report_mode() == ModeReport::Verbatim; let verbatim = self.mode.report_mode() == ModeReport::Verbatim;
self.reader.feed_bytes(bytes, verbatim); self.reader.feed_bytes(bytes, verbatim);
} }
@@ -302,19 +313,21 @@ impl ShedVi {
self.needs_redraw = true; self.needs_redraw = true;
} }
pub fn fix_column(&mut self) -> ShResult<()> { pub fn fix_column(&mut self) -> ShResult<()> {
self.writer.fix_cursor_column(&mut TermReader::new(*TTY_FILENO)) self
} .writer
.fix_cursor_column(&mut TermReader::new(*TTY_FILENO))
}
pub fn reset_active_widget(&mut self, full_redraw: bool) -> ShResult<()> { pub fn reset_active_widget(&mut self, full_redraw: bool) -> ShResult<()> {
if self.completer.is_active() { if self.completer.is_active() {
self.completer.reset_stay_active(); self.completer.reset_stay_active();
self.needs_redraw = true; self.needs_redraw = true;
Ok(()) Ok(())
} else { } else {
self.reset(full_redraw) self.reset(full_redraw)
} }
} }
/// Reset readline state for a new prompt /// Reset readline state for a new prompt
pub fn reset(&mut self, full_redraw: bool) -> ShResult<()> { pub fn reset(&mut self, full_redraw: bool) -> ShResult<()> {
@@ -322,7 +335,7 @@ impl ShedVi {
// so print_line can call clear_rows with the full multi-line layout // so print_line can call clear_rows with the full multi-line layout
self.prompt.refresh(); self.prompt.refresh();
self.editor = Default::default(); self.editor = Default::default();
self.swap_mode(&mut (Box::new(ViInsert::new()) as Box<dyn ViMode>)); self.swap_mode(&mut (Box::new(ViInsert::new()) as Box<dyn ViMode>));
self.needs_redraw = true; self.needs_redraw = true;
if full_redraw { if full_redraw {
self.old_layout = None; self.old_layout = None;
@@ -340,24 +353,24 @@ impl ShedVi {
&mut self.prompt &mut self.prompt
} }
pub fn curr_keymap_flags(&self) -> KeyMapFlags { pub fn curr_keymap_flags(&self) -> KeyMapFlags {
let mut flags = KeyMapFlags::empty(); let mut flags = KeyMapFlags::empty();
match self.mode.report_mode() { match self.mode.report_mode() {
ModeReport::Insert => flags |= KeyMapFlags::INSERT, ModeReport::Insert => flags |= KeyMapFlags::INSERT,
ModeReport::Normal => flags |= KeyMapFlags::NORMAL, ModeReport::Normal => flags |= KeyMapFlags::NORMAL,
ModeReport::Ex => flags |= KeyMapFlags::EX, ModeReport::Ex => flags |= KeyMapFlags::EX,
ModeReport::Visual => flags |= KeyMapFlags::VISUAL, ModeReport::Visual => flags |= KeyMapFlags::VISUAL,
ModeReport::Replace => flags |= KeyMapFlags::REPLACE, ModeReport::Replace => flags |= KeyMapFlags::REPLACE,
ModeReport::Verbatim => flags |= KeyMapFlags::VERBATIM, ModeReport::Verbatim => flags |= KeyMapFlags::VERBATIM,
ModeReport::Unknown => todo!(), ModeReport::Unknown => todo!(),
} }
if self.mode.pending_seq().is_some_and(|seq| !seq.is_empty()) { if self.mode.pending_seq().is_some_and(|seq| !seq.is_empty()) {
flags |= KeyMapFlags::OP_PENDING; flags |= KeyMapFlags::OP_PENDING;
} }
flags flags
} }
fn should_submit(&mut self) -> ShResult<bool> { fn should_submit(&mut self) -> ShResult<bool> {
if self.mode.report_mode() == ModeReport::Normal { if self.mode.report_mode() == ModeReport::Normal {
@@ -398,7 +411,7 @@ impl ShedVi {
while let Some(key) = self.reader.read_key()? { while let Some(key) = self.reader.read_key()? {
// If completer is active, delegate input to it // If completer is active, delegate input to it
if self.completer.is_active() { if self.completer.is_active() {
self.print_line(false)?; self.print_line(false)?;
match self.completer.handle_key(key.clone())? { match self.completer.handle_key(key.clone())? {
CompResponse::Accept(candidate) => { CompResponse::Accept(candidate) => {
let span_start = self.completer.token_span().0; let span_start = self.completer.token_span().0;
@@ -416,57 +429,58 @@ impl ShedVi {
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get())); .update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
let hint = self.history.get_hint(); let hint = self.history.get_hint();
self.editor.set_hint(hint); self.editor.set_hint(hint);
self.completer.clear(&mut self.writer)?; self.completer.clear(&mut self.writer)?;
self.needs_redraw = true; self.needs_redraw = true;
self.completer.reset(); self.completer.reset();
continue; continue;
} }
CompResponse::Dismiss => { CompResponse::Dismiss => {
let hint = self.history.get_hint(); let hint = self.history.get_hint();
self.editor.set_hint(hint); self.editor.set_hint(hint);
self.completer.clear(&mut self.writer)?; self.completer.clear(&mut self.writer)?;
self.completer.reset(); self.completer.reset();
continue; continue;
} }
CompResponse::Consumed => { CompResponse::Consumed => {
/* just redraw */ /* just redraw */
self.needs_redraw = true; self.needs_redraw = true;
continue; continue;
} }
CompResponse::Passthrough => { /* fall through to normal handling below */ } CompResponse::Passthrough => { /* fall through to normal handling below */ }
} }
} else { } else {
let keymap_flags = self.curr_keymap_flags(); let keymap_flags = self.curr_keymap_flags();
self.pending_keymap.push(key.clone()); self.pending_keymap.push(key.clone());
let matches = read_logic(|l| l.keymaps_filtered(keymap_flags, &self.pending_keymap));
if matches.is_empty() {
// No matches. Drain the buffered keys and execute them.
for key in std::mem::take(&mut self.pending_keymap) {
if let Some(event) = self.handle_key(key)? {
return Ok(event);
}
}
self.needs_redraw = true;
continue;
} else if matches.len() == 1 && matches[0].compare(&self.pending_keymap) == KeyMapMatch::IsExact {
// We have a single exact match. Execute it.
let keymap = matches[0].clone();
self.pending_keymap.clear();
let action = keymap.action_expanded();
for key in action {
if let Some(event) = self.handle_key(key)? {
return Ok(event);
}
}
self.needs_redraw = true;
continue;
} else {
// There is ambiguity. Allow the timeout in the main loop to handle this.
continue;
}
}
let matches = read_logic(|l| l.keymaps_filtered(keymap_flags, &self.pending_keymap));
if matches.is_empty() {
// No matches. Drain the buffered keys and execute them.
for key in std::mem::take(&mut self.pending_keymap) {
if let Some(event) = self.handle_key(key)? {
return Ok(event);
}
}
self.needs_redraw = true;
continue;
} else if matches.len() == 1
&& matches[0].compare(&self.pending_keymap) == KeyMapMatch::IsExact
{
// We have a single exact match. Execute it.
let keymap = matches[0].clone();
self.pending_keymap.clear();
let action = keymap.action_expanded();
for key in action {
if let Some(event) = self.handle_key(key)? {
return Ok(event);
}
}
self.needs_redraw = true;
continue;
} else {
// There is ambiguity. Allow the timeout in the main loop to handle this.
continue;
}
}
if let Some(event) = self.handle_key(key)? { if let Some(event) = self.handle_key(key)? {
return Ok(event); return Ok(event);
@@ -542,12 +556,13 @@ impl ShedVi {
return Ok(None); return Ok(None);
} }
if let KeyEvent(KeyCode::Char('\\'), ModKeys::NONE) = key if let KeyEvent(KeyCode::Char('\\'), ModKeys::NONE) = key
&& !self.next_is_escaped { && !self.next_is_escaped
self.next_is_escaped = true; {
} else { self.next_is_escaped = true;
self.next_is_escaped = false; } else {
} self.next_is_escaped = false;
}
let Ok(cmd) = self.mode.handle_key_fallible(key) else { let Ok(cmd) = self.mode.handle_key_fallible(key) else {
// it's an ex mode error // it's an ex mode error
@@ -567,9 +582,10 @@ impl ShedVi {
} }
if cmd.is_submit_action() if cmd.is_submit_action()
&& !self.next_is_escaped && !self.next_is_escaped
&& !self.editor.buffer.ends_with('\\') && !self.editor.buffer.ends_with('\\')
&& (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete)) { && (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete))
{
self.editor.set_hint(None); self.editor.set_hint(None);
self.editor.cursor.set(self.editor.cursor_max()); self.editor.cursor.set(self.editor.cursor_max());
self.print_line(true)?; self.print_line(true)?;
@@ -600,11 +616,11 @@ impl ShedVi {
let before = self.editor.buffer.clone(); let before = self.editor.buffer.clone();
self.exec_cmd(cmd)?; self.exec_cmd(cmd)?;
if let Some(keys) = write_meta(|m| m.take_pending_widget_keys()) { if let Some(keys) = write_meta(|m| m.take_pending_widget_keys()) {
for key in keys { for key in keys {
self.handle_key(key)?; self.handle_key(key)?;
} }
} }
let after = self.editor.as_str(); let after = self.editor.as_str();
if before != after { if before != after {
@@ -670,7 +686,7 @@ impl ShedVi {
|| (self.mode.pending_seq().unwrap(/* always Some on normal mode */).is_empty() || (self.mode.pending_seq().unwrap(/* always Some on normal mode */).is_empty()
&& matches!(event, KeyEvent(KeyCode::Char('l'), ModKeys::NONE))) && matches!(event, KeyEvent(KeyCode::Char('l'), ModKeys::NONE)))
} }
ModeReport::Ex | ModeReport::Verbatim | ModeReport::Unknown => false, ModeReport::Ex | ModeReport::Verbatim | ModeReport::Unknown => false,
} }
} else { } else {
false false
@@ -692,13 +708,15 @@ impl ShedVi {
pub fn line_text(&mut self) -> String { pub fn line_text(&mut self) -> String {
let line = self.editor.to_string(); let line = self.editor.to_string();
let hint = self.editor.get_hint_text(); let hint = self.editor.get_hint_text();
let do_hl = state::read_shopts(|s| s.prompt.highlight); let do_hl = state::read_shopts(|s| s.prompt.highlight);
self.highlighter.only_visual(!do_hl); self.highlighter.only_visual(!do_hl);
self.highlighter.load_input(&line, self.editor.cursor_byte_pos()); self
self.highlighter.expand_control_chars(); .highlighter
self.highlighter.highlight(); .load_input(&line, self.editor.cursor_byte_pos());
let highlighted = self.highlighter.take(); self.highlighter.expand_control_chars();
format!("{highlighted}{hint}") self.highlighter.highlight();
let highlighted = self.highlighter.take();
format!("{highlighted}{hint}")
} }
pub fn print_line(&mut self, final_draw: bool) -> ShResult<()> { pub fn print_line(&mut self, final_draw: bool) -> ShResult<()> {
@@ -716,7 +734,7 @@ impl ShedVi {
prompt_string_right = prompt_string_right =
prompt_string_right.map(|psr| psr.lines().next().unwrap_or_default().to_string()); prompt_string_right.map(|psr| psr.lines().next().unwrap_or_default().to_string());
} }
let mut buf = String::new(); let mut buf = String::new();
let row0_used = self let row0_used = self
.prompt .prompt
@@ -734,8 +752,8 @@ impl ShedVi {
self.writer.clear_rows(layout)?; self.writer.clear_rows(layout)?;
} }
let pre_prompt = read_logic(|l| l.get_autocmds(AutoCmdKind::PrePrompt)); let pre_prompt = read_logic(|l| l.get_autocmds(AutoCmdKind::PrePrompt));
pre_prompt.exec(); pre_prompt.exec();
self self
.writer .writer
@@ -753,7 +771,8 @@ impl ShedVi {
&& !seq.is_empty() && !seq.is_empty()
&& !(prompt_string_right.is_some() && one_line) && !(prompt_string_right.is_some() && one_line)
&& seq_fits && seq_fits
&& self.mode.report_mode() != ModeReport::Ex { && self.mode.report_mode() != ModeReport::Ex
{
let to_col = self.writer.t_cols - calc_str_width(&seq); let to_col = self.writer.t_cols - calc_str_width(&seq);
let up = new_layout.cursor.row; // rows to move up from cursor to top line of prompt let up = new_layout.cursor.row; // rows to move up from cursor to top line of prompt
@@ -765,10 +784,11 @@ impl ShedVi {
// Save cursor, move up to top row, move right to column, write sequence, // Save cursor, move up to top row, move right to column, write sequence,
// restore cursor // restore cursor
write!(buf, "\x1b7{move_up}\x1b[{to_col}G{seq}\x1b8").unwrap(); write!(buf, "\x1b7{move_up}\x1b[{to_col}G{seq}\x1b8").unwrap();
} else if !final_draw } else if !final_draw
&& let Some(psr) = prompt_string_right && let Some(psr) = prompt_string_right
&& psr_fits { && psr_fits
{
let to_col = self.writer.t_cols - calc_str_width(&psr); let to_col = self.writer.t_cols - calc_str_width(&psr);
let down = new_layout.end.row - new_layout.cursor.row; let down = new_layout.end.row - new_layout.cursor.row;
let move_down = if down > 0 { let move_down = if down > 0 {
@@ -781,19 +801,28 @@ impl ShedVi {
// Record where the PSR ends so clear_rows can account for wrapping // Record where the PSR ends so clear_rows can account for wrapping
// if the terminal shrinks. // if the terminal shrinks.
let psr_start = Pos { row: new_layout.end.row, col: to_col }; let psr_start = Pos {
new_layout.psr_end = Some(Layout::calc_pos(self.writer.t_cols, &psr, psr_start, 0, false)); row: new_layout.end.row,
col: to_col,
};
new_layout.psr_end = Some(Layout::calc_pos(
self.writer.t_cols,
&psr,
psr_start,
0,
false,
));
} }
if let ModeReport::Ex = self.mode.report_mode() { if let ModeReport::Ex = self.mode.report_mode() {
let pending_seq = self.mode.pending_seq().unwrap_or_default(); let pending_seq = self.mode.pending_seq().unwrap_or_default();
write!(buf, "\n: {pending_seq}").unwrap(); write!(buf, "\n: {pending_seq}").unwrap();
new_layout.end.row += 1; new_layout.end.row += 1;
} }
write!(buf, "{}", &self.mode.cursor_style()).unwrap(); write!(buf, "{}", &self.mode.cursor_style()).unwrap();
self.writer.flush_write(&buf)?; self.writer.flush_write(&buf)?;
// Tell the completer the width of the prompt line above its \n so it can // Tell the completer the width of the prompt line above its \n so it can
// account for wrapping when clearing after a resize. // account for wrapping when clearing after a resize.
@@ -803,30 +832,39 @@ impl ShedVi {
// Without PSR, use the content width on the cursor's row // Without PSR, use the content width on the cursor's row
(new_layout.end.col + 1).max(new_layout.cursor.col + 1) (new_layout.end.col + 1).max(new_layout.cursor.col + 1)
}; };
self.completer.set_prompt_line_context(preceding_width, new_layout.cursor.col); self
.completer
.set_prompt_line_context(preceding_width, new_layout.cursor.col);
self.completer.draw(&mut self.writer)?; self.completer.draw(&mut self.writer)?;
self.old_layout = Some(new_layout); self.old_layout = Some(new_layout);
self.needs_redraw = false; self.needs_redraw = false;
let post_prompt = read_logic(|l| l.get_autocmds(AutoCmdKind::PostPrompt)); let post_prompt = read_logic(|l| l.get_autocmds(AutoCmdKind::PostPrompt));
post_prompt.exec(); post_prompt.exec();
Ok(()) Ok(())
} }
pub fn swap_mode(&mut self, mode: &mut Box<dyn ViMode>) { pub fn swap_mode(&mut self, mode: &mut Box<dyn ViMode>) {
let pre_mode_change = read_logic(|l| l.get_autocmds(AutoCmdKind::PreModeChange)); let pre_mode_change = read_logic(|l| l.get_autocmds(AutoCmdKind::PreModeChange));
pre_mode_change.exec(); pre_mode_change.exec();
std::mem::swap(&mut self.mode, mode); std::mem::swap(&mut self.mode, mode);
self.editor.set_cursor_clamp(self.mode.clamp_cursor()); self.editor.set_cursor_clamp(self.mode.clamp_cursor());
write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(self.mode.report_mode().to_string()), VarFlags::NONE)).ok(); write_vars(|v| {
self.prompt.refresh(); v.set_var(
"SHED_VI_MODE",
VarKind::Str(self.mode.report_mode().to_string()),
VarFlags::NONE,
)
})
.ok();
self.prompt.refresh();
let post_mode_change = read_logic(|l| l.get_autocmds(AutoCmdKind::PostModeChange)); let post_mode_change = read_logic(|l| l.get_autocmds(AutoCmdKind::PostModeChange));
post_mode_change.exec(); post_mode_change.exec();
} }
pub fn exec_cmd(&mut self, mut cmd: ViCmd) -> ShResult<()> { pub fn exec_cmd(&mut self, mut cmd: ViCmd) -> ShResult<()> {
let mut select_mode = None; let mut select_mode = None;
@@ -834,63 +872,72 @@ impl ShedVi {
if cmd.is_mode_transition() { if cmd.is_mode_transition() {
let count = cmd.verb_count(); let count = cmd.verb_count();
let mut mode: Box<dyn ViMode> = if matches!(self.mode.report_mode(), ModeReport::Ex | ModeReport::Verbatim) && cmd.flags.contains(CmdFlags::EXIT_CUR_MODE) { let mut mode: Box<dyn ViMode> = if matches!(
if let Some(saved) = self.saved_mode.take() { self.mode.report_mode(),
saved ModeReport::Ex | ModeReport::Verbatim
} else { ) && cmd.flags.contains(CmdFlags::EXIT_CUR_MODE)
Box::new(ViNormal::new()) {
} if let Some(saved) = self.saved_mode.take() {
} else { saved
match cmd.verb().unwrap().1 { } else {
Verb::Change | Verb::InsertModeLineBreak(_) | Verb::InsertMode => { Box::new(ViNormal::new())
is_insert_mode = true; }
Box::new(ViInsert::new().with_count(count as u16)) } else {
} match cmd.verb().unwrap().1 {
Verb::Change | Verb::InsertModeLineBreak(_) | Verb::InsertMode => {
is_insert_mode = true;
Box::new(ViInsert::new().with_count(count as u16))
}
Verb::ExMode => { Verb::ExMode => Box::new(ViEx::new()),
Box::new(ViEx::new())
}
Verb::VerbatimMode => { Verb::VerbatimMode => Box::new(ViVerbatim::new().with_count(count as u16)),
Box::new(ViVerbatim::new().with_count(count as u16))
}
Verb::NormalMode => Box::new(ViNormal::new()), Verb::NormalMode => Box::new(ViNormal::new()),
Verb::ReplaceMode => Box::new(ViReplace::new()), Verb::ReplaceMode => Box::new(ViReplace::new()),
Verb::VisualModeSelectLast => { Verb::VisualModeSelectLast => {
if self.mode.report_mode() != ModeReport::Visual { if self.mode.report_mode() != ModeReport::Visual {
self self
.editor .editor
.start_selecting(SelectMode::Char(SelectAnchor::End)); .start_selecting(SelectMode::Char(SelectAnchor::End));
} }
let mut mode: Box<dyn ViMode> = Box::new(ViVisual::new()); let mut mode: Box<dyn ViMode> = Box::new(ViVisual::new());
self.swap_mode(&mut mode); self.swap_mode(&mut mode);
return self.editor.exec_cmd(cmd); return self.editor.exec_cmd(cmd);
} }
Verb::VisualMode => { Verb::VisualMode => {
select_mode = Some(SelectMode::Char(SelectAnchor::End)); select_mode = Some(SelectMode::Char(SelectAnchor::End));
Box::new(ViVisual::new()) Box::new(ViVisual::new())
} }
Verb::VisualModeLine => { Verb::VisualModeLine => {
select_mode = Some(SelectMode::Line(SelectAnchor::End)); select_mode = Some(SelectMode::Line(SelectAnchor::End));
Box::new(ViVisual::new()) Box::new(ViVisual::new())
} }
_ => unreachable!(), _ => unreachable!(),
} }
}; };
self.swap_mode(&mut mode); self.swap_mode(&mut mode);
if matches!(self.mode.report_mode(), ModeReport::Ex | ModeReport::Verbatim) { if matches!(
self.saved_mode = Some(mode); self.mode.report_mode(),
write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(self.mode.report_mode().to_string()), VarFlags::NONE))?; ModeReport::Ex | ModeReport::Verbatim
self.prompt.refresh(); ) {
return Ok(()); self.saved_mode = Some(mode);
} write_vars(|v| {
v.set_var(
"SHED_VI_MODE",
VarKind::Str(self.mode.report_mode().to_string()),
VarFlags::NONE,
)
})?;
self.prompt.refresh();
return Ok(());
}
if mode.is_repeatable() { if mode.is_repeatable() {
self.repeat_action = mode.as_replay(); self.repeat_action = mode.as_replay();
@@ -912,9 +959,14 @@ impl ShedVi {
self.editor.clear_insert_mode_start_pos(); self.editor.clear_insert_mode_start_pos();
} }
write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(self.mode.report_mode().to_string()), VarFlags::NONE))?; write_vars(|v| {
self.prompt.refresh(); v.set_var(
"SHED_VI_MODE",
VarKind::Str(self.mode.report_mode().to_string()),
VarFlags::NONE,
)
})?;
self.prompt.refresh();
return Ok(()); return Ok(());
} else if cmd.is_cmd_repeat() { } else if cmd.is_cmd_repeat() {
@@ -989,22 +1041,21 @@ impl ShedVi {
} }
} }
if self.mode.report_mode() == ModeReport::Visual if self.mode.report_mode() == ModeReport::Visual && self.editor.select_range().is_none() {
&& self.editor.select_range().is_none() { self.editor.stop_selecting();
self.editor.stop_selecting(); let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new()); self.swap_mode(&mut mode);
self.swap_mode(&mut mode); }
}
if cmd.is_repeatable() { if cmd.is_repeatable() {
if self.mode.report_mode() == ModeReport::Visual { if self.mode.report_mode() == ModeReport::Visual {
// The motion is assigned in the line buffer execution, so we also have to // The motion is assigned in the line buffer execution, so we also have to
// assign it here in order to be able to repeat it // assign it here in order to be able to repeat it
if let Some(range) = self.editor.select_range() { if let Some(range) = self.editor.select_range() {
cmd.motion = Some(MotionCmd(1, Motion::Range(range.0, range.1))) cmd.motion = Some(MotionCmd(1, Motion::Range(range.0, range.1)))
} else { } else {
log::warn!("You're in visual mode with no select range??"); log::warn!("You're in visual mode with no select range??");
}; };
} }
self.repeat_action = Some(CmdReplay::Single(cmd.clone())); self.repeat_action = Some(CmdReplay::Single(cmd.clone()));
} }
@@ -1018,24 +1069,27 @@ impl ShedVi {
if self.mode.report_mode() == ModeReport::Visual && cmd.verb().is_some_and(|v| v.1.is_edit()) { if self.mode.report_mode() == ModeReport::Visual && cmd.verb().is_some_and(|v| v.1.is_edit()) {
self.editor.stop_selecting(); self.editor.stop_selecting();
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new()); let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
self.swap_mode(&mut mode); self.swap_mode(&mut mode);
} }
if self.mode.report_mode() != ModeReport::Visual && self.editor.select_range().is_some() { if self.mode.report_mode() != ModeReport::Visual && self.editor.select_range().is_some() {
self.editor.stop_selecting(); self.editor.stop_selecting();
} }
if cmd.flags.contains(CmdFlags::EXIT_CUR_MODE) { if cmd.flags.contains(CmdFlags::EXIT_CUR_MODE) {
let mut mode: Box<dyn ViMode> = if matches!(self.mode.report_mode(), ModeReport::Ex | ModeReport::Verbatim) { let mut mode: Box<dyn ViMode> = if matches!(
if let Some(saved) = self.saved_mode.take() { self.mode.report_mode(),
saved ModeReport::Ex | ModeReport::Verbatim
} else { ) {
Box::new(ViNormal::new()) if let Some(saved) = self.saved_mode.take() {
} saved
} else { } else {
Box::new(ViNormal::new()) Box::new(ViNormal::new())
}; }
self.swap_mode(&mut mode); } else {
Box::new(ViNormal::new())
};
self.swap_mode(&mut mode);
} }
Ok(()) Ok(())
@@ -1069,7 +1123,6 @@ pub fn annotate_input(input: &str) -> String {
.filter(|tk| !matches!(tk.class, TkRule::SOI | TkRule::EOI | TkRule::Null)) .filter(|tk| !matches!(tk.class, TkRule::SOI | TkRule::EOI | TkRule::Null))
.collect(); .collect();
for tk in tokens.into_iter().rev() { for tk in tokens.into_iter().rev() {
let insertions = annotate_token(tk); let insertions = annotate_token(tk);
for (pos, marker) in insertions { for (pos, marker) in insertions {
@@ -1112,9 +1165,7 @@ pub fn annotate_input_recursive(input: &str) -> String {
markers::PROC_SUB => match chars.peek().map(|(_, c)| *c) { markers::PROC_SUB => match chars.peek().map(|(_, c)| *c) {
Some('>') => ">(", Some('>') => ">(",
Some('<') => "<(", Some('<') => "<(",
_ => { _ => "<(",
"<("
}
}, },
markers::CMD_SUB => "$(", markers::CMD_SUB => "$(",
markers::SUBSH => "(", markers::SUBSH => "(",
@@ -1256,7 +1307,8 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
let mut insertions: Vec<(usize, Marker)> = vec![]; let mut insertions: Vec<(usize, Marker)> = vec![];
if token.class != TkRule::Str if token.class != TkRule::Str
&& let Some(marker) = marker_for(&token.class) { && let Some(marker) = marker_for(&token.class)
{
insertions.push((token.span.range().end, markers::RESET)); insertions.push((token.span.range().end, markers::RESET));
insertions.push((token.span.range().start, marker)); insertions.push((token.span.range().start, marker));
return insertions; return insertions;
@@ -1279,7 +1331,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
let span_start = token.span.range().start; let span_start = token.span.range().start;
let mut qt_state = QuoteState::default(); let mut qt_state = QuoteState::default();
let mut cmd_sub_depth = 0; let mut cmd_sub_depth = 0;
let mut proc_sub_depth = 0; let mut proc_sub_depth = 0;
@@ -1350,7 +1402,8 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
|| *br_ch == '=' || *br_ch == '='
|| *br_ch == '/' // parameter expansion symbols || *br_ch == '/' // parameter expansion symbols
|| *br_ch == '?' || *br_ch == '?'
|| *br_ch == '$' // we're in some expansion like $foo$bar or ${foo$bar} || *br_ch == '$'
// we're in some expansion like $foo$bar or ${foo$bar}
{ {
token_chars.next(); token_chars.next();
} else if *br_ch == '}' { } else if *br_ch == '}' {
@@ -1370,8 +1423,9 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
// consume the var name // consume the var name
while let Some((cur_i, var_ch)) = token_chars.peek() { while let Some((cur_i, var_ch)) = token_chars.peek() {
if var_ch.is_ascii_alphanumeric() if var_ch.is_ascii_alphanumeric()
|| ShellParam::from_char(var_ch).is_some() || ShellParam::from_char(var_ch).is_some()
|| *var_ch == '_' { || *var_ch == '_'
{
end_pos = *cur_i + 1; end_pos = *cur_i + 1;
token_chars.next(); token_chars.next();
} else { } else {
@@ -1397,12 +1451,12 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
token_chars.next(); // consume the escaped char token_chars.next(); // consume the escaped char
} }
} }
'\\' if qt_state.in_single() => { '\\' if qt_state.in_single() => {
token_chars.next(); token_chars.next();
if let Some(&(_,'\'')) = token_chars.peek() { if let Some(&(_, '\'')) = token_chars.peek() {
token_chars.next(); // consume the escaped single quote token_chars.next(); // consume the escaped single quote
} }
} }
'<' | '>' if !qt_state.in_quote() && cmd_sub_depth == 0 && proc_sub_depth == 0 => { '<' | '>' if !qt_state.in_quote() && cmd_sub_depth == 0 && proc_sub_depth == 0 => {
token_chars.next(); token_chars.next();
if let Some((_, proc_sub_ch)) = token_chars.peek() if let Some((_, proc_sub_ch)) = token_chars.peek()
@@ -1421,7 +1475,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
} else { } else {
insertions.push((span_start + *i, markers::STRING_DQ)); insertions.push((span_start + *i, markers::STRING_DQ));
} }
qt_state.toggle_double(); qt_state.toggle_double();
token_chars.next(); // consume the quote token_chars.next(); // consume the quote
} }
'\'' if !qt_state.in_double() => { '\'' if !qt_state.in_double() => {
@@ -1430,7 +1484,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
} else { } else {
insertions.push((span_start + *i, markers::STRING_SQ)); insertions.push((span_start + *i, markers::STRING_SQ));
} }
qt_state.toggle_single(); qt_state.toggle_single();
token_chars.next(); // consume the quote token_chars.next(); // consume the quote
} }
'[' if !qt_state.in_quote() && !token.flags.contains(TkFlags::ASSIGN) => { '[' if !qt_state.in_quote() && !token.flags.contains(TkFlags::ASSIGN) => {

View File

@@ -3,7 +3,8 @@ use std::{
env, env,
fmt::{Debug, Write}, fmt::{Debug, Write},
io::{BufRead, BufReader, Read}, io::{BufRead, BufReader, Read},
os::fd::{AsFd, BorrowedFd, RawFd}, sync::Arc, os::fd::{AsFd, BorrowedFd, RawFd},
sync::Arc,
}; };
use nix::{ use nix::{
@@ -17,14 +18,12 @@ use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use vte::{Parser, Perform}; use vte::{Parser, Perform};
pub use crate::libsh::guards::{RawModeGuard, raw_mode}; pub use crate::libsh::guards::{RawModeGuard, raw_mode};
use crate::state::{read_meta, write_meta};
use crate::{ use crate::{
libsh::error::{ShErr, ShErrKind, ShResult}, libsh::error::{ShErr, ShErrKind, ShResult},
readline::keys::{KeyCode, ModKeys}, readline::keys::{KeyCode, ModKeys},
state::read_shopts, state::read_shopts,
}; };
use crate::{
state::{read_meta, write_meta},
};
use super::keys::KeyEvent; use super::keys::KeyEvent;
@@ -165,13 +164,13 @@ fn width(s: &str, esc_seq: &mut u8) -> u16 {
0 0
} else if *esc_seq == 2 { } else if *esc_seq == 2 {
if s == ";" || (s.as_bytes()[0] >= b'0' && s.as_bytes()[0] <= b'9') { if s == ";" || (s.as_bytes()[0] >= b'0' && s.as_bytes()[0] <= b'9') {
/*} else if s == "m" { /*} else if s == "m" {
// last // last
*esc_seq = 0;*/ *esc_seq = 0;*/
} else { } else {
// not supported // not supported
*esc_seq = 0; *esc_seq = 0;
} }
0 0
} else if s == "\x1b" { } else if s == "\x1b" {
@@ -457,27 +456,27 @@ impl Perform for KeyCollector {
}; };
KeyEvent(key, mods) KeyEvent(key, mods)
} }
([],'u') => { ([], 'u') => {
let codepoint = params.first().copied().unwrap_or(0); let codepoint = params.first().copied().unwrap_or(0);
let mods = params let mods = params
.get(1) .get(1)
.map(|&m| Self::parse_modifiers(m)) .map(|&m| Self::parse_modifiers(m))
.unwrap_or(ModKeys::empty()); .unwrap_or(ModKeys::empty());
let key = match codepoint { let key = match codepoint {
9 => KeyCode::Tab, 9 => KeyCode::Tab,
13 => KeyCode::Enter, 13 => KeyCode::Enter,
27 => KeyCode::Esc, 27 => KeyCode::Esc,
127 => KeyCode::Backspace, 127 => KeyCode::Backspace,
_ => { _ => {
if let Some(ch) = char::from_u32(codepoint as u32) { if let Some(ch) = char::from_u32(codepoint as u32) {
KeyCode::Char(ch) KeyCode::Char(ch)
} else { } else {
return return;
} }
} }
}; };
KeyEvent(key, mods) KeyEvent(key, mods)
} }
// SGR mouse: CSI < button;x;y M/m (ignore mouse events for now) // SGR mouse: CSI < button;x;y M/m (ignore mouse events for now)
([b'<'], 'M') | ([b'<'], 'm') => { ([b'<'], 'M') | ([b'<'], 'm') => {
return; return;
@@ -516,18 +515,21 @@ impl PollReader {
} }
pub fn feed_bytes(&mut self, bytes: &[u8], verbatim: bool) { pub fn feed_bytes(&mut self, bytes: &[u8], verbatim: bool) {
if verbatim { if verbatim {
let seq = String::from_utf8_lossy(bytes).to_string(); let seq = String::from_utf8_lossy(bytes).to_string();
self.collector.push(KeyEvent(KeyCode::Verbatim(Arc::from(seq.as_str())), ModKeys::empty())); self.collector.push(KeyEvent(
} else if bytes == [b'\x1b'] { KeyCode::Verbatim(Arc::from(seq.as_str())),
ModKeys::empty(),
));
} else if bytes == [b'\x1b'] {
// Single escape byte - user pressed ESC key // Single escape byte - user pressed ESC key
self self
.collector .collector
.push(KeyEvent(KeyCode::Esc, ModKeys::empty())); .push(KeyEvent(KeyCode::Esc, ModKeys::empty()));
} else { } else {
// Feed all bytes through vte parser // Feed all bytes through vte parser
self.parser.advance(&mut self.collector, bytes); self.parser.advance(&mut self.collector, bytes);
} }
} }
} }
@@ -748,13 +750,9 @@ impl Layout {
} }
} }
fn is_ctl_char(gr: &str) -> bool { fn is_ctl_char(gr: &str) -> bool {
gr.len() > 0 && !gr.is_empty() && gr.as_bytes()[0] <= 0x1F && gr != "\n" && gr != "\t" && gr != "\r"
gr.as_bytes()[0] <= 0x1F && }
gr != "\n" &&
gr != "\t" &&
gr != "\r"
}
pub fn calc_pos(term_width: u16, s: &str, orig: Pos, left_margin: u16, raw_calc: bool) -> Pos { pub fn calc_pos(term_width: u16, s: &str, orig: Pos, left_margin: u16, raw_calc: bool) -> Pos {
const TAB_STOP: u16 = 8; const TAB_STOP: u16 = 8;
@@ -767,8 +765,8 @@ impl Layout {
} }
let c_width = if c == "\t" { let c_width = if c == "\t" {
TAB_STOP - (pos.col % TAB_STOP) TAB_STOP - (pos.col % TAB_STOP)
} else if raw_calc && Self::is_ctl_char(c) { } else if raw_calc && Self::is_ctl_char(c) {
2 2
} else { } else {
width(c, &mut esc_seq) width(c, &mut esc_seq)
}; };
@@ -867,21 +865,21 @@ impl TermWriter {
self.t_cols = t_cols; self.t_cols = t_cols;
} }
/// Called before the prompt is drawn. If we are not on column 1, push a vid-inverted '%' and then a '\n\r'. /// Called before the prompt is drawn. If we are not on column 1, push a vid-inverted '%' and then a '\n\r'.
/// ///
/// Aping zsh with this but it's a nice feature. /// Aping zsh with this but it's a nice feature.
pub fn fix_cursor_column(&mut self, rdr: &mut TermReader) -> ShResult<()> { pub fn fix_cursor_column(&mut self, rdr: &mut TermReader) -> ShResult<()> {
let Some((_,c)) = self.get_cursor_pos(rdr)? else { let Some((_, c)) = self.get_cursor_pos(rdr)? else {
return Ok(()); return Ok(());
}; };
if c != 1 { if c != 1 {
self.flush_write("\x1b[7m%\x1b[0m\n\r")?; self.flush_write("\x1b[7m%\x1b[0m\n\r")?;
} }
Ok(()) Ok(())
} }
pub fn get_cursor_pos(&mut self, rdr: &mut TermReader) -> ShResult<Option<(usize, usize)>> { pub fn get_cursor_pos(&mut self, rdr: &mut TermReader) -> ShResult<Option<(usize, usize)>> {
// Ping the cursor's position // Ping the cursor's position
self.flush_write("\x1b[6n")?; self.flush_write("\x1b[6n")?;
@@ -900,14 +898,16 @@ impl TermWriter {
let row = read_digits_until(rdr, ';')?; let row = read_digits_until(rdr, ';')?;
let col = read_digits_until(rdr, 'R')?; let col = read_digits_until(rdr, 'R')?;
let pos = if let Some(row) = row && let Some(col) = col { let pos = if let Some(row) = row
Some((row as usize, col as usize)) && let Some(col) = col
} else { {
None Some((row as usize, col as usize))
}; } else {
None
};
Ok(pos) Ok(pos)
} }
pub fn move_cursor_at_leftmost( pub fn move_cursor_at_leftmost(
&mut self, &mut self,
@@ -996,7 +996,7 @@ impl LineWriter for TermWriter {
) )
}; };
self.buffer.clear(); self.buffer.clear();
self.buffer.push_str("\x1b[J"); // Clear from cursor to end of screen to erase any remnants of the old line after the prompt self.buffer.push_str("\x1b[J"); // Clear from cursor to end of screen to erase any remnants of the old line after the prompt
let end = new_layout.end; let end = new_layout.end;
let cursor = new_layout.cursor; let cursor = new_layout.cursor;

View File

@@ -178,8 +178,8 @@ impl ViCmd {
matches!( matches!(
v.1, v.1,
Verb::Change Verb::Change
| Verb::VerbatimMode | Verb::VerbatimMode
| Verb::ExMode | Verb::ExMode
| Verb::InsertMode | Verb::InsertMode
| Verb::InsertModeLineBreak(_) | Verb::InsertModeLineBreak(_)
| Verb::NormalMode | Verb::NormalMode
@@ -221,8 +221,8 @@ pub enum Verb {
ReplaceCharInplace(char, u16), // char to replace with, number of chars to replace ReplaceCharInplace(char, u16), // char to replace with, number of chars to replace
ToggleCaseInplace(u16), // Number of chars to toggle ToggleCaseInplace(u16), // Number of chars to toggle
ToggleCaseRange, ToggleCaseRange,
IncrementNumber(u16), IncrementNumber(u16),
DecrementNumber(u16), DecrementNumber(u16),
ToLower, ToLower,
ToUpper, ToUpper,
Complete, Complete,
@@ -232,7 +232,7 @@ pub enum Verb {
RepeatLast, RepeatLast,
Put(Anchor), Put(Anchor),
ReplaceMode, ReplaceMode,
VerbatimMode, VerbatimMode,
InsertMode, InsertMode,
InsertModeLineBreak(Anchor), InsertModeLineBreak(Anchor),
NormalMode, NormalMode,
@@ -303,8 +303,8 @@ impl Verb {
| Self::Insert(_) | Self::Insert(_)
| Self::Rot13 | Self::Rot13
| Self::EndOfFile | Self::EndOfFile
| Self::IncrementNumber(_) | Self::IncrementNumber(_)
| Self::DecrementNumber(_) | Self::DecrementNumber(_)
) )
} }
pub fn is_char_insert(&self) -> bool { pub fn is_char_insert(&self) -> bool {

View File

@@ -9,373 +9,399 @@ use crate::libsh::error::{ShErr, ShErrKind, ShResult};
use crate::readline::keys::KeyEvent; use crate::readline::keys::KeyEvent;
use crate::readline::linebuf::LineBuf; use crate::readline::linebuf::LineBuf;
use crate::readline::vicmd::{ use crate::readline::vicmd::{
Anchor, CmdFlags, Motion, MotionCmd, ReadSrc, RegisterName, To, Val, Verb, VerbCmd, Anchor, CmdFlags, Motion, MotionCmd, ReadSrc, RegisterName, To, Val, Verb, VerbCmd, ViCmd,
ViCmd, WriteDest, WriteDest,
}; };
use crate::readline::vimode::{ModeReport, ViInsert, ViMode}; use crate::readline::vimode::{ModeReport, ViInsert, ViMode};
use crate::state::write_meta; use crate::state::write_meta;
bitflags! { bitflags! {
#[derive(Debug,Clone,Copy,PartialEq,Eq)] #[derive(Debug,Clone,Copy,PartialEq,Eq)]
pub struct SubFlags: u16 { pub struct SubFlags: u16 {
const GLOBAL = 1 << 0; // g const GLOBAL = 1 << 0; // g
const CONFIRM = 1 << 1; // c (probably not implemented) const CONFIRM = 1 << 1; // c (probably not implemented)
const IGNORE_CASE = 1 << 2; // i const IGNORE_CASE = 1 << 2; // i
const NO_IGNORE_CASE = 1 << 3; // I const NO_IGNORE_CASE = 1 << 3; // I
const SHOW_COUNT = 1 << 4; // n const SHOW_COUNT = 1 << 4; // n
const PRINT_RESULT = 1 << 5; // p const PRINT_RESULT = 1 << 5; // p
const PRINT_NUMBERED = 1 << 6; // # const PRINT_NUMBERED = 1 << 6; // #
const PRINT_LEFT_ALIGN = 1 << 7; // l const PRINT_LEFT_ALIGN = 1 << 7; // l
} }
} }
#[derive(Default, Clone, Debug)] #[derive(Default, Clone, Debug)]
struct ExEditor { struct ExEditor {
buf: LineBuf, buf: LineBuf,
mode: ViInsert mode: ViInsert,
} }
impl ExEditor { impl ExEditor {
pub fn clear(&mut self) { pub fn clear(&mut self) {
*self = Self::default() *self = Self::default()
} }
pub fn handle_key(&mut self, key: KeyEvent) -> ShResult<()> { pub fn handle_key(&mut self, key: KeyEvent) -> ShResult<()> {
let Some(cmd) = self.mode.handle_key(key) else { let Some(cmd) = self.mode.handle_key(key) else {
return Ok(()) return Ok(());
}; };
self.buf.exec_cmd(cmd) self.buf.exec_cmd(cmd)
} }
} }
#[derive(Default, Clone, Debug)] #[derive(Default, Clone, Debug)]
pub struct ViEx { pub struct ViEx {
pending_cmd: ExEditor, pending_cmd: ExEditor,
} }
impl ViEx { impl ViEx {
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
} }
impl ViMode for ViEx { impl ViMode for ViEx {
// Ex mode can return errors, so we use this fallible method instead of the normal one // Ex mode can return errors, so we use this fallible method instead of the normal one
fn handle_key_fallible(&mut self, key: KeyEvent) -> ShResult<Option<ViCmd>> { fn handle_key_fallible(&mut self, key: KeyEvent) -> ShResult<Option<ViCmd>> {
use crate::readline::keys::{KeyEvent as E, KeyCode as C, ModKeys as M}; use crate::readline::keys::{KeyCode as C, KeyEvent as E, ModKeys as M};
log::debug!("[ViEx] handle_key_fallible: key={:?}", key); log::debug!("[ViEx] handle_key_fallible: key={:?}", key);
match key { match key {
E(C::Char('\r'), M::NONE) | E(C::Char('\r'), M::NONE) | E(C::Enter, M::NONE) => {
E(C::Enter, M::NONE) => { let input = self.pending_cmd.buf.as_str();
let input = self.pending_cmd.buf.as_str(); log::debug!("[ViEx] Enter pressed, pending_cmd={:?}", input);
log::debug!("[ViEx] Enter pressed, pending_cmd={:?}", input); match parse_ex_cmd(input) {
match parse_ex_cmd(input) { Ok(cmd) => {
Ok(cmd) => { log::debug!("[ViEx] parse_ex_cmd Ok: {:?}", cmd);
log::debug!("[ViEx] parse_ex_cmd Ok: {:?}", cmd); Ok(cmd)
Ok(cmd) }
} Err(e) => {
Err(e) => { log::debug!("[ViEx] parse_ex_cmd Err: {:?}", e);
log::debug!("[ViEx] parse_ex_cmd Err: {:?}", e); let msg = e.unwrap_or(format!("Not an editor command: {}", input));
let msg = e.unwrap_or(format!("Not an editor command: {}", input)); write_meta(|m| m.post_system_message(msg.clone()));
write_meta(|m| m.post_system_message(msg.clone())); Err(ShErr::simple(ShErrKind::ParseErr, msg))
Err(ShErr::simple(ShErrKind::ParseErr, msg)) }
} }
} }
} E(C::Char('C'), M::CTRL) => {
E(C::Char('C'), M::CTRL) => { log::debug!("[ViEx] Ctrl-C, clearing");
log::debug!("[ViEx] Ctrl-C, clearing"); self.pending_cmd.clear();
self.pending_cmd.clear(); Ok(None)
Ok(None) }
} E(C::Esc, M::NONE) => {
E(C::Esc, M::NONE) => { log::debug!("[ViEx] Esc, returning to normal mode");
log::debug!("[ViEx] Esc, returning to normal mode"); Ok(Some(ViCmd {
Ok(Some(ViCmd { register: RegisterName::default(),
register: RegisterName::default(), verb: Some(VerbCmd(1, Verb::NormalMode)),
verb: Some(VerbCmd(1, Verb::NormalMode)), motion: None,
motion: None, flags: CmdFlags::empty(),
flags: CmdFlags::empty(), raw_seq: "".into(),
raw_seq: "".into(), }))
})) }
} _ => {
_ => { log::debug!("[ViEx] forwarding key to ExEditor");
log::debug!("[ViEx] forwarding key to ExEditor"); self.pending_cmd.handle_key(key).map(|_| None)
self.pending_cmd.handle_key(key).map(|_| None) }
} }
} }
} fn handle_key(&mut self, key: KeyEvent) -> Option<ViCmd> {
fn handle_key(&mut self, key: KeyEvent) -> Option<ViCmd> { let result = self.handle_key_fallible(key);
let result = self.handle_key_fallible(key); log::debug!("[ViEx] handle_key result: {:?}", result);
log::debug!("[ViEx] handle_key result: {:?}", result); result.ok().flatten()
result.ok().flatten() }
} fn is_repeatable(&self) -> bool {
fn is_repeatable(&self) -> bool { false
false }
}
fn as_replay(&self) -> Option<super::CmdReplay> { fn as_replay(&self) -> Option<super::CmdReplay> {
None None
} }
fn cursor_style(&self) -> String { fn cursor_style(&self) -> String {
"\x1b[3 q".to_string() "\x1b[3 q".to_string()
} }
fn pending_seq(&self) -> Option<String> { fn pending_seq(&self) -> Option<String> {
Some(self.pending_cmd.buf.as_str().to_string()) Some(self.pending_cmd.buf.as_str().to_string())
} }
fn pending_cursor(&self) -> Option<usize> { fn pending_cursor(&self) -> Option<usize> {
Some(self.pending_cmd.buf.cursor.get()) Some(self.pending_cmd.buf.cursor.get())
} }
fn move_cursor_on_undo(&self) -> bool { fn move_cursor_on_undo(&self) -> bool {
false false
} }
fn clamp_cursor(&self) -> bool { fn clamp_cursor(&self) -> bool {
true true
} }
fn hist_scroll_start_pos(&self) -> Option<To> { fn hist_scroll_start_pos(&self) -> Option<To> {
None None
} }
fn report_mode(&self) -> super::ModeReport { fn report_mode(&self) -> super::ModeReport {
ModeReport::Ex ModeReport::Ex
} }
} }
fn parse_ex_cmd(raw: &str) -> Result<Option<ViCmd>,Option<String>> { fn parse_ex_cmd(raw: &str) -> Result<Option<ViCmd>, Option<String>> {
let raw = raw.trim(); let raw = raw.trim();
if raw.is_empty() { if raw.is_empty() {
return Ok(None) return Ok(None);
} }
let mut chars = raw.chars().peekable(); let mut chars = raw.chars().peekable();
let (verb, motion) = { let (verb, motion) = {
if chars.peek() == Some(&'g') { if chars.peek() == Some(&'g') {
let mut cmd_name = String::new(); let mut cmd_name = String::new();
while let Some(ch) = chars.peek() { while let Some(ch) = chars.peek() {
if ch.is_alphanumeric() { if ch.is_alphanumeric() {
cmd_name.push(*ch); cmd_name.push(*ch);
chars.next(); chars.next();
} else { } else {
break break;
} }
} }
if !"global".starts_with(&cmd_name) { if !"global".starts_with(&cmd_name) {
return Err(None) return Err(None);
} }
let Some(result) = parse_global(&mut chars)? else { return Ok(None) }; let Some(result) = parse_global(&mut chars)? else {
(Some(VerbCmd(1,result.1)), Some(MotionCmd(1,result.0))) return Ok(None);
} else { };
(parse_ex_command(&mut chars)?.map(|v| VerbCmd(1, v)), None) (Some(VerbCmd(1, result.1)), Some(MotionCmd(1, result.0)))
} } else {
}; (parse_ex_command(&mut chars)?.map(|v| VerbCmd(1, v)), None)
}
};
Ok(Some(ViCmd { Ok(Some(ViCmd {
register: RegisterName::default(), register: RegisterName::default(),
verb, verb,
motion, motion,
raw_seq: raw.to_string(), raw_seq: raw.to_string(),
flags: CmdFlags::EXIT_CUR_MODE, flags: CmdFlags::EXIT_CUR_MODE,
})) }))
} }
/// Unescape shell command arguments /// Unescape shell command arguments
fn unescape_shell_cmd(cmd: &str) -> String { fn unescape_shell_cmd(cmd: &str) -> String {
// The pest grammar uses double quotes for vicut commands // The pest grammar uses double quotes for vicut commands
// So shell commands need to escape double quotes // So shell commands need to escape double quotes
// We will be removing a single layer of escaping from double quotes // We will be removing a single layer of escaping from double quotes
let mut result = String::new(); let mut result = String::new();
let mut chars = cmd.chars().peekable(); let mut chars = cmd.chars().peekable();
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
if ch == '\\' { if ch == '\\' {
if let Some(&'"') = chars.peek() { if let Some(&'"') = chars.peek() {
chars.next(); chars.next();
result.push('"'); result.push('"');
} else { } else {
result.push(ch); result.push(ch);
} }
} else { } else {
result.push(ch); result.push(ch);
} }
} }
result result
} }
fn parse_ex_command(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>,Option<String>> { fn parse_ex_command(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>, Option<String>> {
let mut cmd_name = String::new(); let mut cmd_name = String::new();
while let Some(ch) = chars.peek() { while let Some(ch) = chars.peek() {
if ch == &'!' { if ch == &'!' {
cmd_name.push(*ch); cmd_name.push(*ch);
chars.next(); chars.next();
break break;
} else if !ch.is_alphanumeric() { } else if !ch.is_alphanumeric() {
break break;
} }
cmd_name.push(*ch); cmd_name.push(*ch);
chars.next(); chars.next();
} }
match cmd_name.as_str() { match cmd_name.as_str() {
"!" => { "!" => {
let cmd = chars.collect::<String>(); let cmd = chars.collect::<String>();
let cmd = unescape_shell_cmd(&cmd); let cmd = unescape_shell_cmd(&cmd);
Ok(Some(Verb::ShellCmd(cmd))) Ok(Some(Verb::ShellCmd(cmd)))
} }
"normal!" => parse_normal(chars), "normal!" => parse_normal(chars),
_ if "delete".starts_with(&cmd_name) => Ok(Some(Verb::Delete)), _ if "delete".starts_with(&cmd_name) => Ok(Some(Verb::Delete)),
_ if "yank".starts_with(&cmd_name) => Ok(Some(Verb::Yank)), _ if "yank".starts_with(&cmd_name) => Ok(Some(Verb::Yank)),
_ if "put".starts_with(&cmd_name) => Ok(Some(Verb::Put(Anchor::After))), _ if "put".starts_with(&cmd_name) => Ok(Some(Verb::Put(Anchor::After))),
_ if "read".starts_with(&cmd_name) => parse_read(chars), _ if "read".starts_with(&cmd_name) => parse_read(chars),
_ if "write".starts_with(&cmd_name) => parse_write(chars), _ if "write".starts_with(&cmd_name) => parse_write(chars),
_ if "substitute".starts_with(&cmd_name) => parse_substitute(chars), _ if "substitute".starts_with(&cmd_name) => parse_substitute(chars),
_ => Err(None) _ => Err(None),
} }
} }
fn parse_normal(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>,Option<String>> { fn parse_normal(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>, Option<String>> {
chars.peeking_take_while(|c| c.is_whitespace()).for_each(drop); chars
.peeking_take_while(|c| c.is_whitespace())
.for_each(drop);
let seq: String = chars.collect(); let seq: String = chars.collect();
Ok(Some(Verb::Normal(seq))) Ok(Some(Verb::Normal(seq)))
} }
fn parse_read(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>,Option<String>> { fn parse_read(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>, Option<String>> {
chars.peeking_take_while(|c| c.is_whitespace()).for_each(drop); chars
.peeking_take_while(|c| c.is_whitespace())
.for_each(drop);
let is_shell_read = if chars.peek() == Some(&'!') { chars.next(); true } else { false }; let is_shell_read = if chars.peek() == Some(&'!') {
let arg: String = chars.collect(); chars.next();
true
} else {
false
};
let arg: String = chars.collect();
if arg.trim().is_empty() { if arg.trim().is_empty() {
return Err(Some("Expected file path or shell command after ':r'".into())) return Err(Some(
} "Expected file path or shell command after ':r'".into(),
));
}
if is_shell_read { if is_shell_read {
Ok(Some(Verb::Read(ReadSrc::Cmd(arg)))) Ok(Some(Verb::Read(ReadSrc::Cmd(arg))))
} else { } else {
let arg_path = get_path(arg.trim()); let arg_path = get_path(arg.trim());
Ok(Some(Verb::Read(ReadSrc::File(arg_path)))) Ok(Some(Verb::Read(ReadSrc::File(arg_path))))
} }
} }
fn get_path(path: &str) -> PathBuf { fn get_path(path: &str) -> PathBuf {
if let Some(stripped) = path.strip_prefix("~/") if let Some(stripped) = path.strip_prefix("~/")
&& let Some(home) = std::env::var_os("HOME") { && let Some(home) = std::env::var_os("HOME")
return PathBuf::from(home).join(stripped) {
} return PathBuf::from(home).join(stripped);
if path == "~" }
&& let Some(home) = std::env::var_os("HOME") { if path == "~"
return PathBuf::from(home) && let Some(home) = std::env::var_os("HOME")
} {
PathBuf::from(path) return PathBuf::from(home);
}
PathBuf::from(path)
} }
fn parse_write(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>,Option<String>> { fn parse_write(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>, Option<String>> {
chars.peeking_take_while(|c| c.is_whitespace()).for_each(drop); chars
.peeking_take_while(|c| c.is_whitespace())
.for_each(drop);
let is_shell_write = chars.peek() == Some(&'!'); let is_shell_write = chars.peek() == Some(&'!');
if is_shell_write { if is_shell_write {
chars.next(); // consume '!' chars.next(); // consume '!'
let arg: String = chars.collect(); let arg: String = chars.collect();
return Ok(Some(Verb::Write(WriteDest::Cmd(arg)))); return Ok(Some(Verb::Write(WriteDest::Cmd(arg))));
} }
// Check for >> // Check for >>
let mut append_check = chars.clone(); let mut append_check = chars.clone();
let is_file_append = append_check.next() == Some('>') && append_check.next() == Some('>'); let is_file_append = append_check.next() == Some('>') && append_check.next() == Some('>');
if is_file_append { if is_file_append {
*chars = append_check; *chars = append_check;
} }
let arg: String = chars.collect(); let arg: String = chars.collect();
let arg_path = get_path(arg.trim()); let arg_path = get_path(arg.trim());
let dest = if is_file_append { let dest = if is_file_append {
WriteDest::FileAppend(arg_path) WriteDest::FileAppend(arg_path)
} else { } else {
WriteDest::File(arg_path) WriteDest::File(arg_path)
}; };
Ok(Some(Verb::Write(dest))) Ok(Some(Verb::Write(dest)))
} }
fn parse_global(chars: &mut Peekable<Chars<'_>>) -> Result<Option<(Motion,Verb)>,Option<String>> { fn parse_global(chars: &mut Peekable<Chars<'_>>) -> Result<Option<(Motion, Verb)>, Option<String>> {
let is_negated = if chars.peek() == Some(&'!') { chars.next(); true } else { false }; let is_negated = if chars.peek() == Some(&'!') {
chars.next();
true
} else {
false
};
chars.peeking_take_while(|c| c.is_whitespace()).for_each(drop); // Ignore whitespace chars
.peeking_take_while(|c| c.is_whitespace())
.for_each(drop); // Ignore whitespace
let Some(delimiter) = chars.next() else { let Some(delimiter) = chars.next() else {
return Ok(Some((Motion::Null,Verb::RepeatGlobal))) return Ok(Some((Motion::Null, Verb::RepeatGlobal)));
}; };
if delimiter.is_alphanumeric() { if delimiter.is_alphanumeric() {
return Err(None) return Err(None);
} }
let global_pat = parse_pattern(chars, delimiter)?; let global_pat = parse_pattern(chars, delimiter)?;
let Some(command) = parse_ex_command(chars)? else { let Some(command) = parse_ex_command(chars)? else {
return Err(Some("Expected a command after global pattern".into())) return Err(Some("Expected a command after global pattern".into()));
}; };
if is_negated { if is_negated {
Ok(Some((Motion::NotGlobal(Val::new_str(global_pat)), command))) Ok(Some((Motion::NotGlobal(Val::new_str(global_pat)), command)))
} else { } else {
Ok(Some((Motion::Global(Val::new_str(global_pat)), command))) Ok(Some((Motion::Global(Val::new_str(global_pat)), command)))
} }
} }
fn parse_substitute(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>,Option<String>> { fn parse_substitute(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>, Option<String>> {
while chars.peek().is_some_and(|c| c.is_whitespace()) { chars.next(); } // Ignore whitespace while chars.peek().is_some_and(|c| c.is_whitespace()) {
chars.next();
} // Ignore whitespace
let Some(delimiter) = chars.next() else { let Some(delimiter) = chars.next() else {
return Ok(Some(Verb::RepeatSubstitute)) return Ok(Some(Verb::RepeatSubstitute));
}; };
if delimiter.is_alphanumeric() { if delimiter.is_alphanumeric() {
return Err(None) return Err(None);
} }
let old_pat = parse_pattern(chars, delimiter)?; let old_pat = parse_pattern(chars, delimiter)?;
let new_pat = parse_pattern(chars, delimiter)?; let new_pat = parse_pattern(chars, delimiter)?;
let mut flags = SubFlags::empty(); let mut flags = SubFlags::empty();
while let Some(ch) = chars.next() { while let Some(ch) = chars.next() {
match ch { match ch {
'g' => flags |= SubFlags::GLOBAL, 'g' => flags |= SubFlags::GLOBAL,
'i' => flags |= SubFlags::IGNORE_CASE, 'i' => flags |= SubFlags::IGNORE_CASE,
'I' => flags |= SubFlags::NO_IGNORE_CASE, 'I' => flags |= SubFlags::NO_IGNORE_CASE,
'n' => flags |= SubFlags::SHOW_COUNT, 'n' => flags |= SubFlags::SHOW_COUNT,
_ => return Err(None) _ => return Err(None),
} }
} }
Ok(Some(Verb::Substitute(old_pat, new_pat, flags))) Ok(Some(Verb::Substitute(old_pat, new_pat, flags)))
} }
fn parse_pattern(chars: &mut Peekable<Chars<'_>>, delimiter: char) -> Result<String,Option<String>> { fn parse_pattern(
let mut pat = String::new(); chars: &mut Peekable<Chars<'_>>,
let mut closed = false; delimiter: char,
while let Some(ch) = chars.next() { ) -> Result<String, Option<String>> {
match ch { let mut pat = String::new();
'\\' => { let mut closed = false;
if chars.peek().is_some_and(|c| *c == delimiter) { while let Some(ch) = chars.next() {
// We escaped the delimiter, so we consume the escape char and continue match ch {
pat.push(chars.next().unwrap()); '\\' => {
continue if chars.peek().is_some_and(|c| *c == delimiter) {
} else { // We escaped the delimiter, so we consume the escape char and continue
// The escape char is probably for the regex in the pattern pat.push(chars.next().unwrap());
pat.push(ch); continue;
if let Some(esc_ch) = chars.next() { } else {
pat.push(esc_ch) // The escape char is probably for the regex in the pattern
} pat.push(ch);
} if let Some(esc_ch) = chars.next() {
} pat.push(esc_ch)
_ if ch == delimiter => { }
closed = true; }
break }
} _ if ch == delimiter => {
_ => pat.push(ch) closed = true;
} break;
} }
if !closed { _ => pat.push(ch),
Err(Some("Unclosed pattern in ex command".into())) }
} else { }
Ok(pat) if !closed {
} Err(Some("Unclosed pattern in ex command".into()))
} else {
Ok(pat)
}
} }

View File

@@ -1,8 +1,6 @@
use super::{common_cmds, CmdReplay, ModeReport, ViMode}; use super::{CmdReplay, ModeReport, ViMode, common_cmds};
use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M}; use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M};
use crate::readline::vicmd::{ use crate::readline::vicmd::{Direction, Motion, MotionCmd, To, Verb, VerbCmd, ViCmd, Word};
Direction, Motion, MotionCmd, To, Verb, VerbCmd, ViCmd, Word,
};
#[derive(Default, Clone, Debug)] #[derive(Default, Clone, Debug)]
pub struct ViInsert { pub struct ViInsert {

View File

@@ -4,47 +4,45 @@ use unicode_segmentation::UnicodeSegmentation;
use crate::libsh::error::ShResult; use crate::libsh::error::ShResult;
use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M}; use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M};
use crate::readline::vicmd::{ use crate::readline::vicmd::{Motion, MotionCmd, To, Verb, VerbCmd, ViCmd};
Motion, MotionCmd, To, Verb, VerbCmd, ViCmd,
};
pub mod ex;
pub mod insert; pub mod insert;
pub mod normal; pub mod normal;
pub mod replace; pub mod replace;
pub mod visual;
pub mod ex;
pub mod verbatim; pub mod verbatim;
pub mod visual;
pub use ex::ViEx; pub use ex::ViEx;
pub use insert::ViInsert; pub use insert::ViInsert;
pub use normal::ViNormal; pub use normal::ViNormal;
pub use replace::ViReplace; pub use replace::ViReplace;
pub use visual::ViVisual;
pub use verbatim::ViVerbatim; pub use verbatim::ViVerbatim;
pub use visual::ViVisual;
#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ModeReport { pub enum ModeReport {
Insert, Insert,
Normal, Normal,
Ex, Ex,
Visual, Visual,
Replace, Replace,
Verbatim, Verbatim,
Unknown, Unknown,
} }
impl Display for ModeReport { impl Display for ModeReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
ModeReport::Insert => write!(f, "INSERT"), ModeReport::Insert => write!(f, "INSERT"),
ModeReport::Normal => write!(f, "NORMAL"), ModeReport::Normal => write!(f, "NORMAL"),
ModeReport::Ex => write!(f, "COMMAND"), ModeReport::Ex => write!(f, "COMMAND"),
ModeReport::Visual => write!(f, "VISUAL"), ModeReport::Visual => write!(f, "VISUAL"),
ModeReport::Replace => write!(f, "REPLACE"), ModeReport::Replace => write!(f, "REPLACE"),
ModeReport::Verbatim => write!(f, "VERBATIM"), ModeReport::Verbatim => write!(f, "VERBATIM"),
ModeReport::Unknown => write!(f, "UNKNOWN"), ModeReport::Unknown => write!(f, "UNKNOWN"),
} }
} }
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@@ -73,13 +71,17 @@ pub enum CmdState {
} }
pub trait ViMode { pub trait ViMode {
fn handle_key_fallible(&mut self, key: E) -> ShResult<Option<ViCmd>> { Ok(self.handle_key(key)) } fn handle_key_fallible(&mut self, key: E) -> ShResult<Option<ViCmd>> {
Ok(self.handle_key(key))
}
fn handle_key(&mut self, key: E) -> Option<ViCmd>; fn handle_key(&mut self, key: E) -> Option<ViCmd>;
fn is_repeatable(&self) -> bool; fn is_repeatable(&self) -> bool;
fn as_replay(&self) -> Option<CmdReplay>; fn as_replay(&self) -> Option<CmdReplay>;
fn cursor_style(&self) -> String; fn cursor_style(&self) -> String;
fn pending_seq(&self) -> Option<String>; fn pending_seq(&self) -> Option<String>;
fn pending_cursor(&self) -> Option<usize> { None } fn pending_cursor(&self) -> Option<usize> {
None
}
fn move_cursor_on_undo(&self) -> bool; fn move_cursor_on_undo(&self) -> bool;
fn clamp_cursor(&self) -> bool; fn clamp_cursor(&self) -> bool;
fn hist_scroll_start_pos(&self) -> Option<To>; fn hist_scroll_start_pos(&self) -> Option<To>;

View File

@@ -1,7 +1,7 @@
use std::iter::Peekable; use std::iter::Peekable;
use std::str::Chars; use std::str::Chars;
use super::{common_cmds, CmdReplay, CmdState, ModeReport, ViMode}; use super::{CmdReplay, CmdState, ModeReport, ViMode, common_cmds};
use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M}; use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M};
use crate::readline::vicmd::{ use crate::readline::vicmd::{
Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb, Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb,
@@ -309,15 +309,15 @@ impl ViNormal {
flags: self.flags(), flags: self.flags(),
}); });
} }
':' => { ':' => {
return Some(ViCmd { return Some(ViCmd {
register, register,
verb: Some(VerbCmd(count, Verb::ExMode)), verb: Some(VerbCmd(count, Verb::ExMode)),
motion: None, motion: None,
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: self.flags(), flags: self.flags(),
}) });
} }
'i' => { 'i' => {
return Some(ViCmd { return Some(ViCmd {
register, register,
@@ -724,7 +724,7 @@ impl ViNormal {
} }
}; };
let _ = chars; // suppresses unused warnings, creates an error if we decide to use chars later let _ = chars; // suppresses unused warnings, creates an error if we decide to use chars later
let verb_ref = verb.as_ref().map(|v| &v.1); let verb_ref = verb.as_ref().map(|v| &v.1);
let motion_ref = motion.as_ref().map(|m| &m.1); let motion_ref = motion.as_ref().map(|m| &m.1);
@@ -756,28 +756,32 @@ impl ViMode for ViNormal {
raw_seq: "".into(), raw_seq: "".into(),
flags: self.flags(), flags: self.flags(),
}), }),
E(K::Char('A'), M::CTRL) => { E(K::Char('A'), M::CTRL) => {
let count = self.parse_count(&mut self.pending_seq.chars().peekable()).unwrap_or(1) as u16; let count = self
self.pending_seq.clear(); .parse_count(&mut self.pending_seq.chars().peekable())
Some(ViCmd { .unwrap_or(1) as u16;
register: Default::default(), self.pending_seq.clear();
verb: Some(VerbCmd(1, Verb::IncrementNumber(count))), Some(ViCmd {
motion: None, register: Default::default(),
raw_seq: "".into(), verb: Some(VerbCmd(1, Verb::IncrementNumber(count))),
flags: self.flags(), motion: None,
}) raw_seq: "".into(),
}, flags: self.flags(),
E(K::Char('X'), M::CTRL) => { })
let count = self.parse_count(&mut self.pending_seq.chars().peekable()).unwrap_or(1) as u16; }
self.pending_seq.clear(); E(K::Char('X'), M::CTRL) => {
Some(ViCmd { let count = self
register: Default::default(), .parse_count(&mut self.pending_seq.chars().peekable())
verb: Some(VerbCmd(1, Verb::DecrementNumber(count))), .unwrap_or(1) as u16;
motion: None, self.pending_seq.clear();
raw_seq: "".into(), Some(ViCmd {
flags: self.flags(), register: Default::default(),
}) verb: Some(VerbCmd(1, Verb::DecrementNumber(count))),
}, motion: None,
raw_seq: "".into(),
flags: self.flags(),
})
}
E(K::Char(ch), M::NONE) => self.try_parse(ch), E(K::Char(ch), M::NONE) => self.try_parse(ch),
E(K::Backspace, M::NONE) => Some(ViCmd { E(K::Backspace, M::NONE) => Some(ViCmd {

View File

@@ -1,8 +1,6 @@
use super::{common_cmds, CmdReplay, ModeReport, ViMode}; use super::{CmdReplay, ModeReport, ViMode, common_cmds};
use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M}; use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M};
use crate::readline::vicmd::{ use crate::readline::vicmd::{Direction, Motion, MotionCmd, To, Verb, VerbCmd, ViCmd, Word};
Direction, Motion, MotionCmd, To, Verb, VerbCmd, ViCmd, Word,
};
#[derive(Default, Debug)] #[derive(Default, Debug)]
pub struct ViReplace { pub struct ViReplace {

View File

@@ -1,39 +1,40 @@
use super::{common_cmds, CmdReplay, ModeReport, ViMode}; use super::{CmdReplay, ModeReport, ViMode, common_cmds};
use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M}; use crate::readline::keys::{KeyCode as K, KeyEvent as E};
use crate::readline::register::Register; use crate::readline::vicmd::{CmdFlags, RegisterName, To, Verb, VerbCmd, ViCmd};
use crate::readline::vicmd::{
CmdFlags, Direction, Motion, MotionCmd, RegisterName, To, Verb, VerbCmd, ViCmd, Word
};
#[derive(Default, Clone, Debug)] #[derive(Default, Clone, Debug)]
pub struct ViVerbatim { pub struct ViVerbatim {
sent_cmd: Vec<ViCmd>, sent_cmd: Vec<ViCmd>,
repeat_count: u16 repeat_count: u16,
} }
impl ViVerbatim { impl ViVerbatim {
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
pub fn with_count(self, repeat_count: u16) -> Self { pub fn with_count(self, repeat_count: u16) -> Self {
Self { repeat_count, ..self } Self {
} repeat_count,
..self
}
}
} }
impl ViMode for ViVerbatim { impl ViMode for ViVerbatim {
fn handle_key(&mut self, key: E) -> Option<ViCmd> { fn handle_key(&mut self, key: E) -> Option<ViCmd> {
match key { match key {
E(K::Verbatim(seq),_mods) => { E(K::Verbatim(seq), _mods) => {
log::debug!("Received verbatim key sequence: {:?}", seq); log::debug!("Received verbatim key sequence: {:?}", seq);
let cmd = ViCmd { register: RegisterName::default(), let cmd = ViCmd {
verb: Some(VerbCmd(1,Verb::Insert(seq.to_string()))), register: RegisterName::default(),
motion: None, verb: Some(VerbCmd(1, Verb::Insert(seq.to_string()))),
raw_seq: seq.to_string(), motion: None,
flags: CmdFlags::EXIT_CUR_MODE raw_seq: seq.to_string(),
}; flags: CmdFlags::EXIT_CUR_MODE,
self.sent_cmd.push(cmd.clone()); };
Some(cmd) self.sent_cmd.push(cmd.clone());
} Some(cmd)
}
_ => common_cmds(key), _ => common_cmds(key),
} }
} }

View File

@@ -1,7 +1,7 @@
use std::iter::Peekable; use std::iter::Peekable;
use std::str::Chars; use std::str::Chars;
use super::{common_cmds, CmdReplay, CmdState, ModeReport, ViMode}; use super::{CmdReplay, CmdState, ModeReport, ViMode, common_cmds};
use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M}; use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M};
use crate::readline::vicmd::{ use crate::readline::vicmd::{
Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb, Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb,
@@ -129,15 +129,15 @@ impl ViVisual {
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}); });
} }
':' => { ':' => {
return Some(ViCmd { return Some(ViCmd {
register, register,
verb: Some(VerbCmd(count, Verb::ExMode)), verb: Some(VerbCmd(count, Verb::ExMode)),
motion: None, motion: None,
raw_seq: self.take_cmd(), raw_seq: self.take_cmd(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}) });
} }
'x' => { 'x' => {
chars = chars_clone; chars = chars_clone;
break 'verb_parse Some(VerbCmd(count, Verb::Delete)); break 'verb_parse Some(VerbCmd(count, Verb::Delete));
@@ -581,7 +581,7 @@ impl ViVisual {
} }
}; };
let _ = chars; // suppresses unused warnings, creates an error if we decide to use chars later let _ = chars; // suppresses unused warnings, creates an error if we decide to use chars later
let verb_ref = verb.as_ref().map(|v| &v.1); let verb_ref = verb.as_ref().map(|v| &v.1);
let motion_ref = motion.as_ref().map(|m| &m.1); let motion_ref = motion.as_ref().map(|m| &m.1);
@@ -614,28 +614,32 @@ impl ViMode for ViVisual {
raw_seq: "".into(), raw_seq: "".into(),
flags: CmdFlags::empty(), flags: CmdFlags::empty(),
}), }),
E(K::Char('A'), M::CTRL) => { E(K::Char('A'), M::CTRL) => {
let count = self.parse_count(&mut self.pending_seq.chars().peekable()).unwrap_or(1) as u16; let count = self
self.pending_seq.clear(); .parse_count(&mut self.pending_seq.chars().peekable())
Some(ViCmd { .unwrap_or(1) as u16;
register: Default::default(), self.pending_seq.clear();
verb: Some(VerbCmd(1, Verb::IncrementNumber(count))), Some(ViCmd {
motion: None, register: Default::default(),
raw_seq: "".into(), verb: Some(VerbCmd(1, Verb::IncrementNumber(count))),
flags: CmdFlags::empty(), motion: None,
}) raw_seq: "".into(),
}, flags: CmdFlags::empty(),
E(K::Char('X'), M::CTRL) => { })
let count = self.parse_count(&mut self.pending_seq.chars().peekable()).unwrap_or(1) as u16; }
self.pending_seq.clear(); E(K::Char('X'), M::CTRL) => {
Some(ViCmd { let count = self
register: Default::default(), .parse_count(&mut self.pending_seq.chars().peekable())
verb: Some(VerbCmd(1, Verb::DecrementNumber(count))), .unwrap_or(1) as u16;
motion: None, self.pending_seq.clear();
raw_seq: "".into(), Some(ViCmd {
flags: CmdFlags::empty(), register: Default::default(),
}) verb: Some(VerbCmd(1, Verb::DecrementNumber(count))),
} motion: None,
raw_seq: "".into(),
flags: CmdFlags::empty(),
})
}
E(K::Char('R'), M::CTRL) => { E(K::Char('R'), M::CTRL) => {
let mut chars = self.pending_seq.chars().peekable(); let mut chars = self.pending_seq.chars().peekable();
let count = self.parse_count(&mut chars).unwrap_or(1); let count = self.parse_count(&mut chars).unwrap_or(1);

View File

@@ -104,12 +104,10 @@ impl ShOpts {
"core" => self.core.set(&remainder, val)?, "core" => self.core.set(&remainder, val)?,
"prompt" => self.prompt.set(&remainder, val)?, "prompt" => self.prompt.set(&remainder, val)?,
_ => { _ => {
return Err( return Err(ShErr::simple(
ShErr::simple( ShErrKind::SyntaxErr,
ShErrKind::SyntaxErr, "shopt: expected 'core' or 'prompt' in shopt key",
"shopt: expected 'core' or 'prompt' in shopt key", ));
)
);
} }
} }
Ok(()) Ok(())
@@ -129,12 +127,10 @@ impl ShOpts {
match key { match key {
"core" => self.core.get(&remainder), "core" => self.core.get(&remainder),
"prompt" => self.prompt.get(&remainder), "prompt" => self.prompt.get(&remainder),
_ => Err( _ => Err(ShErr::simple(
ShErr::simple( ShErrKind::SyntaxErr,
ShErrKind::SyntaxErr, "shopt: Expected 'core' or 'prompt' in shopt key",
"shopt: Expected 'core' or 'prompt' in shopt key", )),
)
),
} }
} }
} }
@@ -227,12 +223,10 @@ impl ShOptCore {
self.max_recurse_depth = val; self.max_recurse_depth = val;
} }
_ => { _ => {
return Err( return Err(ShErr::simple(
ShErr::simple( ShErrKind::SyntaxErr,
ShErrKind::SyntaxErr, format!("shopt: Unexpected 'core' option '{opt}'"),
format!("shopt: Unexpected 'core' option '{opt}'"), ));
)
);
} }
} }
Ok(()) Ok(())
@@ -289,12 +283,10 @@ impl ShOptCore {
output.push_str(&format!("{}", self.max_recurse_depth)); output.push_str(&format!("{}", self.max_recurse_depth));
Ok(Some(output)) Ok(Some(output))
} }
_ => Err( _ => Err(ShErr::simple(
ShErr::simple( ShErrKind::SyntaxErr,
ShErrKind::SyntaxErr, format!("shopt: Unexpected 'core' option '{query}'"),
format!("shopt: Unexpected 'core' option '{query}'"), )),
)
),
} }
} }
} }
@@ -344,6 +336,7 @@ pub struct ShOptPrompt {
pub auto_indent: bool, pub auto_indent: bool,
pub linebreak_on_incomplete: bool, pub linebreak_on_incomplete: bool,
pub leader: String, pub leader: String,
pub line_numbers: bool,
} }
impl ShOptPrompt { impl ShOptPrompt {
@@ -406,16 +399,23 @@ impl ShOptPrompt {
"leader" => { "leader" => {
self.leader = val.to_string(); self.leader = val.to_string();
} }
"line_numbers" => {
let Ok(val) = val.parse::<bool>() else {
return Err(ShErr::simple(
ShErrKind::SyntaxErr,
"shopt: expected 'true' or 'false' for line_numbers value",
));
};
self.line_numbers = val;
}
"custom" => { "custom" => {
todo!() todo!()
} }
_ => { _ => {
return Err( return Err(ShErr::simple(
ShErr::simple( ShErrKind::SyntaxErr,
ShErrKind::SyntaxErr, format!("shopt: Unexpected 'prompt' option '{opt}'"),
format!("shopt: Unexpected 'prompt' option '{opt}'"), ));
)
);
} }
} }
Ok(()) Ok(())
@@ -464,17 +464,19 @@ impl ShOptPrompt {
Ok(Some(output)) Ok(Some(output))
} }
"leader" => { "leader" => {
let mut output = let mut output = String::from("The leader key sequence used in keymap bindings\n");
String::from("The leader key sequence used in keymap bindings\n");
output.push_str(&self.leader); output.push_str(&self.leader);
Ok(Some(output)) Ok(Some(output))
} }
_ => Err( "line_numbers" => {
ShErr::simple( let mut output = String::from("Whether to display line numbers in multiline input\n");
ShErrKind::SyntaxErr, output.push_str(&format!("{}", self.line_numbers));
format!("shopt: Unexpected 'prompt' option '{query}'"), Ok(Some(output))
) }
), _ => Err(ShErr::simple(
ShErrKind::SyntaxErr,
format!("shopt: Unexpected 'prompt' option '{query}'"),
)),
} }
} }
} }
@@ -493,6 +495,7 @@ impl Display for ShOptPrompt {
self.linebreak_on_incomplete self.linebreak_on_incomplete
)); ));
output.push(format!("leader = {}", self.leader)); output.push(format!("leader = {}", self.leader));
output.push(format!("line_numbers = {}", self.line_numbers));
let final_output = output.join("\n"); let final_output = output.join("\n");
@@ -510,6 +513,7 @@ impl Default for ShOptPrompt {
auto_indent: true, auto_indent: true,
linebreak_on_incomplete: true, linebreak_on_incomplete: true,
leader: "\\".to_string(), leader: "\\".to_string(),
line_numbers: true,
} }
} }
} }

View File

@@ -148,11 +148,10 @@ pub fn sig_setup(is_login: bool) {
sigaction(Signal::SIGSYS, &action).unwrap(); sigaction(Signal::SIGSYS, &action).unwrap();
} }
if is_login {
if is_login { let _ = setpgid(Pid::from_raw(0), Pid::from_raw(0));
let _ = setpgid(Pid::from_raw(0), Pid::from_raw(0)); take_term().ok();
take_term().ok(); }
}
} }
/// Reset all signal dispositions to SIG_DFL. /// Reset all signal dispositions to SIG_DFL.
@@ -307,29 +306,30 @@ pub fn child_exited(pid: Pid, status: WtStat) -> ShResult<()> {
{ {
if is_fg { if is_fg {
take_term()?; take_term()?;
} else { } else {
JOB_DONE.store(true, Ordering::SeqCst); JOB_DONE.store(true, Ordering::SeqCst);
let job_order = read_jobs(|j| j.order().to_vec()); let job_order = read_jobs(|j| j.order().to_vec());
let result = read_jobs(|j| j.query(JobID::Pgid(pgid)).cloned()); let result = read_jobs(|j| j.query(JobID::Pgid(pgid)).cloned());
if let Some(job) = result { if let Some(job) = result {
let job_complete_msg = job.display(&job_order, JobCmdFlags::PIDS).to_string(); let job_complete_msg = job.display(&job_order, JobCmdFlags::PIDS).to_string();
let post_job_hooks = read_logic(|l| l.get_autocmds(AutoCmdKind::OnJobFinish)); let post_job_hooks = read_logic(|l| l.get_autocmds(AutoCmdKind::OnJobFinish));
for cmd in post_job_hooks { for cmd in post_job_hooks {
let AutoCmd { pattern, command } = cmd; let AutoCmd { pattern, command } = cmd;
if let Some(pat) = pattern if let Some(pat) = pattern
&& job.get_cmds().iter().all(|p| !pat.is_match(p)) { && job.get_cmds().iter().all(|p| !pat.is_match(p))
continue; {
} continue;
}
if let Err(e) = exec_input(command.clone(), None, false, Some("autocmd".into())) { if let Err(e) = exec_input(command.clone(), None, false, Some("autocmd".into())) {
e.print_error(); e.print_error();
} }
} }
write_meta(|m| m.post_system_message(job_complete_msg)) write_meta(|m| m.post_system_message(job_complete_msg))
} }
} }
} }
Ok(()) Ok(())
} }

View File

@@ -12,14 +12,30 @@ use nix::unistd::{User, gethostname, getppid};
use regex::Regex; use regex::Regex;
use crate::{ use crate::{
builtin::{BUILTINS, keymap::{KeyMap, KeyMapFlags, KeyMapMatch}, map::MapNode, trap::TrapTarget}, exec_input, expand::expand_keymap, jobs::JobTab, libsh::{ builtin::{
error::{ShErr, ShErrKind, ShResult}, utils::VecDequeExt BUILTINS,
}, parse::{ keymap::{KeyMap, KeyMapFlags, KeyMapMatch},
map::MapNode,
trap::TrapTarget,
},
exec_input,
expand::expand_keymap,
jobs::JobTab,
libsh::{
error::{ShErr, ShErrKind, ShResult},
utils::VecDequeExt,
},
parse::{
ConjunctNode, NdRule, Node, ParsedSrc, ConjunctNode, NdRule, Node, ParsedSrc,
lex::{LexFlags, LexStream, Span, Tk}, lex::{LexFlags, LexStream, Span, Tk},
}, prelude::*, readline::{ },
complete::{BashCompSpec, CompSpec}, keys::KeyEvent, markers prelude::*,
}, shopt::ShOpts readline::{
complete::{BashCompSpec, CompSpec},
keys::KeyEvent,
markers,
},
shopt::ShOpts,
}; };
pub struct Shed { pub struct Shed {
@@ -71,18 +87,18 @@ impl ShellParam {
) )
} }
pub fn from_char(c: &char) -> Option<Self> { pub fn from_char(c: &char) -> Option<Self> {
match c { match c {
'?' => Some(Self::Status), '?' => Some(Self::Status),
'$' => Some(Self::ShPid), '$' => Some(Self::ShPid),
'!' => Some(Self::LastJob), '!' => Some(Self::LastJob),
'0' => Some(Self::ShellName), '0' => Some(Self::ShellName),
'@' => Some(Self::AllArgs), '@' => Some(Self::AllArgs),
'*' => Some(Self::AllArgsStr), '*' => Some(Self::AllArgsStr),
'#' => Some(Self::ArgCount), '#' => Some(Self::ArgCount),
_ => None, _ => None,
} }
} }
} }
impl Display for ShellParam { impl Display for ShellParam {
@@ -299,10 +315,12 @@ impl ScopeStack {
{ {
match var.kind_mut() { match var.kind_mut() {
VarKind::Arr(items) => return Ok(items), VarKind::Arr(items) => return Ok(items),
_ => return Err(ShErr::simple( _ => {
ShErrKind::ExecFail, return Err(ShErr::simple(
format!("Variable '{}' is not an array", var_name), ShErrKind::ExecFail,
)), format!("Variable '{}' is not an array", var_name),
));
}
} }
} }
} }
@@ -358,38 +376,37 @@ impl ScopeStack {
} }
Ok("".into()) Ok("".into())
} }
pub fn remove_map(&mut self, map_name: &str) -> Option<MapNode> { pub fn remove_map(&mut self, map_name: &str) -> Option<MapNode> {
for scope in self.scopes.iter_mut().rev() { for scope in self.scopes.iter_mut().rev() {
if scope.get_map(map_name).is_some() { if scope.get_map(map_name).is_some() {
return scope.remove_map(map_name); return scope.remove_map(map_name);
} }
} }
None None
} }
pub fn get_map(&self, map_name: &str) -> Option<&MapNode> { pub fn get_map(&self, map_name: &str) -> Option<&MapNode> {
for scope in self.scopes.iter().rev() { for scope in self.scopes.iter().rev() {
if let Some(map) = scope.get_map(map_name) { if let Some(map) = scope.get_map(map_name) {
return Some(map) return Some(map);
} }
} }
None None
} }
pub fn get_map_mut(&mut self, map_name: &str) -> Option<&mut MapNode> { pub fn get_map_mut(&mut self, map_name: &str) -> Option<&mut MapNode> {
for scope in self.scopes.iter_mut().rev() { for scope in self.scopes.iter_mut().rev() {
if let Some(map) = scope.get_map_mut(map_name) { if let Some(map) = scope.get_map_mut(map_name) {
return Some(map) return Some(map);
} }
} }
None None
} }
pub fn set_map(&mut self, map_name: &str, map: MapNode, local: bool) { pub fn set_map(&mut self, map_name: &str, map: MapNode, local: bool) {
if local if local && let Some(scope) = self.scopes.last_mut() {
&& let Some(scope) = self.scopes.last_mut() { scope.set_map(map_name, map);
scope.set_map(map_name, map); } else if let Some(scope) = self.scopes.first_mut() {
} else if let Some(scope) = self.scopes.first_mut() { scope.set_map(map_name, map);
scope.set_map(map_name, map); }
} }
}
pub fn try_get_var(&self, var_name: &str) -> Option<String> { pub fn try_get_var(&self, var_name: &str) -> Option<String> {
// This version of get_var() is mainly used internally // This version of get_var() is mainly used internally
// so that we have access to Option methods // so that we have access to Option methods
@@ -410,11 +427,11 @@ impl ScopeStack {
None None
} }
pub fn take_var(&mut self, var_name: &str) -> String { pub fn take_var(&mut self, var_name: &str) -> String {
let var = self.get_var(var_name); let var = self.get_var(var_name);
self.unset_var(var_name).ok(); self.unset_var(var_name).ok();
var var
} }
pub fn get_var(&self, var_name: &str) -> String { pub fn get_var(&self, var_name: &str) -> String {
if let Ok(param) = var_name.parse::<ShellParam>() { if let Ok(param) = var_name.parse::<ShellParam>() {
return self.get_param(param); return self.get_param(param);
@@ -480,14 +497,14 @@ thread_local! {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ShAlias { pub struct ShAlias {
pub body: String, pub body: String,
pub source: Span pub source: Span,
} }
impl Display for ShAlias { impl Display for ShAlias {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.body) write!(f, "{}", self.body)
} }
} }
/// A shell function /// A shell function
@@ -495,14 +512,14 @@ impl Display for ShAlias {
/// Wraps the BraceGrp Node that forms the body of the function, and provides some helper methods to extract it from the parse tree /// Wraps the BraceGrp Node that forms the body of the function, and provides some helper methods to extract it from the parse tree
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ShFunc { pub struct ShFunc {
pub body: Node, pub body: Node,
pub source: Span pub source: Span,
} }
impl ShFunc { impl ShFunc {
pub fn new(mut src: ParsedSrc, source: Span) -> Self { pub fn new(mut src: ParsedSrc, source: Span) -> Self {
let body = Self::extract_brc_grp_hack(src.extract_nodes()); let body = Self::extract_brc_grp_hack(src.extract_nodes());
Self{ body, source } Self { body, source }
} }
fn extract_brc_grp_hack(mut tree: Vec<Node>) -> Node { fn extract_brc_grp_hack(mut tree: Vec<Node>) -> Node {
// FIXME: find a better way to do this // FIXME: find a better way to do this
@@ -524,61 +541,61 @@ impl ShFunc {
#[derive(Clone, Debug, Eq, PartialEq, Hash)] #[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub enum AutoCmdKind { pub enum AutoCmdKind {
PreCmd, PreCmd,
PostCmd, PostCmd,
PreChangeDir, PreChangeDir,
PostChangeDir, PostChangeDir,
OnJobFinish, OnJobFinish,
PrePrompt, PrePrompt,
PostPrompt, PostPrompt,
PreModeChange, PreModeChange,
PostModeChange, PostModeChange,
OnExit OnExit,
} }
impl Display for AutoCmdKind { impl Display for AutoCmdKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Self::PreCmd => write!(f, "pre-cmd"), Self::PreCmd => write!(f, "pre-cmd"),
Self::PostCmd => write!(f, "post-cmd"), Self::PostCmd => write!(f, "post-cmd"),
Self::PreChangeDir => write!(f, "pre-change-dir"), Self::PreChangeDir => write!(f, "pre-change-dir"),
Self::PostChangeDir => write!(f, "post-change-dir"), Self::PostChangeDir => write!(f, "post-change-dir"),
Self::OnJobFinish => write!(f, "on-job-finish"), Self::OnJobFinish => write!(f, "on-job-finish"),
Self::PrePrompt => write!(f, "pre-prompt"), Self::PrePrompt => write!(f, "pre-prompt"),
Self::PostPrompt => write!(f, "post-prompt"), Self::PostPrompt => write!(f, "post-prompt"),
Self::PreModeChange => write!(f, "pre-mode-change"), Self::PreModeChange => write!(f, "pre-mode-change"),
Self::PostModeChange => write!(f, "post-mode-change"), Self::PostModeChange => write!(f, "post-mode-change"),
Self::OnExit => write!(f, "on-exit"), Self::OnExit => write!(f, "on-exit"),
} }
} }
} }
impl FromStr for AutoCmdKind { impl FromStr for AutoCmdKind {
type Err = ShErr; type Err = ShErr;
fn from_str(s: &str) -> Result<Self, Self::Err> { fn from_str(s: &str) -> Result<Self, Self::Err> {
match s { match s {
"pre-cmd" => Ok(Self::PreCmd), "pre-cmd" => Ok(Self::PreCmd),
"post-cmd" => Ok(Self::PostCmd), "post-cmd" => Ok(Self::PostCmd),
"pre-change-dir" => Ok(Self::PreChangeDir), "pre-change-dir" => Ok(Self::PreChangeDir),
"post-change-dir" => Ok(Self::PostChangeDir), "post-change-dir" => Ok(Self::PostChangeDir),
"on-job-finish" => Ok(Self::OnJobFinish), "on-job-finish" => Ok(Self::OnJobFinish),
"pre-prompt" => Ok(Self::PrePrompt), "pre-prompt" => Ok(Self::PrePrompt),
"post-prompt" => Ok(Self::PostPrompt), "post-prompt" => Ok(Self::PostPrompt),
"pre-mode-change" => Ok(Self::PreModeChange), "pre-mode-change" => Ok(Self::PreModeChange),
"post-mode-change" => Ok(Self::PostModeChange), "post-mode-change" => Ok(Self::PostModeChange),
"on-exit" => Ok(Self::OnExit), "on-exit" => Ok(Self::OnExit),
_ => Err(ShErr::simple( _ => Err(ShErr::simple(
ShErrKind::ParseErr, ShErrKind::ParseErr,
format!("Invalid autocmd kind: {}", s), format!("Invalid autocmd kind: {}", s),
)), )),
} }
} }
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct AutoCmd { pub struct AutoCmd {
pub pattern: Option<Regex>, pub pattern: Option<Regex>,
pub command: String, pub command: String,
} }
/// The logic table for the shell /// The logic table for the shell
@@ -589,58 +606,59 @@ pub struct LogTab {
functions: HashMap<String, ShFunc>, functions: HashMap<String, ShFunc>,
aliases: HashMap<String, ShAlias>, aliases: HashMap<String, ShAlias>,
traps: HashMap<TrapTarget, String>, traps: HashMap<TrapTarget, String>,
keymaps: Vec<KeyMap>, keymaps: Vec<KeyMap>,
autocmds: HashMap<AutoCmdKind, Vec<AutoCmd>> autocmds: HashMap<AutoCmdKind, Vec<AutoCmd>>,
} }
impl LogTab { impl LogTab {
pub fn new() -> Self { pub fn new() -> Self {
Self::default() Self::default()
} }
pub fn autocmds(&self) -> &HashMap<AutoCmdKind, Vec<AutoCmd>> { pub fn autocmds(&self) -> &HashMap<AutoCmdKind, Vec<AutoCmd>> {
&self.autocmds &self.autocmds
} }
pub fn autocmds_mut(&mut self) -> &mut HashMap<AutoCmdKind, Vec<AutoCmd>> { pub fn autocmds_mut(&mut self) -> &mut HashMap<AutoCmdKind, Vec<AutoCmd>> {
&mut self.autocmds &mut self.autocmds
} }
pub fn insert_autocmd(&mut self, kind: AutoCmdKind, cmd: AutoCmd) { pub fn insert_autocmd(&mut self, kind: AutoCmdKind, cmd: AutoCmd) {
self.autocmds.entry(kind).or_default().push(cmd); self.autocmds.entry(kind).or_default().push(cmd);
} }
pub fn get_autocmds(&self, kind: AutoCmdKind) -> Vec<AutoCmd> { pub fn get_autocmds(&self, kind: AutoCmdKind) -> Vec<AutoCmd> {
self.autocmds.get(&kind).cloned().unwrap_or_default() self.autocmds.get(&kind).cloned().unwrap_or_default()
} }
pub fn clear_autocmds(&mut self, kind: AutoCmdKind) { pub fn clear_autocmds(&mut self, kind: AutoCmdKind) {
self.autocmds.remove(&kind); self.autocmds.remove(&kind);
} }
pub fn keymaps(&self) -> &Vec<KeyMap> { pub fn keymaps(&self) -> &Vec<KeyMap> {
&self.keymaps &self.keymaps
} }
pub fn keymaps_mut(&mut self) -> &mut Vec<KeyMap> { pub fn keymaps_mut(&mut self) -> &mut Vec<KeyMap> {
&mut self.keymaps &mut self.keymaps
} }
pub fn insert_keymap(&mut self, keymap: KeyMap) { pub fn insert_keymap(&mut self, keymap: KeyMap) {
let mut found_dup = false; let mut found_dup = false;
for map in self.keymaps.iter_mut() { for map in self.keymaps.iter_mut() {
if map.keys == keymap.keys { if map.keys == keymap.keys {
*map = keymap.clone(); *map = keymap.clone();
found_dup = true; found_dup = true;
break; break;
} }
} }
if !found_dup { if !found_dup {
self.keymaps.push(keymap); self.keymaps.push(keymap);
} }
} }
pub fn remove_keymap(&mut self, keys: &str) { pub fn remove_keymap(&mut self, keys: &str) {
self.keymaps.retain(|km| km.keys != keys); self.keymaps.retain(|km| km.keys != keys);
} }
pub fn keymaps_filtered(&self, flags: KeyMapFlags, pending: &[KeyEvent]) -> Vec<KeyMap> { pub fn keymaps_filtered(&self, flags: KeyMapFlags, pending: &[KeyEvent]) -> Vec<KeyMap> {
self.keymaps self
.iter() .keymaps
.filter(|km| km.flags.intersects(flags) && km.compare(pending) != KeyMapMatch::NoMatch) .iter()
.cloned() .filter(|km| km.flags.intersects(flags) && km.compare(pending) != KeyMapMatch::NoMatch)
.collect() .cloned()
} .collect()
}
pub fn insert_func(&mut self, name: &str, src: ShFunc) { pub fn insert_func(&mut self, name: &str, src: ShFunc) {
self.functions.insert(name.into(), src); self.functions.insert(name.into(), src);
} }
@@ -666,7 +684,13 @@ impl LogTab {
&self.aliases &self.aliases
} }
pub fn insert_alias(&mut self, name: &str, body: &str, source: Span) { pub fn insert_alias(&mut self, name: &str, body: &str, source: Span) {
self.aliases.insert(name.into(), ShAlias { body: body.into(), source }); self.aliases.insert(
name.into(),
ShAlias {
body: body.into(),
source,
},
);
} }
pub fn get_alias(&self, name: &str) -> Option<ShAlias> { pub fn get_alias(&self, name: &str) -> Option<ShAlias> {
self.aliases.get(name).cloned() self.aliases.get(name).cloned()
@@ -751,7 +775,7 @@ impl VarFlags {
pub enum ArrIndex { pub enum ArrIndex {
Literal(usize), Literal(usize),
FromBack(usize), FromBack(usize),
ArgCount, ArgCount,
AllJoined, AllJoined,
AllSplit, AllSplit,
} }
@@ -762,7 +786,7 @@ impl FromStr for ArrIndex {
match s { match s {
"@" => Ok(Self::AllSplit), "@" => Ok(Self::AllSplit),
"*" => Ok(Self::AllJoined), "*" => Ok(Self::AllJoined),
"#" => Ok(Self::ArgCount), "#" => Ok(Self::ArgCount),
_ if s.starts_with('-') && s[1..].chars().all(|c| c.is_digit(1)) => { _ if s.starts_with('-') && s[1..].chars().all(|c| c.is_digit(1)) => {
let idx = s[1..].parse::<usize>().unwrap(); let idx = s[1..].parse::<usize>().unwrap();
Ok(Self::FromBack(idx)) Ok(Self::FromBack(idx))
@@ -879,15 +903,15 @@ impl Display for Var {
} }
impl From<String> for Var { impl From<String> for Var {
fn from(value: String) -> Self { fn from(value: String) -> Self {
Self::new(VarKind::Str(value), VarFlags::NONE) Self::new(VarKind::Str(value), VarFlags::NONE)
} }
} }
impl From<&str> for Var { impl From<&str> for Var {
fn from(value: &str) -> Self { fn from(value: &str) -> Self {
Self::new(VarKind::Str(value.into()), VarFlags::NONE) Self::new(VarKind::Str(value.into()), VarFlags::NONE)
} }
} }
#[derive(Default, Clone, Debug)] #[derive(Default, Clone, Debug)]
@@ -896,7 +920,7 @@ pub struct VarTab {
params: HashMap<ShellParam, String>, params: HashMap<ShellParam, String>,
sh_argv: VecDeque<String>, /* Using a VecDeque makes the implementation of `shift` straightforward */ sh_argv: VecDeque<String>, /* Using a VecDeque makes the implementation of `shift` straightforward */
maps: HashMap<String, MapNode> maps: HashMap<String, MapNode>,
} }
impl VarTab { impl VarTab {
@@ -908,7 +932,7 @@ impl VarTab {
vars, vars,
params, params,
sh_argv: VecDeque::new(), sh_argv: VecDeque::new(),
maps: HashMap::new(), maps: HashMap::new(),
}; };
var_tab.init_sh_argv(); var_tab.init_sh_argv();
var_tab var_tab
@@ -1027,24 +1051,24 @@ impl VarTab {
self.update_arg_params(); self.update_arg_params();
arg arg
} }
pub fn set_map(&mut self, map_name: &str, map: MapNode) { pub fn set_map(&mut self, map_name: &str, map: MapNode) {
self.maps.insert(map_name.to_string(), map); self.maps.insert(map_name.to_string(), map);
} }
pub fn remove_map(&mut self, map_name: &str) -> Option<MapNode> { pub fn remove_map(&mut self, map_name: &str) -> Option<MapNode> {
self.maps.remove(map_name) self.maps.remove(map_name)
} }
pub fn get_map(&self, map_name: &str) -> Option<&MapNode> { pub fn get_map(&self, map_name: &str) -> Option<&MapNode> {
self.maps.get(map_name) self.maps.get(map_name)
} }
pub fn get_map_mut(&mut self, map_name: &str) -> Option<&mut MapNode> { pub fn get_map_mut(&mut self, map_name: &str) -> Option<&mut MapNode> {
self.maps.get_mut(map_name) self.maps.get_mut(map_name)
} }
pub fn maps(&self) -> &HashMap<String, MapNode> { pub fn maps(&self) -> &HashMap<String, MapNode> {
&self.maps &self.maps
} }
pub fn maps_mut(&mut self) -> &mut HashMap<String, MapNode> { pub fn maps_mut(&mut self) -> &mut HashMap<String, MapNode> {
&mut self.maps &mut self.maps
} }
pub fn vars(&self) -> &HashMap<String, Var> { pub fn vars(&self) -> &HashMap<String, Var> {
&self.vars &self.vars
} }
@@ -1160,9 +1184,9 @@ impl VarTab {
} }
Ok(()) Ok(())
} }
pub fn map_exists(&self, map_name: &str) -> bool { pub fn map_exists(&self, map_name: &str) -> bool {
self.maps.contains_key(map_name) self.maps.contains_key(map_name)
} }
pub fn var_exists(&self, var_name: &str) -> bool { pub fn var_exists(&self, var_name: &str) -> bool {
if let Ok(param) = var_name.parse::<ShellParam>() { if let Ok(param) = var_name.parse::<ShellParam>() {
return self.params.contains_key(&param); return self.params.contains_key(&param);
@@ -1205,8 +1229,8 @@ pub struct MetaTab {
// pushd/popd stack // pushd/popd stack
dir_stack: VecDeque<PathBuf>, dir_stack: VecDeque<PathBuf>,
// getopts char offset for opts like -abc // getopts char offset for opts like -abc
getopts_offset: usize, getopts_offset: usize,
old_path: Option<String>, old_path: Option<String>,
old_pwd: Option<String>, old_pwd: Option<String>,
@@ -1216,8 +1240,8 @@ pub struct MetaTab {
// programmable completion specs // programmable completion specs
comp_specs: HashMap<String, Box<dyn CompSpec>>, comp_specs: HashMap<String, Box<dyn CompSpec>>,
// pending keys from widget function // pending keys from widget function
pending_widget_keys: Vec<KeyEvent> pending_widget_keys: Vec<KeyEvent>,
} }
impl MetaTab { impl MetaTab {
@@ -1227,28 +1251,28 @@ impl MetaTab {
..Default::default() ..Default::default()
} }
} }
pub fn set_pending_widget_keys(&mut self, keys: &str) { pub fn set_pending_widget_keys(&mut self, keys: &str) {
let exp = expand_keymap(keys); let exp = expand_keymap(keys);
self.pending_widget_keys = exp; self.pending_widget_keys = exp;
} }
pub fn take_pending_widget_keys(&mut self) -> Option<Vec<KeyEvent>> { pub fn take_pending_widget_keys(&mut self) -> Option<Vec<KeyEvent>> {
if self.pending_widget_keys.is_empty() { if self.pending_widget_keys.is_empty() {
None None
} else { } else {
Some(std::mem::take(&mut self.pending_widget_keys)) Some(std::mem::take(&mut self.pending_widget_keys))
} }
} }
pub fn getopts_char_offset(&self) -> usize { pub fn getopts_char_offset(&self) -> usize {
self.getopts_offset self.getopts_offset
} }
pub fn inc_getopts_char_offset(&mut self) -> usize { pub fn inc_getopts_char_offset(&mut self) -> usize {
let offset = self.getopts_offset; let offset = self.getopts_offset;
self.getopts_offset += 1; self.getopts_offset += 1;
offset offset
} }
pub fn reset_getopts_char_offset(&mut self) { pub fn reset_getopts_char_offset(&mut self) {
self.getopts_offset = 0; self.getopts_offset = 0;
} }
pub fn get_builtin_comp_specs() -> HashMap<String, Box<dyn CompSpec>> { pub fn get_builtin_comp_specs() -> HashMap<String, Box<dyn CompSpec>> {
let mut map = HashMap::new(); let mut map = HashMap::new();
@@ -1280,14 +1304,14 @@ impl MetaTab {
"disown".into(), "disown".into(),
Box::new(BashCompSpec::new().jobs(true)) as Box<dyn CompSpec>, Box::new(BashCompSpec::new().jobs(true)) as Box<dyn CompSpec>,
); );
map.insert( map.insert(
"alias".into(), "alias".into(),
Box::new(BashCompSpec::new().aliases(true)) as Box<dyn CompSpec>, Box::new(BashCompSpec::new().aliases(true)) as Box<dyn CompSpec>,
); );
map.insert( map.insert(
"trap".into(), "trap".into(),
Box::new(BashCompSpec::new().signals(true)) as Box<dyn CompSpec>, Box::new(BashCompSpec::new().signals(true)) as Box<dyn CompSpec>,
); );
map map
} }
@@ -1312,29 +1336,29 @@ impl MetaTab {
pub fn remove_comp_spec(&mut self, cmd: &str) -> bool { pub fn remove_comp_spec(&mut self, cmd: &str) -> bool {
self.comp_specs.remove(cmd).is_some() self.comp_specs.remove(cmd).is_some()
} }
pub fn get_cmds_in_path() -> Vec<String> { pub fn get_cmds_in_path() -> Vec<String> {
let path = env::var("PATH").unwrap_or_default(); let path = env::var("PATH").unwrap_or_default();
let paths = path.split(":").map(PathBuf::from); let paths = path.split(":").map(PathBuf::from);
let mut cmds = vec![]; let mut cmds = vec![];
for path in paths { for path in paths {
if let Ok(entries) = path.read_dir() { if let Ok(entries) = path.read_dir() {
for entry in entries.flatten() { for entry in entries.flatten() {
let Ok(meta) = std::fs::metadata(entry.path()) else { let Ok(meta) = std::fs::metadata(entry.path()) else {
continue; continue;
}; };
let is_exec = meta.permissions().mode() & 0o111 != 0; let is_exec = meta.permissions().mode() & 0o111 != 0;
if meta.is_file() if meta.is_file()
&& is_exec && is_exec
&& let Some(name) = entry.file_name().to_str() && let Some(name) = entry.file_name().to_str()
{ {
cmds.push(name.to_string()); cmds.push(name.to_string());
} }
} }
} }
} }
cmds cmds
} }
pub fn try_rehash_commands(&mut self) { pub fn try_rehash_commands(&mut self) {
let path = env::var("PATH").unwrap_or_default(); let path = env::var("PATH").unwrap_or_default();
let cwd = env::var("PWD").unwrap_or_default(); let cwd = env::var("PWD").unwrap_or_default();
@@ -1350,10 +1374,10 @@ impl MetaTab {
self.path_cache.clear(); self.path_cache.clear();
self.old_path = Some(path.clone()); self.old_path = Some(path.clone());
self.old_pwd = Some(cwd.clone()); self.old_pwd = Some(cwd.clone());
let cmds_in_path = Self::get_cmds_in_path(); let cmds_in_path = Self::get_cmds_in_path();
for cmd in cmds_in_path { for cmd in cmds_in_path {
self.path_cache.insert(cmd); self.path_cache.insert(cmd);
} }
if let Ok(entries) = Path::new(&cwd).read_dir() { if let Ok(entries) = Path::new(&cwd).read_dir() {
for entry in entries.flatten() { for entry in entries.flatten() {
let Ok(meta) = std::fs::metadata(entry.path()) else { let Ok(meta) = std::fs::metadata(entry.path()) else {
@@ -1589,63 +1613,86 @@ pub fn get_shopt(path: &str) -> String {
read_shopts(|s| s.get(path)).unwrap().unwrap() read_shopts(|s| s.get(path)).unwrap().unwrap()
} }
pub fn with_vars<F,H,V,T>(vars: H, f: F) -> T pub fn with_vars<F, H, V, T>(vars: H, f: F) -> T
where where
F: FnOnce() -> T, F: FnOnce() -> T,
H: Into<HashMap<String,V>>, H: Into<HashMap<String, V>>,
V: Into<Var> { V: Into<Var>,
{
let snapshot = read_vars(|v| v.clone()); let snapshot = read_vars(|v| v.clone());
let vars = vars.into(); let vars = vars.into();
for (name, val) in vars { for (name, val) in vars {
let val = val.into(); let val = val.into();
write_vars(|v| v.set_var(&name, val.kind, val.flags).unwrap()); write_vars(|v| v.set_var(&name, val.kind, val.flags).unwrap());
} }
let _guard = scopeguard::guard(snapshot, |snap| { let _guard = scopeguard::guard(snapshot, |snap| {
write_vars(|v| *v = snap); write_vars(|v| *v = snap);
}); });
f() f()
} }
pub fn change_dir<P: AsRef<Path>>(dir: P) -> ShResult<()> { pub fn change_dir<P: AsRef<Path>>(dir: P) -> ShResult<()> {
let dir = dir.as_ref(); let dir = dir.as_ref();
let dir_raw = &dir.display().to_string(); let dir_raw = &dir.display().to_string();
let pre_cd = read_logic(|l| l.get_autocmds(AutoCmdKind::PreChangeDir)); let pre_cd = read_logic(|l| l.get_autocmds(AutoCmdKind::PreChangeDir));
let post_cd = read_logic(|l| l.get_autocmds(AutoCmdKind::PostChangeDir)); let post_cd = read_logic(|l| l.get_autocmds(AutoCmdKind::PostChangeDir));
let current_dir = env::current_dir()?.display().to_string(); let current_dir = env::current_dir()?.display().to_string();
with_vars([("_NEW_DIR".into(), dir_raw.as_str()), ("_OLD_DIR".into(), current_dir.as_str())], || { with_vars(
for cmd in pre_cd { [
let AutoCmd { command, pattern } = cmd; ("_NEW_DIR".into(), dir_raw.as_str()),
if let Some(pat) = pattern ("_OLD_DIR".into(), current_dir.as_str()),
&& !pat.is_match(dir_raw) { ],
continue; || {
} for cmd in pre_cd {
let AutoCmd { command, pattern } = cmd;
if let Some(pat) = pattern
&& !pat.is_match(dir_raw)
{
continue;
}
if let Err(e) = exec_input(command.clone(), None, false, Some("autocmd (pre-changedir)".to_string())) { if let Err(e) = exec_input(
e.print_error(); command.clone(),
}; None,
} false,
}); Some("autocmd (pre-changedir)".to_string()),
) {
e.print_error();
};
}
},
);
env::set_current_dir(dir)?;
env::set_current_dir(dir)?; with_vars(
[
("_NEW_DIR".into(), dir_raw.as_str()),
("_OLD_DIR".into(), current_dir.as_str()),
],
|| {
for cmd in post_cd {
let AutoCmd { command, pattern } = cmd;
if let Some(pat) = pattern
&& !pat.is_match(dir_raw)
{
continue;
}
with_vars([("_NEW_DIR".into(), dir_raw.as_str()), ("_OLD_DIR".into(), current_dir.as_str())], || { if let Err(e) = exec_input(
for cmd in post_cd { command.clone(),
let AutoCmd { command, pattern } = cmd; None,
if let Some(pat) = pattern false,
&& !pat.is_match(dir_raw) { Some("autocmd (post-changedir)".to_string()),
continue; ) {
} e.print_error();
};
}
},
);
if let Err(e) = exec_input(command.clone(), None, false, Some("autocmd (post-changedir)".to_string())) { Ok(())
e.print_error();
};
}
});
Ok(())
} }
pub fn get_status() -> i32 { pub fn get_status() -> i32 {
@@ -1671,7 +1718,7 @@ pub fn source_rc() -> ShResult<()> {
} }
pub fn source_file(path: PathBuf) -> ShResult<()> { pub fn source_file(path: PathBuf) -> ShResult<()> {
let source_name = path.to_string_lossy().to_string(); let source_name = path.to_string_lossy().to_string();
let mut file = OpenOptions::new().read(true).open(path)?; let mut file = OpenOptions::new().read(true).open(path)?;
let mut buf = String::new(); let mut buf = String::new();