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)?;
if !argv.is_empty() { argv.remove(0); }
if !argv.is_empty() {
argv.remove(0);
}
if argv.is_empty() {
// Display the environment variables
@@ -37,11 +39,19 @@ pub fn alias(node: Node) -> ShResult<()> {
} else {
for (arg, span) in argv {
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 {
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()));
}
@@ -60,7 +70,9 @@ pub fn unalias(node: Node) -> ShResult<()> {
};
let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); }
if !argv.is_empty() {
argv.remove(0);
}
if argv.is_empty() {
// Display the environment variables
@@ -79,7 +91,11 @@ pub fn unalias(node: Node) -> ShResult<()> {
} else {
for (arg, span) in argv {
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))
}

View File

@@ -1,23 +1,28 @@
use std::collections::VecDeque;
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> {
vec![
OptSpec {
opt: Opt::Short('c'),
takes_arg: true
takes_arg: true,
},
OptSpec {
opt: Opt::Short('r'),
takes_arg: false
takes_arg: false,
},
OptSpec {
opt: Opt::Short('v'),
takes_arg: true
}
takes_arg: true,
},
]
}
@@ -38,7 +43,10 @@ impl Default for ArrOpOpts {
}
#[derive(Clone, Copy)]
enum End { Front, Back }
enum End {
Front,
Back,
}
fn arr_pop_inner(node: Node, end: End) -> ShResult<()> {
let NdRule::Command {
@@ -52,7 +60,9 @@ fn arr_pop_inner(node: Node, end: End) -> ShResult<()> {
let (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?;
let arr_op_opts = get_arr_op_opts(opts)?;
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 mut status = 0;
@@ -94,11 +104,17 @@ fn arr_push_inner(node: Node, end: End) -> ShResult<()> {
let (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?;
let _arr_op_opts = get_arr_op_opts(opts)?;
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 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 {
@@ -111,9 +127,14 @@ fn arr_push_inner(node: Node, end: End) -> ShResult<()> {
}
Ok(())
} else {
v.set_var(&name, VarKind::Arr(VecDeque::from([push_val])), VarFlags::NONE)
v.set_var(
&name,
VarKind::Arr(VecDeque::from([push_val])),
VarFlags::NONE,
)
}
}).blame(span)?;
})
.blame(span)?;
}
state::set_status(0);
@@ -148,7 +169,9 @@ pub fn arr_rotate(node: Node) -> ShResult<()> {
let (argv, opts) = get_opts_from_tokens(argv, &arr_op_optspec())?;
let arr_op_opts = get_arr_op_opts(opts)?;
let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); }
if !argv.is_empty() {
argv.remove(0);
}
for (arg, _) in argv {
write_vars(|v| -> ShResult<()> {
@@ -171,12 +194,15 @@ pub fn get_arr_op_opts(opts: Vec<Opt>) -> ShResult<ArrOpOpts> {
for opt in opts {
match opt {
Opt::ShortWithArg('c', count) => {
arr_op_opts.count = count.parse::<usize>().map_err(|_| {
ShErr::simple(ShErrKind::ParseErr, format!("invalid count: {}", count))
})?;
arr_op_opts.count = count
.parse::<usize>()
.map_err(|_| ShErr::simple(ShErrKind::ParseErr, format!("invalid count: {}", count)))?;
}
Opt::Short('c') => {
return Err(ShErr::simple(ShErrKind::ParseErr, "missing count for -c".to_string()));
return Err(ShErr::simple(
ShErrKind::ParseErr,
"missing count for -c".to_string(),
));
}
Opt::Short('r') => {
arr_op_opts.reverse = true;
@@ -185,10 +211,16 @@ pub fn get_arr_op_opts(opts: Vec<Opt>) -> ShResult<ArrOpOpts> {
arr_op_opts.var = Some(var);
}
Opt::Short('v') => {
return Err(ShErr::simple(ShErrKind::ParseErr, "missing variable name for -v".to_string()));
return Err(ShErr::simple(
ShErrKind::ParseErr,
"missing variable name for -v".to_string(),
));
}
_ => {
return Err(ShErr::simple(ShErrKind::ParseErr, format!("invalid option: {}", opt)));
return Err(ShErr::simple(
ShErrKind::ParseErr,
format!("invalid option: {}", opt),
));
}
}
}

View File

@@ -1,43 +1,51 @@
use regex::Regex;
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 {
pattern: Option<Regex>,
clear: bool
clear: bool,
}
fn autocmd_optspec() -> [OptSpec; 2] {
[
OptSpec {
opt: Opt::Short('p'),
takes_arg: true
takes_arg: true,
},
OptSpec {
opt: Opt::Short('c'),
takes_arg: false
}
takes_arg: false,
},
]
}
pub fn get_autocmd_opts(opts: &[Opt]) -> ShResult<AutoCmdOpts> {
let mut autocmd_opts = AutoCmdOpts {
pattern: None,
clear: false
clear: false,
};
let mut opts = opts.iter();
while let Some(arg) = opts.next() {
match 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;
}
_ => {
return Err(ShErr::simple(ShErrKind::ExecFail, format!("unexpected option: {}", arg)));
return Err(ShErr::simple(
ShErrKind::ExecFail,
format!("unexpected option: {}", arg),
));
}
}
}
@@ -58,15 +66,25 @@ pub fn autocmd(node: Node) -> ShResult<()> {
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 mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); }
if !argv.is_empty() {
argv.remove(0);
}
let mut args = argv.iter();
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 {
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 {
@@ -76,7 +94,11 @@ pub fn autocmd(node: Node) -> ShResult<()> {
}
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 {

View File

@@ -20,7 +20,9 @@ pub fn cd(node: Node) -> ShResult<()> {
let cd_span = argv.first().unwrap().span.clone();
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() {
(PathBuf::from(arg), Some(span))
@@ -29,28 +31,44 @@ pub fn cd(node: Node) -> ShResult<()> {
};
if !new_dir.exists() {
let mut err = ShErr::new(
ShErrKind::ExecFail,
span.clone(),
).labeled(cd_span.clone(), "Failed to change directory");
let mut err = ShErr::new(ShErrKind::ExecFail, span.clone())
.labeled(cd_span.clone(), "Failed to change directory");
if let Some(span) = arg_span {
err = err.labeled(span, format!("No such file or directory '{}'", new_dir.display().fg(next_color())));
err = err.labeled(
span,
format!(
"No such file or directory '{}'",
new_dir.display().fg(next_color())
),
);
}
return Err(err);
}
if !new_dir.is_dir() {
return Err(ShErr::new(ShErrKind::ExecFail, span.clone())
.labeled(cd_span.clone(), format!("cd: Not a directory '{}'", new_dir.display().fg(next_color()))));
return Err(ShErr::new(ShErrKind::ExecFail, span.clone()).labeled(
cd_span.clone(),
format!(
"cd: Not a directory '{}'",
new_dir.display().fg(next_color())
),
));
}
if let Err(e) = state::change_dir(new_dir) {
return Err(ShErr::new(ShErrKind::ExecFail, span.clone())
.labeled(cd_span.clone(), format!("cd: Failed to change directory: '{}'", e.fg(Color::Red))));
return Err(ShErr::new(ShErrKind::ExecFail, span.clone()).labeled(
cd_span.clone(),
format!("cd: Failed to change directory: '{}'", e.fg(Color::Red)),
));
}
let new_dir = env::current_dir().map_err(|e| {
ShErr::new(ShErrKind::ExecFail, span.clone())
.labeled(cd_span.clone(), format!("cd: Failed to get current directory: '{}'", e.fg(Color::Red)))
ShErr::new(ShErrKind::ExecFail, span.clone()).labeled(
cd_span.clone(),
format!(
"cd: Failed to get current directory: '{}'",
e.fg(Color::Red)
),
)
})?;
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 comp_opts = get_comp_opts(opts)?;
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 argv.is_empty() {
@@ -204,7 +206,11 @@ pub fn complete_builtin(node: Node) -> ShResult<()> {
if argv.is_empty() {
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);
@@ -279,7 +285,11 @@ pub fn get_comp_opts(opts: Vec<Opt>) -> ShResult<CompOpts> {
"space" => comp_opts.opt_flags |= CompOptFlags::SPACE,
_ => {
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),
));
}
},

View File

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

View File

@@ -1,6 +1,9 @@
use crate::{
libsh::error::ShResult,
parse::{NdRule, Node, execute::{exec_input, prepare_argv}},
parse::{
NdRule, Node,
execute::{exec_input, prepare_argv},
},
state,
};
@@ -14,7 +17,9 @@ pub fn eval(node: Node) -> ShResult<()> {
};
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() {
state::set_status(0);

View File

@@ -2,7 +2,10 @@ use nix::{errno::Errno, unistd::execvpe};
use crate::{
libsh::error::{ShErr, ShErrKind, ShResult},
parse::{NdRule, Node, execute::{ExecArgs, prepare_argv}},
parse::{
NdRule, Node,
execute::{ExecArgs, prepare_argv},
},
state,
};
@@ -16,7 +19,9 @@ pub fn exec_builtin(node: Node) -> ShResult<()> {
};
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() {
state::set_status(0);
@@ -35,7 +40,7 @@ pub fn exec_builtin(node: Node) -> ShResult<()> {
match e {
Errno::ENOENT => Err(
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}"))),
}

View File

@@ -21,7 +21,11 @@ pub fn flowctl(node: Node, kind: ShErrKind) -> ShResult<()> {
let (arg, span) = argv.into_iter().next().unwrap();
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;

View File

@@ -3,18 +3,21 @@ use std::str::FromStr;
use ariadne::Fmt;
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 {
NoMatch,
IsMatch,
WantsArg
WantsArg,
}
struct GetOptsSpec {
silent_err: bool,
opt_specs: Vec<OptSpec>
opt_specs: Vec<OptSpec>,
}
impl GetOptsSpec {
@@ -24,12 +27,12 @@ impl GetOptsSpec {
match opt {
Opt::Short(opt_ch) if ch == *opt_ch => {
if *takes_arg {
return OptMatch::WantsArg
return OptMatch::WantsArg;
} else {
return OptMatch::IsMatch
return OptMatch::IsMatch;
}
}
_ => { continue }
_ => continue,
}
}
OptMatch::NoMatch
@@ -59,29 +62,45 @@ impl FromStr for GetOptsSpec {
}
opt_specs.push(OptSpec { opt, takes_arg })
}
_ => return Err(ShErr::simple(
_ => {
return Err(ShErr::simple(
ShErrKind::ParseErr,
format!("unexpected character '{}'", ch.fg(next_color()))
)),
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<()> {
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(
opts_spec: &GetOptsSpec,
opt_var: &str,
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 {
state::set_status(1);
return Ok(())
return Ok(());
};
// "--" stops option processing
@@ -89,7 +108,7 @@ fn getopts_inner(opts_spec: &GetOptsSpec, opt_var: &str, argv: &[String], blame:
advance_optind(opt_index, 1)?;
write_meta(|m| m.reset_getopts_char_offset());
state::set_status(1);
return Ok(())
return Ok(());
}
// Not an option — done
@@ -139,8 +158,9 @@ fn getopts_inner(opts_spec: &GetOptsSpec, opt_var: &str, argv: &[String], blame:
ShErr::at(
ShErrKind::ExecFail,
blame.clone(),
format!("illegal option '-{}'", ch.fg(next_color()))
).print_error();
format!("illegal option '-{}'", ch.fg(next_color())),
)
.print_error();
}
state::set_status(0);
}
@@ -172,8 +192,9 @@ fn getopts_inner(opts_spec: &GetOptsSpec, opt_var: &str, argv: &[String], blame:
ShErr::at(
ShErrKind::ExecFail,
blame.clone(),
format!("option '-{}' requires an argument", ch.fg(next_color()))
).print_error();
format!("option '-{}' requires an argument", ch.fg(next_color())),
)
.print_error();
}
advance_optind(opt_index, 1)?;
state::set_status(0);
@@ -199,26 +220,27 @@ pub fn getopts(node: Node) -> ShResult<()> {
};
let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); }
if !argv.is_empty() {
argv.remove(0);
}
let mut args = argv.into_iter();
let Some(arg_string) = args.next() else {
return Err(ShErr::at(
ShErrKind::ExecFail,
span,
"getopts: missing option spec"
))
"getopts: missing option spec",
));
};
let Some(opt_var) = args.next() else {
return Err(ShErr::at(
ShErrKind::ExecFail,
span,
"getopts: missing variable name"
))
"getopts: missing variable name",
));
};
let opts_spec = GetOptsSpec::from_str(&arg_string.0)
.promote_err(arg_string.1.clone())?;
let opts_spec = GetOptsSpec::from_str(&arg_string.0).promote_err(arg_string.1.clone())?;
let explicit_args: Vec<String> = args.map(|s| s.0).collect();

View File

@@ -2,7 +2,12 @@ use std::{env, os::unix::fs::PermissionsExt, path::Path};
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<()> {
let NdRule::Command {
@@ -14,7 +19,9 @@ pub fn type_builtin(node: Node) -> ShResult<()> {
};
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
@@ -28,21 +35,27 @@ pub fn type_builtin(node: Node) -> ShResult<()> {
let ShFunc { body: _, source } = func;
let (line, col) = source.line_and_col();
let name = source.source().name();
println!("{arg} is a function defined at {name}:{}:{}", line + 1, col + 1);
println!(
"{arg} is a function defined at {name}:{}:{}",
line + 1,
col + 1
);
} else if let Some(alias) = read_logic(|v| v.get_alias(&arg)) {
let ShAlias { body, source } = alias;
let (line, col) = source.line_and_col();
let name = source.source().name();
println!("{arg} is an alias for '{body}' defined at {name}:{}:{}", line + 1, col + 1);
println!(
"{arg} is an alias for '{body}' defined at {name}:{}:{}",
line + 1,
col + 1
);
} 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<_>>();
let paths = path.split(':').map(Path::new).collect::<Vec<_>>();
for path in paths {
if let Ok(entries) = path.read_dir() {
@@ -55,7 +68,8 @@ pub fn type_builtin(node: Node) -> ShResult<()> {
if meta.is_file()
&& is_exec
&& let Some(name) = entry.file_name().to_str()
&& name == arg {
&& name == arg
{
println!("{arg} is {}", entry.path().display());
continue 'outer;
}
@@ -64,7 +78,14 @@ pub fn type_builtin(node: Node) -> ShResult<()> {
}
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())
),
));
}
}

View File

@@ -31,11 +31,17 @@ pub fn continue_job(node: Node, behavior: JobBehavior) -> ShResult<()> {
};
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();
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()) {
@@ -55,7 +61,11 @@ pub fn continue_job(node: Node, behavior: JobBehavior) -> ShResult<()> {
if query_result.is_some() {
Ok(j.remove_job(id.clone()).unwrap())
} 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),
))
}
})?;
@@ -84,7 +94,11 @@ fn parse_job_id(arg: &str, blame: Span) -> ShResult<usize> {
if arg.chars().all(|ch| ch.is_ascii_digit()) {
let num = arg.parse::<usize>().unwrap_or_default();
if num == 0 {
Err(ShErr::at(ShErrKind::SyntaxErr, blame, format!("Invalid job id: {}", arg.fg(next_color()))))
Err(ShErr::at(
ShErrKind::SyntaxErr,
blame,
format!("Invalid job id: {}", arg.fg(next_color())),
))
} else {
Ok(num.saturating_sub(1))
}
@@ -95,7 +109,11 @@ fn parse_job_id(arg: &str, blame: Span) -> ShResult<usize> {
});
match result {
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()) {
@@ -115,10 +133,18 @@ fn parse_job_id(arg: &str, blame: Span) -> ShResult<usize> {
match result {
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 {
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)?;
if !argv.is_empty() { argv.remove(0); }
if !argv.is_empty() {
argv.remove(0);
}
let mut flags = JobCmdFlags::empty();
for (arg, span) in argv {
let mut chars = arg.chars().peekable();
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();
for ch in chars {
@@ -149,7 +181,11 @@ pub fn jobs(node: Node) -> ShResult<()> {
'r' => JobCmdFlags::RUNNING,
'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
@@ -172,12 +208,15 @@ pub fn wait(node: Node) -> ShResult<()> {
};
let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); }
if !argv.is_empty() {
argv.remove(0);
}
if read_jobs(|j| j.curr_job().is_none()) {
state::set_status(0);
return Err(ShErr::at(ShErrKind::ExecFail, blame, "wait: No jobs found"));
}
let argv = argv.into_iter()
let argv = argv
.into_iter()
.map(|arg| {
if arg.0.as_str().chars().all(|ch| ch.is_ascii_digit()) {
Ok(JobID::Pid(Pid::from_raw(arg.0.parse::<i32>().unwrap())))
@@ -210,13 +249,19 @@ pub fn disown(node: Node) -> ShResult<()> {
};
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 curr_job_id = if let Some(id) = read_jobs(|j| j.curr_job()) {
id
} 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;

View File

@@ -1,5 +1,11 @@
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! {
@@ -33,11 +39,19 @@ impl KeyMapOpts {
Opt::Short('r') => flags |= KeyMapFlags::REPLACE,
Opt::LongWithArg(name, arg) if name == "remove" => {
if remove.is_some() {
return Err(ShErr::simple(ShErrKind::ExecFail, "Duplicate --remove option for keymap".to_string()));
return Err(ShErr::simple(
ShErrKind::ExecFail,
"Duplicate --remove option for keymap".to_string(),
));
}
remove = Some(arg.clone());
},
_ => return Err(ShErr::simple(ShErrKind::ExecFail, format!("Invalid option for keymap: {:?}", opt))),
}
_ => {
return Err(ShErr::simple(
ShErrKind::ExecFail,
format!("Invalid option for keymap: {:?}", opt),
));
}
}
}
if flags.is_empty() {
@@ -49,27 +63,27 @@ impl KeyMapOpts {
[
OptSpec {
opt: Opt::Short('n'), // normal mode
takes_arg: false
takes_arg: false,
},
OptSpec {
opt: Opt::Short('i'), // insert mode
takes_arg: false
takes_arg: false,
},
OptSpec {
opt: Opt::Short('v'), // visual mode
takes_arg: false
takes_arg: false,
},
OptSpec {
opt: Opt::Short('x'), // ex mode
takes_arg: false
takes_arg: false,
},
OptSpec {
opt: Opt::Short('o'), // operator-pending mode
takes_arg: false
takes_arg: false,
},
OptSpec {
opt: Opt::Short('r'), // replace mode
takes_arg: false
takes_arg: false,
},
]
}
@@ -79,14 +93,14 @@ impl KeyMapOpts {
pub enum KeyMapMatch {
NoMatch,
IsPrefix,
IsExact
IsExact,
}
#[derive(Debug, Clone)]
pub struct KeyMap {
pub flags: KeyMapFlags,
pub keys: String,
pub action: String
pub action: String,
}
impl KeyMap {
@@ -97,7 +111,11 @@ impl KeyMap {
expand_keymap(&self.action)
}
pub fn compare(&self, other: &[KeyEvent]) -> KeyMapMatch {
log::debug!("Comparing keymap keys {:?} with input {:?}", self.keys_expanded(), other);
log::debug!(
"Comparing keymap keys {:?} with input {:?}",
self.keys_expanded(),
other
);
let ours = self.keys_expanded();
if other == ours {
KeyMapMatch::IsExact
@@ -128,14 +146,24 @@ pub fn keymap(node: Node) -> ShResult<()> {
}
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 {
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 {
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 {

View File

@@ -5,7 +5,15 @@ use nix::{libc::STDOUT_FILENO, unistd::write};
use serde_json::{Map, Value};
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)]
@@ -26,24 +34,18 @@ impl From<MapNode> for serde_json::Value {
fn from(val: MapNode) -> Self {
match val {
MapNode::Branch(map) => {
let val_map = map.into_iter()
.map(|(k,v)| {
(k,v.into())
})
let val_map = map
.into_iter()
.map(|(k, v)| (k, v.into()))
.collect::<Map<String, Value>>();
Value::Object(val_map)
}
MapNode::Array(nodes) => {
let arr = nodes
.into_iter()
.map(|node| node.into())
.collect();
let arr = nodes.into_iter().map(|node| node.into()).collect();
Value::Array(arr)
}
MapNode::StaticLeaf(leaf) | MapNode::DynamicLeaf(leaf) => {
Value::String(leaf)
}
MapNode::StaticLeaf(leaf) | MapNode::DynamicLeaf(leaf) => Value::String(leaf),
}
}
}
@@ -52,23 +54,19 @@ impl From<Value> for MapNode {
fn from(value: Value) -> Self {
match value {
Value::Object(map) => {
let node_map = map.into_iter()
.map(|(k,v)| {
(k, v.into())
})
let node_map = map
.into_iter()
.map(|(k, v)| (k, v.into()))
.collect::<HashMap<String, MapNode>>();
MapNode::Branch(node_map)
}
Value::Array(arr) => {
let nodes = arr
.into_iter()
.map(|v| v.into())
.collect();
let nodes = arr.into_iter().map(|v| v.into()).collect();
MapNode::Array(nodes)
}
Value::String(s) => MapNode::StaticLeaf(s),
v => MapNode::StaticLeaf(v.to_string())
v => MapNode::StaticLeaf(v.to_string()),
}
}
}
@@ -83,8 +81,8 @@ impl MapNode {
let idx: usize = key.parse().ok()?;
map_nodes.get(idx)?.get(rest)
}
MapNode::Branch(map) => map.get(key)?.get(rest)
}
MapNode::Branch(map) => map.get(key)?.get(rest),
},
}
}
@@ -98,9 +96,7 @@ impl MapNode {
}
match self {
MapNode::Branch(map) => {
let child = map
.entry(key.to_string())
.or_insert_with(Self::default);
let child = map.entry(key.to_string()).or_insert_with(Self::default);
child.set(rest, value);
}
MapNode::Array(map_nodes) => {
@@ -110,7 +106,7 @@ impl MapNode {
}
map_nodes[idx].set(rest, value);
}
_ => unreachable!()
_ => unreachable!(),
}
}
}
@@ -128,8 +124,8 @@ impl MapNode {
}
Some(nodes.remove(idx))
}
_ => None
}
_ => None,
},
[key, rest @ ..] => match self {
MapNode::Branch(map) => map.get_mut(key)?.remove(rest),
MapNode::Array(nodes) => {
@@ -139,15 +135,18 @@ impl MapNode {
}
nodes[idx].remove(rest)
}
_ => None
}
_ => None,
},
}
}
fn keys(&self) -> Vec<String> {
match self {
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
.iter()
.filter_map(|n| n.display(false, false).ok())
.collect(),
MapNode::StaticLeaf(_) | MapNode::DynamicLeaf(_) => vec![],
}
}
@@ -160,16 +159,16 @@ impl MapNode {
Ok(s) => Ok(s),
Err(e) => Err(ShErr::simple(
ShErrKind::InternalErr,
format!("failed to serialize map: {e}")
))
format!("failed to serialize map: {e}"),
)),
}
} else {
match serde_json::to_string(&val) {
Ok(s) => Ok(s),
Err(e) => Err(ShErr::simple(
ShErrKind::InternalErr,
format!("failed to serialize map: {e}")
))
format!("failed to serialize map: {e}"),
)),
}
}
} else {
@@ -189,7 +188,7 @@ impl MapNode {
}
Ok(s.trim_end_matches('\n').to_string())
}
_ => unreachable!()
_ => unreachable!(),
}
}
}
@@ -199,27 +198,27 @@ fn map_opts_spec() -> [OptSpec; 6] {
[
OptSpec {
opt: Opt::Short('r'),
takes_arg: false
takes_arg: false,
},
OptSpec {
opt: Opt::Short('j'),
takes_arg: false
takes_arg: false,
},
OptSpec {
opt: Opt::Short('k'),
takes_arg: false
takes_arg: false,
},
OptSpec {
opt: Opt::Long("pretty".into()),
takes_arg: false
takes_arg: false,
},
OptSpec {
opt: Opt::Short('F'),
takes_arg: false
takes_arg: false,
},
OptSpec {
opt: Opt::Short('l'),
takes_arg: false
takes_arg: false,
},
]
}
@@ -265,14 +264,18 @@ pub fn map(node: Node) -> ShResult<()> {
let Some(name) = path.first() else {
return Err(ShErr::simple(
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_func = map_opts.flags.contains(MapFlags::FUNC);
let make_leaf = |s: String| {
if is_func { MapNode::DynamicLeaf(s) } else { MapNode::StaticLeaf(s) }
if is_func {
MapNode::DynamicLeaf(s)
} else {
MapNode::StaticLeaf(s)
}
};
let expanded = rhs.expand()?.get_words().join(" ");
let found = write_vars(|v| -> ShResult<bool> {
@@ -294,7 +297,9 @@ pub fn map(node: Node) -> ShResult<()> {
if !found? {
let mut new = MapNode::default();
if is_json /*&& let Ok(parsed) = serde_json::from_str::<Value>(rhs.as_str()) */{
if is_json
/*&& let Ok(parsed) = serde_json::from_str::<Value>(rhs.as_str()) */
{
let parsed = serde_json::from_str::<Value>(expanded.as_str()).unwrap();
let node: MapNode = parsed.into();
new.set(&path[1..], node);
@@ -309,7 +314,7 @@ pub fn map(node: Node) -> ShResult<()> {
let Some(name) = path.first() else {
return Err(ShErr::simple(
ShErrKind::InternalErr,
format!("invalid map path: {}", expanded)
format!("invalid map path: {}", expanded),
));
};
@@ -321,7 +326,7 @@ pub fn map(node: Node) -> ShResult<()> {
let Some(map) = v.get_map_mut(name) else {
return Err(ShErr::simple(
ShErrKind::ExecFail,
format!("map not found: {}", name)
format!("map not found: {}", name),
));
};
map.remove(&path[1..]);
@@ -339,13 +344,11 @@ pub fn map(node: Node) -> ShResult<()> {
if !has_map {
return Err(ShErr::simple(
ShErrKind::ExecFail,
format!("map not found: {}", name)
format!("map not found: {}", name),
));
}
let Some(node) = read_vars(|v| {
v.get_map(name)
.and_then(|map| map.get(&path[1..]).cloned())
}) else {
let Some(node) = read_vars(|v| v.get_map(name).and_then(|map| map.get(&path[1..]).cloned()))
else {
state::set_status(1);
continue;
};
@@ -367,7 +370,7 @@ pub fn map(node: Node) -> ShResult<()> {
pub fn get_map_opts(opts: Vec<Opt>) -> MapOpts {
let mut map_opts = MapOpts {
flags: MapFlags::empty()
flags: MapFlags::empty(),
};
for opt in opts {
@@ -378,7 +381,7 @@ pub fn get_map_opts(opts: Vec<Opt>) -> MapOpts {
Opt::Short('l') => map_opts.flags |= MapFlags::LOCAL,
Opt::Long(ref s) if s == "pretty" => map_opts.flags |= MapFlags::PRETTY,
Opt::Short('F') => map_opts.flags |= MapFlags::FUNC,
_ => unreachable!()
_ => unreachable!(),
}
}
map_opts

View File

@@ -1,9 +1,8 @@
use crate::{
libsh::error::ShResult,
state,
};
use crate::{libsh::error::ShResult, state};
pub mod alias;
pub mod arrops;
pub mod autocmd;
pub mod cd;
pub mod complete;
pub mod dirstack;
@@ -11,7 +10,11 @@ pub mod echo;
pub mod eval;
pub mod exec;
pub mod flowctl;
pub mod getopts;
pub mod intro;
pub mod jobctl;
pub mod keymap;
pub mod map;
pub mod pwd;
pub mod read;
pub mod shift;
@@ -21,19 +24,13 @@ pub mod test; // [[ ]] thing
pub mod trap;
pub mod varcmds;
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] = [
"echo", "cd", "read", "export", "local", "pwd", "source", "shift", "jobs", "fg", "bg", "disown",
"alias", "unalias", "return", "break", "continue", "exit", "zoltraak", "shopt", "builtin",
"command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly",
"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<()> {

View File

@@ -6,7 +6,16 @@ use nix::{
};
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] = [
@@ -43,16 +52,16 @@ pub const READ_OPTS: [OptSpec; 7] = [
pub const READ_KEY_OPTS: [OptSpec; 3] = [
OptSpec {
opt: Opt::Short('v'), // var name
takes_arg: true
takes_arg: true,
},
OptSpec {
opt: Opt::Short('w'), // char whitelist
takes_arg: true
takes_arg: true,
},
OptSpec {
opt: Opt::Short('b'), // char blacklist
takes_arg: true
}
takes_arg: true,
},
];
bitflags! {
@@ -84,7 +93,9 @@ pub fn read_builtin(node: Node) -> ShResult<()> {
let (argv, opts) = get_opts_from_tokens(argv, &READ_OPTS)?;
let read_opts = get_read_flags(opts).blame(blame.clone())?;
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 {
write(borrow_fd(STDOUT_FILENO), prompt.as_bytes())?;
@@ -259,12 +270,14 @@ pub fn get_read_flags(opts: Vec<Opt>) -> ShResult<ReadOpts> {
pub struct ReadKeyOpts {
var_name: Option<String>,
char_whitelist: Option<String>,
char_blacklist: Option<String>
char_blacklist: Option<String>,
}
pub fn read_key(node: Node) -> ShResult<()> {
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)? {
state::set_status(1);
@@ -290,7 +303,7 @@ pub fn read_key(node: Node) -> ShResult<()> {
return Ok(());
};
key
},
}
Err(Errno::EINTR) => {
state::set_status(130);
return Ok(());
@@ -331,7 +344,7 @@ pub fn get_read_key_opts(opts: Vec<Opt>) -> ShResult<ReadKeyOpts> {
let mut read_key_opts = ReadKeyOpts {
var_name: None,
char_whitelist: None,
char_blacklist: None
char_blacklist: None,
};
for opt in opts {
@@ -342,7 +355,7 @@ pub fn get_read_key_opts(opts: Vec<Opt>) -> ShResult<ReadKeyOpts> {
_ => {
return Err(ShErr::simple(
ShErrKind::ExecFail,
format!("read_key: Unexpected flag '{opt}'")
format!("read_key: Unexpected flag '{opt}'"),
));
}
}

View File

@@ -14,12 +14,18 @@ pub fn shift(node: Node) -> ShResult<()> {
};
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();
if let Some((arg, span)) = argv.next() {
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 {
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)?;
if !argv.is_empty() { argv.remove(0); }
if !argv.is_empty() {
argv.remove(0);
}
if argv.is_empty() {
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)?;
if !argv.is_empty() { argv.remove(0); }
if !argv.is_empty() {
argv.remove(0);
}
for (arg, span) in argv {
let path = PathBuf::from(arg);
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() {
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)?;
}

View File

@@ -61,10 +61,7 @@ impl FromStr for UnaryOp {
"-t" => Ok(Self::Terminal),
"-n" => Ok(Self::NonNull),
"-z" => Ok(Self::Null),
_ => Err(ShErr::simple(
ShErrKind::SyntaxErr,
"Invalid test operator",
)),
_ => Err(ShErr::simple(ShErrKind::SyntaxErr, "Invalid test operator")),
}
}
}
@@ -97,10 +94,7 @@ impl FromStr for TestOp {
"-ge" => Ok(Self::IntGe),
"-le" => Ok(Self::IntLe),
_ if TEST_UNARY_OPS.contains(&s) => Ok(Self::Unary(s.parse::<UnaryOp>()?)),
_ => Err(ShErr::simple(
ShErrKind::SyntaxErr,
"Invalid test operator",
)),
_ => Err(ShErr::simple(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(" ");
conjunct_op = conjunct;
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 {
UnaryOp::Exists => {
@@ -241,7 +239,11 @@ pub fn double_bracket_test(node: Node) -> ShResult<bool> {
let test_op = operator.as_str().parse::<TestOp>()?;
match test_op {
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 => {
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::IntLe
| 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 {
return Err(err);
};

View File

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

View File

@@ -16,7 +16,11 @@ pub fn readonly(node: Node) -> ShResult<()> {
};
// 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() {
// Display the local variables
@@ -67,15 +71,25 @@ pub fn unset(node: Node) -> ShResult<()> {
};
let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); }
if !argv.is_empty() {
argv.remove(0);
}
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 {
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))?;
}
@@ -94,7 +108,11 @@ pub fn export(node: Node) -> ShResult<()> {
};
// 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() {
// Display the environment variables
@@ -137,7 +155,11 @@ pub fn local(node: Node) -> ShResult<()> {
};
// 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() {
// Display the local variables

View File

@@ -104,7 +104,9 @@ pub fn zoltraak(node: Node) -> ShResult<()> {
}
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 {
if &arg == "/" && !flags.contains(ZoltFlags::NO_PRESERVE_ROOT) {
@@ -113,9 +115,7 @@ pub fn zoltraak(node: Node) -> ShResult<()> {
ShErrKind::ExecFail,
"zoltraak: Attempted to destroy root directory '/'",
)
.with_note(
"If you really want to do this, you can use the --no-preserve-root flag"
),
.with_note("If you really want to do this, you can use the --no-preserve-root flag"),
);
}
annihilate(&arg, flags).blame(span)?
@@ -176,9 +176,7 @@ fn annihilate(path: &str, flags: ZoltFlags) -> ShResult<()> {
ShErrKind::ExecFail,
format!("zoltraak: '{path}' is a directory"),
)
.with_note(
"Use the '-r' flag to recursively shred directories"
),
.with_note("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::lex::{LexFlags, LexStream, QuoteState, Tk, TkFlags, TkRule, is_hard_sep};
use crate::parse::{Redir, RedirType};
use crate::prelude::*;
use crate::procio::{IoBuf, IoFrame, IoMode, IoStack};
use crate::readline::keys::{KeyCode, KeyEvent, ModKeys};
use crate::readline::markers;
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'];
@@ -25,9 +26,7 @@ impl Tk {
pub fn expand(self) -> ShResult<Self> {
let flags = self.flags;
let span = self.span.clone();
let exp = Expander::new(self)?
.expand()
.promote_err(span.clone())?;
let exp = Expander::new(self)?.expand().promote_err(span.clone())?;
let class = TkRule::Expanded { exp };
Ok(Self { class, span, flags })
}
@@ -534,11 +533,9 @@ pub fn expand_var(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
let arg_sep = markers::ARG_SEP.to_string();
read_vars(|v| v.get_arr_elems(&var_name))?.join(&arg_sep)
}
ArrIndex::ArgCount => {
read_vars(|v| v.get_arr_elems(&var_name))
ArrIndex::ArgCount => read_vars(|v| v.get_arr_elems(&var_name))
.map(|elems| elems.len().to_string())
.unwrap_or_else(|_| "0".to_string())
}
.unwrap_or_else(|_| "0".to_string()),
ArrIndex::AllJoined => {
let ifs = read_vars(|v| v.try_get_var("IFS"))
.unwrap_or_else(|| " \t\n".to_string())
@@ -653,7 +650,7 @@ enum ArithTk {
Op(ArithOp),
LParen,
RParen,
Var(String)
Var(String),
}
impl ArithTk {
@@ -704,7 +701,7 @@ impl ArithTk {
var_name.push(*ch);
chars.next();
}
_ => break
_ => break,
}
}
@@ -757,13 +754,19 @@ impl ArithTk {
let Some(val) = read_vars(|v| v.try_get_var(&var)) else {
return Err(ShErr::simple(
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())),
format!(
"Variable '{}' does not contain a number",
var.fg(next_color())
),
));
};
@@ -894,7 +897,12 @@ pub fn expand_proc_sub(raw: &str, is_input: bool) -> ShResult<String> {
let mut io_stack = IoStack::new();
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();
exit(1);
}
@@ -925,7 +933,12 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult<String> {
match unsafe { fork()? } {
ForkResult::Child => {
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();
unsafe { libc::_exit(1) };
}
@@ -958,7 +971,7 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult<String> {
WtStat::Exited(_, code) => {
state::set_status(code);
Ok(io_buf.as_str()?.trim_end().to_string())
},
}
_ => Err(ShErr::simple(ShErrKind::InternalErr, "Command sub failed")),
}
}
@@ -1554,10 +1567,7 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
Some(val) => Ok(val),
None => {
let expanded = expand_raw(&mut err.chars().peekable())?;
Err(ShErr::simple(
ShErrKind::ExecFail,
expanded,
))
Err(ShErr::simple(ShErrKind::ExecFail, expanded))
}
}
}
@@ -1565,10 +1575,7 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
Some(val) => Ok(val),
None => {
let expanded = expand_raw(&mut err.chars().peekable())?;
Err(ShErr::simple(
ShErrKind::ExecFail,
expanded,
))
Err(ShErr::simple(ShErrKind::ExecFail, expanded))
}
},
ParamExp::Substr(pos) => {
@@ -1630,7 +1637,8 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
ParamExp::RemLongestSuffix(suffix) => {
let value = vars.get_var(&var_name);
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();
for i in 0..=value.len() {
let sliced = &value[i..];
@@ -2373,7 +2381,7 @@ pub fn parse_key_alias(alias: &str) -> Option<KeyEvent> {
"PGUP" | "PAGEUP" => KeyCode::PageUp,
"PGDN" | "PAGEDOWN" => KeyCode::PageDown,
k if k.len() == 1 => KeyCode::Char(k.chars().next().unwrap()),
_ => return None
_ => return None,
};
Some(KeyEvent(key, mods))

View File

@@ -60,16 +60,10 @@ impl fmt::Display for DisplayWaitStatus {
pub fn code_from_status(stat: &WtStat) -> Option<i32> {
match stat {
WtStat::Exited(_, 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)
}
_ => { None }
WtStat::Exited(_, 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),
_ => None,
}
}
@@ -186,7 +180,12 @@ impl JobTab {
}
pub fn curr_job(&self) -> Option<usize> {
// 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> {
// Find the second most recent valid job
@@ -283,18 +282,16 @@ impl JobTab {
}
pub fn update_by_id(&mut self, id: JobID, stat: WtStat) -> ShResult<()> {
let Some(job) = self.query_mut(id.clone()) else {
return Ok(())
return Ok(());
};
match id {
JobID::Pid(pid) => {
let Some(child) = job.children_mut().iter_mut().find(|c| c.pid() == pid) else {
return Ok(())
return Ok(());
};
child.set_stat(stat);
}
JobID::Pgid(_) |
JobID::TableID(_) |
JobID::Command(_) => {
JobID::Pgid(_) | JobID::TableID(_) | JobID::Command(_) => {
job.set_stats(stat);
}
}
@@ -612,10 +609,9 @@ impl Job {
&mut self.children
}
pub fn is_done(&self) -> bool {
self
.children
.iter()
.all(|chld| chld.exited() || chld.stat() == WtStat::Signaled(chld.pid(), Signal::SIGHUP, true))
self.children.iter().all(|chld| {
chld.exited() || chld.stat() == WtStat::Signaled(chld.pid(), Signal::SIGHUP, true)
})
}
pub fn killpg(&mut self, sig: Signal) -> ShResult<()> {
let stat = match sig {
@@ -795,7 +791,10 @@ pub fn wait_bg(id: JobID) -> ShResult<()> {
}
_ => {
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,
format!("wait: No such job with id {:?}", id),
));
};
let statuses = job.wait_pgrp()?;
let mut was_stopped = false;

View File

@@ -1,9 +1,9 @@
use std::cell::RefCell;
use std::collections::{HashMap, VecDeque};
use std::fmt::Display;
use ariadne::Color;
use ariadne::{Report, ReportKind};
use rand::TryRng;
use std::cell::RefCell;
use std::collections::{HashMap, VecDeque};
use std::fmt::Display;
use crate::procio::RedirGuard;
use crate::{
@@ -173,22 +173,36 @@ pub struct ShErr {
/// the RedirGuard(s) so that redirections stay alive until the error
/// is printed. Multiple guards can accumulate as the error bubbles
/// through nested redirect scopes.
io_guards: Vec<RedirGuard>
io_guards: Vec<RedirGuard>,
}
impl ShErr {
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,
src_span: Some(span),
labels: vec![],
sources: vec![],
notes: vec![],
io_guards: vec![],
}
}
pub fn simple(kind: ShErrKind, msg: impl Into<String>) -> Self {
Self { kind, src_span: None, labels: vec![], sources: vec![], notes: vec![msg.into()], io_guards: vec![] }
Self {
kind,
src_span: None,
labels: 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
return self;
}
let first = self.notes[0].clone();
if self.notes.len() > 1 {
@@ -205,23 +219,60 @@ impl ShErr {
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 msg: String = msg.into();
Self::new(kind, span.clone())
.with_label(src, ariadne::Label::new(span).with_color(color).with_message(msg))
Self::new(kind, span.clone()).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();
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))
self.with_label(
src,
ariadne::Label::new(span)
.with_color(color)
.with_message(msg),
)
}
pub fn blame(self, span: Span) -> Self {
let ShErr { kind, src_span: _, labels, sources, notes, io_guards } = self;
Self { kind, src_span: Some(span), labels, sources, notes, io_guards }
let ShErr {
kind,
src_span: _,
labels,
sources,
notes,
io_guards,
} = self;
Self {
kind,
src_span: Some(span),
labels,
sources,
notes,
io_guards,
}
}
pub fn try_blame(self, span: Span) -> Self {
match self {
ShErr { kind, src_span: None, labels, sources, notes, io_guards } => Self { kind, src_span: Some(span), labels, sources, notes, io_guards },
_ => self
ShErr {
kind,
src_span: None,
labels,
sources,
notes,
io_guards,
} => Self {
kind,
src_span: Some(span),
labels,
sources,
notes,
io_guards,
},
_ => self,
}
}
pub fn kind(&self) -> &ShErrKind {
@@ -234,23 +285,65 @@ impl ShErr {
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;
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 }
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;
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 }
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;
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 }
Self {
kind,
src_span,
labels,
sources,
notes,
io_guards,
}
}
pub fn build_report(&self) -> Option<Report<'_, Span>> {
let span = self.src_span.as_ref()?;
@@ -276,11 +369,13 @@ impl ShErr {
let mut source_map = HashMap::new();
if let Some(span) = &self.src_span {
let src = span.span_source().clone();
source_map.entry(src.clone())
source_map
.entry(src.clone())
.or_insert_with(|| src.content().to_string());
}
for src in &self.sources {
source_map.entry(src.clone())
source_map
.entry(src.clone())
.or_insert_with(|| src.content().to_string());
}
source_map
@@ -298,7 +393,8 @@ impl ShErr {
let sources = self.collect_sources();
let cache = ariadne::FnCache::new(move |src: &SpanSource| {
sources.get(src)
sources
.get(src)
.cloned()
.ok_or_else(|| format!("Failed to fetch source '{}'", src.name()))
});
@@ -365,12 +461,13 @@ pub enum ShErrKind {
impl ShErrKind {
pub fn is_flow_control(&self) -> bool {
matches!(self,
Self::CleanExit(_) |
Self::FuncReturn(_) |
Self::LoopContinue(_) |
Self::LoopBreak(_) |
Self::ClearReadline
matches!(
self,
Self::CleanExit(_)
| Self::FuncReturn(_)
| Self::LoopContinue(_)
| Self::LoopBreak(_)
| Self::ClearReadline
)
}
}

View File

@@ -144,7 +144,8 @@ impl RawModeGuard {
F: FnOnce() -> R,
{
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()");
tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &orig)
.expect("Failed to restore cooked mode");

View File

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

View File

@@ -34,7 +34,6 @@ use crate::libsh::sys::TTY_FILENO;
use crate::libsh::utils::AutoCmdVecUtils;
use crate::parse::execute::exec_input;
use crate::prelude::*;
use crate::procio::IoMode;
use crate::readline::term::{LineWriter, RawModeGuard, raw_mode};
use crate::readline::{Prompt, ReadlineEvent, ShedVi};
use crate::signal::{GOT_SIGWINCH, JOB_DONE, QUIT_CODE, check_signals, sig_setup, signals_pending};
@@ -99,7 +98,9 @@ fn setup_panic_handler() {
log_file.write_all(panic_info_raw.as_bytes()).unwrap();
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);
}));
@@ -138,7 +139,8 @@ fn main() -> ExitCode {
};
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();
}
@@ -267,15 +269,26 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
// Timeout — resolve pending keymap ambiguity
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 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
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 {
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();
readline.pending_keymap.clear();
for key in action {
@@ -284,7 +297,9 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
ReadlineEvent::Line(input) => {
let start = Instant::now();
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() {
ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst);
@@ -310,7 +325,10 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
}
}
} 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);
for key in buffered {
if let Some(event) = readline.handle_key(key)? {
@@ -318,7 +336,9 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
ReadlineEvent::Line(input) => {
let start = Instant::now();
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() {
ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst);
@@ -383,7 +403,9 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
let start = Instant::now();
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() {
ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst);

View File

@@ -1,15 +1,37 @@
use std::{
cell::Cell, collections::{HashSet, VecDeque}, os::unix::fs::PermissionsExt
cell::Cell,
collections::{HashSet, VecDeque},
os::unix::fs::PermissionsExt,
};
use ariadne::Fmt;
use crate::{
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},
libsh::{
error::{ShErr, ShErrKind, ShResult, ShResultExt, next_color},
@@ -19,7 +41,7 @@ use crate::{
prelude::*,
procio::{IoMode, IoStack},
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 input = expand_aliases(input, HashSet::new(), &log_tab);
let lex_flags = if interactive {
@@ -119,7 +146,9 @@ pub fn exec_input(input: String, io_stack: Option<IoStack>, interactive: bool, s
super::lex::LexFlags::empty()
};
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() {
for error in errors {
error.print_error();
@@ -208,7 +237,12 @@ impl Dispatcher {
let stack = IoStack {
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 {
self.exec_cmd(node)
}
@@ -310,7 +344,10 @@ impl Dispatcher {
fn exec_func(&mut self, func: Node) -> ShResult<()> {
let mut blame = func.get_span().clone();
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 {
assignments,
mut argv,
@@ -354,7 +391,7 @@ impl Dispatcher {
state::set_status(*code);
Ok(())
}
_ => Err(e)
_ => Err(e),
}
} else {
Ok(())
@@ -423,7 +460,12 @@ impl Dispatcher {
'outer: for block in case_blocks {
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)`
let block_patterns = block_pattern_raw.split('|');
@@ -450,7 +492,9 @@ impl Dispatcher {
}
})
} 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<()> {
@@ -513,7 +557,9 @@ impl Dispatcher {
}
})
} 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<()> {
@@ -591,7 +637,9 @@ impl Dispatcher {
}
})
} 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<()> {
@@ -648,7 +696,9 @@ impl Dispatcher {
}
})
} 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<()> {
@@ -692,8 +742,11 @@ impl Dispatcher {
// SIGTTOU when they try to modify terminal attributes.
// Only for interactive (top-level) pipelines — command substitution
// and other non-interactive contexts must not steal the terminal.
if !tty_attached && !is_bg && self.interactive
&& let Some(pgid) = self.job_stack.curr_job_mut().unwrap().pgid() {
if !tty_attached
&& !is_bg
&& self.interactive
&& let Some(pgid) = self.job_stack.curr_job_mut().unwrap().pgid()
{
attach_tty(pgid).ok();
tty_attached = true;
}

View File

@@ -30,7 +30,7 @@ pub enum QuoteState {
#[default]
Outside,
Single,
Double
Double,
}
impl QuoteState {
@@ -67,7 +67,7 @@ impl QuoteState {
#[derive(Clone, PartialEq, Default, Debug, Eq, Hash)]
pub struct SpanSource {
name: String,
content: Arc<String>
content: Arc<String>,
}
impl SpanSource {
@@ -92,13 +92,16 @@ impl Display for SpanSource {
#[derive(Clone, PartialEq, Default, Debug)]
pub struct Span {
range: Range<usize>,
source: SpanSource
source: SpanSource,
}
impl Span {
/// New `Span`. Wraps a range and a string slice that it refers to.
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 }
}
pub fn from_span_source(range: Range<usize>, source: SpanSource) -> Self {
@@ -994,7 +997,10 @@ pub fn split_at_unescaped(slice: &str, pat: &str) -> Option<(String,String)> {
while let Some((i, ch)) = chars.next() {
match ch {
'\\' => { chars.next(); continue; }
'\\' => {
chars.next();
continue;
}
'\'' => qt_state.toggle_single(),
'"' => qt_state.toggle_double(),
_ if qt_state.in_quote() => continue,
@@ -1008,7 +1014,6 @@ pub fn split_at_unescaped(slice: &str, pat: &str) -> Option<(String,String)> {
}
}
None
}
@@ -1017,12 +1022,18 @@ pub fn split_tk(tk: &Tk, pat: &str) -> Vec<Tk> {
let mut cursor = 0;
let mut splits = vec![];
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(
tk.span.range().start + cursor..tk.span.range().start + cursor + split.0.len(),
tk.source().clone(),
);
splits.push(Tk::new(tk.class.clone(), before_span));
cursor += split.0.len() + pat.len();
}
if slice.get(cursor..).is_some_and(|s| !s.is_empty()) {
let remaining_span = Span::new(tk.span.range().start + cursor..tk.span.range().end, tk.source().clone());
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
@@ -1035,7 +1046,10 @@ pub fn split_tk_at(tk: &Tk, pat: &str) -> Option<(Tk, Tk)> {
while let Some((i, ch)) = chars.next() {
match ch {
'\\' => { chars.next(); continue; }
'\\' => {
chars.next();
continue;
}
'\'' => qt_state.toggle_single(),
'"' => qt_state.toggle_double(),
_ if qt_state.in_quote() => continue,
@@ -1043,8 +1057,14 @@ pub fn split_tk_at(tk: &Tk, pat: &str) -> Option<(Tk, Tk)> {
}
if slice[i..].starts_with(pat) {
let before_span = Span::new(tk.span.range().start..tk.span.range().start + i, tk.source().clone());
let after_span = Span::new(tk.span.range().start + i + pat.len()..tk.span.range().end, tk.source().clone());
let before_span = Span::new(
tk.span.range().start..tk.span.range().start + i,
tk.source().clone(),
);
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));

View File

@@ -9,7 +9,10 @@ use crate::{
libsh::{
error::{ShErr, ShErrKind, ShResult, last_color, next_color},
utils::{NodeVecUtils, TkVecUtils},
}, parse::lex::clean_input, prelude::*, procio::IoMode
},
parse::lex::clean_input,
prelude::*,
procio::IoMode,
};
pub mod execute;
@@ -77,7 +80,8 @@ impl ParsedSrc {
}
pub fn parse_src(&mut self) -> Result<(), Vec<ShErr>> {
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 {
Ok(token) => tokens.push(token),
Err(error) => return Err(vec![error]),
@@ -148,7 +152,7 @@ impl Node {
let span = self.get_span().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) {
@@ -662,7 +666,7 @@ pub enum NdRule {
pub struct ParseStream {
pub tokens: Vec<Tk>,
pub context: LabelCtx
pub context: LabelCtx,
}
impl Debug for ParseStream {
@@ -675,7 +679,10 @@ impl Debug for ParseStream {
impl ParseStream {
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 {
Self { tokens, context }
@@ -839,7 +846,10 @@ impl ParseStream {
self.context.push_back((
src.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!(
"in function '{}' defined here",
name_raw.clone().fg(color)
))
.with_color(color),
));
@@ -848,7 +858,7 @@ impl ParseStream {
return Err(parse_err_full(
"Expected a brace group after function name",
&node_tks.get_span().unwrap(),
self.context.clone()
self.context.clone(),
));
};
body = Box::new(brc_grp);
@@ -860,7 +870,7 @@ impl ParseStream {
flags: NdFlags::empty(),
redirs: vec![],
tokens: node_tks,
context: self.context.clone()
context: self.context.clone(),
};
self.context.pop_back();
@@ -893,7 +903,7 @@ impl ParseStream {
return Err(parse_err_full(
"Malformed test call",
&node_tks.get_span().unwrap(),
self.context.clone()
self.context.clone(),
));
} else {
break;
@@ -920,7 +930,7 @@ impl ParseStream {
return Err(parse_err_full(
"Invalid placement for logical operator in test",
&node_tks.get_span().unwrap(),
self.context.clone()
self.context.clone(),
));
}
let op = match tk.class {
@@ -936,7 +946,7 @@ impl ParseStream {
return Err(parse_err_full(
"Invalid placement for logical operator in test",
&node_tks.get_span().unwrap(),
self.context.clone()
self.context.clone(),
));
}
}
@@ -982,7 +992,7 @@ impl ParseStream {
return Err(parse_err_full(
"Expected a closing brace for this brace group",
&node_tks.get_span().unwrap(),
self.context.clone()
self.context.clone(),
));
}
}
@@ -1049,11 +1059,9 @@ impl ParseStream {
let pat_err = parse_err_full(
"Expected a pattern after 'case' keyword",
&node_tks.get_span().unwrap(),
self.context.clone()
self.context.clone(),
)
.with_note(
"Patterns can be raw text, or anything that gets substituted with raw text"
);
.with_note("Patterns can be raw text, or anything that gets substituted with raw text");
let Some(pat_tk) = self.next_tk() else {
self.panic_mode(&mut node_tks);
@@ -1073,7 +1081,7 @@ impl ParseStream {
return Err(parse_err_full(
"Expected 'in' after case variable name",
&node_tks.get_span().unwrap(),
self.context.clone()
self.context.clone(),
));
}
node_tks.push(self.next_tk().unwrap());
@@ -1086,7 +1094,7 @@ impl ParseStream {
return Err(parse_err_full(
"Expected a case pattern here",
&node_tks.get_span().unwrap(),
self.context.clone()
self.context.clone(),
));
}
let case_pat_tk = self.next_tk().unwrap();
@@ -1123,7 +1131,7 @@ impl ParseStream {
return Err(parse_err_full(
"Expected 'esac' after case block",
&node_tks.get_span().unwrap(),
self.context.clone()
self.context.clone(),
));
}
}
@@ -1166,12 +1174,17 @@ impl ParseStream {
self.panic_mode(&mut node_tks);
let span = node_tks.get_span().unwrap();
let color = next_color();
return Err(self.make_err(span.clone(),
return Err(
self.make_err(
span.clone(),
Label::new(span)
.with_message(format!("Expected an expression after '{}'", prefix_keywrd.fg(color)))
.with_color(color)
));
.with_message(format!(
"Expected an expression after '{}'",
prefix_keywrd.fg(color)
))
.with_color(color),
),
);
};
node_tks.extend(cond.tokens.clone());
@@ -1180,7 +1193,7 @@ impl ParseStream {
return Err(parse_err_full(
&format!("Expected 'then' after '{prefix_keywrd}' condition"),
&node_tks.get_span().unwrap(),
self.context.clone()
self.context.clone(),
));
}
node_tks.push(self.next_tk().unwrap());
@@ -1196,7 +1209,7 @@ impl ParseStream {
return Err(parse_err_full(
"Expected an expression after 'then'",
&node_tks.get_span().unwrap(),
self.context.clone()
self.context.clone(),
));
};
let cond_node = CondNode {
@@ -1226,7 +1239,7 @@ impl ParseStream {
return Err(parse_err_full(
"Expected an expression after 'else'",
&node_tks.get_span().unwrap(),
self.context.clone()
self.context.clone(),
));
}
}
@@ -1237,7 +1250,7 @@ impl ParseStream {
return Err(parse_err_full(
"Expected 'fi' after if statement",
&node_tks.get_span().unwrap(),
self.context.clone()
self.context.clone(),
));
}
node_tks.push(self.next_tk().unwrap());
@@ -1293,7 +1306,7 @@ impl ParseStream {
return Err(parse_err_full(
"This for loop is missing a variable",
&node_tks.get_span().unwrap(),
self.context.clone()
self.context.clone(),
));
}
if arr.is_empty() {
@@ -1301,7 +1314,7 @@ impl ParseStream {
return Err(parse_err_full(
"This for loop is missing an array",
&node_tks.get_span().unwrap(),
self.context.clone()
self.context.clone(),
));
}
if !self.check_keyword("do") || !self.next_tk_is_some() {
@@ -1309,7 +1322,7 @@ impl ParseStream {
return Err(parse_err_full(
"Missing a 'do' for this for loop",
&node_tks.get_span().unwrap(),
self.context.clone()
self.context.clone(),
));
}
node_tks.push(self.next_tk().unwrap());
@@ -1325,7 +1338,7 @@ impl ParseStream {
return Err(parse_err_full(
"Missing a 'done' after this for loop",
&node_tks.get_span().unwrap(),
self.context.clone()
self.context.clone(),
));
}
node_tks.push(self.next_tk().unwrap());
@@ -1366,7 +1379,7 @@ impl ParseStream {
return Err(parse_err_full(
&format!("Expected an expression after '{loop_kind}'"), // It also implements Display
&node_tks.get_span().unwrap(),
self.context.clone()
self.context.clone(),
));
};
node_tks.extend(cond.tokens.clone());
@@ -1376,7 +1389,7 @@ impl ParseStream {
return Err(parse_err_full(
"Expected 'do' after loop condition",
&node_tks.get_span().unwrap(),
self.context.clone()
self.context.clone(),
));
}
node_tks.push(self.next_tk().unwrap());
@@ -1392,7 +1405,7 @@ impl ParseStream {
return Err(parse_err_full(
"Expected an expression after 'do'",
&node_tks.get_span().unwrap(),
self.context.clone()
self.context.clone(),
));
};
@@ -1402,7 +1415,7 @@ impl ParseStream {
return Err(parse_err_full(
"Expected 'done' after loop body",
&node_tks.get_span().unwrap(),
self.context.clone()
self.context.clone(),
));
}
node_tks.push(self.next_tk().unwrap());
@@ -1479,7 +1492,7 @@ impl ParseStream {
return Err(parse_err_full(
"Found case pattern in command",
&prefix_tk.span,
self.context.clone()
self.context.clone(),
));
}
let is_cmd = prefix_tk.flags.contains(TkFlags::IS_CMD);
@@ -1521,7 +1534,7 @@ impl ParseStream {
assignments_span.source().clone(),
Label::new(assignments_span)
.with_message("in variable assignment defined here".to_string())
.with_color(next_color())
.with_color(next_color()),
));
return Ok(Some(Node {
class: NdRule::Command { assignments, argv },
@@ -1745,10 +1758,7 @@ pub fn get_redir_file<P: AsRef<Path>>(class: RedirType, path: P) -> ShResult<Fil
.create(true)
.truncate(true)
.open(path),
RedirType::Append => OpenOptions::new()
.create(true)
.append(true)
.open(path),
RedirType::Append => OpenOptions::new().create(true).append(true).open(path),
_ => unimplemented!(),
};
Ok(result?)
@@ -1759,7 +1769,9 @@ fn parse_err_full(reason: &str, blame: &Span, context: LabelCtx) -> ShErr {
ShErr::new(ShErrKind::ParseErr, blame.clone())
.with_label(
blame.span_source().clone(),
Label::new(blame.clone()).with_message(reason).with_color(color)
Label::new(blame.clone())
.with_message(reason)
.with_color(color),
)
.with_context(context)
}

View File

@@ -1,20 +1,26 @@
use std::{
collections::HashSet, fmt::{Write,Debug}, path::PathBuf, sync::Arc,
collections::HashSet,
fmt::{Debug, Write},
path::PathBuf,
sync::Arc,
};
use nix::sys::signal::Signal;
use crate::{
builtin::complete::{CompFlags, CompOptFlags, CompOpts},
libsh::{
error::ShResult, guards::var_ctx_guard, sys::TTY_FILENO, utils::TkVecUtils
},
libsh::{error::ShResult, guards::var_ctx_guard, sys::TTY_FILENO, utils::TkVecUtils},
parse::{
execute::exec_input,
lex::{self, LexFlags, Tk, TkRule, ends_with_unescaped},
},
readline::{
Marker, annotate_input_recursive, keys::{KeyCode as C, KeyEvent as K, ModKeys as M}, linebuf::{ClampedUsize, LineBuf}, markers::{self, is_marker}, term::{LineWriter, TermWriter, calc_str_width, get_win_size}, vimode::{ViInsert, ViMode}
Marker, annotate_input_recursive,
keys::{KeyCode as C, KeyEvent as K, ModKeys as M},
linebuf::{ClampedUsize, LineBuf},
markers::{self, is_marker},
term::{LineWriter, TermWriter, calc_str_width, get_win_size},
vimode::{ViInsert, ViMode},
},
state::{VarFlags, VarKind, read_jobs, read_logic, read_meta, read_vars, write_vars},
};
@@ -224,9 +230,14 @@ fn complete_filename(start: &str) -> Vec<String> {
pub enum CompSpecResult {
NoSpec, // No compspec registered
NoMatch { flags: CompOptFlags }, /* Compspec found but no candidates matched, returns
NoMatch {
flags: CompOptFlags,
}, /* Compspec found but no candidates matched, returns
* behavior flags */
Match { result: CompResult, flags: CompOptFlags }, // Compspec found and candidates returned
Match {
result: CompResult,
flags: CompOptFlags,
}, // Compspec found and candidates returned
}
#[derive(Default, Debug, Clone)]
@@ -431,7 +442,8 @@ impl CompSpec for BashCompSpec {
if self.function.is_some() {
candidates.extend(self.exec_comp_func(ctx)?);
}
candidates = candidates.into_iter()
candidates = candidates
.into_iter()
.map(|c| {
let stripped = c.strip_prefix(&expanded).unwrap_or_default();
format!("{prefix}{stripped}")
@@ -513,11 +525,16 @@ pub enum CompResponse {
Passthrough, // key falls through
Accept(String), // user accepted completion
Dismiss, // user canceled completion
Consumed // key was handled, but completion remains active
Consumed, // key was handled, but completion remains active
}
pub trait Completer {
fn complete(&mut self, line: String, cursor_pos: usize, direction: i32) -> ShResult<Option<String>>;
fn complete(
&mut self,
line: String,
cursor_pos: usize,
direction: i32,
) -> ShResult<Option<String>>;
fn reset(&mut self);
fn reset_stay_active(&mut self);
fn is_active(&self) -> bool;
@@ -525,7 +542,9 @@ pub trait Completer {
fn token_span(&self) -> (usize, usize);
fn original_input(&self) -> &str;
fn draw(&mut self, writer: &mut TermWriter) -> ShResult<()>;
fn clear(&mut self, _writer: &mut TermWriter) -> ShResult<()> { Ok(()) }
fn clear(&mut self, _writer: &mut TermWriter) -> ShResult<()> {
Ok(())
}
fn set_prompt_line_context(&mut self, _line_width: u16, _cursor_col: u16) {}
fn handle_key(&mut self, key: K) -> ShResult<CompResponse>;
fn get_completed_line(&self, candidate: &str) -> String;
@@ -545,7 +564,10 @@ impl ScoredCandidate {
const PENALTY_GAP_EXTEND: i32 = 1;
pub fn new(content: String) -> Self {
Self { content, score: None }
Self {
content,
score: None,
}
}
fn is_word_bound(prev: char, curr: char) -> bool {
match prev {
@@ -583,7 +605,6 @@ impl ScoredCandidate {
score += Self::BONUS_FIRST_CHAR;
}
if idx == 0 || Self::is_word_bound(content_chars[idx - 1], content_chars[idx]) {
score += Self::BONUS_BOUNDARY;
}
@@ -605,7 +626,10 @@ impl ScoredCandidate {
impl From<String> for ScoredCandidate {
fn from(content: String) -> Self {
Self { content, score: None }
Self {
content,
score: None,
}
}
}
@@ -626,7 +650,7 @@ pub struct QueryEditor {
mode: ViInsert,
scroll_offset: usize,
available_width: usize,
linebuf: LineBuf
linebuf: LineBuf,
}
impl QueryEditor {
@@ -645,9 +669,16 @@ impl QueryEditor {
self.scroll_offset = self.linebuf.cursor.ret_sub(1);
}
if cursor_pos >= self.scroll_offset + self.available_width.saturating_sub(1) {
self.scroll_offset = self.linebuf.cursor.ret_sub(self.available_width.saturating_sub(1));
self.scroll_offset = self
.linebuf
.cursor
.ret_sub(self.available_width.saturating_sub(1));
}
let max_offset = self.linebuf.grapheme_indices().len().saturating_sub(self.available_width);
let max_offset = self
.linebuf
.grapheme_indices()
.len()
.saturating_sub(self.available_width);
self.scroll_offset = self.scroll_offset.min(max_offset);
}
pub fn get_window(&mut self) -> String {
@@ -656,19 +687,20 @@ impl QueryEditor {
if buf_len <= self.available_width {
return self.linebuf.as_str().to_string();
}
let start = self.scroll_offset.min(buf_len.saturating_sub(self.available_width));
let start = self
.scroll_offset
.min(buf_len.saturating_sub(self.available_width));
let end = (start + self.available_width).min(buf_len);
self.linebuf.slice(start..end).unwrap_or("").to_string()
}
pub fn handle_key(&mut self, key: K) -> ShResult<()> {
let Some(cmd) = self.mode.handle_key(key) else {
return Ok(())
return Ok(());
};
self.linebuf.exec_cmd(cmd)
}
}
#[derive(Clone, Debug)]
pub struct FuzzyCompleter {
completer: SimpleCompleter,
@@ -702,7 +734,6 @@ impl FuzzyCompleter {
//const TREE_TOP: &str = "\x1b[90m┬\x1b[0m";
//const CROSS: &str = "\x1b[90m┼\x1b[0m";
fn get_window(&mut self) -> &[ScoredCandidate] {
let height = self.filtered.len().min(self.max_height);
@@ -718,21 +749,21 @@ impl FuzzyCompleter {
if self.cursor.get() >= self.scroll_offset + height.saturating_sub(1) {
self.scroll_offset = self.cursor.ret_sub(height.saturating_sub(2));
}
self.scroll_offset = self.scroll_offset.min(self.filtered.len().saturating_sub(height));
self.scroll_offset = self
.scroll_offset
.min(self.filtered.len().saturating_sub(height));
}
pub fn score_candidates(&mut self) {
let mut scored: Vec<_> = self.candidates
let mut scored: Vec<_> = self
.candidates
.clone()
.into_iter()
.filter_map(|c| {
let mut sc = ScoredCandidate::new(c);
let score = sc.fuzzy_score(self.query.linebuf.as_str());
if score > i32::MIN {
Some(sc)
} else {
None
}
}).collect();
if score > i32::MIN { Some(sc) } else { None }
})
.collect();
scored.sort_by_key(|sc| sc.score.unwrap_or(i32::MIN));
scored.reverse();
self.cursor.set_max(scored.len());
@@ -785,7 +816,12 @@ impl Completer for FuzzyCompleter {
log::debug!("Completed line: {}", ret);
ret
}
fn complete(&mut self, line: String, cursor_pos: usize, direction: i32) -> ShResult<Option<String>> {
fn complete(
&mut self,
line: String,
cursor_pos: usize,
direction: i32,
) -> ShResult<Option<String>> {
self.completer.complete(line, cursor_pos, direction)?;
let candidates: Vec<_> = self.completer.candidates.clone();
if candidates.is_empty() {
@@ -806,28 +842,29 @@ impl Completer for FuzzyCompleter {
fn handle_key(&mut self, key: K) -> ShResult<CompResponse> {
match key {
K(C::Char('D'), M::CTRL) |
K(C::Esc, M::NONE) => {
K(C::Char('D'), M::CTRL) | K(C::Esc, M::NONE) => {
self.active = false;
self.filtered.clear();
Ok(CompResponse::Dismiss)
}
K(C::Enter, M::NONE) => {
self.active = false;
if let Some(selected) = self.filtered.get(self.cursor.get()).map(|c| c.content.clone()) {
if let Some(selected) = self
.filtered
.get(self.cursor.get())
.map(|c| c.content.clone())
{
Ok(CompResponse::Accept(selected))
} else {
Ok(CompResponse::Dismiss)
}
}
K(C::Tab, M::SHIFT) |
K(C::Up, M::NONE) => {
K(C::Tab, M::SHIFT) | K(C::Up, M::NONE) => {
self.cursor.wrap_sub(1);
self.update_scroll_offset();
Ok(CompResponse::Consumed)
}
K(C::Tab, M::NONE) |
K(C::Down, M::NONE) => {
K(C::Tab, M::NONE) | K(C::Down, M::NONE) => {
self.cursor.wrap_add(1);
self.update_scroll_offset();
Ok(CompResponse::Consumed)
@@ -895,14 +932,17 @@ impl Completer for FuzzyCompleter {
let mut buf = String::new();
let cursor_pos = self.cursor.get();
let offset = self.scroll_offset;
self.query.set_available_width(cols.saturating_sub(6) as usize);
self
.query
.set_available_width(cols.saturating_sub(6) as usize);
self.query.update_scroll_offset();
let query = self.query.get_window();
let num_filtered = format!("\x1b[33m{}\x1b[0m", self.filtered.len());
let num_candidates = format!("\x1b[33m{}\x1b[0m", self.candidates.len());
let visible = self.get_window();
let mut rows: u16 = 0;
let top_bar = format!("\n{}{} \x1b[1mComplete\x1b[0m {}{}",
let top_bar = format!(
"\n{}{} \x1b[1mComplete\x1b[0m {}{}",
Self::TOP_LEFT,
Self::HOR_LINE,
Self::HOR_LINE.repeat(cols.saturating_sub(13) as usize),
@@ -910,8 +950,7 @@ impl Completer for FuzzyCompleter {
);
buf.push_str(&top_bar);
rows += 1;
for _ in 0..rows {
}
for _ in 0..rows {}
let prompt = format!("{} {} {}", Self::VERT_LINE, Self::PROMPT_ARROW, &query);
let cols_used = calc_str_width(&prompt);
@@ -920,7 +959,8 @@ impl Completer for FuzzyCompleter {
buf.push_str(&prompt_line_final);
rows += 1;
let sep_line_left = format!("{}{}{}/{}",
let sep_line_left = format!(
"{}{}{}/{}",
Self::TREE_LEFT,
Self::HOR_LINE.repeat(2),
&num_filtered,
@@ -932,7 +972,6 @@ impl Completer for FuzzyCompleter {
buf.push_str(&sep_line_final);
rows += 1;
for (i, candidate) in visible.iter().enumerate() {
let selector = if i + offset == cursor_pos {
Self::SELECTOR_HL
@@ -945,11 +984,7 @@ impl Completer for FuzzyCompleter {
content.truncate(col_lim.saturating_sub(6) as usize); // ui bars + elipses length
content.push_str("...");
}
let left = format!("{} {}{}\x1b[0m",
Self::VERT_LINE,
&selector,
&content
);
let left = format!("{} {}{}\x1b[0m", Self::VERT_LINE, &selector, &content);
let cols_used = calc_str_width(&left);
let right_pad = " ".repeat(cols.saturating_sub(cols_used + 1) as usize);
let hl_cand_line = format!("{}{}{}", left, right_pad, Self::VERT_LINE);
@@ -957,9 +992,12 @@ impl Completer for FuzzyCompleter {
rows += 1;
}
let bot_bar = format!("{}{}{}",
let bot_bar = format!(
"{}{}{}",
Self::BOT_LEFT,
Self::HOR_LINE.to_string().repeat(cols.saturating_sub(2) as usize),
Self::HOR_LINE
.to_string()
.repeat(cols.saturating_sub(2) as usize),
Self::BOT_RIGHT
);
buf.push_str(&bot_bar);
@@ -967,7 +1005,12 @@ impl Completer for FuzzyCompleter {
// Move cursor back up to the prompt line (skip: separator + candidates + bottom border)
let lines_below_prompt = rows.saturating_sub(2); // total rows minus top_bar and prompt
let cursor_in_window = self.query.linebuf.cursor.get().saturating_sub(self.query.scroll_offset);
let cursor_in_window = self
.query
.linebuf
.cursor
.get()
.saturating_sub(self.query.scroll_offset);
let cursor_col = (cursor_in_window + 4) as u16; // "| > ".len() == 4
write!(buf, "\x1b[{}A\r\x1b[{}C", lines_below_prompt, cursor_col).unwrap();
@@ -993,7 +1036,10 @@ impl Completer for FuzzyCompleter {
self.active
}
fn selected_candidate(&self) -> Option<String> {
self.filtered.get(self.cursor.get()).map(|c| c.content.clone())
self
.filtered
.get(self.cursor.get())
.map(|c| c.content.clone())
}
fn original_input(&self) -> &str {
&self.completer.original_input
@@ -1020,7 +1066,12 @@ impl Completer for SimpleCompleter {
fn get_completed_line(&self, _candidate: &str) -> String {
self.get_completed_line()
}
fn complete(&mut self, line: String, cursor_pos: usize, direction: i32) -> ShResult<Option<String>> {
fn complete(
&mut self,
line: String,
cursor_pos: usize,
direction: i32,
) -> ShResult<Option<String>> {
if self.active {
Ok(Some(self.cycle_completion(direction)))
} else {
@@ -1295,7 +1346,8 @@ impl SimpleCompleter {
let (mut marker_ctx, token_start) = self.get_subtoken_completion(&line, cursor_pos);
if marker_ctx.last() == Some(&markers::VAR_SUB)
&& let Some(cur) = ctx.words.get(ctx.cword) {
&& let Some(cur) = ctx.words.get(ctx.cword)
{
self.token_span.0 = token_start;
let mut span = cur.span.clone();
span.set_range(token_start..self.token_span.1);

View File

@@ -26,7 +26,7 @@ pub struct Highlighter {
style_stack: Vec<StyleSet>,
last_was_reset: bool,
in_selection: bool,
only_hl_visual: bool
only_hl_visual: bool,
}
impl Highlighter {
@@ -39,7 +39,7 @@ impl Highlighter {
style_stack: Vec::new(),
last_was_reset: true, // start as true so we don't emit a leading reset
in_selection: false,
only_hl_visual: false
only_hl_visual: false,
}
}
pub fn only_visual(&mut self, only_visual: bool) {

View File

@@ -108,10 +108,12 @@ impl KeyEvent {
}
match event {
KeyCode::UnknownEscSeq => return Err(ShErr::simple(
KeyCode::UnknownEscSeq => {
return Err(ShErr::simple(
ShErrKind::ParseErr,
"Cannot convert unknown escape sequence to Vim key sequence".to_string(),
)),
));
}
KeyCode::Backspace => {
seq.push_str("BS");
needs_angle_bracket = true;

View File

@@ -1,5 +1,7 @@
use std::{
collections::HashSet, fmt::Display, ops::{Range, RangeInclusive}
collections::HashSet,
fmt::Display,
ops::{Range, RangeInclusive},
};
use unicode_segmentation::UnicodeSegmentation;
@@ -11,7 +13,10 @@ use super::vicmd::{
};
use crate::{
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::*,
readline::{
markers,
@@ -2367,7 +2372,7 @@ impl LineBuf {
} else {
MotionKind::On(self.grapheme_indices().len())
}
},
}
MotionCmd(_count, Motion::ToColumn) => todo!(),
MotionCmd(count, Motion::Range(start, end)) => {
let mut final_end = end;
@@ -2794,7 +2799,11 @@ impl LineBuf {
match content {
RegisterContent::Span(ref text) => {
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(),
};
self.insert_str_at(insert_idx, text);
@@ -2861,7 +2870,8 @@ impl LineBuf {
self.cursor.add(1);
let before = self.auto_indent_level;
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" | "}" => {
self.calc_indent_level();
@@ -3039,9 +3049,12 @@ impl LineBuf {
}
}
}
Verb::IncrementNumber(n) |
Verb::DecrementNumber(n) => {
let inc = if matches!(verb, Verb::IncrementNumber(_)) { n as i64 } else { -(n as i64) };
Verb::IncrementNumber(n) | Verb::DecrementNumber(n) => {
let inc = if matches!(verb, Verb::IncrementNumber(_)) {
n as i64
} else {
-(n as i64)
};
let (s, e) = self.select_range().unwrap_or(self.this_word(Word::Normal));
let end = if self.select_range().is_some() {
if e < self.grapheme_indices().len() - 1 {
@@ -3066,7 +3079,9 @@ impl LineBuf {
let width = body.len();
if let Ok(num) = i64::from_str_radix(body, 16) {
let new_num = num + inc;
self.buffer.replace_range(byte_start..byte_end, &format!("0x{new_num:0>width$x}"));
self
.buffer
.replace_range(byte_start..byte_end, &format!("0x{new_num:0>width$x}"));
self.update_graphemes();
self.cursor.set(s);
}
@@ -3075,7 +3090,9 @@ impl LineBuf {
let width = body.len();
if let Ok(num) = i64::from_str_radix(body, 2) {
let new_num = num + inc;
self.buffer.replace_range(byte_start..byte_end, &format!("0b{new_num:0>width$b}"));
self
.buffer
.replace_range(byte_start..byte_end, &format!("0b{new_num:0>width$b}"));
self.update_graphemes();
self.cursor.set(s);
}
@@ -3084,14 +3101,18 @@ impl LineBuf {
let width = body.len();
if let Ok(num) = i64::from_str_radix(body, 8) {
let new_num = num + inc;
self.buffer.replace_range(byte_start..byte_end, &format!("0o{new_num:0>width$o}"));
self
.buffer
.replace_range(byte_start..byte_end, &format!("0o{new_num:0>width$o}"));
self.update_graphemes();
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
.buffer
.replace_range(byte_start..byte_end, &format!("{new_num:0>width$}"));
self.update_graphemes();
self.cursor.set(s);
}
@@ -3120,15 +3141,28 @@ impl LineBuf {
let mut buf = self.as_str().to_string();
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| {
v.set_var("_BUFFER", VarKind::Str(buf.clone()), VarFlags::EXPORT)?;
v.set_var("_CURSOR", VarKind::Str(cursor.to_string()), VarFlags::EXPORT)?;
v.set_var("_ANCHOR", VarKind::Str(anchor.to_string()), VarFlags::EXPORT)
v.set_var(
"_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| {
buf = v.take_var("_BUFFER");
@@ -3141,7 +3175,12 @@ impl LineBuf {
self.update_graphemes();
self.cursor.set_max(self.buffer.graphemes(true).count());
self.cursor.set(cursor);
log::debug!("[ShellCmd] post-widget: cursor={}, anchor={}, select_range={:?}", cursor, anchor, self.select_range);
log::debug!(
"[ShellCmd] post-widget: cursor={}, anchor={}, select_range={:?}",
cursor,
anchor,
self.select_range
);
if anchor != cursor && self.select_range.is_some() {
self.select_range = Some(ordered(cursor, anchor));
}
@@ -3149,7 +3188,6 @@ impl LineBuf {
log::debug!("Pending widget keys from shell command: {keys}");
write_meta(|m| m.set_pending_widget_keys(&keys))
}
}
Verb::Normal(_)
| Verb::Read(_)

View File

@@ -1,7 +1,7 @@
use std::fmt::Write;
use history::History;
use keys::{KeyCode, KeyEvent, ModKeys};
use linebuf::{LineBuf, SelectAnchor, SelectMode};
use std::fmt::Write;
use term::{KeyReader, Layout, LineWriter, PollReader, TermWriter, get_win_size};
use unicode_width::UnicodeWidthStr;
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::utils::AutoCmdVecUtils;
use crate::parse::lex::{LexStream, QuoteState};
use crate::{prelude::*, state};
use crate::readline::complete::FuzzyCompleter;
use crate::readline::term::{Pos, TermReader, calc_str_width};
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::{
libsh::error::ShResult,
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 highlight;
@@ -208,11 +213,11 @@ impl Prompt {
if let Ok(expanded) = expand_prompt(&self.ps1_raw) {
self.ps1_expanded = expanded;
}
if let Some(psr_raw) = &self.psr_raw {
if let Ok(expanded) = expand_prompt(psr_raw) {
if let Some(psr_raw) = &self.psr_raw
&& let Ok(expanded) = expand_prompt(psr_raw)
{
self.psr_expanded = Some(expanded);
}
}
state::set_status(saved_status);
self.dirty = false;
}
@@ -276,7 +281,13 @@ impl ShedVi {
history: History::new()?,
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| {
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.print_line(false)?;
@@ -303,7 +314,9 @@ impl ShedVi {
}
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<()> {
@@ -449,7 +462,9 @@ impl ShedVi {
}
self.needs_redraw = true;
continue;
} else if matches.len() == 1 && matches[0].compare(&self.pending_keymap) == KeyMapMatch::IsExact {
} 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();
@@ -467,7 +482,6 @@ impl ShedVi {
}
}
if let Some(event) = self.handle_key(key)? {
return Ok(event);
}
@@ -543,7 +557,8 @@ impl ShedVi {
}
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 = false;
@@ -569,7 +584,8 @@ impl ShedVi {
if cmd.is_submit_action()
&& !self.next_is_escaped
&& !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.cursor.set(self.editor.cursor_max());
self.print_line(true)?;
@@ -694,7 +710,9 @@ impl ShedVi {
let hint = self.editor.get_hint_text();
let do_hl = state::read_shopts(|s| s.prompt.highlight);
self.highlighter.only_visual(!do_hl);
self.highlighter.load_input(&line, self.editor.cursor_byte_pos());
self
.highlighter
.load_input(&line, self.editor.cursor_byte_pos());
self.highlighter.expand_control_chars();
self.highlighter.highlight();
let highlighted = self.highlighter.take();
@@ -753,7 +771,8 @@ impl ShedVi {
&& !seq.is_empty()
&& !(prompt_string_right.is_some() && one_line)
&& 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 up = new_layout.cursor.row; // rows to move up from cursor to top line of prompt
@@ -768,7 +787,8 @@ impl ShedVi {
write!(buf, "\x1b7{move_up}\x1b[{to_col}G{seq}\x1b8").unwrap();
} else if !final_draw
&& let Some(psr) = prompt_string_right
&& psr_fits {
&& psr_fits
{
let to_col = self.writer.t_cols - calc_str_width(&psr);
let down = new_layout.end.row - new_layout.cursor.row;
let move_down = if down > 0 {
@@ -781,8 +801,17 @@ impl ShedVi {
// Record where the PSR ends so clear_rows can account for wrapping
// if the terminal shrinks.
let psr_start = Pos { 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));
let psr_start = Pos {
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() {
@@ -803,7 +832,9 @@ impl ShedVi {
// Without PSR, use the content width on the cursor's row
(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.old_layout = Some(new_layout);
@@ -821,7 +852,14 @@ impl ShedVi {
std::mem::swap(&mut self.mode, mode);
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| {
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));
@@ -834,7 +872,11 @@ impl ShedVi {
if cmd.is_mode_transition() {
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!(
self.mode.report_mode(),
ModeReport::Ex | ModeReport::Verbatim
) && cmd.flags.contains(CmdFlags::EXIT_CUR_MODE)
{
if let Some(saved) = self.saved_mode.take() {
saved
} else {
@@ -847,13 +889,9 @@ impl ShedVi {
Box::new(ViInsert::new().with_count(count as u16))
}
Verb::ExMode => {
Box::new(ViEx::new())
}
Verb::ExMode => Box::new(ViEx::new()),
Verb::VerbatimMode => {
Box::new(ViVerbatim::new().with_count(count as u16))
}
Verb::VerbatimMode => Box::new(ViVerbatim::new().with_count(count as u16)),
Verb::NormalMode => Box::new(ViNormal::new()),
@@ -885,9 +923,18 @@ impl ShedVi {
self.swap_mode(&mut mode);
if matches!(self.mode.report_mode(), ModeReport::Ex | ModeReport::Verbatim) {
if matches!(
self.mode.report_mode(),
ModeReport::Ex | ModeReport::Verbatim
) {
self.saved_mode = Some(mode);
write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(self.mode.report_mode().to_string()), VarFlags::NONE))?;
write_vars(|v| {
v.set_var(
"SHED_VI_MODE",
VarKind::Str(self.mode.report_mode().to_string()),
VarFlags::NONE,
)
})?;
self.prompt.refresh();
return Ok(());
}
@@ -912,10 +959,15 @@ impl ShedVi {
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| {
v.set_var(
"SHED_VI_MODE",
VarKind::Str(self.mode.report_mode().to_string()),
VarFlags::NONE,
)
})?;
self.prompt.refresh();
return Ok(());
} else if cmd.is_cmd_repeat() {
let Some(replay) = self.repeat_action.clone() else {
@@ -989,8 +1041,7 @@ impl ShedVi {
}
}
if self.mode.report_mode() == ModeReport::Visual
&& self.editor.select_range().is_none() {
if self.mode.report_mode() == ModeReport::Visual && self.editor.select_range().is_none() {
self.editor.stop_selecting();
let mut mode: Box<dyn ViMode> = Box::new(ViNormal::new());
self.swap_mode(&mut mode);
@@ -1026,7 +1077,10 @@ impl ShedVi {
}
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!(
self.mode.report_mode(),
ModeReport::Ex | ModeReport::Verbatim
) {
if let Some(saved) = self.saved_mode.take() {
saved
} else {
@@ -1069,7 +1123,6 @@ pub fn annotate_input(input: &str) -> String {
.filter(|tk| !matches!(tk.class, TkRule::SOI | TkRule::EOI | TkRule::Null))
.collect();
for tk in tokens.into_iter().rev() {
let insertions = annotate_token(tk);
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) {
Some('>') => ">(",
Some('<') => "<(",
_ => {
"<("
}
_ => "<(",
},
markers::CMD_SUB => "$(",
markers::SUBSH => "(",
@@ -1256,7 +1307,8 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
let mut insertions: Vec<(usize, Marker)> = vec![];
if token.class != TkRule::Str
&& let Some(marker) = marker_for(&token.class) {
&& let Some(marker) = marker_for(&token.class)
{
insertions.push((token.span.range().end, markers::RESET));
insertions.push((token.span.range().start, marker));
return insertions;
@@ -1350,7 +1402,8 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
|| *br_ch == '='
|| *br_ch == '/' // parameter expansion symbols
|| *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();
} else if *br_ch == '}' {
@@ -1371,7 +1424,8 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
while let Some((cur_i, var_ch)) = token_chars.peek() {
if var_ch.is_ascii_alphanumeric()
|| ShellParam::from_char(var_ch).is_some()
|| *var_ch == '_' {
|| *var_ch == '_'
{
end_pos = *cur_i + 1;
token_chars.next();
} else {

View File

@@ -3,7 +3,8 @@ use std::{
env,
fmt::{Debug, Write},
io::{BufRead, BufReader, Read},
os::fd::{AsFd, BorrowedFd, RawFd}, sync::Arc,
os::fd::{AsFd, BorrowedFd, RawFd},
sync::Arc,
};
use nix::{
@@ -17,14 +18,12 @@ use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
use vte::{Parser, Perform};
pub use crate::libsh::guards::{RawModeGuard, raw_mode};
use crate::state::{read_meta, write_meta};
use crate::{
libsh::error::{ShErr, ShErrKind, ShResult},
readline::keys::{KeyCode, ModKeys},
state::read_shopts,
};
use crate::{
state::{read_meta, write_meta},
};
use super::keys::KeyEvent;
@@ -472,7 +471,7 @@ impl Perform for KeyCollector {
if let Some(ch) = char::from_u32(codepoint as u32) {
KeyCode::Char(ch)
} else {
return
return;
}
}
};
@@ -518,7 +517,10 @@ impl PollReader {
pub fn feed_bytes(&mut self, bytes: &[u8], verbatim: bool) {
if verbatim {
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(
KeyCode::Verbatim(Arc::from(seq.as_str())),
ModKeys::empty(),
));
} else if bytes == [b'\x1b'] {
// Single escape byte - user pressed ESC key
self
@@ -749,11 +751,7 @@ impl Layout {
}
fn is_ctl_char(gr: &str) -> bool {
gr.len() > 0 &&
gr.as_bytes()[0] <= 0x1F &&
gr != "\n" &&
gr != "\t" &&
gr != "\r"
!gr.is_empty() && 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 {
@@ -900,7 +898,9 @@ impl TermWriter {
let row = read_digits_until(rdr, ';')?;
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
&& let Some(col) = col
{
Some((row as usize, col as usize))
} else {
None

View File

@@ -9,8 +9,8 @@ use crate::libsh::error::{ShErr, ShErrKind, ShResult};
use crate::readline::keys::KeyEvent;
use crate::readline::linebuf::LineBuf;
use crate::readline::vicmd::{
Anchor, CmdFlags, Motion, MotionCmd, ReadSrc, RegisterName, To, Val, Verb, VerbCmd,
ViCmd, WriteDest,
Anchor, CmdFlags, Motion, MotionCmd, ReadSrc, RegisterName, To, Val, Verb, VerbCmd, ViCmd,
WriteDest,
};
use crate::readline::vimode::{ModeReport, ViInsert, ViMode};
use crate::state::write_meta;
@@ -29,11 +29,10 @@ bitflags! {
}
}
#[derive(Default, Clone, Debug)]
struct ExEditor {
buf: LineBuf,
mode: ViInsert
mode: ViInsert,
}
impl ExEditor {
@@ -42,13 +41,12 @@ impl ExEditor {
}
pub fn handle_key(&mut self, key: KeyEvent) -> ShResult<()> {
let Some(cmd) = self.mode.handle_key(key) else {
return Ok(())
return Ok(());
};
self.buf.exec_cmd(cmd)
}
}
#[derive(Default, Clone, Debug)]
pub struct ViEx {
pending_cmd: ExEditor,
@@ -63,11 +61,10 @@ impl ViEx {
impl ViMode for ViEx {
// 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>> {
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);
match key {
E(C::Char('\r'), M::NONE) |
E(C::Enter, M::NONE) => {
E(C::Char('\r'), M::NONE) | E(C::Enter, M::NONE) => {
let input = self.pending_cmd.buf.as_str();
log::debug!("[ViEx] Enter pressed, pending_cmd={:?}", input);
match parse_ex_cmd(input) {
@@ -149,7 +146,7 @@ impl ViMode for ViEx {
fn parse_ex_cmd(raw: &str) -> Result<Option<ViCmd>, Option<String>> {
let raw = raw.trim();
if raw.is_empty() {
return Ok(None)
return Ok(None);
}
let mut chars = raw.chars().peekable();
let (verb, motion) = {
@@ -160,13 +157,15 @@ fn parse_ex_cmd(raw: &str) -> Result<Option<ViCmd>,Option<String>> {
cmd_name.push(*ch);
chars.next();
} else {
break
break;
}
}
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 {
return Ok(None);
};
(Some(VerbCmd(1, result.1)), Some(MotionCmd(1, result.0)))
} else {
(parse_ex_command(&mut chars)?.map(|v| VerbCmd(1, v)), None)
@@ -211,9 +210,9 @@ fn parse_ex_command(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>,Opti
if ch == &'!' {
cmd_name.push(*ch);
chars.next();
break
break;
} else if !ch.is_alphanumeric() {
break
break;
}
cmd_name.push(*ch);
chars.next();
@@ -232,25 +231,36 @@ fn parse_ex_command(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>,Opti
_ if "read".starts_with(&cmd_name) => parse_read(chars),
_ if "write".starts_with(&cmd_name) => parse_write(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>> {
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();
Ok(Some(Verb::Normal(seq)))
}
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(&'!') {
chars.next();
true
} else {
false
};
let arg: String = chars.collect();
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 {
@@ -263,18 +273,22 @@ fn parse_read(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>,Option<Str
fn get_path(path: &str) -> PathBuf {
if let Some(stripped) = path.strip_prefix("~/")
&& let Some(home) = std::env::var_os("HOME") {
return PathBuf::from(home).join(stripped)
&& let Some(home) = std::env::var_os("HOME")
{
return PathBuf::from(home).join(stripped);
}
if path == "~"
&& let Some(home) = std::env::var_os("HOME") {
return PathBuf::from(home)
&& let Some(home) = std::env::var_os("HOME")
{
return PathBuf::from(home);
}
PathBuf::from(path)
}
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(&'!');
if is_shell_write {
@@ -303,19 +317,26 @@ fn parse_write(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>,Option<St
}
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 {
return Ok(Some((Motion::Null,Verb::RepeatGlobal)))
return Ok(Some((Motion::Null, Verb::RepeatGlobal)));
};
if delimiter.is_alphanumeric() {
return Err(None)
return Err(None);
}
let global_pat = parse_pattern(chars, delimiter)?;
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 {
Ok(Some((Motion::NotGlobal(Val::new_str(global_pat)), command)))
@@ -325,13 +346,15 @@ fn parse_global(chars: &mut Peekable<Chars<'_>>) -> Result<Option<(Motion,Verb)>
}
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 {
return Ok(Some(Verb::RepeatSubstitute))
return Ok(Some(Verb::RepeatSubstitute));
};
if delimiter.is_alphanumeric() {
return Err(None)
return Err(None);
}
let old_pat = parse_pattern(chars, delimiter)?;
let new_pat = parse_pattern(chars, delimiter)?;
@@ -342,13 +365,16 @@ fn parse_substitute(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>,Opti
'i' => flags |= SubFlags::IGNORE_CASE,
'I' => flags |= SubFlags::NO_IGNORE_CASE,
'n' => flags |= SubFlags::SHOW_COUNT,
_ => return Err(None)
_ => return Err(None),
}
}
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(
chars: &mut Peekable<Chars<'_>>,
delimiter: char,
) -> Result<String, Option<String>> {
let mut pat = String::new();
let mut closed = false;
while let Some(ch) = chars.next() {
@@ -357,7 +383,7 @@ fn parse_pattern(chars: &mut Peekable<Chars<'_>>, delimiter: char) -> Result<Str
if chars.peek().is_some_and(|c| *c == delimiter) {
// We escaped the delimiter, so we consume the escape char and continue
pat.push(chars.next().unwrap());
continue
continue;
} else {
// The escape char is probably for the regex in the pattern
pat.push(ch);
@@ -368,9 +394,9 @@ fn parse_pattern(chars: &mut Peekable<Chars<'_>>, delimiter: char) -> Result<Str
}
_ if ch == delimiter => {
closed = true;
break
break;
}
_ => pat.push(ch)
_ => pat.push(ch),
}
}
if !closed {

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::vicmd::{
Direction, Motion, MotionCmd, To, Verb, VerbCmd, ViCmd, Word,
};
use crate::readline::vicmd::{Direction, Motion, MotionCmd, To, Verb, VerbCmd, ViCmd, Word};
#[derive(Default, Clone, Debug)]
pub struct ViInsert {

View File

@@ -4,23 +4,21 @@ use unicode_segmentation::UnicodeSegmentation;
use crate::libsh::error::ShResult;
use crate::readline::keys::{KeyCode as K, KeyEvent as E, ModKeys as M};
use crate::readline::vicmd::{
Motion, MotionCmd, To, Verb, VerbCmd, ViCmd,
};
use crate::readline::vicmd::{Motion, MotionCmd, To, Verb, VerbCmd, ViCmd};
pub mod ex;
pub mod insert;
pub mod normal;
pub mod replace;
pub mod visual;
pub mod ex;
pub mod verbatim;
pub mod visual;
pub use ex::ViEx;
pub use insert::ViInsert;
pub use normal::ViNormal;
pub use replace::ViReplace;
pub use visual::ViVisual;
pub use verbatim::ViVerbatim;
pub use visual::ViVisual;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ModeReport {
@@ -73,13 +71,17 @@ pub enum CmdState {
}
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 is_repeatable(&self) -> bool;
fn as_replay(&self) -> Option<CmdReplay>;
fn cursor_style(&self) -> 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 clamp_cursor(&self) -> bool;
fn hist_scroll_start_pos(&self) -> Option<To>;

View File

@@ -1,7 +1,7 @@
use std::iter::Peekable;
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::vicmd::{
Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb,
@@ -316,7 +316,7 @@ impl ViNormal {
motion: None,
raw_seq: self.take_cmd(),
flags: self.flags(),
})
});
}
'i' => {
return Some(ViCmd {
@@ -757,7 +757,9 @@ impl ViMode for ViNormal {
flags: self.flags(),
}),
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
.parse_count(&mut self.pending_seq.chars().peekable())
.unwrap_or(1) as u16;
self.pending_seq.clear();
Some(ViCmd {
register: Default::default(),
@@ -766,9 +768,11 @@ impl ViMode for ViNormal {
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;
let count = self
.parse_count(&mut self.pending_seq.chars().peekable())
.unwrap_or(1) as u16;
self.pending_seq.clear();
Some(ViCmd {
register: Default::default(),
@@ -777,7 +781,7 @@ impl ViMode for ViNormal {
raw_seq: "".into(),
flags: self.flags(),
})
},
}
E(K::Char(ch), M::NONE) => self.try_parse(ch),
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::vicmd::{
Direction, Motion, MotionCmd, To, Verb, VerbCmd, ViCmd, Word,
};
use crate::readline::vicmd::{Direction, Motion, MotionCmd, To, Verb, VerbCmd, ViCmd, Word};
#[derive(Default, Debug)]
pub struct ViReplace {

View File

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

View File

@@ -1,7 +1,7 @@
use std::iter::Peekable;
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::vicmd::{
Anchor, Bound, CmdFlags, Dest, Direction, Motion, MotionCmd, RegisterName, TextObj, To, Verb,
@@ -136,7 +136,7 @@ impl ViVisual {
motion: None,
raw_seq: self.take_cmd(),
flags: CmdFlags::empty(),
})
});
}
'x' => {
chars = chars_clone;
@@ -615,7 +615,9 @@ impl ViMode for ViVisual {
flags: CmdFlags::empty(),
}),
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
.parse_count(&mut self.pending_seq.chars().peekable())
.unwrap_or(1) as u16;
self.pending_seq.clear();
Some(ViCmd {
register: Default::default(),
@@ -624,9 +626,11 @@ impl ViMode for ViVisual {
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;
let count = self
.parse_count(&mut self.pending_seq.chars().peekable())
.unwrap_or(1) as u16;
self.pending_seq.clear();
Some(ViCmd {
register: Default::default(),

View File

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

View File

@@ -148,7 +148,6 @@ pub fn sig_setup(is_login: bool) {
sigaction(Signal::SIGSYS, &action).unwrap();
}
if is_login {
let _ = setpgid(Pid::from_raw(0), Pid::from_raw(0));
take_term().ok();
@@ -318,7 +317,8 @@ pub fn child_exited(pid: Pid, status: WtStat) -> ShResult<()> {
for cmd in post_job_hooks {
let AutoCmd { pattern, command } = cmd;
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;
}

View File

@@ -12,14 +12,30 @@ use nix::unistd::{User, gethostname, getppid};
use regex::Regex;
use crate::{
builtin::{BUILTINS, keymap::{KeyMap, KeyMapFlags, KeyMapMatch}, map::MapNode, trap::TrapTarget}, exec_input, expand::expand_keymap, jobs::JobTab, libsh::{
error::{ShErr, ShErrKind, ShResult}, utils::VecDequeExt
}, parse::{
builtin::{
BUILTINS,
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,
lex::{LexFlags, LexStream, Span, Tk},
}, prelude::*, readline::{
complete::{BashCompSpec, CompSpec}, keys::KeyEvent, markers
}, shopt::ShOpts
},
prelude::*,
readline::{
complete::{BashCompSpec, CompSpec},
keys::KeyEvent,
markers,
},
shopt::ShOpts,
};
pub struct Shed {
@@ -299,10 +315,12 @@ impl ScopeStack {
{
match var.kind_mut() {
VarKind::Arr(items) => return Ok(items),
_ => return Err(ShErr::simple(
_ => {
return Err(ShErr::simple(
ShErrKind::ExecFail,
format!("Variable '{}' is not an array", var_name),
)),
));
}
}
}
}
@@ -369,7 +387,7 @@ impl ScopeStack {
pub fn get_map(&self, map_name: &str) -> Option<&MapNode> {
for scope in self.scopes.iter().rev() {
if let Some(map) = scope.get_map(map_name) {
return Some(map)
return Some(map);
}
}
None
@@ -377,14 +395,13 @@ impl ScopeStack {
pub fn get_map_mut(&mut self, map_name: &str) -> Option<&mut MapNode> {
for scope in self.scopes.iter_mut().rev() {
if let Some(map) = scope.get_map_mut(map_name) {
return Some(map)
return Some(map);
}
}
None
}
pub fn set_map(&mut self, map_name: &str, map: MapNode, local: bool) {
if local
&& let Some(scope) = self.scopes.last_mut() {
if local && let Some(scope) = self.scopes.last_mut() {
scope.set_map(map_name, map);
} else if let Some(scope) = self.scopes.first_mut() {
scope.set_map(map_name, map);
@@ -481,7 +498,7 @@ thread_local! {
#[derive(Clone, Debug)]
pub struct ShAlias {
pub body: String,
pub source: Span
pub source: Span,
}
impl Display for ShAlias {
@@ -496,7 +513,7 @@ impl Display for ShAlias {
#[derive(Clone, Debug)]
pub struct ShFunc {
pub body: Node,
pub source: Span
pub source: Span,
}
impl ShFunc {
@@ -533,7 +550,7 @@ pub enum AutoCmdKind {
PostPrompt,
PreModeChange,
PostModeChange,
OnExit
OnExit,
}
impl Display for AutoCmdKind {
@@ -590,7 +607,7 @@ pub struct LogTab {
aliases: HashMap<String, ShAlias>,
traps: HashMap<TrapTarget, String>,
keymaps: Vec<KeyMap>,
autocmds: HashMap<AutoCmdKind, Vec<AutoCmd>>
autocmds: HashMap<AutoCmdKind, Vec<AutoCmd>>,
}
impl LogTab {
@@ -635,7 +652,8 @@ impl LogTab {
self.keymaps.retain(|km| km.keys != keys);
}
pub fn keymaps_filtered(&self, flags: KeyMapFlags, pending: &[KeyEvent]) -> Vec<KeyMap> {
self.keymaps
self
.keymaps
.iter()
.filter(|km| km.flags.intersects(flags) && km.compare(pending) != KeyMapMatch::NoMatch)
.cloned()
@@ -666,7 +684,13 @@ impl LogTab {
&self.aliases
}
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> {
self.aliases.get(name).cloned()
@@ -896,7 +920,7 @@ pub struct VarTab {
params: HashMap<ShellParam, String>,
sh_argv: VecDeque<String>, /* Using a VecDeque makes the implementation of `shift` straightforward */
maps: HashMap<String, MapNode>
maps: HashMap<String, MapNode>,
}
impl VarTab {
@@ -1217,7 +1241,7 @@ pub struct MetaTab {
comp_specs: HashMap<String, Box<dyn CompSpec>>,
// pending keys from widget function
pending_widget_keys: Vec<KeyEvent>
pending_widget_keys: Vec<KeyEvent>,
}
impl MetaTab {
@@ -1593,8 +1617,8 @@ pub fn with_vars<F,H,V,T>(vars: H, f: F) -> T
where
F: FnOnce() -> T,
H: Into<HashMap<String, V>>,
V: Into<Var> {
V: Into<Var>,
{
let snapshot = read_vars(|v| v.clone());
let vars = vars.into();
for (name, val) in vars {
@@ -1614,36 +1638,59 @@ pub fn change_dir<P: AsRef<Path>>(dir: P) -> ShResult<()> {
let post_cd = read_logic(|l| l.get_autocmds(AutoCmdKind::PostChangeDir));
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(
[
("_NEW_DIR".into(), dir_raw.as_str()),
("_OLD_DIR".into(), current_dir.as_str()),
],
|| {
for cmd in pre_cd {
let AutoCmd { command, pattern } = cmd;
if let Some(pat) = pattern
&& !pat.is_match(dir_raw) {
&& !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(
command.clone(),
None,
false,
Some("autocmd (pre-changedir)".to_string()),
) {
e.print_error();
};
}
});
},
);
env::set_current_dir(dir)?;
with_vars([("_NEW_DIR".into(), dir_raw.as_str()), ("_OLD_DIR".into(), current_dir.as_str())], || {
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) {
&& !pat.is_match(dir_raw)
{
continue;
}
if let Err(e) = exec_input(command.clone(), None, false, Some("autocmd (post-changedir)".to_string())) {
if let Err(e) = exec_input(
command.clone(),
None,
false,
Some("autocmd (post-changedir)".to_string()),
) {
e.print_error();
};
}
});
},
);
Ok(())
}