rustfmt'd the codebase
This commit is contained in:
@@ -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))
|
||||
}
|
||||
|
||||
@@ -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,11 +60,13 @@ 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;
|
||||
|
||||
for (arg,_) in argv {
|
||||
for (arg, _) in argv {
|
||||
for _ in 0..arr_op_opts.count {
|
||||
let pop = |arr: &mut std::collections::VecDeque<String>| match end {
|
||||
End::Front => arr.pop_front(),
|
||||
@@ -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),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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] {
|
||||
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),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -55,18 +63,28 @@ pub fn autocmd(node: Node) -> ShResult<()> {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let (argv,opts) = get_opts_from_tokens(argv, &autocmd_optspec()).promote_err(span.clone())?;
|
||||
let (argv, opts) = get_opts_from_tokens(argv, &autocmd_optspec()).promote_err(span.clone())?;
|
||||
let autocmd_opts = get_autocmd_opts(&opts).promote_err(span.clone())?;
|
||||
let 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 {
|
||||
|
||||
@@ -20,37 +20,55 @@ 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))
|
||||
let (new_dir, arg_span) = if let Some((arg, span)) = argv.into_iter().next() {
|
||||
(PathBuf::from(arg), Some(span))
|
||||
} else {
|
||||
(PathBuf::from(env::var("HOME").unwrap()),None)
|
||||
(PathBuf::from(env::var("HOME").unwrap()), None)
|
||||
};
|
||||
|
||||
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) };
|
||||
|
||||
|
||||
@@ -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),
|
||||
));
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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}"),
|
||||
}
|
||||
))
|
||||
);
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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}"))),
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -23,26 +30,32 @@ pub fn type_builtin(node: Node) -> ShResult<()> {
|
||||
* 3. command
|
||||
*/
|
||||
|
||||
'outer: for (arg,span) in argv {
|
||||
'outer: for (arg, span) in argv {
|
||||
if let Some(func) = read_logic(|v| v.get_func(&arg)) {
|
||||
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())
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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() {
|
||||
@@ -45,31 +59,31 @@ impl KeyMapOpts {
|
||||
}
|
||||
Ok(Self { remove, flags })
|
||||
}
|
||||
pub fn keymap_opts() -> [OptSpec;6] {
|
||||
pub fn keymap_opts() -> [OptSpec; 6] {
|
||||
[
|
||||
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()));
|
||||
let Some((keys, _)) = argv.first() else {
|
||||
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()));
|
||||
let Some((action, _)) = argv.get(1) else {
|
||||
return Err(ShErr::at(
|
||||
ShErrKind::ExecFail,
|
||||
span,
|
||||
"missing action argument".to_string(),
|
||||
));
|
||||
};
|
||||
|
||||
let keymap = KeyMap {
|
||||
|
||||
@@ -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())
|
||||
})
|
||||
.collect::<Map<String,Value>>();
|
||||
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,
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -257,7 +256,7 @@ pub fn map(node: Node) -> ShResult<()> {
|
||||
}
|
||||
|
||||
for arg in argv {
|
||||
if let Some((lhs,rhs)) = split_tk_at(&arg, "=") {
|
||||
if let Some((lhs, rhs)) = split_tk_at(&arg, "=") {
|
||||
let path = split_tk(&lhs, ".")
|
||||
.into_iter()
|
||||
.map(|s| s.expand().map(|exp| exp.get_words().join(" ")))
|
||||
@@ -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
|
||||
|
||||
@@ -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<()> {
|
||||
|
||||
@@ -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] = [
|
||||
@@ -40,19 +49,19 @@ pub const READ_OPTS: [OptSpec; 7] = [
|
||||
}, // read until delimiter
|
||||
];
|
||||
|
||||
pub const READ_KEY_OPTS: [OptSpec;3] = [
|
||||
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}'"),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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())?;
|
||||
|
||||
@@ -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)?;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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")),
|
||||
}
|
||||
}
|
||||
@@ -1390,7 +1403,7 @@ impl FromStr for ParamExp {
|
||||
Err(ShErr::simple(
|
||||
ShErrKind::SyntaxErr,
|
||||
"Invalid parameter expansion",
|
||||
) )
|
||||
))
|
||||
};
|
||||
|
||||
log::debug!("Parsing parameter expansion: '{:?}'", s);
|
||||
@@ -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))
|
||||
|
||||
41
src/jobs.rs
41
src/jobs.rs
@@ -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;
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
|
||||
|
||||
44
src/main.rs
44
src/main.rs
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -274,7 +308,7 @@ impl Dispatcher {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let func = ShFunc::new(func_parser,blame);
|
||||
let func = ShFunc::new(func_parser, blame);
|
||||
write_logic(|l| l.insert_func(name, func)); // Store the AST
|
||||
Ok(())
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -25,12 +25,12 @@ pub const KEYWORDS: [&str; 16] = [
|
||||
pub const OPENERS: [&str; 6] = ["if", "while", "until", "for", "select", "case"];
|
||||
|
||||
/// Used to track whether the lexer is currently inside a quote, and if so, which type
|
||||
#[derive(Default,Debug)]
|
||||
#[derive(Default, Debug)]
|
||||
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 {
|
||||
@@ -111,7 +114,7 @@ impl Span {
|
||||
self.source.name = name;
|
||||
self
|
||||
}
|
||||
pub fn line_and_col(&self) -> (usize,usize) {
|
||||
pub fn line_and_col(&self) -> (usize, usize) {
|
||||
let content = self.source.content();
|
||||
let source = ariadne::Source::from(content.as_str());
|
||||
let (_, line, col) = source.get_byte_line(self.range.start).unwrap();
|
||||
@@ -988,13 +991,16 @@ pub fn split_all_unescaped(slice: &str, pat: &str) -> Vec<String> {
|
||||
|
||||
/// Splits a string at the first occurrence of a pattern, but only if the pattern is not escaped by a backslash
|
||||
/// and not in quotes. Returns None if the pattern is not found or only found escaped.
|
||||
pub fn split_at_unescaped(slice: &str, pat: &str) -> Option<(String,String)> {
|
||||
pub fn split_at_unescaped(slice: &str, pat: &str) -> Option<(String, String)> {
|
||||
let mut chars = slice.char_indices().peekable();
|
||||
let mut qt_state = QuoteState::default();
|
||||
|
||||
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));
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -890,19 +927,22 @@ impl Completer for FuzzyCompleter {
|
||||
if !self.active {
|
||||
return Ok(());
|
||||
}
|
||||
let (cols,_) = get_win_size(*TTY_FILENO);
|
||||
let (cols, _) = get_win_size(*TTY_FILENO);
|
||||
|
||||
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 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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
@@ -828,7 +833,7 @@ impl LineBuf {
|
||||
(start, end)
|
||||
}
|
||||
pub fn this_line_content(&mut self) -> Option<&str> {
|
||||
let (start,end) = self.this_line_exclusive();
|
||||
let (start, end) = self.this_line_exclusive();
|
||||
self.slice(start..end)
|
||||
}
|
||||
pub fn this_line(&mut self) -> (usize, usize) {
|
||||
@@ -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(_)
|
||||
|
||||
@@ -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 {
|
||||
@@ -1399,7 +1453,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
|
||||
}
|
||||
'\\' if qt_state.in_single() => {
|
||||
token_chars.next();
|
||||
if let Some(&(_,'\'')) = token_chars.peek() {
|
||||
if let Some(&(_, '\'')) = token_chars.peek() {
|
||||
token_chars.next(); // consume the escaped single quote
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -457,7 +456,7 @@ impl Perform for KeyCollector {
|
||||
};
|
||||
KeyEvent(key, mods)
|
||||
}
|
||||
([],'u') => {
|
||||
([], 'u') => {
|
||||
let codepoint = params.first().copied().unwrap_or(0);
|
||||
let mods = params
|
||||
.get(1)
|
||||
@@ -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 {
|
||||
@@ -871,7 +869,7 @@ impl TermWriter {
|
||||
///
|
||||
/// Aping zsh with this but it's a nice feature.
|
||||
pub fn fix_cursor_column(&mut self, rdr: &mut TermReader) -> ShResult<()> {
|
||||
let Some((_,c)) = self.get_cursor_pos(rdr)? else {
|
||||
let Some((_, c)) = self.get_cursor_pos(rdr)? else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
@@ -146,10 +143,10 @@ impl ViMode for ViEx {
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_ex_cmd(raw: &str) -> Result<Option<ViCmd>,Option<String>> {
|
||||
fn parse_ex_cmd(raw: &str) -> Result<Option<ViCmd>, Option<String>> {
|
||||
let raw = raw.trim();
|
||||
if raw.is_empty() {
|
||||
return Ok(None)
|
||||
return Ok(None);
|
||||
}
|
||||
let mut chars = raw.chars().peekable();
|
||||
let (verb, motion) = {
|
||||
@@ -160,14 +157,16 @@ 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) };
|
||||
(Some(VerbCmd(1,result.1)), Some(MotionCmd(1,result.0)))
|
||||
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)
|
||||
}
|
||||
@@ -204,16 +203,16 @@ fn unescape_shell_cmd(cmd: &str) -> String {
|
||||
result
|
||||
}
|
||||
|
||||
fn parse_ex_command(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>,Option<String>> {
|
||||
fn parse_ex_command(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>, Option<String>> {
|
||||
let mut cmd_name = String::new();
|
||||
|
||||
while let Some(ch) = chars.peek() {
|
||||
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);
|
||||
fn parse_normal(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>, Option<String>> {
|
||||
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);
|
||||
fn parse_read(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>, Option<String>> {
|
||||
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);
|
||||
fn parse_write(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>, Option<String>> {
|
||||
chars
|
||||
.peeking_take_while(|c| c.is_whitespace())
|
||||
.for_each(drop);
|
||||
|
||||
let is_shell_write = chars.peek() == Some(&'!');
|
||||
if is_shell_write {
|
||||
@@ -302,20 +316,27 @@ fn parse_write(chars: &mut Peekable<Chars<'_>>) -> Result<Option<Verb>,Option<St
|
||||
Ok(Some(Verb::Write(dest)))
|
||||
}
|
||||
|
||||
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 };
|
||||
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
|
||||
};
|
||||
|
||||
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)))
|
||||
@@ -324,14 +345,16 @@ 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
|
||||
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
|
||||
|
||||
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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,20 +13,24 @@ impl ViVerbatim {
|
||||
Self::default()
|
||||
}
|
||||
pub fn with_count(self, repeat_count: u16) -> Self {
|
||||
Self { repeat_count, ..self }
|
||||
Self {
|
||||
repeat_count,
|
||||
..self
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ViMode for ViVerbatim {
|
||||
fn handle_key(&mut self, key: E) -> Option<ViCmd> {
|
||||
match key {
|
||||
E(K::Verbatim(seq),_mods) => {
|
||||
E(K::Verbatim(seq), _mods) => {
|
||||
log::debug!("Received verbatim key sequence: {:?}", seq);
|
||||
let cmd = ViCmd { register: RegisterName::default(),
|
||||
verb: Some(VerbCmd(1,Verb::Insert(seq.to_string()))),
|
||||
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)
|
||||
|
||||
@@ -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(),
|
||||
|
||||
56
src/shopt.rs
56
src/shopt.rs
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
115
src/state.rs
115
src/state.rs
@@ -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,13 +513,13 @@ impl Display for ShAlias {
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ShFunc {
|
||||
pub body: Node,
|
||||
pub source: Span
|
||||
pub source: Span,
|
||||
}
|
||||
|
||||
impl ShFunc {
|
||||
pub fn new(mut src: ParsedSrc, source: Span) -> Self {
|
||||
let body = Self::extract_brc_grp_hack(src.extract_nodes());
|
||||
Self{ body, source }
|
||||
Self { body, source }
|
||||
}
|
||||
fn extract_brc_grp_hack(mut tree: Vec<Node>) -> Node {
|
||||
// FIXME: find a better way to do this
|
||||
@@ -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 {
|
||||
@@ -1589,12 +1613,12 @@ pub fn get_shopt(path: &str) -> String {
|
||||
read_shopts(|s| s.get(path)).unwrap().unwrap()
|
||||
}
|
||||
|
||||
pub fn with_vars<F,H,V,T>(vars: H, f: F) -> T
|
||||
pub fn with_vars<F, H, V, T>(vars: H, f: F) -> T
|
||||
where
|
||||
F: FnOnce() -> T,
|
||||
H: Into<HashMap<String,V>>,
|
||||
V: Into<Var> {
|
||||
|
||||
H: Into<HashMap<String, V>>,
|
||||
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(())
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user