rustfmt'd the codebase

This commit is contained in:
2026-03-04 19:52:29 -05:00
parent ecd6eda424
commit 7be79a3803
51 changed files with 4926 additions and 4131 deletions

View File

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

View File

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

View File

@@ -1,48 +1,56 @@
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
pattern: Option<Regex>,
clear: bool,
}
fn autocmd_optspec() -> [OptSpec;2] {
[
OptSpec {
opt: Opt::Short('p'),
takes_arg: true
},
OptSpec {
opt: Opt::Short('c'),
takes_arg: false
}
]
fn autocmd_optspec() -> [OptSpec; 2] {
[
OptSpec {
opt: Opt::Short('p'),
takes_arg: true,
},
OptSpec {
opt: Opt::Short('c'),
takes_arg: false,
},
]
}
pub fn get_autocmd_opts(opts: &[Opt]) -> ShResult<AutoCmdOpts> {
let mut autocmd_opts = AutoCmdOpts {
pattern: None,
clear: false
};
let mut autocmd_opts = AutoCmdOpts {
pattern: None,
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)))?);
}
Opt::Short('c') => {
autocmd_opts.clear = true;
}
_ => {
return Err(ShErr::simple(ShErrKind::ExecFail, format!("unexpected option: {}", arg)));
}
}
}
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))
})?);
}
Opt::Short('c') => {
autocmd_opts.clear = true;
}
_ => {
return Err(ShErr::simple(
ShErrKind::ExecFail,
format!("unexpected option: {}", arg),
));
}
}
}
Ok(autocmd_opts)
Ok(autocmd_opts)
}
pub fn autocmd(node: Node) -> ShResult<()> {
@@ -55,36 +63,50 @@ pub fn autocmd(node: Node) -> ShResult<()> {
unreachable!()
};
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 (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); }
let mut args = argv.iter();
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()));
};
let Some(autocmd_kind) = args.next() else {
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)));
};
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),
));
};
if autocmd_opts.clear {
write_logic(|l| l.clear_autocmds(autocmd_kind));
state::set_status(0);
return Ok(());
}
if autocmd_opts.clear {
write_logic(|l| l.clear_autocmds(autocmd_kind));
state::set_status(0);
return Ok(());
}
let Some(autocmd_cmd) = args.next() else {
return Err(ShErr::at(ShErrKind::ExecFail, span, "expected an autocmd command".to_string()));
};
let Some(autocmd_cmd) = args.next() else {
return Err(ShErr::at(
ShErrKind::ExecFail,
span,
"expected an autocmd command".to_string(),
));
};
let autocmd = AutoCmd {
pattern: autocmd_opts.pattern,
command: autocmd_cmd.0.clone(),
};
let autocmd = AutoCmd {
pattern: autocmd_opts.pattern,
command: autocmd_cmd.0.clone(),
};
write_logic(|l| l.insert_autocmd(autocmd_kind, autocmd));
write_logic(|l| l.insert_autocmd(autocmd_kind, autocmd));
state::set_status(0);
Ok(())

View File

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

View File

@@ -167,7 +167,9 @@ pub fn complete_builtin(node: Node) -> ShResult<()> {
let (argv, opts) = get_opts_from_tokens(argv, &COMP_OPTS)?;
let comp_opts = get_comp_opts(opts)?;
let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); }
if !argv.is_empty() {
argv.remove(0);
}
if comp_opts.flags.contains(CompFlags::PRINT) {
if argv.is_empty() {
@@ -204,7 +206,11 @@ pub fn complete_builtin(node: Node) -> ShResult<()> {
if argv.is_empty() {
state::set_status(1);
return Err(ShErr::at(ShErrKind::ExecFail, blame, "complete: no command specified"));
return Err(ShErr::at(
ShErrKind::ExecFail,
blame,
"complete: no command specified",
));
}
let comp_spec = BashCompSpec::from_comp_opts(comp_opts).with_source(src);
@@ -279,12 +285,16 @@ 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),
));
}
},
Opt::Short('a') => comp_opts.flags |= CompFlags::ALIAS,
Opt::Short('S') => comp_opts.flags |= CompFlags::SIGNALS,
Opt::Short('a') => comp_opts.flags |= CompFlags::ALIAS,
Opt::Short('S') => comp_opts.flags |= CompFlags::SIGNALS,
Opt::Short('r') => comp_opts.flags |= CompFlags::REMOVE,
Opt::Short('j') => comp_opts.flags |= CompFlags::JOBS,
Opt::Short('p') => comp_opts.flags |= CompFlags::PRINT,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -16,8 +16,8 @@ pub enum JobBehavior {
pub fn continue_job(node: Node, behavior: JobBehavior) -> ShResult<()> {
let blame = node.get_span().clone();
let cmd_tk = node.get_command();
let cmd_span = cmd_tk.unwrap().span.clone();
let cmd_tk = node.get_command();
let cmd_span = cmd_tk.unwrap().span.clone();
let cmd = match behavior {
JobBehavior::Foregound => "fg",
JobBehavior::Background => "bg",
@@ -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),
))
}
})?;
@@ -82,12 +92,16 @@ fn parse_job_id(arg: &str, blame: Span) -> ShResult<usize> {
if arg.starts_with('%') {
let arg = arg.strip_prefix('%').unwrap();
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()))))
} else {
Ok(num.saturating_sub(1))
}
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())),
))
} else {
Ok(num.saturating_sub(1))
}
} else {
let result = write_jobs(|j| {
let query_result = j.query(JobID::Command(arg.into()));
@@ -95,7 +109,11 @@ fn parse_job_id(arg: &str, blame: Span) -> ShResult<usize> {
});
match result {
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
@@ -162,41 +198,44 @@ pub fn jobs(node: Node) -> ShResult<()> {
}
pub fn wait(node: Node) -> ShResult<()> {
let blame = node.get_span().clone();
let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!()
};
let blame = node.get_span().clone();
let NdRule::Command {
assignments: _,
argv,
} = node.class
else {
unreachable!()
};
let mut argv = prepare_argv(argv)?;
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()
.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())))
} else {
Ok(JobID::TableID(parse_job_id(&arg.0, arg.1)?))
}
})
.collect::<ShResult<Vec<JobID>>>()?;
let mut argv = prepare_argv(argv)?;
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()
.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())))
} else {
Ok(JobID::TableID(parse_job_id(&arg.0, arg.1)?))
}
})
.collect::<ShResult<Vec<JobID>>>()?;
if argv.is_empty() {
write_jobs(|j| j.wait_all_bg())?;
} else {
for arg in argv {
wait_bg(arg)?;
}
}
if argv.is_empty() {
write_jobs(|j| j.wait_all_bg())?;
} else {
for arg in argv {
wait_bg(arg)?;
}
}
// don't set status here, the status of the waited-on job should be the status of the wait builtin
Ok(())
// don't set status here, the status of the waited-on job should be the status of the wait builtin
Ok(())
}
pub fn disown(node: Node) -> ShResult<()> {
@@ -210,13 +249,19 @@ pub fn disown(node: Node) -> ShResult<()> {
};
let mut argv = prepare_argv(argv)?;
if !argv.is_empty() { argv.remove(0); }
if !argv.is_empty() {
argv.remove(0);
}
let mut argv = argv.into_iter();
let curr_job_id = if let Some(id) = read_jobs(|j| j.curr_job()) {
id
} else {
return Err(ShErr::at(ShErrKind::ExecFail, blame, "disown: No jobs to disown"));
return Err(ShErr::at(
ShErrKind::ExecFail,
blame,
"disown: No jobs to disown",
));
};
let mut tabid = curr_job_id;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -25,8 +25,8 @@ pub trait TkVecUtils<Tk> {
}
pub trait AutoCmdVecUtils {
fn exec(&self);
fn exec_with(&self, pattern: &str);
fn exec(&self);
fn exec_with(&self, pattern: &str);
}
pub trait RedirVecUtils<Redir> {
@@ -37,36 +37,44 @@ pub trait RedirVecUtils<Redir> {
}
pub trait NodeVecUtils<Node> {
fn get_span(&self) -> Option<Span>;
fn get_span(&self) -> Option<Span>;
}
impl AutoCmdVecUtils for Vec<AutoCmd> {
fn exec(&self) {
let saved_status = crate::state::get_status();
for cmd in self {
let AutoCmd { pattern: _, command } = cmd;
if let Err(e) = exec_input(command.clone(), None, false, Some("autocmd".into())) {
e.print_error();
}
}
crate::state::set_status(saved_status);
}
fn exec_with(&self, other_pattern: &str) {
let saved_status = crate::state::get_status();
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);
continue;
}
fn exec(&self) {
let saved_status = crate::state::get_status();
for cmd in self {
let AutoCmd {
pattern: _,
command,
} = cmd;
if let Err(e) = exec_input(command.clone(), None, false, Some("autocmd".into())) {
e.print_error();
}
}
crate::state::set_status(saved_status);
}
fn exec_with(&self, other_pattern: &str) {
let saved_status = crate::state::get_status();
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
);
continue;
}
if let Err(e) = exec_input(command.clone(), None, false, Some("autocmd".into())) {
e.print_error();
}
}
crate::state::set_status(saved_status);
}
if let Err(e) = exec_input(command.clone(), None, false, Some("autocmd".into())) {
e.print_error();
}
}
crate::state::set_status(saved_status);
}
}
impl<T> VecDequeExt<T> for VecDeque<T> {
@@ -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,13 +181,17 @@ 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 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()));
}
&& 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(),
));
}
}
None
None
}
}

View File

@@ -2,7 +2,7 @@
clippy::derivable_impls,
clippy::tabs_in_doc_comments,
clippy::while_let_on_iterator,
clippy::result_large_err
clippy::result_large_err
)]
pub mod builtin;
pub mod expand;
@@ -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};
@@ -86,20 +85,22 @@ fn setup_panic_handler() {
}
});
let data_dir = env::var("XDG_DATA_HOME").unwrap_or_else(|_| {
let home = env::var("HOME").unwrap();
format!("{home}/.local/share")
});
let log_dir = Path::new(&data_dir).join("shed").join("log");
std::fs::create_dir_all(&log_dir).unwrap();
let log_file_path = log_dir.join("panic.log");
let mut log_file = parse::get_redir_file(parse::RedirType::Output, log_file_path).unwrap();
let data_dir = env::var("XDG_DATA_HOME").unwrap_or_else(|_| {
let home = env::var("HOME").unwrap();
format!("{home}/.local/share")
});
let log_dir = Path::new(&data_dir).join("shed").join("log");
std::fs::create_dir_all(&log_dir).unwrap();
let log_file_path = log_dir.join("panic.log");
let mut log_file = parse::get_redir_file(parse::RedirType::Output, log_file_path).unwrap();
let panic_info_raw = info.to_string();
log_file.write_all(panic_info_raw.as_bytes()).unwrap();
let panic_info_raw = info.to_string();
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();
let backtrace = std::backtrace::Backtrace::force_capture();
log_file
.write_all(format!("\nBacktrace:\n{:?}", backtrace).as_bytes())
.unwrap();
default_panic_hook(info);
}));
@@ -134,16 +135,17 @@ fn main() -> ExitCode {
} else {
shed_interactive(args)
} {
e.print_error();
e.print_error();
};
if let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Exit))
&& let Err(e) = exec_input(trap, None, false, Some("trap".into())) {
e.print_error();
&& let Err(e) = exec_input(trap, None, false, Some("trap".into()))
{
e.print_error();
}
let on_exit_autocmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnExit));
on_exit_autocmds.exec();
let on_exit_autocmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnExit));
on_exit_autocmds.exec();
write_jobs(|j| j.hang_up());
ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8)
@@ -151,7 +153,7 @@ fn main() -> ExitCode {
fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) -> ShResult<()> {
let path = path.as_ref();
let path_raw = path.to_string_lossy().to_string();
let path_raw = path.to_string_lossy().to_string();
if !path.is_file() {
eprintln!("shed: Failed to open input file: {}", path.display());
QUIT_CODE.store(1, Ordering::SeqCst);
@@ -207,7 +209,7 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
m.try_rehash_commands();
m.try_rehash_cwd_listing();
});
error::clear_color();
error::clear_color();
// Handle any pending signals
while signals_pending() {
@@ -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);
@@ -376,14 +396,16 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
// Process any available input
match readline.process_input() {
Ok(ReadlineEvent::Line(input)) => {
let pre_exec = read_logic(|l| l.get_autocmds(AutoCmdKind::PreCmd));
let post_exec = read_logic(|l| l.get_autocmds(AutoCmdKind::PostCmd));
let pre_exec = read_logic(|l| l.get_autocmds(AutoCmdKind::PreCmd));
let post_exec = read_logic(|l| l.get_autocmds(AutoCmdKind::PostCmd));
pre_exec.exec_with(&input);
pre_exec.exec_with(&input);
let start = Instant::now();
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);
@@ -396,10 +418,10 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
log::info!("Command executed in {:.2?}", command_run_time);
write_meta(|m| m.stop_timer());
post_exec.exec_with(&input);
post_exec.exec_with(&input);
readline.fix_column()?;
readline.writer.flush_write("\n\r")?;
readline.fix_column()?;
readline.writer.flush_write("\n\r")?;
// Reset for next command with fresh prompt
readline.reset(true)?;

View File

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

View File

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

View File

@@ -9,7 +9,10 @@ use crate::{
libsh::{
error::{ShErr, ShErrKind, ShResult, last_color, next_color},
utils::{NodeVecUtils, TkVecUtils},
}, parse::lex::clean_input, prelude::*, procio::IoMode
},
parse::lex::clean_input,
prelude::*,
procio::IoMode,
};
pub mod execute;
@@ -42,7 +45,7 @@ macro_rules! try_match {
#[derive(Clone, Debug)]
pub struct ParsedSrc {
pub src: Arc<String>,
pub name: String,
pub name: String,
pub ast: Ast,
pub lex_flags: LexFlags,
pub context: LabelCtx,
@@ -57,16 +60,16 @@ impl ParsedSrc {
};
Self {
src,
name: "<stdin>".into(),
name: "<stdin>".into(),
ast: Ast::new(vec![]),
lex_flags: LexFlags::empty(),
context: VecDeque::new(),
}
}
pub fn with_name(mut self, name: String) -> Self {
self.name = name;
self
}
pub fn with_name(mut self, name: String) -> Self {
self.name = name;
self
}
pub fn with_lex_flags(mut self, flags: LexFlags) -> Self {
self.lex_flags = flags;
self
@@ -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]),
@@ -128,7 +132,7 @@ pub struct Node {
pub flags: NdFlags,
pub redirs: Vec<Redir>,
pub tokens: Vec<Tk>,
pub context: LabelCtx,
pub context: LabelCtx,
}
impl Node {
@@ -143,108 +147,108 @@ impl Node {
None
}
}
pub fn get_context(&self, msg: String) -> (SpanSource, Label<Span>) {
let color = last_color();
let span = self.get_span().clone();
(
span.clone().source().clone(),
Label::new(span).with_color(color).with_message(msg)
)
}
fn walk_tree<F: Fn(&mut Node)>(&mut self, f: &F) {
f(self);
pub fn get_context(&self, msg: String) -> (SpanSource, Label<Span>) {
let color = last_color();
let span = self.get_span().clone();
(
span.clone().source().clone(),
Label::new(span).with_color(color).with_message(msg),
)
}
fn walk_tree<F: Fn(&mut Node)>(&mut self, f: &F) {
f(self);
match self.class {
NdRule::IfNode {
ref mut cond_nodes,
ref mut else_block,
} => {
for node in cond_nodes {
let CondNode { cond, body } = node;
cond.walk_tree(f);
for body_node in body {
body_node.walk_tree(f);
}
}
match self.class {
NdRule::IfNode {
ref mut cond_nodes,
ref mut else_block,
} => {
for node in cond_nodes {
let CondNode { cond, body } = node;
cond.walk_tree(f);
for body_node in body {
body_node.walk_tree(f);
}
}
for else_node in else_block {
else_node.walk_tree(f);
}
}
NdRule::LoopNode {
kind: _,
ref mut cond_node,
} => {
let CondNode { cond, body } = cond_node;
cond.walk_tree(f);
for body_node in body {
body_node.walk_tree(f);
}
}
NdRule::ForNode {
vars: _,
arr: _,
ref mut body,
} => {
for body_node in body {
body_node.walk_tree(f);
}
}
NdRule::CaseNode {
pattern: _,
ref mut case_blocks,
} => {
for block in case_blocks {
let CaseNode { pattern: _, body } = block;
for body_node in body {
body_node.walk_tree(f);
}
}
}
NdRule::Command {
ref mut assignments,
argv: _,
} => {
for assign_node in assignments {
assign_node.walk_tree(f);
}
}
NdRule::Pipeline {
ref mut cmds,
pipe_err: _,
} => {
for cmd_node in cmds {
cmd_node.walk_tree(f);
}
}
NdRule::Conjunction { ref mut elements } => {
for node in elements.iter_mut() {
let ConjunctNode { cmd, operator: _ } = node;
cmd.walk_tree(f);
}
}
NdRule::Assignment {
kind: _,
var: _,
val: _,
} => (), // No nodes to check
NdRule::BraceGrp { ref mut body } => {
for body_node in body {
body_node.walk_tree(f);
}
}
NdRule::FuncDef {
name: _,
ref mut body,
} => {
body.walk_tree(f);
}
NdRule::Test { cases: _ } => (),
}
}
pub fn propagate_context(&mut self, ctx: (SpanSource, Label<Span>)) {
self.walk_tree(&|nd| nd.context.push_back(ctx.clone()));
}
for else_node in else_block {
else_node.walk_tree(f);
}
}
NdRule::LoopNode {
kind: _,
ref mut cond_node,
} => {
let CondNode { cond, body } = cond_node;
cond.walk_tree(f);
for body_node in body {
body_node.walk_tree(f);
}
}
NdRule::ForNode {
vars: _,
arr: _,
ref mut body,
} => {
for body_node in body {
body_node.walk_tree(f);
}
}
NdRule::CaseNode {
pattern: _,
ref mut case_blocks,
} => {
for block in case_blocks {
let CaseNode { pattern: _, body } = block;
for body_node in body {
body_node.walk_tree(f);
}
}
}
NdRule::Command {
ref mut assignments,
argv: _,
} => {
for assign_node in assignments {
assign_node.walk_tree(f);
}
}
NdRule::Pipeline {
ref mut cmds,
pipe_err: _,
} => {
for cmd_node in cmds {
cmd_node.walk_tree(f);
}
}
NdRule::Conjunction { ref mut elements } => {
for node in elements.iter_mut() {
let ConjunctNode { cmd, operator: _ } = node;
cmd.walk_tree(f);
}
}
NdRule::Assignment {
kind: _,
var: _,
val: _,
} => (), // No nodes to check
NdRule::BraceGrp { ref mut body } => {
for body_node in body {
body_node.walk_tree(f);
}
}
NdRule::FuncDef {
name: _,
ref mut body,
} => {
body.walk_tree(f);
}
NdRule::Test { cases: _ } => (),
}
}
pub fn propagate_context(&mut self, ctx: (SpanSource, Label<Span>)) {
self.walk_tree(&|nd| nd.context.push_back(ctx.clone()));
}
pub fn get_span(&self) -> Span {
let Some(first_tk) = self.tokens.first() else {
unreachable!()
@@ -662,20 +666,23 @@ pub enum NdRule {
pub struct ParseStream {
pub tokens: Vec<Tk>,
pub context: LabelCtx
pub context: LabelCtx,
}
impl Debug for ParseStream {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ParseStream")
.field("tokens", &self.tokens)
.finish()
}
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ParseStream")
.field("tokens", &self.tokens)
.finish()
}
}
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 }
@@ -831,39 +838,42 @@ impl ParseStream {
let name_tk = self.next_tk().unwrap();
node_tks.push(name_tk.clone());
let name = name_tk.clone();
let name_raw = name.to_string();
let mut src = name_tk.span.span_source().clone();
src.rename(name_raw.clone());
let color = next_color();
// Push a placeholder context so child nodes inherit it
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_color(color),
));
let name_raw = name.to_string();
let mut src = name_tk.span.span_source().clone();
src.rename(name_raw.clone());
let color = next_color();
// Push a placeholder context so child nodes inherit it
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_color(color),
));
let Some(brc_grp) = self.parse_brc_grp(true /* from_func_def */)? else {
self.context.pop_back();
self.context.pop_back();
return Err(parse_err_full(
"Expected a brace group after function name",
&node_tks.get_span().unwrap(),
self.context.clone()
self.context.clone(),
));
};
body = Box::new(brc_grp);
// Replace placeholder with full-span label
self.context.pop_back();
// Replace placeholder with full-span label
self.context.pop_back();
let node = Node {
class: NdRule::FuncDef { name, body },
flags: NdFlags::empty(),
redirs: vec![],
tokens: node_tks,
context: self.context.clone()
context: self.context.clone(),
};
self.context.pop_back();
self.context.pop_back();
Ok(Some(node))
}
fn panic_mode(&mut self, node_tks: &mut Vec<Tk>) {
@@ -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(),
));
}
}
@@ -1140,12 +1148,12 @@ impl ParseStream {
};
Ok(Some(node))
}
fn make_err(&self, span: lex::Span, label: Label<lex::Span>) -> ShErr {
let src = span.span_source().clone();
ShErr::new(ShErrKind::ParseErr, span)
.with_label(src, label)
.with_context(self.context.clone())
}
fn make_err(&self, span: lex::Span, label: Label<lex::Span>) -> ShErr {
let src = span.span_source().clone();
ShErr::new(ShErrKind::ParseErr, span)
.with_label(src, label)
.with_context(self.context.clone())
}
fn parse_if(&mut self) -> ShResult<Option<Node>> {
// Needs at last one 'if-then',
// Any number of 'elif-then',
@@ -1164,14 +1172,19 @@ impl ParseStream {
let prefix_keywrd = if cond_nodes.is_empty() { "if" } else { "elif" };
let Some(cond) = self.parse_cmd_list()? else {
self.panic_mode(&mut node_tks);
let span = node_tks.get_span().unwrap();
let color = next_color();
return Err(self.make_err(span.clone(),
Label::new(span)
.with_message(format!("Expected an expression after '{}'", prefix_keywrd.fg(color)))
.with_color(color)
));
let span = node_tks.get_span().unwrap();
let color = next_color();
return Err(
self.make_err(
span.clone(),
Label::new(span)
.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 {
@@ -1205,7 +1218,7 @@ impl ParseStream {
};
cond_nodes.push(cond_node);
self.catch_separator(&mut node_tks);
self.catch_separator(&mut node_tks);
if !self.check_keyword("elif") || !self.next_tk_is_some() {
break;
} else {
@@ -1214,7 +1227,7 @@ impl ParseStream {
}
}
self.catch_separator(&mut node_tks);
self.catch_separator(&mut node_tks);
if self.check_keyword("else") {
node_tks.push(self.next_tk().unwrap());
self.catch_separator(&mut node_tks);
@@ -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);
@@ -1515,14 +1528,14 @@ impl ParseStream {
// If we have assignments but no command word,
// return the assignment-only command without parsing more tokens
self.commit(node_tks.len());
let mut context = self.context.clone();
let assignments_span = assignments.get_span().unwrap();
context.push_back((
assignments_span.source().clone(),
Label::new(assignments_span)
.with_message("in variable assignment defined here".to_string())
.with_color(next_color())
));
let mut context = self.context.clone();
let assignments_span = assignments.get_span().unwrap();
context.push_back((
assignments_span.source().clone(),
Label::new(assignments_span)
.with_message("in variable assignment defined here".to_string())
.with_color(next_color()),
));
return Ok(Some(Node {
class: NdRule::Command { assignments, argv },
tokens: node_tks,
@@ -1559,7 +1572,7 @@ impl ParseStream {
let path_tk = tk_iter.next();
if path_tk.is_none_or(|tk| tk.class == TkRule::EOI) {
self.panic_mode(&mut node_tks);
self.panic_mode(&mut node_tks);
return Err(ShErr::at(
ShErrKind::ParseErr,
tk.span.clone(),
@@ -1737,7 +1750,7 @@ fn node_is_punctuated(tokens: &[Tk]) -> bool {
}
pub fn get_redir_file<P: AsRef<Path>>(class: RedirType, path: P) -> ShResult<File> {
let path = path.as_ref();
let path = path.as_ref();
let result = match class {
RedirType::Input => OpenOptions::new().read(true).open(Path::new(&path)),
RedirType::Output => OpenOptions::new()
@@ -1745,23 +1758,22 @@ 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?)
}
fn parse_err_full(reason: &str, blame: &Span, context: LabelCtx) -> ShErr {
let color = last_color();
ShErr::new(ShErrKind::ParseErr, blame.clone())
.with_label(
blame.span_source().clone(),
Label::new(blame.clone()).with_message(reason).with_color(color)
)
.with_context(context)
let color = last_color();
ShErr::new(ShErrKind::ParseErr, blame.clone())
.with_label(
blame.span_source().clone(),
Label::new(blame.clone())
.with_message(reason)
.with_color(color),
)
.with_context(context)
}
fn is_func_name(tk: Option<&Tk>) -> bool {

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -4,47 +4,45 @@ 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 {
Insert,
Normal,
Ex,
Ex,
Visual,
Replace,
Verbatim,
Verbatim,
Unknown,
}
impl Display for ModeReport {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ModeReport::Insert => write!(f, "INSERT"),
ModeReport::Normal => write!(f, "NORMAL"),
ModeReport::Ex => write!(f, "COMMAND"),
ModeReport::Visual => write!(f, "VISUAL"),
ModeReport::Replace => write!(f, "REPLACE"),
ModeReport::Verbatim => write!(f, "VERBATIM"),
ModeReport::Unknown => write!(f, "UNKNOWN"),
}
}
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ModeReport::Insert => write!(f, "INSERT"),
ModeReport::Normal => write!(f, "NORMAL"),
ModeReport::Ex => write!(f, "COMMAND"),
ModeReport::Visual => write!(f, "VISUAL"),
ModeReport::Replace => write!(f, "REPLACE"),
ModeReport::Verbatim => write!(f, "VERBATIM"),
ModeReport::Unknown => write!(f, "UNKNOWN"),
}
}
}
#[derive(Debug, Clone)]
@@ -73,13 +71,17 @@ pub enum CmdState {
}
pub trait ViMode {
fn handle_key_fallible(&mut self, key: E) -> ShResult<Option<ViCmd>> { Ok(self.handle_key(key)) }
fn handle_key_fallible(&mut self, key: E) -> ShResult<Option<ViCmd>> {
Ok(self.handle_key(key))
}
fn handle_key(&mut self, key: E) -> Option<ViCmd>;
fn is_repeatable(&self) -> bool;
fn as_replay(&self) -> Option<CmdReplay>;
fn cursor_style(&self) -> String;
fn pending_seq(&self) -> Option<String>;
fn pending_cursor(&self) -> Option<usize> { None }
fn pending_cursor(&self) -> Option<usize> {
None
}
fn move_cursor_on_undo(&self) -> bool;
fn clamp_cursor(&self) -> bool;
fn hist_scroll_start_pos(&self) -> Option<To>;

View File

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

View File

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

View File

@@ -1,39 +1,40 @@
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
sent_cmd: Vec<ViCmd>,
repeat_count: u16,
}
impl ViVerbatim {
pub fn new() -> Self {
Self::default()
}
pub fn with_count(self, repeat_count: u16) -> Self {
Self { repeat_count, ..self }
}
pub fn with_count(self, repeat_count: u16) -> 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) => {
log::debug!("Received verbatim key sequence: {:?}", seq);
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
};
self.sent_cmd.push(cmd.clone());
Some(cmd)
}
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()))),
motion: None,
raw_seq: seq.to_string(),
flags: CmdFlags::EXIT_CUR_MODE,
};
self.sent_cmd.push(cmd.clone());
Some(cmd)
}
_ => common_cmds(key),
}
}

View File

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

View File

@@ -104,12 +104,10 @@ impl ShOpts {
"core" => self.core.set(&remainder, val)?,
"prompt" => self.prompt.set(&remainder, val)?,
_ => {
return Err(
ShErr::simple(
ShErrKind::SyntaxErr,
"shopt: expected 'core' or 'prompt' in shopt key",
)
);
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(
ShErrKind::SyntaxErr,
"shopt: Expected 'core' or 'prompt' in shopt key",
)
),
_ => 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(
ShErrKind::SyntaxErr,
format!("shopt: Unexpected 'core' option '{opt}'"),
)
);
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(
ShErrKind::SyntaxErr,
format!("shopt: Unexpected 'core' option '{query}'"),
)
),
_ => 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(
ShErrKind::SyntaxErr,
format!("shopt: Unexpected 'prompt' option '{opt}'"),
)
);
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(
ShErrKind::SyntaxErr,
format!("shopt: Unexpected 'prompt' option '{query}'"),
)
),
"line_numbers" => {
let mut output = String::from("Whether to display line numbers in multiline input\n");
output.push_str(&format!("{}", self.line_numbers));
Ok(Some(output))
}
_ => Err(ShErr::simple(
ShErrKind::SyntaxErr,
format!("shopt: Unexpected 'prompt' option '{query}'"),
)),
}
}
}
@@ -493,6 +495,7 @@ impl Display for ShOptPrompt {
self.linebreak_on_incomplete
));
output.push(format!("leader = {}", self.leader));
output.push(format!("line_numbers = {}", self.line_numbers));
let final_output = output.join("\n");
@@ -510,6 +513,7 @@ impl Default for ShOptPrompt {
auto_indent: true,
linebreak_on_incomplete: true,
leader: "\\".to_string(),
line_numbers: true,
}
}
}

View File

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

View File

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