Added -j flag to 'complete' for completing job names/pids

This commit is contained in:
2026-02-27 11:03:56 -05:00
parent e141e39c7e
commit c508180228
44 changed files with 3259 additions and 2853 deletions

View File

@@ -3,7 +3,7 @@ use crate::{
libsh::error::{ShErr, ShErrKind, ShResult}, libsh::error::{ShErr, ShErrKind, ShResult},
parse::{NdRule, Node}, parse::{NdRule, Node},
prelude::*, prelude::*,
procio::{borrow_fd, IoStack}, procio::{IoStack, borrow_fd},
state::{self, read_logic, write_logic}, state::{self, read_logic, write_logic},
}; };

View File

@@ -1,120 +1,138 @@
use bitflags::bitflags; use bitflags::bitflags;
use nix::{libc::STDOUT_FILENO, unistd::write}; use nix::{libc::STDOUT_FILENO, unistd::write};
use crate::{builtin::setup_builtin, getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node}, procio::{IoStack, borrow_fd}, readline::complete::{BashCompSpec, CompContext, CompSpec}, state::{self, read_meta, write_meta}}; use crate::{
builtin::setup_builtin,
getopt::{Opt, OptSpec, get_opts_from_tokens},
jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult},
parse::{NdRule, Node},
procio::{IoStack, borrow_fd},
readline::complete::{BashCompSpec, CompContext, CompSpec},
state::{self, read_meta, write_meta},
};
pub const COMPGEN_OPTS: [OptSpec;8] = [ pub const COMPGEN_OPTS: [OptSpec; 9] = [
OptSpec { OptSpec {
opt: Opt::Short('F'), opt: Opt::Short('F'),
takes_arg: true takes_arg: true,
}, },
OptSpec { OptSpec {
opt: Opt::Short('W'), opt: Opt::Short('W'),
takes_arg: true takes_arg: true,
}, },
OptSpec { OptSpec {
opt: Opt::Short('f'), opt: Opt::Short('j'),
takes_arg: false takes_arg: false,
}, },
OptSpec { OptSpec {
opt: Opt::Short('d'), opt: Opt::Short('f'),
takes_arg: false takes_arg: false,
}, },
OptSpec { OptSpec {
opt: Opt::Short('c'), opt: Opt::Short('d'),
takes_arg: false takes_arg: false,
}, },
OptSpec { OptSpec {
opt: Opt::Short('u'), opt: Opt::Short('c'),
takes_arg: false takes_arg: false,
}, },
OptSpec { OptSpec {
opt: Opt::Short('v'), opt: Opt::Short('u'),
takes_arg: false takes_arg: false,
}, },
OptSpec { OptSpec {
opt: Opt::Short('o'), opt: Opt::Short('v'),
takes_arg: true takes_arg: false,
} },
OptSpec {
opt: Opt::Short('o'),
takes_arg: true,
},
]; ];
pub const COMP_OPTS: [OptSpec;11] = [ pub const COMP_OPTS: [OptSpec; 12] = [
OptSpec { OptSpec {
opt: Opt::Short('F'), opt: Opt::Short('F'),
takes_arg: true takes_arg: true,
}, },
OptSpec { OptSpec {
opt: Opt::Short('W'), opt: Opt::Short('W'),
takes_arg: true takes_arg: true,
}, },
OptSpec { OptSpec {
opt: Opt::Short('A'), opt: Opt::Short('A'),
takes_arg: true takes_arg: true,
}, },
OptSpec { OptSpec {
opt: Opt::Short('p'), opt: Opt::Short('j'),
takes_arg: false takes_arg: false,
}, },
OptSpec { OptSpec {
opt: Opt::Short('r'), opt: Opt::Short('p'),
takes_arg: false takes_arg: false,
}, },
OptSpec { OptSpec {
opt: Opt::Short('f'), opt: Opt::Short('r'),
takes_arg: false takes_arg: false,
}, },
OptSpec { OptSpec {
opt: Opt::Short('d'), opt: Opt::Short('f'),
takes_arg: false takes_arg: false,
}, },
OptSpec { OptSpec {
opt: Opt::Short('c'), opt: Opt::Short('d'),
takes_arg: false takes_arg: false,
}, },
OptSpec { OptSpec {
opt: Opt::Short('u'), opt: Opt::Short('c'),
takes_arg: false takes_arg: false,
}, },
OptSpec { OptSpec {
opt: Opt::Short('v'), opt: Opt::Short('u'),
takes_arg: false takes_arg: false,
}, },
OptSpec { OptSpec {
opt: Opt::Short('o'), opt: Opt::Short('v'),
takes_arg: true takes_arg: false,
} },
OptSpec {
opt: Opt::Short('o'),
takes_arg: true,
},
]; ];
bitflags! { bitflags! {
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct CompFlags: u32 { pub struct CompFlags: u32 {
const FILES = 0b0000000001; const FILES = 0b0000000001;
const DIRS = 0b0000000010; const DIRS = 0b0000000010;
const CMDS = 0b0000000100; const CMDS = 0b0000000100;
const USERS = 0b0000001000; const USERS = 0b0000001000;
const VARS = 0b0000010000; const VARS = 0b0000010000;
const PRINT = 0b0000100000; const JOBS = 0b0000100000;
const REMOVE = 0b0001000000; const PRINT = 0b0001000000;
} const REMOVE = 0b0010000000;
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] }
pub struct CompOptFlags: u32 { #[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)]
const DEFAULT = 0b0000000001; pub struct CompOptFlags: u32 {
const DIRNAMES = 0b0000000010; const DEFAULT = 0b0000000001;
const NOSPACE = 0b0000000100; const DIRNAMES = 0b0000000010;
} const NOSPACE = 0b0000000100;
}
} }
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone)]
pub struct CompOpts { pub struct CompOpts {
pub func: Option<String>, pub func: Option<String>,
pub wordlist: Option<Vec<String>>, pub wordlist: Option<Vec<String>>,
pub action: Option<String>, pub action: Option<String>,
pub flags: CompFlags, pub flags: CompFlags,
pub opt_flags: CompOptFlags, pub opt_flags: CompOptFlags,
} }
pub fn complete_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn complete_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let blame = node.get_span().clone(); let blame = node.get_span().clone();
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
argv, argv,
@@ -123,152 +141,150 @@ pub fn complete_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -
unreachable!() unreachable!()
}; };
assert!(!argv.is_empty()); assert!(!argv.is_empty());
let src = argv.clone() let src = argv
.into_iter() .clone()
.map(|tk| tk.expand().map(|tk| tk.get_words().join(" "))) .into_iter()
.collect::<ShResult<Vec<String>>>()? .map(|tk| tk.expand().map(|tk| tk.get_words().join(" ")))
.join(" "); .collect::<ShResult<Vec<String>>>()?
.join(" ");
let (argv, opts) = get_opts_from_tokens(argv, &COMP_OPTS)?; let (argv, opts) = get_opts_from_tokens(argv, &COMP_OPTS)?;
let comp_opts = get_comp_opts(opts)?; let comp_opts = get_comp_opts(opts)?;
let (argv, _) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; let (argv, _) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
if comp_opts.flags.contains(CompFlags::PRINT) { if comp_opts.flags.contains(CompFlags::PRINT) {
if argv.is_empty() { if argv.is_empty() {
read_meta(|m| { read_meta(|m| {
let specs = m.comp_specs().values(); let specs = m.comp_specs().values();
for spec in specs { for spec in specs {
println!("{}", spec.source()); println!("{}", spec.source());
} }
}) })
} else { } else {
read_meta(|m| { read_meta(|m| {
for (cmd,_) in &argv { for (cmd, _) in &argv {
if let Some(spec) = m.comp_specs().get(cmd) { if let Some(spec) = m.comp_specs().get(cmd) {
println!("{}", spec.source()); println!("{}", spec.source());
} }
} }
}) })
} }
state::set_status(0); state::set_status(0);
return Ok(()); return Ok(());
} }
if comp_opts.flags.contains(CompFlags::REMOVE) { if comp_opts.flags.contains(CompFlags::REMOVE) {
write_meta(|m| { write_meta(|m| {
for (cmd,_) in &argv { for (cmd, _) in &argv {
m.remove_comp_spec(cmd); m.remove_comp_spec(cmd);
} }
}); });
state::set_status(0); state::set_status(0);
return Ok(()); return Ok(());
} }
if argv.is_empty() { if argv.is_empty() {
state::set_status(1); state::set_status(1);
return Err(ShErr::full(ShErrKind::ExecFail, "complete: no command specified", blame)); return Err(ShErr::full(
} ShErrKind::ExecFail,
"complete: no command specified",
blame,
));
}
let comp_spec = BashCompSpec::from_comp_opts(comp_opts) let comp_spec = BashCompSpec::from_comp_opts(comp_opts).with_source(src);
.with_source(src);
for (cmd,_) in argv { for (cmd, _) in argv {
write_meta(|m| m.set_comp_spec(cmd, Box::new(comp_spec.clone()))); write_meta(|m| m.set_comp_spec(cmd, Box::new(comp_spec.clone())));
} }
state::set_status(0); state::set_status(0);
Ok(()) Ok(())
} }
pub fn compgen_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn compgen_builtin(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let blame = node.get_span().clone(); let blame = node.get_span().clone();
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
argv, argv,
} = node.class } = node.class
else { else {
unreachable!() unreachable!()
}; };
assert!(!argv.is_empty()); assert!(!argv.is_empty());
let src = argv.clone() let src = argv
.into_iter() .clone()
.map(|tk| tk.expand().map(|tk| tk.get_words().join(" "))) .into_iter()
.collect::<ShResult<Vec<String>>>()? .map(|tk| tk.expand().map(|tk| tk.get_words().join(" ")))
.join(" "); .collect::<ShResult<Vec<String>>>()?
.join(" ");
let (argv, opts) = get_opts_from_tokens(argv, &COMPGEN_OPTS)?; let (argv, opts) = get_opts_from_tokens(argv, &COMPGEN_OPTS)?;
let prefix = argv let prefix = argv.clone().into_iter().nth(1).unwrap_or_default();
.clone() let comp_opts = get_comp_opts(opts)?;
.into_iter() let (_, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
.nth(1)
.unwrap_or_default();
let comp_opts = get_comp_opts(opts)?;
let (_, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
let comp_spec = BashCompSpec::from_comp_opts(comp_opts).with_source(src);
let comp_spec = BashCompSpec::from_comp_opts(comp_opts) let dummy_ctx = CompContext {
.with_source(src); words: vec![prefix.clone()],
cword: 0,
line: prefix.to_string(),
cursor_pos: prefix.as_str().len(),
};
let dummy_ctx = CompContext { let results = comp_spec.complete(&dummy_ctx)?;
words: vec![prefix.clone()],
cword: 0,
line: prefix.to_string(),
cursor_pos: prefix.as_str().len()
};
let results = comp_spec.complete(&dummy_ctx)?; let stdout = borrow_fd(STDOUT_FILENO);
for result in &results {
write(stdout, result.as_bytes())?;
write(stdout, b"\n")?;
}
let stdout = borrow_fd(STDOUT_FILENO); state::set_status(0);
for result in &results { Ok(())
write(stdout, result.as_bytes())?;
write(stdout, b"\n")?;
}
state::set_status(0);
Ok(())
} }
pub fn get_comp_opts(opts: Vec<Opt>) -> ShResult<CompOpts> { pub fn get_comp_opts(opts: Vec<Opt>) -> ShResult<CompOpts> {
let mut comp_opts = CompOpts::default(); let mut comp_opts = CompOpts::default();
for opt in opts { for opt in opts {
match opt { match opt {
Opt::ShortWithArg('F',func) => { Opt::ShortWithArg('F', func) => {
comp_opts.func = Some(func); comp_opts.func = Some(func);
}, }
Opt::ShortWithArg('W',wordlist) => { Opt::ShortWithArg('W', wordlist) => {
comp_opts.wordlist = Some(wordlist.split_whitespace().map(|s| s.to_string()).collect()); comp_opts.wordlist = Some(wordlist.split_whitespace().map(|s| s.to_string()).collect());
}, }
Opt::ShortWithArg('A',action) => { Opt::ShortWithArg('A', action) => {
comp_opts.action = Some(action); comp_opts.action = Some(action);
} }
Opt::ShortWithArg('o', opt_flag) => { Opt::ShortWithArg('o', opt_flag) => match opt_flag.as_str() {
match opt_flag.as_str() { "default" => comp_opts.opt_flags |= CompOptFlags::DEFAULT,
"default" => comp_opts.opt_flags |= CompOptFlags::DEFAULT, "dirnames" => comp_opts.opt_flags |= CompOptFlags::DIRNAMES,
"dirnames" => comp_opts.opt_flags |= CompOptFlags::DIRNAMES, "nospace" => comp_opts.opt_flags |= CompOptFlags::NOSPACE,
"nospace" => comp_opts.opt_flags |= CompOptFlags::NOSPACE, _ => {
_ => { return Err(ShErr::full(
return Err(ShErr::full( ShErrKind::InvalidOpt,
ShErrKind::InvalidOpt, format!("complete: invalid option: {}", opt_flag),
format!("complete: invalid option: {}", opt_flag), Default::default(),
Default::default() ));
)); }
} },
}
}
Opt::Short('r') => comp_opts.flags |= CompFlags::REMOVE, Opt::Short('r') => comp_opts.flags |= CompFlags::REMOVE,
Opt::Short('p') => comp_opts.flags |= CompFlags::PRINT, Opt::Short('j') => comp_opts.flags |= CompFlags::JOBS,
Opt::Short('f') => comp_opts.flags |= CompFlags::FILES, Opt::Short('p') => comp_opts.flags |= CompFlags::PRINT,
Opt::Short('d') => comp_opts.flags |= CompFlags::DIRS, Opt::Short('f') => comp_opts.flags |= CompFlags::FILES,
Opt::Short('c') => comp_opts.flags |= CompFlags::CMDS, Opt::Short('d') => comp_opts.flags |= CompFlags::DIRS,
Opt::Short('u') => comp_opts.flags |= CompFlags::USERS, Opt::Short('c') => comp_opts.flags |= CompFlags::CMDS,
Opt::Short('v') => comp_opts.flags |= CompFlags::VARS, Opt::Short('u') => comp_opts.flags |= CompFlags::USERS,
_ => unreachable!() Opt::Short('v') => comp_opts.flags |= CompFlags::VARS,
} _ => unreachable!(),
} }
}
Ok(comp_opts) Ok(comp_opts)
} }

View File

@@ -2,396 +2,427 @@ use std::{env, path::PathBuf};
use nix::{libc::STDOUT_FILENO, unistd::write}; use nix::{libc::STDOUT_FILENO, unistd::write};
use crate::{builtin::setup_builtin, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node, lex::Span}, procio::{IoStack, borrow_fd}, state::{self, read_meta, write_meta}}; use crate::{
builtin::setup_builtin,
jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult},
parse::{NdRule, Node, lex::Span},
procio::{IoStack, borrow_fd},
state::{self, read_meta, write_meta},
};
enum StackIdx { enum StackIdx {
FromTop(usize), FromTop(usize),
FromBottom(usize), FromBottom(usize),
} }
fn print_dirs() -> ShResult<()> { fn print_dirs() -> ShResult<()> {
let current_dir = env::current_dir()?; let current_dir = env::current_dir()?;
let dirs_iter = read_meta(|m| { let dirs_iter = read_meta(|m| m.dirs().clone().into_iter());
m.dirs() let all_dirs = [current_dir]
.clone() .into_iter()
.into_iter() .chain(dirs_iter)
}); .map(|d| d.to_string_lossy().to_string())
let all_dirs = [current_dir].into_iter().chain(dirs_iter) .map(|d| {
.map(|d| d.to_string_lossy().to_string()) let Ok(home) = env::var("HOME") else {
.map(|d| { return d;
let Ok(home) = env::var("HOME") else { };
return d;
};
if d.starts_with(&home) { if d.starts_with(&home) {
let new = d.strip_prefix(&home).unwrap(); let new = d.strip_prefix(&home).unwrap();
format!("~{new}") format!("~{new}")
} else { } else {
d d
} }
}).collect::<Vec<_>>() })
.join(" "); .collect::<Vec<_>>()
.join(" ");
let stdout = borrow_fd(STDOUT_FILENO); let stdout = borrow_fd(STDOUT_FILENO);
write(stdout, all_dirs.as_bytes())?; write(stdout, all_dirs.as_bytes())?;
write(stdout, b"\n")?; write(stdout, b"\n")?;
Ok(()) Ok(())
} }
fn change_directory(target: &PathBuf, blame: Span) -> ShResult<()> { fn change_directory(target: &PathBuf, blame: Span) -> ShResult<()> {
if !target.is_dir() { if !target.is_dir() {
return Err(ShErr::full( return Err(ShErr::full(
ShErrKind::ExecFail, ShErrKind::ExecFail,
format!("not a directory: {}", target.display()), format!("not a directory: {}", target.display()),
blame, blame,
)); ));
} }
if let Err(e) = env::set_current_dir(target) { if let Err(e) = env::set_current_dir(target) {
return Err(ShErr::full( return Err(ShErr::full(
ShErrKind::ExecFail, ShErrKind::ExecFail,
format!("Failed to change directory: {}", e), format!("Failed to change directory: {}", e),
blame, blame,
)); ));
} }
let new_dir = env::current_dir().map_err(|e| { let new_dir = env::current_dir().map_err(|e| {
ShErr::full( ShErr::full(
ShErrKind::ExecFail, ShErrKind::ExecFail,
format!("Failed to get current directory: {}", e), format!("Failed to get current directory: {}", e),
blame, blame,
) )
})?; })?;
unsafe { env::set_var("PWD", new_dir) }; unsafe { env::set_var("PWD", new_dir) };
Ok(()) Ok(())
} }
fn parse_stack_idx(arg: &str, blame: Span, cmd: &str) -> ShResult<StackIdx> { fn parse_stack_idx(arg: &str, blame: Span, cmd: &str) -> ShResult<StackIdx> {
let (from_top, digits) = if let Some(rest) = arg.strip_prefix('+') { let (from_top, digits) = if let Some(rest) = arg.strip_prefix('+') {
(true, rest) (true, rest)
} else if let Some(rest) = arg.strip_prefix('-') { } else if let Some(rest) = arg.strip_prefix('-') {
(false, rest) (false, rest)
} else { } else {
unreachable!() unreachable!()
}; };
if digits.is_empty() { if digits.is_empty() {
return Err(ShErr::full( return Err(ShErr::full(
ShErrKind::ExecFail, ShErrKind::ExecFail,
format!("{cmd}: missing index after '{}'", if from_top { "+" } else { "-" }), format!(
blame, "{cmd}: missing index after '{}'",
)); if from_top { "+" } else { "-" }
} ),
blame,
));
}
for ch in digits.chars() { for ch in digits.chars() {
if !ch.is_ascii_digit() { if !ch.is_ascii_digit() {
return Err(ShErr::full( return Err(ShErr::full(
ShErrKind::ExecFail, ShErrKind::ExecFail,
format!("{cmd}: invalid argument: {arg}"), format!("{cmd}: invalid argument: {arg}"),
blame, blame,
)); ));
} }
} }
let n = digits.parse::<usize>().map_err(|e| { let n = digits.parse::<usize>().map_err(|e| {
ShErr::full( ShErr::full(
ShErrKind::ExecFail, ShErrKind::ExecFail,
format!("{cmd}: invalid index: {e}"), format!("{cmd}: invalid index: {e}"),
blame, blame,
) )
})?; })?;
if from_top { if from_top {
Ok(StackIdx::FromTop(n)) Ok(StackIdx::FromTop(n))
} else { } else {
Ok(StackIdx::FromBottom(n)) Ok(StackIdx::FromBottom(n))
} }
} }
pub fn pushd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn pushd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let blame = node.get_span().clone(); let blame = node.get_span().clone();
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
argv argv,
} = node.class else { unreachable!() }; } = node.class
else {
unreachable!()
};
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
let mut dir = None; let mut dir = None;
let mut rotate_idx = None; let mut rotate_idx = None;
let mut no_cd = false; let mut no_cd = false;
for (arg, _) in argv { for (arg, _) in argv {
if arg.starts_with('+') || (arg.starts_with('-') && arg.len() > 1 && arg.as_bytes()[1].is_ascii_digit()) { if arg.starts_with('+')
rotate_idx = Some(parse_stack_idx(&arg, blame.clone(), "pushd")?); || (arg.starts_with('-') && arg.len() > 1 && arg.as_bytes()[1].is_ascii_digit())
} else if arg == "-n" { {
no_cd = true; rotate_idx = Some(parse_stack_idx(&arg, blame.clone(), "pushd")?);
} else if arg.starts_with('-') { } else if arg == "-n" {
return Err(ShErr::full( no_cd = true;
ShErrKind::ExecFail, } else if arg.starts_with('-') {
format!("pushd: invalid option: {arg}"), return Err(ShErr::full(
blame.clone(), ShErrKind::ExecFail,
)); format!("pushd: invalid option: {arg}"),
} else { blame.clone(),
if dir.is_some() { ));
return Err(ShErr::full( } else {
ShErrKind::ExecFail, if dir.is_some() {
"pushd: too many arguments".to_string(), return Err(ShErr::full(
blame.clone(), ShErrKind::ExecFail,
)); "pushd: too many arguments".to_string(),
} blame.clone(),
let target = PathBuf::from(&arg); ));
if !target.is_dir() { }
return Err(ShErr::full( let target = PathBuf::from(&arg);
ShErrKind::ExecFail, if !target.is_dir() {
format!("pushd: not a directory: {arg}"), return Err(ShErr::full(
blame.clone(), ShErrKind::ExecFail,
)); format!("pushd: not a directory: {arg}"),
} blame.clone(),
dir = Some(target); ));
} }
} dir = Some(target);
}
}
if let Some(idx) = rotate_idx { if let Some(idx) = rotate_idx {
let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("/")); let cwd = env::current_dir().unwrap_or_else(|_| PathBuf::from("/"));
let new_cwd = write_meta(|m| { let new_cwd = write_meta(|m| {
let dirs = m.dirs_mut(); let dirs = m.dirs_mut();
dirs.push_front(cwd); dirs.push_front(cwd);
match idx { match idx {
StackIdx::FromTop(n) => dirs.rotate_left(n), StackIdx::FromTop(n) => dirs.rotate_left(n),
StackIdx::FromBottom(n) => dirs.rotate_right(n + 1), StackIdx::FromBottom(n) => dirs.rotate_right(n + 1),
} }
dirs.pop_front() dirs.pop_front()
}); });
if let Some(dir) = new_cwd if let Some(dir) = new_cwd
&& !no_cd { && !no_cd
change_directory(&dir, blame)?; {
print_dirs()?; change_directory(&dir, blame)?;
} print_dirs()?;
} else if let Some(dir) = dir { }
let old_dir = env::current_dir()?; } else if let Some(dir) = dir {
if old_dir != dir { let old_dir = env::current_dir()?;
write_meta(|m| m.push_dir(old_dir)); if old_dir != dir {
} write_meta(|m| m.push_dir(old_dir));
}
if no_cd { if no_cd {
state::set_status(0); state::set_status(0);
return Ok(()); return Ok(());
} }
change_directory(&dir, blame)?; change_directory(&dir, blame)?;
print_dirs()?; print_dirs()?;
} }
state::set_status(0); state::set_status(0);
Ok(()) Ok(())
} }
pub fn popd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn popd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let blame = node.get_span().clone(); let blame = node.get_span().clone();
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
argv argv,
} = node.class else { unreachable!() }; } = node.class
else {
unreachable!()
};
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
let mut remove_idx = None; let mut remove_idx = None;
let mut no_cd = false; let mut no_cd = false;
for (arg, _) in argv { for (arg, _) in argv {
if arg.starts_with('+') || (arg.starts_with('-') && arg.len() > 1 && arg.as_bytes()[1].is_ascii_digit()) { if arg.starts_with('+')
remove_idx = Some(parse_stack_idx(&arg, blame.clone(), "popd")?); || (arg.starts_with('-') && arg.len() > 1 && arg.as_bytes()[1].is_ascii_digit())
} else if arg == "-n" { {
no_cd = true; remove_idx = Some(parse_stack_idx(&arg, blame.clone(), "popd")?);
} else if arg.starts_with('-') { } else if arg == "-n" {
return Err(ShErr::full( no_cd = true;
ShErrKind::ExecFail, } else if arg.starts_with('-') {
format!("popd: invalid option: {arg}"), return Err(ShErr::full(
blame.clone(), ShErrKind::ExecFail,
)); format!("popd: invalid option: {arg}"),
} blame.clone(),
} ));
}
}
if let Some(idx) = remove_idx { if let Some(idx) = remove_idx {
match idx { match idx {
StackIdx::FromTop(0) => { StackIdx::FromTop(0) => {
// +0 is same as plain popd: pop top, cd to it // +0 is same as plain popd: pop top, cd to it
let dir = write_meta(|m| m.pop_dir()); let dir = write_meta(|m| m.pop_dir());
if !no_cd { if !no_cd {
if let Some(dir) = dir { if let Some(dir) = dir {
change_directory(&dir, blame.clone())?; change_directory(&dir, blame.clone())?;
} else { } else {
return Err(ShErr::full( return Err(ShErr::full(
ShErrKind::ExecFail, ShErrKind::ExecFail,
"popd: directory stack empty".to_string(), "popd: directory stack empty".to_string(),
blame.clone(), blame.clone(),
)); ));
} }
} }
} }
StackIdx::FromTop(n) => { StackIdx::FromTop(n) => {
// +N (N>0): remove (N-1)th stored entry, no cd // +N (N>0): remove (N-1)th stored entry, no cd
write_meta(|m| { write_meta(|m| {
let dirs = m.dirs_mut(); let dirs = m.dirs_mut();
let idx = n - 1; let idx = n - 1;
if idx >= dirs.len() { if idx >= dirs.len() {
return Err(ShErr::full( return Err(ShErr::full(
ShErrKind::ExecFail, ShErrKind::ExecFail,
format!("popd: directory index out of range: +{n}"), format!("popd: directory index out of range: +{n}"),
blame.clone(), blame.clone(),
)); ));
} }
dirs.remove(idx); dirs.remove(idx);
Ok(()) Ok(())
})?; })?;
} }
StackIdx::FromBottom(n) => { StackIdx::FromBottom(n) => {
write_meta(|m| -> ShResult<()> { write_meta(|m| -> ShResult<()> {
let dirs = m.dirs_mut(); let dirs = m.dirs_mut();
let actual = dirs.len().checked_sub(n + 1).ok_or_else(|| { let actual = dirs.len().checked_sub(n + 1).ok_or_else(|| {
ShErr::full( ShErr::full(
ShErrKind::ExecFail, ShErrKind::ExecFail,
format!("popd: directory index out of range: -{n}"), format!("popd: directory index out of range: -{n}"),
blame.clone(), blame.clone(),
) )
})?; })?;
dirs.remove(actual); dirs.remove(actual);
Ok(()) Ok(())
})?; })?;
} }
} }
print_dirs()?; print_dirs()?;
} else { } else {
let dir = write_meta(|m| m.pop_dir()); let dir = write_meta(|m| m.pop_dir());
if no_cd { if no_cd {
state::set_status(0); state::set_status(0);
return Ok(()); return Ok(());
} }
if let Some(dir) = dir { if let Some(dir) = dir {
change_directory(&dir, blame.clone())?; change_directory(&dir, blame.clone())?;
print_dirs()?; print_dirs()?;
} else { } else {
return Err(ShErr::full( return Err(ShErr::full(
ShErrKind::ExecFail, ShErrKind::ExecFail,
"popd: directory stack empty".to_string(), "popd: directory stack empty".to_string(),
blame.clone(), blame.clone(),
)); ));
} }
} }
Ok(()) Ok(())
} }
pub fn dirs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn dirs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let blame = node.get_span().clone(); let blame = node.get_span().clone();
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
argv argv,
} = node.class else { unreachable!() }; } = node.class
else {
unreachable!()
};
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
let mut abbreviate_home = true; let mut abbreviate_home = true;
let mut one_per_line = false; let mut one_per_line = false;
let mut one_per_line_indexed = false; let mut one_per_line_indexed = false;
let mut clear_stack = false; let mut clear_stack = false;
let mut target_idx: Option<StackIdx> = None; let mut target_idx: Option<StackIdx> = None;
for (arg,_) in argv { for (arg, _) in argv {
match arg.as_str() { match arg.as_str() {
"-p" => one_per_line = true, "-p" => one_per_line = true,
"-v" => one_per_line_indexed = true, "-v" => one_per_line_indexed = true,
"-c" => clear_stack = true, "-c" => clear_stack = true,
"-l" => abbreviate_home = false, "-l" => abbreviate_home = false,
_ if (arg.starts_with('+') || arg.starts_with('-')) && arg.len() > 1 && arg.as_bytes()[1].is_ascii_digit() => { _ if (arg.starts_with('+') || arg.starts_with('-'))
target_idx = Some(parse_stack_idx(&arg, blame.clone(), "dirs")?); && arg.len() > 1
} && arg.as_bytes()[1].is_ascii_digit() =>
_ if arg.starts_with('-') => { {
return Err(ShErr::full( target_idx = Some(parse_stack_idx(&arg, blame.clone(), "dirs")?);
ShErrKind::ExecFail, }
format!("dirs: invalid option: {arg}"), _ if arg.starts_with('-') => {
blame.clone(), return Err(ShErr::full(
)); ShErrKind::ExecFail,
} format!("dirs: invalid option: {arg}"),
_ => { blame.clone(),
return Err(ShErr::full( ));
ShErrKind::ExecFail, }
format!("dirs: unexpected argument: {arg}"), _ => {
blame.clone(), return Err(ShErr::full(
)); ShErrKind::ExecFail,
} format!("dirs: unexpected argument: {arg}"),
} blame.clone(),
} ));
}
}
}
if clear_stack { if clear_stack {
write_meta(|m| m.dirs_mut().clear()); write_meta(|m| m.dirs_mut().clear());
return Ok(()) return Ok(());
} }
let mut dirs: Vec<String> = read_meta(|m| {
let current_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from("/"));
let stack = [current_dir]
.into_iter()
.chain(m.dirs().clone())
.map(|d| d.to_string_lossy().to_string());
let mut dirs: Vec<String> = read_meta(|m| { if abbreviate_home {
let current_dir = env::current_dir().unwrap_or_else(|_| PathBuf::from("/")); let Ok(home) = env::var("HOME") else {
let stack = [current_dir].into_iter() return stack.collect();
.chain(m.dirs().clone()) };
.map(|d| d.to_string_lossy().to_string()); stack
.map(|d| {
if d.starts_with(&home) {
let new = d.strip_prefix(&home).unwrap();
format!("~{new}")
} else {
d
}
})
.collect()
} else {
stack.collect()
}
});
if abbreviate_home { if let Some(idx) = target_idx {
let Ok(home) = env::var("HOME") else { let target = match idx {
return stack.collect(); StackIdx::FromTop(n) => dirs.get(n),
}; StackIdx::FromBottom(n) => dirs.get(dirs.len().saturating_sub(n + 1)),
stack.map(|d| { };
if d.starts_with(&home) {
let new = d.strip_prefix(&home).unwrap();
format!("~{new}")
} else {
d
}
}).collect()
} else {
stack.collect()
}
});
if let Some(idx) = target_idx { if let Some(dir) = target {
let target = match idx { dirs = vec![dir.clone()];
StackIdx::FromTop(n) => dirs.get(n), } else {
StackIdx::FromBottom(n) => dirs.get(dirs.len().saturating_sub(n + 1)), return Err(ShErr::full(
}; ShErrKind::ExecFail,
format!(
"dirs: directory index out of range: {}",
match idx {
StackIdx::FromTop(n) => format!("+{n}"),
StackIdx::FromBottom(n) => format!("-{n}"),
}
),
blame.clone(),
));
}
}
if let Some(dir) = target { let mut output = String::new();
dirs = vec![dir.clone()];
} else {
return Err(ShErr::full(
ShErrKind::ExecFail,
format!("dirs: directory index out of range: {}", match idx {
StackIdx::FromTop(n) => format!("+{n}"),
StackIdx::FromBottom(n) => format!("-{n}"),
}),
blame.clone(),
));
}
}
let mut output = String::new(); if one_per_line {
output = dirs.join("\n");
} else if one_per_line_indexed {
for (i, dir) in dirs.iter_mut().enumerate() {
*dir = format!("{i}\t{dir}");
}
output = dirs.join("\n");
output.push('\n');
} else {
print_dirs()?;
}
if one_per_line { let stdout = borrow_fd(STDOUT_FILENO);
output = dirs.join("\n"); write(stdout, output.as_bytes())?;
} else if one_per_line_indexed {
for (i, dir) in dirs.iter_mut().enumerate() {
*dir = format!("{i}\t{dir}");
}
output = dirs.join("\n");
output.push('\n');
} else {
print_dirs()?;
}
let stdout = borrow_fd(STDOUT_FILENO); Ok(())
write(stdout, output.as_bytes())?;
Ok(())
} }

View File

@@ -1,12 +1,12 @@
use crate::{ use crate::{
builtin::setup_builtin, builtin::setup_builtin,
expand::expand_prompt, expand::expand_prompt,
getopt::{get_opts_from_tokens, Opt, OptSpec}, getopt::{Opt, OptSpec, get_opts_from_tokens},
jobs::JobBldr, jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
parse::{NdRule, Node}, parse::{NdRule, Node},
prelude::*, prelude::*,
procio::{borrow_fd, IoStack}, procio::{IoStack, borrow_fd},
state, state,
}; };
@@ -30,7 +30,7 @@ pub const ECHO_OPTS: [OptSpec; 4] = [
]; ];
bitflags! { bitflags! {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct EchoFlags: u32 { pub struct EchoFlags: u32 {
const NO_NEWLINE = 0b000001; const NO_NEWLINE = 0b000001;
const USE_STDERR = 0b000010; const USE_STDERR = 0b000010;
@@ -59,7 +59,6 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<(
borrow_fd(STDOUT_FILENO) borrow_fd(STDOUT_FILENO)
}; };
let mut echo_output = prepare_echo_args( let mut echo_output = prepare_echo_args(
argv argv
.into_iter() .into_iter()
@@ -197,7 +196,6 @@ pub fn prepare_echo_args(
prepared_args.push(prepared_arg); prepared_args.push(prepared_arg);
} }
Ok(prepared_args) Ok(prepared_args)
} }

View File

@@ -2,7 +2,7 @@ use crate::{
builtin::setup_builtin, builtin::setup_builtin,
jobs::JobBldr, jobs::JobBldr,
libsh::error::ShResult, libsh::error::ShResult,
parse::{execute::exec_input, NdRule, Node}, parse::{NdRule, Node, execute::exec_input},
procio::IoStack, procio::IoStack,
state, state,
}; };

View File

@@ -4,7 +4,7 @@ use crate::{
builtin::setup_builtin, builtin::setup_builtin,
jobs::JobBldr, jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult}, libsh::error::{ShErr, ShErrKind, ShResult},
parse::{execute::ExecArgs, NdRule, Node}, parse::{NdRule, Node, execute::ExecArgs},
procio::IoStack, procio::IoStack,
state, state,
}; };

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
libsh::error::{ShErr, ShErrKind, ShResult}, libsh::error::{ShErr, ShErrKind, ShResult},
parse::{execute::prepare_argv, NdRule, Node}, parse::{NdRule, Node, execute::prepare_argv},
}; };
pub fn flowctl(node: Node, kind: ShErrKind) -> ShResult<()> { pub fn flowctl(node: Node, kind: ShErrKind) -> ShResult<()> {
@@ -31,7 +31,7 @@ pub fn flowctl(node: Node, kind: ShErrKind) -> ShResult<()> {
code = status; code = status;
} }
let (kind,message) = match kind { let (kind, message) = match kind {
LoopContinue(_) => (LoopContinue(code), "'continue' found outside of loop"), LoopContinue(_) => (LoopContinue(code), "'continue' found outside of loop"),
LoopBreak(_) => (LoopBreak(code), "'break' found outside of loop"), LoopBreak(_) => (LoopBreak(code), "'break' found outside of loop"),
FuncReturn(_) => (FuncReturn(code), "'return' found outside of function"), FuncReturn(_) => (FuncReturn(code), "'return' found outside of function"),

View File

@@ -1,9 +1,9 @@
use crate::{ use crate::{
jobs::{JobBldr, JobCmdFlags, JobID}, jobs::{JobBldr, JobCmdFlags, JobID},
libsh::error::{ShErr, ShErrKind, ShResult}, libsh::error::{ShErr, ShErrKind, ShResult},
parse::{lex::Span, NdRule, Node}, parse::{NdRule, Node, lex::Span},
prelude::*, prelude::*,
procio::{borrow_fd, IoStack}, procio::{IoStack, borrow_fd},
state::{self, read_jobs, write_jobs}, state::{self, read_jobs, write_jobs},
}; };

View File

@@ -8,13 +8,17 @@ use crate::{
execute::prepare_argv, execute::prepare_argv,
lex::{Span, Tk}, lex::{Span, Tk},
}, },
procio::{IoStack, RedirGuard}, state, procio::{IoStack, RedirGuard},
state,
}; };
pub mod alias; pub mod alias;
pub mod cd; pub mod cd;
pub mod complete;
pub mod dirstack;
pub mod echo; pub mod echo;
pub mod varcmds; pub mod eval;
pub mod exec;
pub mod flowctl; pub mod flowctl;
pub mod jobctl; pub mod jobctl;
pub mod pwd; pub mod pwd;
@@ -24,16 +28,14 @@ pub mod shopt;
pub mod source; pub mod source;
pub mod test; // [[ ]] thing pub mod test; // [[ ]] thing
pub mod trap; pub mod trap;
pub mod varcmds;
pub mod zoltraak; pub mod zoltraak;
pub mod dirstack;
pub mod exec;
pub mod eval;
pub mod complete;
pub const BUILTINS: [&str; 35] = [ pub const BUILTINS: [&str; 35] = [
"echo", "cd", "read", "export", "local", "pwd", "source", "shift", "jobs", "fg", "bg", "disown", "alias", "unalias", "echo", "cd", "read", "export", "local", "pwd", "source", "shift", "jobs", "fg", "bg", "disown",
"return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "command", "trap", "alias", "unalias", "return", "break", "continue", "exit", "zoltraak", "shopt", "builtin",
"pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly", "unset", "complete", "compgen" "command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly",
"unset", "complete", "compgen",
]; ];
/// Sets up a builtin command /// Sets up a builtin command
@@ -96,16 +98,16 @@ pub fn setup_builtin(
} }
pub fn true_builtin() -> ShResult<()> { pub fn true_builtin() -> ShResult<()> {
state::set_status(0); state::set_status(0);
Ok(()) Ok(())
} }
pub fn false_builtin() -> ShResult<()> { pub fn false_builtin() -> ShResult<()> {
state::set_status(1); state::set_status(1);
Ok(()) Ok(())
} }
pub fn noop_builtin() -> ShResult<()> { pub fn noop_builtin() -> ShResult<()> {
state::set_status(0); state::set_status(0);
Ok(()) Ok(())
} }

View File

@@ -3,7 +3,7 @@ use crate::{
libsh::error::ShResult, libsh::error::ShResult,
parse::{NdRule, Node}, parse::{NdRule, Node},
prelude::*, prelude::*,
procio::{borrow_fd, IoStack}, procio::{IoStack, borrow_fd},
state, state,
}; };

View File

@@ -7,13 +7,13 @@ use nix::{
use crate::{ use crate::{
builtin::setup_builtin, builtin::setup_builtin,
getopt::{get_opts_from_tokens, Opt, OptSpec}, getopt::{Opt, OptSpec, get_opts_from_tokens},
jobs::JobBldr, jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt},
parse::{NdRule, Node}, parse::{NdRule, Node},
procio::{borrow_fd, IoStack}, procio::{IoStack, borrow_fd},
readline::term::RawModeGuard, readline::term::RawModeGuard,
state::{self, read_vars, write_vars, VarFlags, VarKind}, state::{self, VarFlags, VarKind, read_vars, write_vars},
}; };
pub const READ_OPTS: [OptSpec; 7] = [ pub const READ_OPTS: [OptSpec; 7] = [
@@ -81,7 +81,10 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S
write(borrow_fd(STDOUT_FILENO), prompt.as_bytes())?; write(borrow_fd(STDOUT_FILENO), prompt.as_bytes())?;
} }
log::info!("read_builtin: starting read with delim={}", read_opts.delim as char); log::info!(
"read_builtin: starting read with delim={}",
read_opts.delim as char
);
let input = if isatty(STDIN_FILENO)? { let input = if isatty(STDIN_FILENO)? {
// Restore default terminal settings // Restore default terminal settings
@@ -182,9 +185,7 @@ pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> S
}; };
if argv.is_empty() { if argv.is_empty() {
write_vars(|v| { write_vars(|v| v.set_var("REPLY", VarKind::Str(input.clone()), VarFlags::NONE))?;
v.set_var("REPLY", VarKind::Str(input.clone()), VarFlags::NONE)
})?;
} else { } else {
// get our field separator // get our field separator
let mut field_sep = read_vars(|v| v.get_var("IFS")); let mut field_sep = read_vars(|v| v.get_var("IFS"));

View File

@@ -20,16 +20,16 @@ pub fn shopt(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
if argv.is_empty() { if argv.is_empty() {
let mut output = write_shopts(|s| s.display_opts())?; let mut output = write_shopts(|s| s.display_opts())?;
let output_channel = borrow_fd(STDOUT_FILENO); let output_channel = borrow_fd(STDOUT_FILENO);
output.push('\n'); output.push('\n');
write(output_channel, output.as_bytes())?; write(output_channel, output.as_bytes())?;
state::set_status(0); state::set_status(0);
return Ok(()) return Ok(());
} }
for (arg, span) in argv { for (arg, span) in argv {
let Some(mut output) = write_shopts(|s| s.query(&arg)).blame(span)? else { let Some(mut output) = write_shopts(|s| s.query(&arg)).blame(span)? else {
@@ -42,7 +42,6 @@ pub fn shopt(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
write(output_channel, output.as_bytes())?; write(output_channel, output.as_bytes())?;
} }
state::set_status(0);
state::set_status(0);
Ok(()) Ok(())
} }

View File

@@ -8,7 +8,7 @@ use regex::Regex;
use crate::{ use crate::{
libsh::error::{ShErr, ShErrKind, ShResult}, libsh::error::{ShErr, ShErrKind, ShResult},
parse::{ConjunctOp, NdRule, Node, TestCase, TEST_UNARY_OPS}, parse::{ConjunctOp, NdRule, Node, TEST_UNARY_OPS, TestCase},
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone)]

View File

@@ -11,7 +11,7 @@ use crate::{
jobs::JobBldr, jobs::JobBldr,
libsh::error::{ShErr, ShErrKind, ShResult}, libsh::error::{ShErr, ShErrKind, ShResult},
parse::{NdRule, Node}, parse::{NdRule, Node},
procio::{borrow_fd, IoStack}, procio::{IoStack, borrow_fd},
state::{self, read_logic, write_logic}, state::{self, read_logic, write_logic},
}; };
@@ -59,12 +59,10 @@ impl FromStr for TrapTarget {
"IO" => Ok(TrapTarget::Signal(Signal::SIGIO)), "IO" => Ok(TrapTarget::Signal(Signal::SIGIO)),
"PWR" => Ok(TrapTarget::Signal(Signal::SIGPWR)), "PWR" => Ok(TrapTarget::Signal(Signal::SIGPWR)),
"SYS" => Ok(TrapTarget::Signal(Signal::SIGSYS)), "SYS" => Ok(TrapTarget::Signal(Signal::SIGSYS)),
_ => { _ => Err(ShErr::simple(
Err(ShErr::simple( ShErrKind::ExecFail,
ShErrKind::ExecFail, format!("invalid trap target '{}'", s),
format!("invalid trap target '{}'", s), )),
))
}
} }
} }
} }

View File

@@ -26,7 +26,7 @@ pub fn readonly(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu
let mut vars = v let mut vars = v
.flatten_vars() .flatten_vars()
.into_iter() .into_iter()
.filter(|(_, v)| v.flags().contains(VarFlags::READONLY)) .filter(|(_, v)| v.flags().contains(VarFlags::READONLY))
.map(|(k, v)| format!("{}={}", k, v)) .map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<String>>(); .collect::<Vec<String>>();
vars.sort(); vars.sort();
@@ -47,12 +47,12 @@ pub fn readonly(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu
} }
} }
state::set_status(0); state::set_status(0);
Ok(()) Ok(())
} }
pub fn unset(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn unset(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
let blame = node.get_span().clone(); let blame = node.get_span().clone();
let NdRule::Command { let NdRule::Command {
assignments: _, assignments: _,
argv, argv,
@@ -63,27 +63,27 @@ pub fn unset(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<
let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?;
if argv.is_empty() { if argv.is_empty() {
return Err(ShErr::full( return Err(ShErr::full(
ShErrKind::SyntaxErr, ShErrKind::SyntaxErr,
"unset: Expected at least one argument", "unset: Expected at least one argument",
blame blame,
)); ));
} }
for (arg,span) in argv { for (arg, span) in argv {
if !read_vars(|v| v.var_exists(&arg)) { if !read_vars(|v| v.var_exists(&arg)) {
return Err(ShErr::full( return Err(ShErr::full(
ShErrKind::ExecFail, ShErrKind::ExecFail,
format!("unset: No such variable '{arg}'"), format!("unset: No such variable '{arg}'"),
span span,
)); ));
} }
write_vars(|v| v.unset_var(&arg))?; write_vars(|v| v.unset_var(&arg))?;
} }
state::set_status(0); state::set_status(0);
Ok(()) Ok(())
} }
pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> {
@@ -114,7 +114,7 @@ pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult
write_vars(|v| v.set_var(var, VarKind::Str(val.to_string()), VarFlags::EXPORT))?; write_vars(|v| v.set_var(var, VarKind::Str(val.to_string()), VarFlags::EXPORT))?;
} else { } else {
write_vars(|v| v.export_var(&arg)); // Export an existing variable, if write_vars(|v| v.export_var(&arg)); // Export an existing variable, if
// any // any
} }
} }
} }

View File

@@ -1,12 +1,12 @@
use std::os::unix::fs::OpenOptionsExt; use std::os::unix::fs::OpenOptionsExt;
use crate::{ use crate::{
getopt::{get_opts_from_tokens, Opt, OptSpec}, getopt::{Opt, OptSpec, get_opts_from_tokens},
jobs::JobBldr, jobs::JobBldr,
libsh::error::{Note, ShErr, ShErrKind, ShResult, ShResultExt}, libsh::error::{Note, ShErr, ShErrKind, ShResult, ShResultExt},
parse::{NdRule, Node}, parse::{NdRule, Node},
prelude::*, prelude::*,
procio::{borrow_fd, IoStack}, procio::{IoStack, borrow_fd},
}; };
use super::setup_builtin; use super::setup_builtin;

View File

@@ -12,13 +12,13 @@ use crate::parse::{Redir, RedirType};
use crate::procio::{IoBuf, IoFrame, IoMode, IoStack}; use crate::procio::{IoBuf, IoFrame, IoMode, IoStack};
use crate::readline::markers; use crate::readline::markers;
use crate::state::{ use crate::state::{
ArrIndex, LogTab, VarFlags, VarKind, read_jobs, read_logic, read_vars, write_jobs, write_meta, write_vars ArrIndex, LogTab, VarFlags, VarKind, read_jobs, read_logic, read_vars, write_jobs, write_meta,
write_vars,
}; };
use crate::{jobs, prelude::*}; use crate::{jobs, prelude::*};
const PARAMETERS: [char; 7] = ['@', '*', '#', '$', '?', '!', '0']; const PARAMETERS: [char; 7] = ['@', '*', '#', '$', '?', '!', '0'];
impl Tk { impl Tk {
/// Create a new expanded token /// Create a new expanded token
pub fn expand(self) -> ShResult<Self> { pub fn expand(self) -> ShResult<Self> {
@@ -80,16 +80,16 @@ impl Expander {
let mut chars = self.raw.chars(); let mut chars = self.raw.chars();
let mut cur_word = String::new(); let mut cur_word = String::new();
let mut was_quoted = false; let mut was_quoted = false;
let ifs = env::var("IFS").unwrap_or_else(|_| " \t\n".to_string()); let ifs = env::var("IFS").unwrap_or_else(|_| " \t\n".to_string());
'outer: while let Some(ch) = chars.next() { 'outer: while let Some(ch) = chars.next() {
match ch { match ch {
markers::DUB_QUOTE | markers::SNG_QUOTE | markers::SUBSH => { markers::DUB_QUOTE | markers::SNG_QUOTE | markers::SUBSH => {
while let Some(q_ch) = chars.next() { while let Some(q_ch) = chars.next() {
match q_ch { match q_ch {
markers::ARG_SEP if ch == markers::DUB_QUOTE => { markers::ARG_SEP if ch == markers::DUB_QUOTE => {
words.push(mem::take(&mut cur_word)); words.push(mem::take(&mut cur_word));
} }
_ if q_ch == ch => { _ if q_ch == ch => {
was_quoted = true; was_quoted = true;
continue 'outer; // Isn't rust cool continue 'outer; // Isn't rust cool
@@ -518,11 +518,11 @@ pub fn expand_raw(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
pub fn expand_var(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> { pub fn expand_var(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
let mut var_name = String::new(); let mut var_name = String::new();
let mut brace_depth: i32 = 0; let mut brace_depth: i32 = 0;
let mut inner_brace_depth: i32 = 0; let mut inner_brace_depth: i32 = 0;
let mut bracket_depth: i32 = 0; let mut bracket_depth: i32 = 0;
let mut idx_brace_depth: i32 = 0; let mut idx_brace_depth: i32 = 0;
let mut idx_raw = String::new(); let mut idx_raw = String::new();
let mut idx = None; let mut idx = None;
while let Some(&ch) = chars.peek() { while let Some(&ch) = chars.peek() {
match ch { match ch {
markers::SUBSH if var_name.is_empty() => { markers::SUBSH if var_name.is_empty() => {
@@ -551,51 +551,63 @@ pub fn expand_var(chars: &mut Peekable<Chars<'_>>) -> ShResult<String> {
'}' if brace_depth > 0 && bracket_depth == 0 && inner_brace_depth == 0 => { '}' if brace_depth > 0 && bracket_depth == 0 && inner_brace_depth == 0 => {
chars.next(); // consume the brace chars.next(); // consume the brace
let val = if let Some(idx) = idx { let val = if let Some(idx) = idx {
match idx { match idx {
ArrIndex::AllSplit => { ArrIndex::AllSplit => {
let arg_sep = markers::ARG_SEP.to_string(); let arg_sep = markers::ARG_SEP.to_string();
read_vars(|v| v.get_arr_elems(&var_name))?.join(&arg_sep) read_vars(|v| v.get_arr_elems(&var_name))?.join(&arg_sep)
} }
ArrIndex::AllJoined => { ArrIndex::AllJoined => {
let ifs = read_vars(|v| v.try_get_var("IFS")) let ifs = read_vars(|v| v.try_get_var("IFS"))
.unwrap_or_else(|| " \t\n".to_string()) .unwrap_or_else(|| " \t\n".to_string())
.chars() .chars()
.next() .next()
.unwrap_or(' ') .unwrap_or(' ')
.to_string(); .to_string();
read_vars(|v| v.get_arr_elems(&var_name))?.join(&ifs) read_vars(|v| v.get_arr_elems(&var_name))?.join(&ifs)
}, }
_ => read_vars(|v| v.index_var(&var_name, idx))? _ => read_vars(|v| v.index_var(&var_name, idx))?,
} }
} else {
} else { perform_param_expansion(&var_name)?
perform_param_expansion(&var_name)? };
};
return Ok(val); return Ok(val);
} }
'[' if brace_depth > 0 && bracket_depth == 0 && inner_brace_depth == 0 => { '[' if brace_depth > 0 && bracket_depth == 0 && inner_brace_depth == 0 => {
chars.next(); // consume the bracket chars.next(); // consume the bracket
bracket_depth += 1; bracket_depth += 1;
} }
']' if bracket_depth > 0 && idx_brace_depth == 0 => { ']' if bracket_depth > 0 && idx_brace_depth == 0 => {
bracket_depth -= 1; bracket_depth -= 1;
chars.next(); // consume the bracket chars.next(); // consume the bracket
if bracket_depth == 0 { if bracket_depth == 0 {
let expanded_idx = expand_raw(&mut idx_raw.chars().peekable())?; let expanded_idx = expand_raw(&mut idx_raw.chars().peekable())?;
idx = Some(expanded_idx.parse::<ArrIndex>().map_err(|_| ShErr::simple(ShErrKind::ParseErr, format!("Array index must be a number, got '{expanded_idx}'")))?); idx = Some(expanded_idx.parse::<ArrIndex>().map_err(|_| {
} ShErr::simple(
} ShErrKind::ParseErr,
ch if bracket_depth > 0 => { format!("Array index must be a number, got '{expanded_idx}'"),
chars.next(); // safe to consume )
if ch == '{' { idx_brace_depth += 1; } })?);
if ch == '}' { idx_brace_depth -= 1; } }
idx_raw.push(ch); }
} ch if bracket_depth > 0 => {
chars.next(); // safe to consume
if ch == '{' {
idx_brace_depth += 1;
}
if ch == '}' {
idx_brace_depth -= 1;
}
idx_raw.push(ch);
}
ch if brace_depth > 0 => { ch if brace_depth > 0 => {
chars.next(); // safe to consume chars.next(); // safe to consume
if ch == '{' { inner_brace_depth += 1; } if ch == '{' {
if ch == '}' { inner_brace_depth -= 1; } inner_brace_depth += 1;
}
if ch == '}' {
inner_brace_depth -= 1;
}
var_name.push(ch); var_name.push(ch);
} }
ch if var_name.is_empty() && PARAMETERS.contains(&ch) => { ch if var_name.is_empty() && PARAMETERS.contains(&ch) => {
@@ -1411,12 +1423,10 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
None => expand_raw(&mut default.chars().peekable()), None => expand_raw(&mut default.chars().peekable()),
} }
} }
ParamExp::DefaultUnset(default) => { ParamExp::DefaultUnset(default) => match vars.try_get_var(&var_name) {
match vars.try_get_var(&var_name) { Some(val) => Ok(val),
Some(val) => Ok(val), None => expand_raw(&mut default.chars().peekable()),
None => expand_raw(&mut default.chars().peekable()), },
}
}
ParamExp::SetDefaultUnsetOrNull(default) => { ParamExp::SetDefaultUnsetOrNull(default) => {
match vars.try_get_var(&var_name).filter(|v| !v.is_empty()) { match vars.try_get_var(&var_name).filter(|v| !v.is_empty()) {
Some(val) => Ok(val), Some(val) => Ok(val),
@@ -1427,28 +1437,22 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
} }
} }
} }
ParamExp::SetDefaultUnset(default) => { ParamExp::SetDefaultUnset(default) => match vars.try_get_var(&var_name) {
match vars.try_get_var(&var_name) { Some(val) => Ok(val),
Some(val) => Ok(val), None => {
None => { let expanded = expand_raw(&mut default.chars().peekable())?;
let expanded = expand_raw(&mut default.chars().peekable())?; write_vars(|v| v.set_var(&var_name, VarKind::Str(expanded.clone()), VarFlags::NONE))?;
write_vars(|v| v.set_var(&var_name, VarKind::Str(expanded.clone()), VarFlags::NONE))?; Ok(expanded)
Ok(expanded)
}
} }
} },
ParamExp::AltSetNotNull(alt) => { ParamExp::AltSetNotNull(alt) => match vars.try_get_var(&var_name).filter(|v| !v.is_empty()) {
match vars.try_get_var(&var_name).filter(|v| !v.is_empty()) { Some(_) => expand_raw(&mut alt.chars().peekable()),
Some(_) => expand_raw(&mut alt.chars().peekable()), None => Ok("".into()),
None => Ok("".into()), },
} ParamExp::AltNotNull(alt) => match vars.try_get_var(&var_name) {
} Some(_) => expand_raw(&mut alt.chars().peekable()),
ParamExp::AltNotNull(alt) => { None => Ok("".into()),
match vars.try_get_var(&var_name) { },
Some(_) => expand_raw(&mut alt.chars().peekable()),
None => Ok("".into()),
}
}
ParamExp::ErrUnsetOrNull(err) => { ParamExp::ErrUnsetOrNull(err) => {
match vars.try_get_var(&var_name).filter(|v| !v.is_empty()) { match vars.try_get_var(&var_name).filter(|v| !v.is_empty()) {
Some(val) => Ok(val), Some(val) => Ok(val),
@@ -1462,19 +1466,17 @@ pub fn perform_param_expansion(raw: &str) -> ShResult<String> {
} }
} }
} }
ParamExp::ErrUnset(err) => { ParamExp::ErrUnset(err) => match vars.try_get_var(&var_name) {
match vars.try_get_var(&var_name) { Some(val) => Ok(val),
Some(val) => Ok(val), None => {
None => { let expanded = expand_raw(&mut err.chars().peekable())?;
let expanded = expand_raw(&mut err.chars().peekable())?; Err(ShErr::Simple {
Err(ShErr::Simple { kind: ShErrKind::ExecFail,
kind: ShErrKind::ExecFail, msg: expanded,
msg: expanded, notes: vec![],
notes: vec![], })
})
}
} }
} },
ParamExp::Substr(pos) => { ParamExp::Substr(pos) => {
let value = vars.get_var(&var_name); let value = vars.get_var(&var_name);
if let Some(substr) = value.get(pos..) { if let Some(substr) = value.get(pos..) {
@@ -1861,7 +1863,7 @@ fn tokenize_prompt(raw: &str) -> Vec<PromptTk> {
'n' => tokens.push(PromptTk::Text("\n".into())), 'n' => tokens.push(PromptTk::Text("\n".into())),
'r' => tokens.push(PromptTk::Text("\r".into())), 'r' => tokens.push(PromptTk::Text("\r".into())),
't' => tokens.push(PromptTk::RuntimeMillis), 't' => tokens.push(PromptTk::RuntimeMillis),
'j' => tokens.push(PromptTk::JobCount), 'j' => tokens.push(PromptTk::JobCount),
'T' => tokens.push(PromptTk::RuntimeFormatted), 'T' => tokens.push(PromptTk::RuntimeFormatted),
'\\' => tokens.push(PromptTk::Text("\\".into())), '\\' => tokens.push(PromptTk::Text("\\".into())),
'"' => tokens.push(PromptTk::Text("\"".into())), '"' => tokens.push(PromptTk::Text("\"".into())),
@@ -2058,9 +2060,20 @@ pub fn expand_prompt(raw: &str) -> ShResult<String> {
PromptTk::SuccessSymbol => todo!(), PromptTk::SuccessSymbol => todo!(),
PromptTk::FailureSymbol => todo!(), PromptTk::FailureSymbol => todo!(),
PromptTk::JobCount => { PromptTk::JobCount => {
let count = read_jobs(|j| j.jobs().iter().filter(|j| j.as_ref().is_some_and(|j| j.get_stats().iter().all(|st| matches!(st, WtStat::StillAlive)))).count()); let count = read_jobs(|j| {
result.push_str(&count.to_string()); j.jobs()
} .iter()
.filter(|j| {
j.as_ref().is_some_and(|j| {
j.get_stats()
.iter()
.all(|st| matches!(st, WtStat::StillAlive))
})
})
.count()
});
result.push_str(&count.to_string());
}
PromptTk::Function(f) => { PromptTk::Function(f) => {
let output = expand_cmd_sub(&f)?; let output = expand_cmd_sub(&f)?;
result.push_str(&output); result.push_str(&output);

View File

@@ -67,12 +67,15 @@ pub fn get_opts(words: Vec<String>) -> (Vec<String>, Vec<Opt>) {
(non_opts, opts) (non_opts, opts)
} }
pub fn get_opts_from_tokens(tokens: Vec<Tk>, opt_specs: &[OptSpec]) -> ShResult<(Vec<Tk>, Vec<Opt>)> { pub fn get_opts_from_tokens(
tokens: Vec<Tk>,
opt_specs: &[OptSpec],
) -> ShResult<(Vec<Tk>, Vec<Opt>)> {
let mut tokens_iter = tokens let mut tokens_iter = tokens
.into_iter() .into_iter()
.map(|t| t.expand()) .map(|t| t.expand())
.collect::<ShResult<Vec<_>>>()? .collect::<ShResult<Vec<_>>>()?
.into_iter(); .into_iter();
let mut opts = vec![]; let mut opts = vec![];
let mut non_opts = vec![]; let mut non_opts = vec![];

View File

@@ -4,7 +4,7 @@ use crate::{
term::{Style, Styled}, term::{Style, Styled},
}, },
prelude::*, prelude::*,
procio::{borrow_fd, IoMode}, procio::{IoMode, borrow_fd},
signal::{disable_reaping, enable_reaping}, signal::{disable_reaping, enable_reaping},
state::{self, read_jobs, set_status, write_jobs}, state::{self, read_jobs, set_status, write_jobs},
}; };
@@ -632,6 +632,9 @@ impl Job {
} }
Ok(()) Ok(())
} }
pub fn name(&self) -> Option<&str> {
self.children().first().and_then(|child| child.cmd())
}
pub fn display(&self, job_order: &[usize], flags: JobCmdFlags) -> String { pub fn display(&self, job_order: &[usize], flags: JobCmdFlags) -> String {
let long = flags.contains(JobCmdFlags::LONG); let long = flags.contains(JobCmdFlags::LONG);
let init = flags.contains(JobCmdFlags::INIT); let init = flags.contains(JobCmdFlags::INIT);

View File

@@ -1,7 +1,10 @@
use std::fmt::Display; use std::fmt::Display;
use crate::{ use crate::{
getopt::Opt, libsh::term::{Style, Styled}, parse::lex::Span, prelude::* getopt::Opt,
libsh::term::{Style, Styled},
parse::lex::Span,
prelude::*,
}; };
pub type ShResult<T> = Result<T, ShErr>; pub type ShResult<T> = Result<T, ShErr>;
@@ -393,7 +396,7 @@ impl From<Errno> for ShErr {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub enum ShErrKind { pub enum ShErrKind {
IoErr(io::ErrorKind), IoErr(io::ErrorKind),
InvalidOpt, InvalidOpt,
SyntaxErr, SyntaxErr,
ParseErr, ParseErr,
InternalErr, InternalErr,
@@ -420,7 +423,7 @@ impl Display for ShErrKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let output = match self { let output = match self {
Self::IoErr(e) => &format!("I/O Error: {e}"), Self::IoErr(e) => &format!("I/O Error: {e}"),
Self::InvalidOpt => &format!("Invalid option"), Self::InvalidOpt => &format!("Invalid option"),
Self::SyntaxErr => "Syntax Error", Self::SyntaxErr => "Syntax Error",
Self::ParseErr => "Parse Error", Self::ParseErr => "Parse Error",
Self::InternalErr => "Internal Error", Self::InternalErr => "Internal Error",

View File

@@ -17,7 +17,7 @@ pub trait CharDequeUtils {
pub trait TkVecUtils<Tk> { pub trait TkVecUtils<Tk> {
fn get_span(&self) -> Option<Span>; fn get_span(&self) -> Option<Span>;
fn debug_tokens(&self); fn debug_tokens(&self);
fn split_at_separators(&self) -> Vec<Vec<Tk>>; fn split_at_separators(&self) -> Vec<Vec<Tk>>;
} }
pub trait RedirVecUtils<Redir> { pub trait RedirVecUtils<Redir> {
@@ -86,29 +86,24 @@ impl TkVecUtils<Tk> for Vec<Tk> {
fn debug_tokens(&self) { fn debug_tokens(&self) {
for token in self {} for token in self {}
} }
fn split_at_separators(&self) -> Vec<Vec<Tk>> { fn split_at_separators(&self) -> Vec<Vec<Tk>> {
let mut splits = vec![]; let mut splits = vec![];
let mut cur_split = vec![]; let mut cur_split = vec![];
for tk in self { for tk in self {
match tk.class { match tk.class {
TkRule::Pipe | TkRule::Pipe | TkRule::ErrPipe | TkRule::And | TkRule::Or | TkRule::Bg | TkRule::Sep => {
TkRule::ErrPipe | splits.push(std::mem::take(&mut cur_split));
TkRule::And | }
TkRule::Or | _ => cur_split.push(tk.clone()),
TkRule::Bg | }
TkRule::Sep => { }
splits.push(std::mem::take(&mut cur_split));
}
_ => cur_split.push(tk.clone()),
}
}
if !cur_split.is_empty() { if !cur_split.is_empty() {
splits.push(cur_split); splits.push(cur_split);
} }
splits splits
} }
} }
impl RedirVecUtils<Redir> for Vec<Redir> { impl RedirVecUtils<Redir> for Vec<Redir> {

View File

@@ -1,7 +1,7 @@
#![allow( #![allow(
clippy::derivable_impls, clippy::derivable_impls,
clippy::tabs_in_doc_comments, clippy::tabs_in_doc_comments,
clippy::while_let_on_iterator clippy::while_let_on_iterator
)] )]
pub mod builtin; pub mod builtin;
pub mod expand; pub mod expand;
@@ -40,22 +40,22 @@ use state::{read_vars, write_vars};
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
struct ShedArgs { struct ShedArgs {
script: Option<String>, script: Option<String>,
#[arg(short)] #[arg(short)]
command: Option<String>, command: Option<String>,
#[arg(trailing_var_arg = true)] #[arg(trailing_var_arg = true)]
script_args: Vec<String>, script_args: Vec<String>,
#[arg(long)] #[arg(long)]
version: bool, version: bool,
#[arg(short)] #[arg(short)]
interactive: bool, interactive: bool,
#[arg(long,short)] #[arg(long, short)]
login_shell: bool, login_shell: bool,
} }
/// Force evaluation of lazily-initialized values early in shell startup. /// Force evaluation of lazily-initialized values early in shell startup.
@@ -69,227 +69,236 @@ struct ShedArgs {
/// closure, which forces access to the variable table and causes its `LazyLock` /// closure, which forces access to the variable table and causes its `LazyLock`
/// constructor to run. /// constructor to run.
fn kickstart_lazy_evals() { fn kickstart_lazy_evals() {
read_vars(|_| {}); read_vars(|_| {});
} }
/// We need to make sure that even if we panic, our child processes get sighup /// We need to make sure that even if we panic, our child processes get sighup
fn setup_panic_handler() { fn setup_panic_handler() {
let default_panic_hook = std::panic::take_hook(); let default_panic_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| { std::panic::set_hook(Box::new(move |info| {
let _ = state::SHED.try_with(|shed| { let _ = state::SHED.try_with(|shed| {
if let Ok(mut jobs) = shed.jobs.try_borrow_mut() { if let Ok(mut jobs) = shed.jobs.try_borrow_mut() {
jobs.hang_up(); jobs.hang_up();
} }
}); });
default_panic_hook(info); default_panic_hook(info);
})); }));
} }
fn main() -> ExitCode { fn main() -> ExitCode {
env_logger::init(); env_logger::init();
kickstart_lazy_evals(); kickstart_lazy_evals();
setup_panic_handler(); setup_panic_handler();
let mut args = ShedArgs::parse(); let mut args = ShedArgs::parse();
if env::args().next().is_some_and(|a| a.starts_with('-')) { if env::args().next().is_some_and(|a| a.starts_with('-')) {
// first arg is '-shed' // first arg is '-shed'
// meaning we are in a login shell // meaning we are in a login shell
args.login_shell = true; args.login_shell = true;
} }
if args.version { if args.version {
println!("shed {} ({} {})", env!("CARGO_PKG_VERSION"), std::env::consts::ARCH, std::env::consts::OS); println!(
return ExitCode::SUCCESS; "shed {} ({} {})",
} env!("CARGO_PKG_VERSION"),
std::env::consts::ARCH,
std::env::consts::OS
);
return ExitCode::SUCCESS;
}
if let Err(e) = if let Some(path) = args.script { if let Err(e) = if let Some(path) = args.script {
run_script(path, args.script_args) run_script(path, args.script_args)
} else if let Some(cmd) = args.command { } else if let Some(cmd) = args.command {
exec_input(cmd, None, false) exec_input(cmd, None, false)
} else { } else {
shed_interactive() shed_interactive()
} { } {
eprintln!("shed: {e}"); eprintln!("shed: {e}");
}; };
if let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Exit)) if let Some(trap) = read_logic(|l| l.get_trap(TrapTarget::Exit))
&& let Err(e) = exec_input(trap, None, false) { && let Err(e) = exec_input(trap, None, false)
eprintln!("shed: error running EXIT trap: {e}"); {
} eprintln!("shed: error running EXIT trap: {e}");
}
write_jobs(|j| j.hang_up()); write_jobs(|j| j.hang_up());
ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8) ExitCode::from(QUIT_CODE.load(Ordering::SeqCst) as u8)
} }
fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) -> ShResult<()> { fn run_script<P: AsRef<Path>>(path: P, args: Vec<String>) -> ShResult<()> {
let path = path.as_ref(); let path = path.as_ref();
if !path.is_file() { if !path.is_file() {
eprintln!("shed: Failed to open input file: {}", path.display()); eprintln!("shed: Failed to open input file: {}", path.display());
QUIT_CODE.store(1, Ordering::SeqCst); QUIT_CODE.store(1, Ordering::SeqCst);
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::CleanExit(1), ShErrKind::CleanExit(1),
"input file not found", "input file not found",
)); ));
} }
let Ok(input) = fs::read_to_string(path) else { let Ok(input) = fs::read_to_string(path) else {
eprintln!("shed: Failed to read input file: {}", path.display()); eprintln!("shed: Failed to read input file: {}", path.display());
QUIT_CODE.store(1, Ordering::SeqCst); QUIT_CODE.store(1, Ordering::SeqCst);
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::CleanExit(1), ShErrKind::CleanExit(1),
"failed to read input file", "failed to read input file",
)); ));
}; };
write_vars(|v| { write_vars(|v| {
v.cur_scope_mut() v.cur_scope_mut()
.bpush_arg(path.to_string_lossy().to_string()) .bpush_arg(path.to_string_lossy().to_string())
}); });
for arg in args { for arg in args {
write_vars(|v| v.cur_scope_mut().bpush_arg(arg)) write_vars(|v| v.cur_scope_mut().bpush_arg(arg))
} }
exec_input(input, None, false) exec_input(input, None, false)
} }
fn shed_interactive() -> ShResult<()> { fn shed_interactive() -> ShResult<()> {
let _raw_mode = raw_mode(); // sets raw mode, restores termios on drop let _raw_mode = raw_mode(); // sets raw mode, restores termios on drop
sig_setup(); sig_setup();
if let Err(e) = source_rc() { if let Err(e) = source_rc() {
eprintln!("{e}"); eprintln!("{e}");
} }
// Create readline instance with initial prompt // Create readline instance with initial prompt
let mut readline = match ShedVi::new(Prompt::new(), *TTY_FILENO) { let mut readline = match ShedVi::new(Prompt::new(), *TTY_FILENO) {
Ok(rl) => rl, Ok(rl) => rl,
Err(e) => { Err(e) => {
eprintln!("Failed to initialize readline: {e}"); eprintln!("Failed to initialize readline: {e}");
QUIT_CODE.store(1, Ordering::SeqCst); QUIT_CODE.store(1, Ordering::SeqCst);
return Err(ShErr::simple( return Err(ShErr::simple(
ShErrKind::CleanExit(1), ShErrKind::CleanExit(1),
"readline initialization failed", "readline initialization failed",
)); ));
} }
}; };
// Main poll loop // Main poll loop
loop { loop {
write_meta(|m| { write_meta(|m| {
m.try_rehash_commands(); m.try_rehash_commands();
m.try_rehash_cwd_listing(); m.try_rehash_cwd_listing();
}); });
// Handle any pending signals // Handle any pending signals
while signals_pending() { while signals_pending() {
if let Err(e) = check_signals() { if let Err(e) = check_signals() {
match e.kind() { match e.kind() {
ShErrKind::ClearReadline => { ShErrKind::ClearReadline => {
// Ctrl+C - clear current input and show new prompt // Ctrl+C - clear current input and show new prompt
readline.reset(false)?; readline.reset(false)?;
} }
ShErrKind::CleanExit(code) => { ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst); QUIT_CODE.store(*code, Ordering::SeqCst);
return Ok(()); return Ok(());
} }
_ => eprintln!("{e}"), _ => eprintln!("{e}"),
} }
} }
} }
if GOT_SIGWINCH.swap(false, Ordering::SeqCst) { if GOT_SIGWINCH.swap(false, Ordering::SeqCst) {
log::info!("Window size change detected, updating readline dimensions"); log::info!("Window size change detected, updating readline dimensions");
readline.writer.update_t_cols(); readline.writer.update_t_cols();
readline.prompt_mut().refresh()?; readline.prompt_mut().refresh()?;
} }
if JOB_DONE.swap(false, Ordering::SeqCst) { if JOB_DONE.swap(false, Ordering::SeqCst) {
// update the prompt so any job count escape sequences update dynamically // update the prompt so any job count escape sequences update dynamically
readline.prompt_mut().refresh()?; readline.prompt_mut().refresh()?;
} }
readline.print_line(false)?; readline.print_line(false)?;
// Poll for stdin input // Poll for stdin input
let mut fds = [PollFd::new( let mut fds = [PollFd::new(
unsafe { BorrowedFd::borrow_raw(*TTY_FILENO) }, unsafe { BorrowedFd::borrow_raw(*TTY_FILENO) },
PollFlags::POLLIN, PollFlags::POLLIN,
)]; )];
match poll(&mut fds, PollTimeout::MAX) { match poll(&mut fds, PollTimeout::MAX) {
Ok(_) => {} Ok(_) => {}
Err(Errno::EINTR) => { Err(Errno::EINTR) => {
// Interrupted by signal, loop back to handle it // Interrupted by signal, loop back to handle it
continue; continue;
} }
Err(e) => { Err(e) => {
eprintln!("poll error: {e}"); eprintln!("poll error: {e}");
break; break;
} }
} }
// Check if stdin has data // Check if stdin has data
if fds[0].revents().is_some_and(|r| r.contains(PollFlags::POLLIN)) { if fds[0]
let mut buffer = [0u8; 1024]; .revents()
match read(*TTY_FILENO, &mut buffer) { .is_some_and(|r| r.contains(PollFlags::POLLIN))
Ok(0) => { {
// EOF let mut buffer = [0u8; 1024];
break; match read(*TTY_FILENO, &mut buffer) {
} Ok(0) => {
Ok(n) => { // EOF
readline.feed_bytes(&buffer[..n]); break;
} }
Err(Errno::EINTR) => { Ok(n) => {
// Interrupted, continue to handle signals readline.feed_bytes(&buffer[..n]);
continue; }
} Err(Errno::EINTR) => {
Err(e) => { // Interrupted, continue to handle signals
eprintln!("read error: {e}"); continue;
break; }
} Err(e) => {
} eprintln!("read error: {e}");
} break;
}
}
}
// Process any available input // Process any available input
match readline.process_input() { match readline.process_input() {
Ok(ReadlineEvent::Line(input)) => { Ok(ReadlineEvent::Line(input)) => {
let start = Instant::now(); let start = Instant::now();
write_meta(|m| m.start_timer()); write_meta(|m| m.start_timer());
if let Err(e) = RawModeGuard::with_cooked_mode(|| exec_input(input, None, true)) { if let Err(e) = RawModeGuard::with_cooked_mode(|| exec_input(input, None, true)) {
match e.kind() { match e.kind() {
ShErrKind::CleanExit(code) => { ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst); QUIT_CODE.store(*code, Ordering::SeqCst);
return Ok(()); return Ok(());
} }
_ => eprintln!("{e}"), _ => eprintln!("{e}"),
} }
} }
let command_run_time = start.elapsed(); let command_run_time = start.elapsed();
log::info!("Command executed in {:.2?}", command_run_time); log::info!("Command executed in {:.2?}", command_run_time);
write_meta(|m| m.stop_timer()); write_meta(|m| m.stop_timer());
readline.writer.flush_write("\n")?; readline.writer.flush_write("\n")?;
// Reset for next command with fresh prompt // Reset for next command with fresh prompt
readline.reset(true)?; readline.reset(true)?;
let real_end = start.elapsed(); let real_end = start.elapsed();
log::info!("Total round trip time: {:.2?}", real_end); log::info!("Total round trip time: {:.2?}", real_end);
} }
Ok(ReadlineEvent::Eof) => { Ok(ReadlineEvent::Eof) => {
// Ctrl+D on empty line // Ctrl+D on empty line
QUIT_CODE.store(0, Ordering::SeqCst); QUIT_CODE.store(0, Ordering::SeqCst);
return Ok(()); return Ok(());
} }
Ok(ReadlineEvent::Pending) => { Ok(ReadlineEvent::Pending) => {
// No complete input yet, keep polling // No complete input yet, keep polling
} }
Err(e) => match e.kind() { Err(e) => match e.kind() {
ShErrKind::CleanExit(code) => { ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst); QUIT_CODE.store(*code, Ordering::SeqCst);
return Ok(()); return Ok(());
} }
_ => eprintln!("{e}"), _ => eprintln!("{e}"),
} },
} }
} }
Ok(()) Ok(())
} }

View File

@@ -1,15 +1,40 @@
use std::{collections::{HashSet, VecDeque}, os::unix::fs::PermissionsExt}; use std::{
collections::{HashSet, VecDeque},
os::unix::fs::PermissionsExt,
};
use crate::{ use crate::{
builtin::{ builtin::{
alias::{alias, unalias}, cd::cd, complete::{compgen_builtin, complete_builtin}, dirstack::{dirs, popd, pushd}, echo::echo, eval, exec, flowctl::flowctl, jobctl::{JobBehavior, continue_job, disown, jobs}, pwd::pwd, read::read_builtin, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, true_builtin, varcmds::{export, local, readonly, unset}, zoltraak::zoltraak alias::{alias, unalias},
cd::cd,
complete::{compgen_builtin, complete_builtin},
dirstack::{dirs, popd, pushd},
echo::echo,
eval, exec,
flowctl::flowctl,
jobctl::{JobBehavior, continue_job, disown, jobs},
pwd::pwd,
read::read_builtin,
shift::shift,
shopt::shopt,
source::source,
test::double_bracket_test,
trap::{TrapTarget, trap},
true_builtin,
varcmds::{export, local, readonly, unset},
zoltraak::zoltraak,
}, },
expand::{expand_aliases, glob_to_regex}, expand::{expand_aliases, glob_to_regex},
jobs::{ChildProc, JobStack, dispatch_job}, jobs::{ChildProc, JobStack, dispatch_job},
libsh::{error::{ShErr, ShErrKind, ShResult, ShResultExt}, utils::RedirVecUtils}, libsh::{
error::{ShErr, ShErrKind, ShResult, ShResultExt},
utils::RedirVecUtils,
},
prelude::*, prelude::*,
procio::{IoMode, IoStack}, procio::{IoMode, IoStack},
state::{self, ShFunc, VarFlags, VarKind, read_logic, read_shopts, write_jobs, write_logic, write_vars}, state::{
self, ShFunc, VarFlags, VarKind, read_logic, read_shopts, write_jobs, write_logic, write_vars,
},
}; };
use super::{ use super::{
@@ -23,35 +48,37 @@ thread_local! {
} }
pub fn is_in_path(name: &str) -> bool { pub fn is_in_path(name: &str) -> bool {
if name.starts_with("./") || name.starts_with("../") || name.starts_with('/') { if name.starts_with("./") || name.starts_with("../") || name.starts_with('/') {
let path = Path::new(name); let path = Path::new(name);
if path.exists() && path.is_file() && !path.is_dir() { if path.exists() && path.is_file() && !path.is_dir() {
let meta = match path.metadata() { let meta = match path.metadata() {
Ok(m) => m, Ok(m) => m,
Err(_) => return false, Err(_) => return false,
}; };
if meta.permissions().mode() & 0o111 != 0 { if meta.permissions().mode() & 0o111 != 0 {
return true; return true;
} }
} }
false false
} else { } else {
let Ok(path) = env::var("PATH") else { return false }; let Ok(path) = env::var("PATH") else {
let paths = path.split(':'); return false;
for path in paths { };
let full_path = Path::new(path).join(name); let paths = path.split(':');
if full_path.exists() && full_path.is_file() && !full_path.is_dir() { for path in paths {
let meta = match full_path.metadata() { let full_path = Path::new(path).join(name);
Ok(m) => m, if full_path.exists() && full_path.is_file() && !full_path.is_dir() {
Err(_) => continue, let meta = match full_path.metadata() {
}; Ok(m) => m,
if meta.permissions().mode() & 0o111 != 0 { Err(_) => continue,
return true; };
} if meta.permissions().mode() & 0o111 != 0 {
} return true;
} }
false }
} }
false
}
} }
pub struct ScopeGuard; pub struct ScopeGuard;
@@ -155,7 +182,7 @@ pub fn exec_input(input: String, io_stack: Option<IoStack>, interactive: bool) -
return Ok(()); return Ok(());
} }
let nodes = parser.extract_nodes(); let nodes = parser.extract_nodes();
let mut dispatcher = Dispatcher::new(nodes, interactive); let mut dispatcher = Dispatcher::new(nodes, interactive);
if let Some(mut stack) = io_stack { if let Some(mut stack) = io_stack {
@@ -225,8 +252,9 @@ impl Dispatcher {
} else if is_subsh(node.get_command().cloned()) { } else if is_subsh(node.get_command().cloned()) {
self.exec_subsh(node) self.exec_subsh(node)
} else if read_shopts(|s| s.core.autocd) } else if read_shopts(|s| s.core.autocd)
&& Path::new(cmd.span.as_str()).is_dir() && Path::new(cmd.span.as_str()).is_dir()
&& !is_in_path(cmd.span.as_str()) { && !is_in_path(cmd.span.as_str())
{
let dir = cmd.span.as_str().to_string(); let dir = cmd.span.as_str().to_string();
let stack = IoStack { let stack = IoStack {
stack: self.io_stack.clone(), stack: self.io_stack.clone(),
@@ -305,27 +333,27 @@ impl Dispatcher {
unreachable!() unreachable!()
}; };
self.run_fork("anonymous_subshell", |s| { self.run_fork("anonymous_subshell", |s| {
if let Err(e) = s.set_assignments(assignments, AssignBehavior::Export) { if let Err(e) = s.set_assignments(assignments, AssignBehavior::Export) {
eprintln!("{e}"); eprintln!("{e}");
return; return;
}; };
s.io_stack.append_to_frame(subsh.redirs); s.io_stack.append_to_frame(subsh.redirs);
let mut argv = match prepare_argv(argv) { let mut argv = match prepare_argv(argv) {
Ok(argv) => argv, Ok(argv) => argv,
Err(e) => { Err(e) => {
eprintln!("{e}"); eprintln!("{e}");
return; return;
} }
}; };
let subsh = argv.remove(0); let subsh = argv.remove(0);
let subsh_body = subsh.0.to_string(); let subsh_body = subsh.0.to_string();
if let Err(e) = exec_input(subsh_body, None, s.interactive) { if let Err(e) = exec_input(subsh_body, None, s.interactive) {
eprintln!("{e}"); eprintln!("{e}");
}; };
}) })
} }
fn exec_func(&mut self, func: Node) -> ShResult<()> { fn exec_func(&mut self, func: Node) -> ShResult<()> {
let blame = func.get_span().clone(); let blame = func.get_span().clone();
@@ -362,7 +390,7 @@ impl Dispatcher {
let result = if let Some(ref mut func_body) = read_logic(|l| l.get_func(&func_name)) { let result = if let Some(ref mut func_body) = read_logic(|l| l.get_func(&func_name)) {
let _guard = ScopeGuard::exclusive_scope(Some(argv)); let _guard = ScopeGuard::exclusive_scope(Some(argv));
func_body.body_mut().flags = func.flags; func_body.body_mut().flags = func.flags;
if let Err(e) = self.exec_brc_grp(func_body.body().clone()) { if let Err(e) = self.exec_brc_grp(func_body.body().clone()) {
match e.kind() { match e.kind() {
@@ -390,33 +418,32 @@ impl Dispatcher {
let NdRule::BraceGrp { body } = brc_grp.class else { let NdRule::BraceGrp { body } = brc_grp.class else {
unreachable!() unreachable!()
}; };
let fork_builtins = brc_grp.flags.contains(NdFlags::FORK_BUILTINS); let fork_builtins = brc_grp.flags.contains(NdFlags::FORK_BUILTINS);
self.io_stack.append_to_frame(brc_grp.redirs); self.io_stack.append_to_frame(brc_grp.redirs);
let _guard = self.io_stack.pop_frame().redirect()?; let _guard = self.io_stack.pop_frame().redirect()?;
let brc_grp_logic = |s: &mut Self| -> ShResult<()> { let brc_grp_logic = |s: &mut Self| -> ShResult<()> {
for node in body {
let blame = node.get_span();
s.dispatch_node(node).try_blame(blame)?;
}
for node in body { Ok(())
let blame = node.get_span(); };
s.dispatch_node(node).try_blame(blame)?;
}
Ok(()) if fork_builtins {
}; log::trace!("Forking brace group");
self.run_fork("brace group", |s| {
if fork_builtins { if let Err(e) = brc_grp_logic(s) {
log::trace!("Forking brace group"); eprintln!("{e}");
self.run_fork("brace group", |s| { }
if let Err(e) = brc_grp_logic(s) { })
eprintln!("{e}"); } else {
} brc_grp_logic(self)
}) }
} else {
brc_grp_logic(self)
}
} }
fn exec_case(&mut self, case_stmt: Node) -> ShResult<()> { fn exec_case(&mut self, case_stmt: Node) -> ShResult<()> {
let blame = case_stmt.get_span().clone(); let blame = case_stmt.get_span().clone();
let NdRule::CaseNode { let NdRule::CaseNode {
pattern, pattern,
case_blocks, case_blocks,
@@ -425,52 +452,52 @@ impl Dispatcher {
unreachable!() unreachable!()
}; };
let fork_builtins = case_stmt.flags.contains(NdFlags::FORK_BUILTINS); let fork_builtins = case_stmt.flags.contains(NdFlags::FORK_BUILTINS);
self.io_stack.append_to_frame(case_stmt.redirs); self.io_stack.append_to_frame(case_stmt.redirs);
let _guard = self.io_stack.pop_frame().redirect()?; let _guard = self.io_stack.pop_frame().redirect()?;
let case_logic = |s: &mut Self| -> ShResult<()> { let case_logic = |s: &mut Self| -> ShResult<()> {
let exp_pattern = pattern.clone().expand()?; let exp_pattern = pattern.clone().expand()?;
let pattern_raw = exp_pattern let pattern_raw = exp_pattern
.get_words() .get_words()
.first() .first()
.map(|s| s.to_string()) .map(|s| s.to_string())
.unwrap_or_default(); .unwrap_or_default();
'outer: for block in case_blocks { 'outer: for block in case_blocks {
let CaseNode { pattern, body } = block; let CaseNode { pattern, body } = block;
let block_pattern_raw = pattern.span.as_str().trim_end_matches(')').trim(); let block_pattern_raw = pattern.span.as_str().trim_end_matches(')').trim();
// Split at '|' to allow for multiple patterns like `foo|bar)` // Split at '|' to allow for multiple patterns like `foo|bar)`
let block_patterns = block_pattern_raw.split('|'); let block_patterns = block_pattern_raw.split('|');
for pattern in block_patterns { for pattern in block_patterns {
let pattern_regex = glob_to_regex(pattern, false); let pattern_regex = glob_to_regex(pattern, false);
if pattern_regex.is_match(&pattern_raw) { if pattern_regex.is_match(&pattern_raw) {
for node in &body { for node in &body {
s.dispatch_node(node.clone())?; s.dispatch_node(node.clone())?;
} }
break 'outer; break 'outer;
} }
} }
} }
Ok(()) Ok(())
}; };
if fork_builtins { if fork_builtins {
log::trace!("Forking builtin: case"); log::trace!("Forking builtin: case");
self.run_fork("case", |s| { self.run_fork("case", |s| {
if let Err(e) = case_logic(s) { if let Err(e) = case_logic(s) {
eprintln!("{e}"); eprintln!("{e}");
} }
}) })
} else { } else {
case_logic(self).try_blame(blame) case_logic(self).try_blame(blame)
} }
} }
fn exec_loop(&mut self, loop_stmt: Node) -> ShResult<()> { fn exec_loop(&mut self, loop_stmt: Node) -> ShResult<()> {
let blame = loop_stmt.get_span().clone(); let blame = loop_stmt.get_span().clone();
let NdRule::LoopNode { kind, cond_node } = loop_stmt.class else { let NdRule::LoopNode { kind, cond_node } = loop_stmt.class else {
unreachable!(); unreachable!();
}; };
@@ -481,64 +508,64 @@ impl Dispatcher {
} }
}; };
let fork_builtins = loop_stmt.flags.contains(NdFlags::FORK_BUILTINS); let fork_builtins = loop_stmt.flags.contains(NdFlags::FORK_BUILTINS);
self.io_stack.append_to_frame(loop_stmt.redirs); self.io_stack.append_to_frame(loop_stmt.redirs);
let _guard = self.io_stack.pop_frame().redirect()?; let _guard = self.io_stack.pop_frame().redirect()?;
let loop_logic = |s: &mut Self| -> ShResult<()> { let loop_logic = |s: &mut Self| -> ShResult<()> {
let CondNode { cond, body } = cond_node; let CondNode { cond, body } = cond_node;
'outer: loop { 'outer: loop {
if let Err(e) = s.dispatch_node(*cond.clone()) { if let Err(e) = s.dispatch_node(*cond.clone()) {
state::set_status(1); state::set_status(1);
return Err(e); return Err(e);
} }
let status = state::get_status(); let status = state::get_status();
if keep_going(kind, status) { if keep_going(kind, status) {
for node in &body { for node in &body {
if let Err(e) = s.dispatch_node(node.clone()) { if let Err(e) = s.dispatch_node(node.clone()) {
match e.kind() { match e.kind() {
ShErrKind::LoopBreak(code) => { ShErrKind::LoopBreak(code) => {
state::set_status(*code); state::set_status(*code);
break 'outer; break 'outer;
} }
ShErrKind::LoopContinue(code) => { ShErrKind::LoopContinue(code) => {
state::set_status(*code); state::set_status(*code);
continue 'outer; continue 'outer;
} }
_ => { _ => {
return Err(e); return Err(e);
} }
} }
} }
} }
} else { } else {
break; break;
} }
} }
Ok(()) Ok(())
}; };
if fork_builtins { if fork_builtins {
log::trace!("Forking builtin: loop"); log::trace!("Forking builtin: loop");
self.run_fork("loop", |s| { self.run_fork("loop", |s| {
if let Err(e) = loop_logic(s) { if let Err(e) = loop_logic(s) {
eprintln!("{e}"); eprintln!("{e}");
} }
}) })
} else { } else {
loop_logic(self).try_blame(blame) loop_logic(self).try_blame(blame)
} }
} }
fn exec_for(&mut self, for_stmt: Node) -> ShResult<()> { fn exec_for(&mut self, for_stmt: Node) -> ShResult<()> {
let blame = for_stmt.get_span().clone(); let blame = for_stmt.get_span().clone();
let NdRule::ForNode { vars, arr, body } = for_stmt.class else { let NdRule::ForNode { vars, arr, body } = for_stmt.class else {
unreachable!(); unreachable!();
}; };
let fork_builtins = for_stmt.flags.contains(NdFlags::FORK_BUILTINS); let fork_builtins = for_stmt.flags.contains(NdFlags::FORK_BUILTINS);
let to_expanded_strings = |tks: Vec<Tk>| -> ShResult<Vec<String>> { let to_expanded_strings = |tks: Vec<Tk>| -> ShResult<Vec<String>> {
Ok( Ok(
@@ -552,60 +579,66 @@ impl Dispatcher {
) )
}; };
self.io_stack.append_to_frame(for_stmt.redirs); self.io_stack.append_to_frame(for_stmt.redirs);
let _guard = self.io_stack.pop_frame().redirect()?; let _guard = self.io_stack.pop_frame().redirect()?;
let for_logic = |s: &mut Self| -> ShResult<()> { let for_logic = |s: &mut Self| -> ShResult<()> {
// Expand all array variables // Expand all array variables
let arr: Vec<String> = to_expanded_strings(arr)?; let arr: Vec<String> = to_expanded_strings(arr)?;
let vars: Vec<String> = to_expanded_strings(vars)?; let vars: Vec<String> = to_expanded_strings(vars)?;
let mut for_guard = VarCtxGuard::new(vars.iter().map(|v| v.to_string()).collect()); let mut for_guard = VarCtxGuard::new(vars.iter().map(|v| v.to_string()).collect());
'outer: for chunk in arr.chunks(vars.len()) { 'outer: for chunk in arr.chunks(vars.len()) {
let empty = String::new(); let empty = String::new();
let chunk_iter = vars let chunk_iter = vars
.iter() .iter()
.zip(chunk.iter().chain(std::iter::repeat(&empty))); .zip(chunk.iter().chain(std::iter::repeat(&empty)));
for (var, val) in chunk_iter { for (var, val) in chunk_iter {
write_vars(|v| v.set_var(&var.to_string(), VarKind::Str(val.to_string()), VarFlags::NONE))?; write_vars(|v| {
for_guard.vars.insert(var.to_string()); v.set_var(
} &var.to_string(),
VarKind::Str(val.to_string()),
VarFlags::NONE,
)
})?;
for_guard.vars.insert(var.to_string());
}
for node in body.clone() { for node in body.clone() {
if let Err(e) = s.dispatch_node(node) { if let Err(e) = s.dispatch_node(node) {
match e.kind() { match e.kind() {
ShErrKind::LoopBreak(code) => { ShErrKind::LoopBreak(code) => {
state::set_status(*code); state::set_status(*code);
break 'outer; break 'outer;
} }
ShErrKind::LoopContinue(code) => { ShErrKind::LoopContinue(code) => {
state::set_status(*code); state::set_status(*code);
continue 'outer; continue 'outer;
} }
_ => return Err(e), _ => return Err(e),
} }
} }
} }
} }
Ok(()) Ok(())
}; };
if fork_builtins { if fork_builtins {
log::trace!("Forking builtin: for"); log::trace!("Forking builtin: for");
self.run_fork("for", |s| { self.run_fork("for", |s| {
if let Err(e) = for_logic(s) { if let Err(e) = for_logic(s) {
eprintln!("{e}"); eprintln!("{e}");
} }
}) })
} else { } else {
for_logic(self).try_blame(blame) for_logic(self).try_blame(blame)
} }
} }
fn exec_if(&mut self, if_stmt: Node) -> ShResult<()> { fn exec_if(&mut self, if_stmt: Node) -> ShResult<()> {
let blame = if_stmt.get_span().clone(); let blame = if_stmt.get_span().clone();
let NdRule::IfNode { let NdRule::IfNode {
cond_nodes, cond_nodes,
else_block, else_block,
@@ -613,61 +646,61 @@ impl Dispatcher {
else { else {
unreachable!(); unreachable!();
}; };
let fork_builtins = if_stmt.flags.contains(NdFlags::FORK_BUILTINS); let fork_builtins = if_stmt.flags.contains(NdFlags::FORK_BUILTINS);
self.io_stack.append_to_frame(if_stmt.redirs); self.io_stack.append_to_frame(if_stmt.redirs);
let _guard = self.io_stack.pop_frame().redirect()?; let _guard = self.io_stack.pop_frame().redirect()?;
let if_logic = |s: &mut Self| -> ShResult<()> { let if_logic = |s: &mut Self| -> ShResult<()> {
let mut matched = false; let mut matched = false;
for node in cond_nodes { for node in cond_nodes {
let CondNode { cond, body } = node; let CondNode { cond, body } = node;
if let Err(e) = s.dispatch_node(*cond) { if let Err(e) = s.dispatch_node(*cond) {
state::set_status(1); state::set_status(1);
return Err(e); return Err(e);
} }
match state::get_status() { match state::get_status() {
0 => { 0 => {
matched = true; matched = true;
for body_node in body { for body_node in body {
s.dispatch_node(body_node)?; s.dispatch_node(body_node)?;
} }
break; // Don't check remaining elif conditions break; // Don't check remaining elif conditions
} }
_ => continue, _ => continue,
} }
} }
if !matched && !else_block.is_empty() { if !matched && !else_block.is_empty() {
for node in else_block { for node in else_block {
s.dispatch_node(node)?; s.dispatch_node(node)?;
} }
} }
Ok(()) Ok(())
}; };
if fork_builtins { if fork_builtins {
log::trace!("Forking builtin: if"); log::trace!("Forking builtin: if");
self.run_fork("if", |s| { self.run_fork("if", |s| {
if let Err(e) = if_logic(s) { if let Err(e) = if_logic(s) {
eprintln!("{e}"); eprintln!("{e}");
state::set_status(1); state::set_status(1);
} }
}) })
} else { } else {
if_logic(self).try_blame(blame) if_logic(self).try_blame(blame)
} }
} }
fn exec_pipeline(&mut self, pipeline: Node) -> ShResult<()> { fn exec_pipeline(&mut self, pipeline: Node) -> ShResult<()> {
let NdRule::Pipeline { cmds, pipe_err: _ } = pipeline.class else { let NdRule::Pipeline { cmds, pipe_err: _ } = pipeline.class else {
unreachable!() unreachable!()
}; };
self.job_stack.new_job(); self.job_stack.new_job();
let fork_builtin = cmds.len() > 1; // If there's more than one command, we need to fork builtins let fork_builtin = cmds.len() > 1; // If there's more than one command, we need to fork builtins
let (mut in_redirs, mut out_redirs) = self.io_stack.pop_frame().redirs.split_by_channel(); let (mut in_redirs, mut out_redirs) = self.io_stack.pop_frame().redirs.split_by_channel();
// Zip the commands and their respective pipes into an iterator // Zip the commands and their respective pipes into an iterator
let pipes_and_cmds = get_pipe_stack(cmds.len()).into_iter().zip(cmds); let pipes_and_cmds = get_pipe_stack(cmds.len()).into_iter().zip(cmds);
@@ -676,21 +709,21 @@ impl Dispatcher {
if let Some(pipe) = rpipe { if let Some(pipe) = rpipe {
self.io_stack.push_to_frame(pipe); self.io_stack.push_to_frame(pipe);
} else { } else {
for redir in std::mem::take(&mut in_redirs) { for redir in std::mem::take(&mut in_redirs) {
self.io_stack.push_to_frame(redir); self.io_stack.push_to_frame(redir);
} }
} }
if let Some(pipe) = wpipe { if let Some(pipe) = wpipe {
self.io_stack.push_to_frame(pipe); self.io_stack.push_to_frame(pipe);
} else { } else {
for redir in std::mem::take(&mut out_redirs) { for redir in std::mem::take(&mut out_redirs) {
self.io_stack.push_to_frame(redir); self.io_stack.push_to_frame(redir);
} }
} }
if fork_builtin { if fork_builtin {
cmd.flags |= NdFlags::FORK_BUILTINS; cmd.flags |= NdFlags::FORK_BUILTINS;
} }
self.dispatch_node(cmd)?; self.dispatch_node(cmd)?;
} }
let job = self.job_stack.finalize_job().unwrap(); let job = self.job_stack.finalize_job().unwrap();
@@ -699,32 +732,35 @@ impl Dispatcher {
Ok(()) Ok(())
} }
fn exec_builtin(&mut self, cmd: Node) -> ShResult<()> { fn exec_builtin(&mut self, cmd: Node) -> ShResult<()> {
let fork_builtins = cmd.flags.contains(NdFlags::FORK_BUILTINS); let fork_builtins = cmd.flags.contains(NdFlags::FORK_BUILTINS);
let cmd_raw = cmd.get_command().unwrap_or_else(|| panic!("expected command NdRule, got {:?}", &cmd.class)).to_string(); let cmd_raw = cmd
.get_command()
.unwrap_or_else(|| panic!("expected command NdRule, got {:?}", &cmd.class))
.to_string();
if fork_builtins { if fork_builtins {
log::trace!("Forking builtin: {}", cmd_raw); log::trace!("Forking builtin: {}", cmd_raw);
let _guard = self.io_stack.pop_frame().redirect()?; let _guard = self.io_stack.pop_frame().redirect()?;
self.run_fork(&cmd_raw, |s| { self.run_fork(&cmd_raw, |s| {
if let Err(e) = s.dispatch_builtin(cmd) { if let Err(e) = s.dispatch_builtin(cmd) {
eprintln!("{e}"); eprintln!("{e}");
} }
}) })
} else { } else {
let result = self.dispatch_builtin(cmd); let result = self.dispatch_builtin(cmd);
if let Err(e) = result { if let Err(e) = result {
let code = state::get_status(); let code = state::get_status();
if code == 0 { if code == 0 {
state::set_status(1); state::set_status(1);
} }
return Err(e); return Err(e);
} }
Ok(()) Ok(())
} }
} }
fn dispatch_builtin(&mut self, mut cmd: Node) -> ShResult<()> { fn dispatch_builtin(&mut self, mut cmd: Node) -> ShResult<()> {
let cmd_raw = cmd.get_command().unwrap().to_string(); let cmd_raw = cmd.get_command().unwrap().to_string();
let NdRule::Command { assignments, argv } = &mut cmd.class else { let NdRule::Command { assignments, argv } = &mut cmd.class else {
unreachable!() unreachable!()
}; };
@@ -751,51 +787,48 @@ impl Dispatcher {
} }
return self.exec_cmd(cmd); return self.exec_cmd(cmd);
} }
match cmd_raw.as_str() { match cmd_raw.as_str() {
"echo" => echo(cmd, io_stack_mut, curr_job_mut), "echo" => echo(cmd, io_stack_mut, curr_job_mut),
"cd" => cd(cmd, curr_job_mut), "cd" => cd(cmd, curr_job_mut),
"export" => export(cmd, io_stack_mut, curr_job_mut), "export" => export(cmd, io_stack_mut, curr_job_mut),
"local" => local(cmd, io_stack_mut, curr_job_mut), "local" => local(cmd, io_stack_mut, curr_job_mut),
"pwd" => pwd(cmd, io_stack_mut, curr_job_mut), "pwd" => pwd(cmd, io_stack_mut, curr_job_mut),
"source" => source(cmd, curr_job_mut), "source" => source(cmd, curr_job_mut),
"shift" => shift(cmd, curr_job_mut), "shift" => shift(cmd, curr_job_mut),
"fg" => continue_job(cmd, curr_job_mut, JobBehavior::Foregound), "fg" => continue_job(cmd, curr_job_mut, JobBehavior::Foregound),
"bg" => continue_job(cmd, curr_job_mut, JobBehavior::Background), "bg" => continue_job(cmd, curr_job_mut, JobBehavior::Background),
"disown" => disown(cmd, io_stack_mut, curr_job_mut), "disown" => disown(cmd, io_stack_mut, curr_job_mut),
"jobs" => jobs(cmd, io_stack_mut, curr_job_mut), "jobs" => jobs(cmd, io_stack_mut, curr_job_mut),
"alias" => alias(cmd, io_stack_mut, curr_job_mut), "alias" => alias(cmd, io_stack_mut, curr_job_mut),
"unalias" => unalias(cmd, io_stack_mut, curr_job_mut), "unalias" => unalias(cmd, io_stack_mut, curr_job_mut),
"return" => flowctl(cmd, ShErrKind::FuncReturn(0)), "return" => flowctl(cmd, ShErrKind::FuncReturn(0)),
"break" => flowctl(cmd, ShErrKind::LoopBreak(0)), "break" => flowctl(cmd, ShErrKind::LoopBreak(0)),
"continue" => flowctl(cmd, ShErrKind::LoopContinue(0)), "continue" => flowctl(cmd, ShErrKind::LoopContinue(0)),
"exit" => flowctl(cmd, ShErrKind::CleanExit(0)), "exit" => flowctl(cmd, ShErrKind::CleanExit(0)),
"zoltraak" => zoltraak(cmd, io_stack_mut, curr_job_mut), "zoltraak" => zoltraak(cmd, io_stack_mut, curr_job_mut),
"shopt" => shopt(cmd, io_stack_mut, curr_job_mut), "shopt" => shopt(cmd, io_stack_mut, curr_job_mut),
"read" => read_builtin(cmd, io_stack_mut, curr_job_mut), "read" => read_builtin(cmd, io_stack_mut, curr_job_mut),
"trap" => trap(cmd, io_stack_mut, curr_job_mut), "trap" => trap(cmd, io_stack_mut, curr_job_mut),
"pushd" => pushd(cmd, io_stack_mut, curr_job_mut), "pushd" => pushd(cmd, io_stack_mut, curr_job_mut),
"popd" => popd(cmd, io_stack_mut, curr_job_mut), "popd" => popd(cmd, io_stack_mut, curr_job_mut),
"dirs" => dirs(cmd, io_stack_mut, curr_job_mut), "dirs" => dirs(cmd, io_stack_mut, curr_job_mut),
"exec" => exec::exec_builtin(cmd, io_stack_mut, curr_job_mut), "exec" => exec::exec_builtin(cmd, io_stack_mut, curr_job_mut),
"eval" => eval::eval(cmd, io_stack_mut, curr_job_mut), "eval" => eval::eval(cmd, io_stack_mut, curr_job_mut),
"readonly" => readonly(cmd, io_stack_mut, curr_job_mut), "readonly" => readonly(cmd, io_stack_mut, curr_job_mut),
"unset" => unset(cmd, io_stack_mut, curr_job_mut), "unset" => unset(cmd, io_stack_mut, curr_job_mut),
"complete" => complete_builtin(cmd, io_stack_mut, curr_job_mut), "complete" => complete_builtin(cmd, io_stack_mut, curr_job_mut),
"compgen" => compgen_builtin(cmd, io_stack_mut, curr_job_mut), "compgen" => compgen_builtin(cmd, io_stack_mut, curr_job_mut),
"true" | ":" => { "true" | ":" => {
state::set_status(0); state::set_status(0);
Ok(()) Ok(())
}, }
"false" => { "false" => {
state::set_status(1); state::set_status(1);
Ok(()) Ok(())
}, }
_ => unimplemented!( _ => unimplemented!("Have not yet added support for builtin '{}'", cmd_raw),
"Have not yet added support for builtin '{}'", }
cmd_raw }
),
}
}
fn exec_cmd(&mut self, cmd: Node) -> ShResult<()> { fn exec_cmd(&mut self, cmd: Node) -> ShResult<()> {
let NdRule::Command { assignments, argv } = cmd.class else { let NdRule::Command { assignments, argv } = cmd.class else {
unreachable!() unreachable!()
@@ -810,10 +843,10 @@ impl Dispatcher {
env_vars_to_unset = self.set_assignments(assignments, assign_behavior)?; env_vars_to_unset = self.set_assignments(assignments, assign_behavior)?;
} }
let no_fork = cmd.flags.contains(NdFlags::NO_FORK); let no_fork = cmd.flags.contains(NdFlags::NO_FORK);
if argv.is_empty() { if argv.is_empty() {
state::set_status(0); state::set_status(0);
return Ok(()); return Ok(());
} }
@@ -823,30 +856,30 @@ impl Dispatcher {
let _guard = self.io_stack.pop_frame().redirect()?; let _guard = self.io_stack.pop_frame().redirect()?;
let job = self.job_stack.curr_job_mut().unwrap(); let job = self.job_stack.curr_job_mut().unwrap();
let child_logic = || -> ! { let child_logic = || -> ! {
let cmd = &exec_args.cmd.0; let cmd = &exec_args.cmd.0;
let span = exec_args.cmd.1; let span = exec_args.cmd.1;
let Err(e) = execvpe(cmd, &exec_args.argv, &exec_args.envp); let Err(e) = execvpe(cmd, &exec_args.argv, &exec_args.envp);
// execvpe only returns on error // execvpe only returns on error
let cmd_str = cmd.to_str().unwrap().to_string(); let cmd_str = cmd.to_str().unwrap().to_string();
match e { match e {
Errno::ENOENT => { Errno::ENOENT => {
let err = ShErr::full(ShErrKind::CmdNotFound(cmd_str), "", span); let err = ShErr::full(ShErrKind::CmdNotFound(cmd_str), "", span);
eprintln!("{err}"); eprintln!("{err}");
} }
_ => { _ => {
let err = ShErr::full(ShErrKind::Errno(e), format!("{e}"), span); let err = ShErr::full(ShErrKind::Errno(e), format!("{e}"), span);
eprintln!("{err}"); eprintln!("{err}");
} }
} }
exit(e as i32) exit(e as i32)
}; };
if no_fork { if no_fork {
child_logic(); child_logic();
} }
match unsafe { fork()? } { match unsafe { fork()? } {
ForkResult::Child => child_logic(), ForkResult::Child => child_logic(),
@@ -875,27 +908,27 @@ impl Dispatcher {
Ok(()) Ok(())
} }
fn run_fork(&mut self, name: &str, f: impl FnOnce(&mut Self)) -> ShResult<()> { fn run_fork(&mut self, name: &str, f: impl FnOnce(&mut Self)) -> ShResult<()> {
match unsafe { fork()? } { match unsafe { fork()? } {
ForkResult::Child => { ForkResult::Child => {
f(self); f(self);
exit(state::get_status()) exit(state::get_status())
} }
ForkResult::Parent { child } => { ForkResult::Parent { child } => {
write_jobs(|j| j.drain_registered_fds()); write_jobs(|j| j.drain_registered_fds());
let job = self.job_stack.curr_job_mut().unwrap(); let job = self.job_stack.curr_job_mut().unwrap();
let child_pgid = if let Some(pgid) = job.pgid() { let child_pgid = if let Some(pgid) = job.pgid() {
pgid pgid
} else { } else {
job.set_pgid(child); job.set_pgid(child);
child child
}; };
let child_proc = ChildProc::new(child, Some(name), Some(child_pgid))?; let child_proc = ChildProc::new(child, Some(name), Some(child_pgid))?;
job.push_child(child_proc); job.push_child(child_proc);
Ok(()) Ok(())
} }
} }
} }
fn set_assignments(&self, assigns: Vec<Node>, behavior: AssignBehavior) -> ShResult<Vec<String>> { fn set_assignments(&self, assigns: Vec<Node>, behavior: AssignBehavior) -> ShResult<Vec<String>> {
let mut new_env_vars = vec![]; let mut new_env_vars = vec![];
let flags = match behavior { let flags = match behavior {
@@ -917,9 +950,7 @@ impl Dispatcher {
// Parse and expand array index BEFORE entering write_vars borrow // Parse and expand array index BEFORE entering write_vars borrow
let indexed = state::parse_arr_bracket(var) let indexed = state::parse_arr_bracket(var)
.map(|(name, idx_raw)| { .map(|(name, idx_raw)| state::expand_arr_index(&idx_raw).map(|idx| (name, idx)))
state::expand_arr_index(&idx_raw).map(|idx| (name, idx))
})
.transpose()?; .transpose()?;
match kind { match kind {

View File

@@ -152,7 +152,7 @@ pub struct LexStream {
source: Arc<String>, source: Arc<String>,
pub cursor: usize, pub cursor: usize,
in_quote: bool, in_quote: bool,
brc_grp_start: Option<usize>, brc_grp_start: Option<usize>,
flags: LexFlags, flags: LexFlags,
} }
@@ -187,7 +187,7 @@ impl LexStream {
source, source,
cursor: 0, cursor: 0,
in_quote: false, in_quote: false,
brc_grp_start: None, brc_grp_start: None,
flags, flags,
} }
} }
@@ -222,10 +222,10 @@ impl LexStream {
pub fn set_in_brc_grp(&mut self, is: bool) { pub fn set_in_brc_grp(&mut self, is: bool) {
if is { if is {
self.flags |= LexFlags::IN_BRC_GRP; self.flags |= LexFlags::IN_BRC_GRP;
self.brc_grp_start = Some(self.cursor); self.brc_grp_start = Some(self.cursor);
} else { } else {
self.flags &= !LexFlags::IN_BRC_GRP; self.flags &= !LexFlags::IN_BRC_GRP;
self.brc_grp_start = None; self.brc_grp_start = None;
} }
} }
pub fn next_is_cmd(&self) -> bool { pub fn next_is_cmd(&self) -> bool {
@@ -269,8 +269,8 @@ impl LexStream {
} }
if !found_fd && !self.flags.contains(LexFlags::LEX_UNFINISHED) { if !found_fd && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
let span_start = self.cursor; let span_start = self.cursor;
self.cursor = pos; self.cursor = pos;
return Some(Err(ShErr::full( return Some(Err(ShErr::full(
ShErrKind::ParseErr, ShErrKind::ParseErr,
"Invalid redirection", "Invalid redirection",
@@ -624,35 +624,35 @@ impl LexStream {
} }
} }
} }
'=' if chars.peek() == Some(&'(') => { '=' if chars.peek() == Some(&'(') => {
pos += 1; // '=' pos += 1; // '='
let mut depth = 1; let mut depth = 1;
chars.next(); chars.next();
pos += 1; // '(' pos += 1; // '('
// looks like an array // looks like an array
while let Some(arr_ch) = chars.next() { while let Some(arr_ch) = chars.next() {
match arr_ch { match arr_ch {
'\\' => { '\\' => {
pos += 1; pos += 1;
if let Some(next_ch) = chars.next() { if let Some(next_ch) = chars.next() {
pos += next_ch.len_utf8(); pos += next_ch.len_utf8();
} }
} }
'(' => { '(' => {
depth += 1; depth += 1;
pos += 1; pos += 1;
} }
')' => { ')' => {
depth -= 1; depth -= 1;
pos += 1; pos += 1;
if depth == 0 { if depth == 0 {
break; break;
} }
} }
_ => pos += arr_ch.len_utf8(), _ => pos += arr_ch.len_utf8(),
} }
} }
} }
_ if !self.in_quote && is_op(ch) => break, _ if !self.in_quote && is_op(ch) => break,
_ if is_hard_sep(ch) => break, _ if is_hard_sep(ch) => break,
_ => pos += ch.len_utf8(), _ => pos += ch.len_utf8(),
@@ -660,7 +660,7 @@ impl LexStream {
} }
let mut new_tk = self.get_token(self.cursor..pos, TkRule::Str); let mut new_tk = self.get_token(self.cursor..pos, TkRule::Str);
if self.in_quote && !self.flags.contains(LexFlags::LEX_UNFINISHED) { if self.in_quote && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
self.cursor = pos; self.cursor = pos;
return Err(ShErr::full( return Err(ShErr::full(
ShErrKind::ParseErr, ShErrKind::ParseErr,
"Unterminated quote", "Unterminated quote",
@@ -692,9 +692,9 @@ impl LexStream {
} }
_ if is_cmd_sub(text) => { _ if is_cmd_sub(text) => {
new_tk.mark(TkFlags::IS_CMDSUB); new_tk.mark(TkFlags::IS_CMDSUB);
if self.next_is_cmd() { if self.next_is_cmd() {
new_tk.mark(TkFlags::IS_CMD); new_tk.mark(TkFlags::IS_CMD);
} }
self.set_next_is_cmd(false); self.set_next_is_cmd(false);
} }
_ => { _ => {
@@ -731,15 +731,16 @@ impl Iterator for LexStream {
return None; return None;
} else { } else {
// Return the EOI token // Return the EOI token
if self.in_brc_grp() && !self.flags.contains(LexFlags::LEX_UNFINISHED) { if self.in_brc_grp() && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
let start = self.brc_grp_start.unwrap_or(self.cursor.saturating_sub(1)); let start = self.brc_grp_start.unwrap_or(self.cursor.saturating_sub(1));
self.flags |= LexFlags::STALE; self.flags |= LexFlags::STALE;
return Err(ShErr::full( return Err(ShErr::full(
ShErrKind::ParseErr, ShErrKind::ParseErr,
"Unclosed brace group", "Unclosed brace group",
Span::new(start..self.cursor, self.source.clone()), Span::new(start..self.cursor, self.source.clone()),
)).into(); ))
} .into();
}
let token = self.get_token(self.cursor..self.cursor, TkRule::EOI); let token = self.get_token(self.cursor..self.cursor, TkRule::EOI);
self.flags |= LexFlags::STALE; self.flags |= LexFlags::STALE;
return Some(Ok(token)); return Some(Ok(token));
@@ -770,14 +771,15 @@ impl Iterator for LexStream {
} }
if self.cursor == self.source.len() { if self.cursor == self.source.len() {
if self.in_brc_grp() && !self.flags.contains(LexFlags::LEX_UNFINISHED) { if self.in_brc_grp() && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
let start = self.brc_grp_start.unwrap_or(self.cursor.saturating_sub(1)); let start = self.brc_grp_start.unwrap_or(self.cursor.saturating_sub(1));
return Err(ShErr::full( return Err(ShErr::full(
ShErrKind::ParseErr, ShErrKind::ParseErr,
"Unclosed brace group", "Unclosed brace group",
Span::new(start..self.cursor, self.source.clone()), Span::new(start..self.cursor, self.source.clone()),
)).into(); ))
} .into();
}
return None; return None;
} }
@@ -899,26 +901,27 @@ pub fn is_field_sep(ch: char) -> bool {
} }
pub fn is_keyword(slice: &str) -> bool { pub fn is_keyword(slice: &str) -> bool {
KEYWORDS.contains(&slice) || (ends_with_unescaped(slice, "()") && !ends_with_unescaped(slice, "=()")) KEYWORDS.contains(&slice)
|| (ends_with_unescaped(slice, "()") && !ends_with_unescaped(slice, "=()"))
} }
pub fn is_cmd_sub(slice: &str) -> bool { pub fn is_cmd_sub(slice: &str) -> bool {
slice.starts_with("$(") && ends_with_unescaped(slice,")") slice.starts_with("$(") && ends_with_unescaped(slice, ")")
} }
pub fn ends_with_unescaped(slice: &str, pat: &str) -> bool { pub fn ends_with_unescaped(slice: &str, pat: &str) -> bool {
slice.ends_with(pat) && !pos_is_escaped(slice, slice.len() - pat.len()) slice.ends_with(pat) && !pos_is_escaped(slice, slice.len() - pat.len())
} }
pub fn pos_is_escaped(slice: &str, pos: usize) -> bool { pub fn pos_is_escaped(slice: &str, pos: usize) -> bool {
let bytes = slice.as_bytes(); let bytes = slice.as_bytes();
let mut escaped = false; let mut escaped = false;
let mut i = pos; let mut i = pos;
while i > 0 && bytes[i - 1] == b'\\' { while i > 0 && bytes[i - 1] == b'\\' {
escaped = !escaped; escaped = !escaped;
i -= 1; i -= 1;
} }
escaped escaped
} }
pub fn lookahead(pat: &str, mut chars: Chars) -> Option<usize> { pub fn lookahead(pat: &str, mut chars: Chars) -> Option<usize> {

View File

@@ -117,9 +117,10 @@ impl Node {
if let NdRule::Command { if let NdRule::Command {
assignments: _, assignments: _,
argv, argv,
} = &self.class { } = &self.class
argv.iter().next() {
} else { argv.iter().next()
} else {
None None
} }
} }
@@ -142,9 +143,9 @@ bitflags! {
#[derive(Clone,Copy,Debug)] #[derive(Clone,Copy,Debug)]
pub struct NdFlags: u32 { pub struct NdFlags: u32 {
const BACKGROUND = 0b000001; const BACKGROUND = 0b000001;
const FORK_BUILTINS = 0b000010; const FORK_BUILTINS = 0b000010;
const NO_FORK = 0b000100; const NO_FORK = 0b000100;
const ARR_ASSIGN = 0b001000; const ARR_ASSIGN = 0b001000;
} }
} }
@@ -1380,7 +1381,7 @@ impl ParseStream {
redirs.push(redir); redirs.push(redir);
} }
} }
TkRule::Comment => { /* Skip comments in command position */ } TkRule::Comment => { /* Skip comments in command position */ }
_ => unimplemented!("Unexpected token rule `{:?}` in parse_cmd()", tk.class), _ => unimplemented!("Unexpected token rule `{:?}` in parse_cmd()", tk.class),
} }
} }
@@ -1473,14 +1474,16 @@ impl ParseStream {
} }
} }
} }
if let Some(assign_kind) = assign_kind && !var_name.is_empty() { if let Some(assign_kind) = assign_kind
&& !var_name.is_empty()
{
let var = Tk::new(TkRule::Str, Span::new(name_range, token.source())); let var = Tk::new(TkRule::Str, Span::new(name_range, token.source()));
let val = Tk::new(TkRule::Str, Span::new(val_range, token.source())); let val = Tk::new(TkRule::Str, Span::new(val_range, token.source()));
let flags = if var_val.starts_with('(') && var_val.ends_with(')') { let flags = if var_val.starts_with('(') && var_val.ends_with(')') {
NdFlags::ARR_ASSIGN NdFlags::ARR_ASSIGN
} else { } else {
NdFlags::empty() NdFlags::empty()
}; };
Some(Node { Some(Node {
class: NdRule::Assignment { class: NdRule::Assignment {
@@ -1493,8 +1496,8 @@ impl ParseStream {
redirs: vec![], redirs: vec![],
}) })
} else { } else {
None None
} }
} }
} }

View File

@@ -19,17 +19,17 @@ pub use std::os::unix::io::{AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd,
pub use bitflags::bitflags; pub use bitflags::bitflags;
pub use nix::{ pub use nix::{
errno::Errno, errno::Errno,
fcntl::{open, OFlag}, fcntl::{OFlag, open},
libc::{self, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO}, libc::{self, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO},
sys::{ sys::{
signal::{self, kill, killpg, pthread_sigmask, signal, SigHandler, SigSet, SigmaskHow, Signal}, signal::{self, SigHandler, SigSet, SigmaskHow, Signal, kill, killpg, pthread_sigmask, signal},
stat::Mode, stat::Mode,
termios::{self}, termios::{self},
wait::{waitpid, WaitPidFlag as WtFlag, WaitStatus as WtStat}, wait::{WaitPidFlag as WtFlag, WaitStatus as WtStat, waitpid},
}, },
unistd::{ unistd::{
close, dup, dup2, execvpe, fork, getpgid, getpgrp, isatty, pipe, read, setpgid, tcgetpgrp, ForkResult, Pid, close, dup, dup2, execvpe, fork, getpgid, getpgrp, isatty, pipe, read,
tcsetpgrp, write, ForkResult, Pid, setpgid, tcgetpgrp, tcsetpgrp, write,
}, },
}; };

View File

@@ -9,7 +9,7 @@ use crate::{
error::{ShErr, ShErrKind, ShResult}, error::{ShErr, ShErrKind, ShResult},
utils::RedirVecUtils, utils::RedirVecUtils,
}, },
parse::{get_redir_file, Redir, RedirType}, parse::{Redir, RedirType, get_redir_file},
prelude::*, prelude::*,
}; };
@@ -79,7 +79,7 @@ impl IoMode {
let path_raw = path.as_os_str().to_str().unwrap_or_default().to_string(); let path_raw = path.as_os_str().to_str().unwrap_or_default().to_string();
let expanded_path = Expander::from_raw(&path_raw)?.expand()?.join(" "); // should just be one string, will have to find some way to handle a return of let expanded_path = Expander::from_raw(&path_raw)?.expand()?.join(" "); // should just be one string, will have to find some way to handle a return of
// multiple // multiple
let expanded_pathbuf = PathBuf::from(expanded_path); let expanded_pathbuf = PathBuf::from(expanded_path);
@@ -343,9 +343,9 @@ impl DerefMut for IoStack {
} }
impl From<Vec<IoFrame>> for IoStack { impl From<Vec<IoFrame>> for IoStack {
fn from(frames: Vec<IoFrame>) -> Self { fn from(frames: Vec<IoFrame>) -> Self {
Self { stack: frames } Self { stack: frames }
} }
} }
pub fn borrow_fd<'f>(fd: i32) -> BorrowedFd<'f> { pub fn borrow_fd<'f>(fd: i32) -> BorrowedFd<'f> {

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,10 @@ use std::{
use crate::{ use crate::{
libsh::term::{Style, StyleSet, Styled}, libsh::term::{Style, StyleSet, Styled},
readline::{annotate_input, markers::{self, is_marker}}, readline::{
annotate_input,
markers::{self, is_marker},
},
state::{read_logic, read_meta, read_shopts}, state::{read_logic, read_meta, read_shopts},
}; };
@@ -20,10 +23,10 @@ use crate::{
pub struct Highlighter { pub struct Highlighter {
input: String, input: String,
output: String, output: String,
linebuf_cursor_pos: usize, linebuf_cursor_pos: usize,
style_stack: Vec<StyleSet>, style_stack: Vec<StyleSet>,
last_was_reset: bool, last_was_reset: bool,
in_selection: bool in_selection: bool,
} }
impl Highlighter { impl Highlighter {
@@ -32,10 +35,10 @@ impl Highlighter {
Self { Self {
input: String::new(), input: String::new(),
output: String::new(), output: String::new(),
linebuf_cursor_pos: 0, linebuf_cursor_pos: 0,
style_stack: Vec::new(), style_stack: Vec::new(),
last_was_reset: true, // start as true so we don't emit a leading reset last_was_reset: true, // start as true so we don't emit a leading reset
in_selection: false in_selection: false,
} }
} }
@@ -46,18 +49,18 @@ impl Highlighter {
pub fn load_input(&mut self, input: &str, linebuf_cursor_pos: usize) { pub fn load_input(&mut self, input: &str, linebuf_cursor_pos: usize) {
let input = annotate_input(input); let input = annotate_input(input);
self.input = input; self.input = input;
self.linebuf_cursor_pos = linebuf_cursor_pos; self.linebuf_cursor_pos = linebuf_cursor_pos;
} }
pub fn strip_markers(str: &str) -> String { pub fn strip_markers(str: &str) -> String {
let mut out = String::new(); let mut out = String::new();
for ch in str.chars() { for ch in str.chars() {
if !is_marker(ch) { if !is_marker(ch) {
out.push(ch); out.push(ch);
} }
} }
out out
} }
/// Processes the annotated input and generates ANSI-styled output /// Processes the annotated input and generates ANSI-styled output
/// ///
@@ -69,14 +72,14 @@ impl Highlighter {
let mut input_chars = input.chars().peekable(); let mut input_chars = input.chars().peekable();
while let Some(ch) = input_chars.next() { while let Some(ch) = input_chars.next() {
match ch { match ch {
markers::VISUAL_MODE_START => { markers::VISUAL_MODE_START => {
self.emit_style(Style::BgWhite | Style::Black); self.emit_style(Style::BgWhite | Style::Black);
self.in_selection = true; self.in_selection = true;
} }
markers::VISUAL_MODE_END => { markers::VISUAL_MODE_END => {
self.reapply_style(); self.reapply_style();
self.in_selection = false; self.in_selection = false;
} }
markers::STRING_DQ_END markers::STRING_DQ_END
| markers::STRING_SQ_END | markers::STRING_SQ_END
| markers::VAR_SUB_END | markers::VAR_SUB_END
@@ -96,16 +99,16 @@ impl Highlighter {
if ch == markers::RESET { if ch == markers::RESET {
break; break;
} }
if !is_marker(ch) { if !is_marker(ch) {
cmd_name.push(ch); cmd_name.push(ch);
} }
} }
match cmd_name.as_str() { match cmd_name.as_str() {
"continue" | "return" | "break" => self.push_style(Style::Magenta), "continue" | "return" | "break" => self.push_style(Style::Magenta),
_ => self.push_style(Style::Green), _ => self.push_style(Style::Green),
} }
} }
markers::CASE_PAT => self.push_style(Style::Blue), markers::CASE_PAT => self.push_style(Style::Blue),
markers::COMMENT => self.push_style(Style::BrightBlack), markers::COMMENT => self.push_style(Style::BrightBlack),
@@ -114,7 +117,6 @@ impl Highlighter {
markers::REDIRECT | markers::OPERATOR => self.push_style(Style::Magenta | Style::Bold), markers::REDIRECT | markers::OPERATOR => self.push_style(Style::Magenta | Style::Bold),
markers::ASSIGNMENT => { markers::ASSIGNMENT => {
let mut var_name = String::new(); let mut var_name = String::new();
@@ -140,28 +142,30 @@ impl Highlighter {
markers::ARG => { markers::ARG => {
let mut arg = String::new(); let mut arg = String::new();
let is_last_arg = !input_chars.clone().any(|c| c == markers::ARG || c.is_whitespace()); let is_last_arg = !input_chars
.clone()
.any(|c| c == markers::ARG || c.is_whitespace());
if !is_last_arg { if !is_last_arg {
self.push_style(Style::White); self.push_style(Style::White);
} else { } else {
let mut chars_clone = input_chars.clone(); let mut chars_clone = input_chars.clone();
while let Some(ch) = chars_clone.next() { while let Some(ch) = chars_clone.next() {
if ch == markers::RESET { if ch == markers::RESET {
break; break;
} }
arg.push(ch); arg.push(ch);
} }
let style = if Self::is_filename(&Self::strip_markers(&arg)) { let style = if Self::is_filename(&Self::strip_markers(&arg)) {
Style::White | Style::Underline Style::White | Style::Underline
} else { } else {
Style::White.into() Style::White.into()
}; };
self.push_style(style); self.push_style(style);
self.last_was_reset = false; self.last_was_reset = false;
} }
} }
markers::COMMAND => { markers::COMMAND => {
@@ -173,9 +177,12 @@ impl Highlighter {
} }
cmd_name.push(ch); cmd_name.push(ch);
} }
let style = if matches!(Self::strip_markers(&cmd_name).as_str(), "break" | "continue" | "return") { let style = if matches!(
Style::Magenta.into() Self::strip_markers(&cmd_name).as_str(),
} else if Self::is_valid(&Self::strip_markers(&cmd_name)) { "break" | "continue" | "return"
) {
Style::Magenta.into()
} else if Self::is_valid(&Self::strip_markers(&cmd_name)) {
Style::Green.into() Style::Green.into()
} else { } else {
Style::Red | Style::Bold Style::Red | Style::Bold
@@ -292,21 +299,21 @@ impl Highlighter {
fn is_valid(command: &str) -> bool { fn is_valid(command: &str) -> bool {
let cmd_path = Path::new(&command); let cmd_path = Path::new(&command);
if cmd_path.is_dir() && read_shopts(|o| o.core.autocd) { if cmd_path.is_dir() && read_shopts(|o| o.core.autocd) {
// this is a directory and autocd is enabled // this is a directory and autocd is enabled
return true; return true;
} }
if cmd_path.is_absolute() { if cmd_path.is_absolute() {
// the user has given us an absolute path // the user has given us an absolute path
let Ok(meta) = cmd_path.metadata() else { let Ok(meta) = cmd_path.metadata() else {
return false; return false;
}; };
// this is a file that is executable by someone // this is a file that is executable by someone
meta.permissions().mode() & 0o111 != 0 meta.permissions().mode() & 0o111 != 0
} else { } else {
read_meta(|m| m.cached_cmds().get(command).is_some()) read_meta(|m| m.cached_cmds().get(command).is_some())
} }
} }
fn is_filename(arg: &str) -> bool { fn is_filename(arg: &str) -> bool {
@@ -316,9 +323,10 @@ impl Highlighter {
return true; return true;
} }
if path.is_absolute() if path.is_absolute()
&& let Some(parent_dir) = path.parent() && let Some(parent_dir) = path.parent()
&& let Ok(entries) = parent_dir.read_dir() { && let Ok(entries) = parent_dir.read_dir()
{
let files = entries let files = entries
.filter_map(|e| e.ok()) .filter_map(|e| e.ok())
.map(|e| e.file_name().to_string_lossy().to_string()) .map(|e| e.file_name().to_string_lossy().to_string())
@@ -334,17 +342,17 @@ impl Highlighter {
return true; return true;
} }
} }
} }
read_meta(|m| { read_meta(|m| {
let files = m.cwd_cache(); let files = m.cwd_cache();
for file in files { for file in files {
if file.starts_with(arg) { if file.starts_with(arg) {
return true; return true;
} }
} }
false false
}) })
} }
/// Emits a reset ANSI code to the output, with deduplication /// Emits a reset ANSI code to the output, with deduplication
@@ -363,10 +371,10 @@ impl Highlighter {
/// Unconditionally appends the ANSI escape sequence for the given style /// Unconditionally appends the ANSI escape sequence for the given style
/// and marks that we're no longer in a reset state. /// and marks that we're no longer in a reset state.
fn emit_style(&mut self, style: StyleSet) { fn emit_style(&mut self, style: StyleSet) {
let mut style = style; let mut style = style;
if !style.styles().contains(&Style::BgWhite) { if !style.styles().contains(&Style::BgWhite) {
style = style.add_style(Style::BgBlack); style = style.add_style(Style::BgBlack);
} }
self.output.push_str(&style.to_string()); self.output.push_str(&style.to_string());
self.last_was_reset = false; self.last_was_reset = false;
} }
@@ -378,9 +386,9 @@ impl Highlighter {
pub fn push_style(&mut self, style: impl Into<StyleSet>) { pub fn push_style(&mut self, style: impl Into<StyleSet>) {
let set: StyleSet = style.into(); let set: StyleSet = style.into();
self.style_stack.push(set.clone()); self.style_stack.push(set.clone());
if !self.in_selection { if !self.in_selection {
self.emit_style(set.clone()); self.emit_style(set.clone());
} }
} }
/// Pops a style from the stack and restores the previous style /// Pops a style from the stack and restores the previous style
@@ -405,18 +413,18 @@ impl Highlighter {
/// the default terminal color between independent commands. /// the default terminal color between independent commands.
pub fn clear_styles(&mut self) { pub fn clear_styles(&mut self) {
self.style_stack.clear(); self.style_stack.clear();
if !self.in_selection { if !self.in_selection {
self.emit_reset(); self.emit_reset();
} }
} }
pub fn reapply_style(&mut self) { pub fn reapply_style(&mut self) {
if let Some(style) = self.style_stack.last().cloned() { if let Some(style) = self.style_stack.last().cloned() {
self.emit_style(style); self.emit_style(style);
} else { } else {
self.emit_reset(); self.emit_reset();
} }
} }
/// Simple marker-to-ANSI replacement (unused in favor of stack-based /// Simple marker-to-ANSI replacement (unused in favor of stack-based
/// highlighting) /// highlighting)

View File

@@ -14,7 +14,14 @@ use crate::{
libsh::{ libsh::{
error::ShResult, error::ShResult,
term::{Style, Styled}, term::{Style, Styled},
}, parse::lex::{LexFlags, LexStream, Tk, TkFlags, TkRule}, prelude::*, readline::{markers, register::{write_register, RegisterContent}}, state::read_shopts },
parse::lex::{LexFlags, LexStream, Tk, TkFlags, TkRule},
prelude::*,
readline::{
markers,
register::{RegisterContent, write_register},
},
state::read_shopts,
}; };
const PUNCTUATION: [&str; 3] = ["?", "!", "."]; const PUNCTUATION: [&str; 3] = ["?", "!", "."];
@@ -326,7 +333,7 @@ pub struct LineBuf {
pub insert_mode_start_pos: Option<usize>, pub insert_mode_start_pos: Option<usize>,
pub saved_col: Option<usize>, pub saved_col: Option<usize>,
pub auto_indent_level: usize, pub auto_indent_level: usize,
pub undo_stack: Vec<Edit>, pub undo_stack: Vec<Edit>,
pub redo_stack: Vec<Edit>, pub redo_stack: Vec<Edit>,
@@ -384,12 +391,12 @@ impl LineBuf {
pub fn set_cursor_clamp(&mut self, yn: bool) { pub fn set_cursor_clamp(&mut self, yn: bool) {
self.cursor.exclusive = yn; self.cursor.exclusive = yn;
} }
pub fn move_cursor_to_end(&mut self) { pub fn move_cursor_to_end(&mut self) {
self.move_cursor(MotionKind::To(self.grapheme_indices().len())) self.move_cursor(MotionKind::To(self.grapheme_indices().len()))
} }
pub fn move_cursor_to_start(&mut self) { pub fn move_cursor_to_start(&mut self) {
self.move_cursor(MotionKind::To(0)) self.move_cursor(MotionKind::To(0))
} }
pub fn cursor_byte_pos(&mut self) -> usize { pub fn cursor_byte_pos(&mut self) -> usize {
self.index_byte_pos(self.cursor.get()) self.index_byte_pos(self.cursor.get())
} }
@@ -496,12 +503,12 @@ impl LineBuf {
pub fn grapheme_at_cursor(&mut self) -> Option<&str> { pub fn grapheme_at_cursor(&mut self) -> Option<&str> {
self.grapheme_at(self.cursor.get()) self.grapheme_at(self.cursor.get())
} }
pub fn grapheme_before_cursor(&mut self) -> Option<&str> { pub fn grapheme_before_cursor(&mut self) -> Option<&str> {
if self.cursor.get() == 0 { if self.cursor.get() == 0 {
return None; return None;
} }
self.grapheme_at(self.cursor.ret_sub(1)) self.grapheme_at(self.cursor.ret_sub(1))
} }
pub fn mark_insert_mode_start_pos(&mut self) { pub fn mark_insert_mode_start_pos(&mut self) {
self.insert_mode_start_pos = Some(self.cursor.get()) self.insert_mode_start_pos = Some(self.cursor.get())
} }
@@ -542,7 +549,7 @@ impl LineBuf {
} }
pub fn slice_to(&mut self, end: usize) -> Option<&str> { pub fn slice_to(&mut self, end: usize) -> Option<&str> {
self.update_graphemes_lazy(); self.update_graphemes_lazy();
self.read_slice_to(end) self.read_slice_to(end)
} }
pub fn read_slice_to(&self, end: usize) -> Option<&str> { pub fn read_slice_to(&self, end: usize) -> Option<&str> {
let grapheme_index = self.grapheme_indices().get(end).copied().or_else(|| { let grapheme_index = self.grapheme_indices().get(end).copied().or_else(|| {
@@ -596,9 +603,9 @@ impl LineBuf {
self.update_graphemes(); self.update_graphemes();
drained drained
} }
pub fn drain_inclusive(&mut self, range: RangeInclusive<usize>) -> String { pub fn drain_inclusive(&mut self, range: RangeInclusive<usize>) -> String {
self.drain(*range.start()..range.end().saturating_add(1)) self.drain(*range.start()..range.end().saturating_add(1))
} }
pub fn push(&mut self, ch: char) { pub fn push(&mut self, ch: char) {
self.buffer.push(ch); self.buffer.push(ch);
self.update_graphemes(); self.update_graphemes();
@@ -620,30 +627,31 @@ impl LineBuf {
self.update_graphemes(); self.update_graphemes();
} }
pub fn select_range(&self) -> Option<(usize, usize)> { pub fn select_range(&self) -> Option<(usize, usize)> {
match self.select_mode? { match self.select_mode? {
SelectMode::Char(_) => { SelectMode::Char(_) => self.select_range,
self.select_range SelectMode::Line(_) => {
} let (start, end) = self.select_range?;
SelectMode::Line(_) => { let start = self.pos_line_number(start);
let (start, end) = self.select_range?; let end = self.pos_line_number(end);
let start = self.pos_line_number(start); let (select_start, _) = self.line_bounds(start);
let end = self.pos_line_number(end); let (_, select_end) = self.line_bounds(end);
let (select_start,_) = self.line_bounds(start); if self
let (_,select_end) = self.line_bounds(end); .read_grapheme_before(select_end)
if self.read_grapheme_before(select_end).is_some_and(|gr| gr == "\n") { .is_some_and(|gr| gr == "\n")
Some((select_start, select_end - 1)) {
} else { Some((select_start, select_end - 1))
Some((select_start, select_end)) } else {
} Some((select_start, select_end))
} }
SelectMode::Block(_) => todo!(), }
} SelectMode::Block(_) => todo!(),
}
} }
pub fn start_selecting(&mut self, mode: SelectMode) { pub fn start_selecting(&mut self, mode: SelectMode) {
let range_start = self.cursor; let range_start = self.cursor;
let mut range_end = self.cursor; let mut range_end = self.cursor;
range_end.add(1); range_end.add(1);
self.select_range = Some((range_start.get(), range_end.get())); self.select_range = Some((range_start.get(), range_end.get()));
self.select_mode = Some(mode); self.select_mode = Some(mode);
} }
pub fn stop_selecting(&mut self) { pub fn stop_selecting(&mut self) {
@@ -656,11 +664,12 @@ impl LineBuf {
self.buffer.graphemes(true).filter(|g| *g == "\n").count() self.buffer.graphemes(true).filter(|g| *g == "\n").count()
} }
pub fn pos_line_number(&self, pos: usize) -> usize { pub fn pos_line_number(&self, pos: usize) -> usize {
self.read_slice_to(pos) self
.map(|slice| slice.graphemes(true).filter(|g| *g == "\n").count()) .read_slice_to(pos)
.unwrap_or(0) .map(|slice| slice.graphemes(true).filter(|g| *g == "\n").count())
} .unwrap_or(0)
}
pub fn cursor_line_number(&self) -> usize { pub fn cursor_line_number(&self) -> usize {
self self
.read_slice_to_cursor() .read_slice_to_cursor()
@@ -771,14 +780,14 @@ impl LineBuf {
} }
Some(self.line_bounds(line_no)) Some(self.line_bounds(line_no))
} }
pub fn this_line_exclusive(&mut self) -> (usize, usize) { pub fn this_line_exclusive(&mut self) -> (usize, usize) {
let line_no = self.cursor_line_number(); let line_no = self.cursor_line_number();
let (start, mut end) = self.line_bounds(line_no); let (start, mut end) = self.line_bounds(line_no);
if self.read_grapheme_before(end).is_some_and(|gr| gr == "\n") { if self.read_grapheme_before(end).is_some_and(|gr| gr == "\n") {
end = end.saturating_sub(1); end = end.saturating_sub(1);
} }
(start, end) (start, end)
} }
pub fn this_line(&mut self) -> (usize, usize) { pub fn this_line(&mut self) -> (usize, usize) {
let line_no = self.cursor_line_number(); let line_no = self.cursor_line_number();
self.line_bounds(line_no) self.line_bounds(line_no)
@@ -789,9 +798,9 @@ impl LineBuf {
pub fn end_of_line(&mut self) -> usize { pub fn end_of_line(&mut self) -> usize {
self.this_line().1 self.this_line().1
} }
pub fn end_of_line_exclusive(&mut self) -> usize { pub fn end_of_line_exclusive(&mut self) -> usize {
self.this_line_exclusive().1 self.this_line_exclusive().1
} }
pub fn select_lines_up(&mut self, n: usize) -> Option<(usize, usize)> { pub fn select_lines_up(&mut self, n: usize) -> Option<(usize, usize)> {
if self.start_of_line() == 0 { if self.start_of_line() == 0 {
return None; return None;
@@ -1929,33 +1938,34 @@ impl LineBuf {
let end = start + gr.len(); let end = start + gr.len();
self.buffer.replace_range(start..end, new); self.buffer.replace_range(start..end, new);
} }
pub fn calc_indent_level(&mut self) { pub fn calc_indent_level(&mut self) {
let to_cursor = self let to_cursor = self
.slice_to_cursor() .slice_to_cursor()
.map(|s| s.to_string()) .map(|s| s.to_string())
.unwrap_or(self.buffer.clone()); .unwrap_or(self.buffer.clone());
let input = Arc::new(to_cursor); let input = Arc::new(to_cursor);
let Ok(tokens) = LexStream::new(input, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>() else { let Ok(tokens) = LexStream::new(input, LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<Tk>>>()
log::error!("Failed to lex buffer for indent calculation"); else {
return; log::error!("Failed to lex buffer for indent calculation");
}; return;
let mut level: usize = 0; };
for tk in tokens { let mut level: usize = 0;
if tk.flags.contains(TkFlags::KEYWORD) { for tk in tokens {
match tk.as_str() { if tk.flags.contains(TkFlags::KEYWORD) {
"then" | "do" | "in" => level += 1, match tk.as_str() {
"done" | "fi" | "esac" => level = level.saturating_sub(1), "then" | "do" | "in" => level += 1,
_ => { /* Continue */ } "done" | "fi" | "esac" => level = level.saturating_sub(1),
} _ => { /* Continue */ }
} else if tk.class == TkRule::BraceGrpStart { }
level += 1; } else if tk.class == TkRule::BraceGrpStart {
} else if tk.class == TkRule::BraceGrpEnd { level += 1;
level = level.saturating_sub(1); } else if tk.class == TkRule::BraceGrpEnd {
} level = level.saturating_sub(1);
} }
}
self.auto_indent_level = level; self.auto_indent_level = level;
} }
pub fn eval_motion(&mut self, verb: Option<&Verb>, motion: MotionCmd) -> MotionKind { pub fn eval_motion(&mut self, verb: Option<&Verb>, motion: MotionCmd) -> MotionKind {
let buffer = self.buffer.clone(); let buffer = self.buffer.clone();
if self.has_hint() { if self.has_hint() {
@@ -1965,7 +1975,7 @@ impl LineBuf {
let eval = match motion { let eval = match motion {
MotionCmd(count, motion @ (Motion::WholeLineInclusive | Motion::WholeLineExclusive)) => { MotionCmd(count, motion @ (Motion::WholeLineInclusive | Motion::WholeLineExclusive)) => {
let exclusive = matches!(motion, Motion::WholeLineExclusive); let exclusive = matches!(motion, Motion::WholeLineExclusive);
let Some((start, mut end)) = (if count == 1 { let Some((start, mut end)) = (if count == 1 {
Some(self.this_line()) Some(self.this_line())
@@ -1975,9 +1985,9 @@ impl LineBuf {
return MotionKind::Null; return MotionKind::Null;
}; };
if exclusive && self.grapheme_before(end).is_some_and(|gr| gr == "\n") { if exclusive && self.grapheme_before(end).is_some_and(|gr| gr == "\n") {
end = end.saturating_sub(1); end = end.saturating_sub(1);
} }
let target_col = if let Some(col) = self.saved_col { let target_col = if let Some(col) = self.saved_col {
col col
@@ -1994,7 +2004,8 @@ impl LineBuf {
if self.cursor.exclusive if self.cursor.exclusive
&& line.ends_with("\n") && line.ends_with("\n")
&& self.grapheme_at(target_pos) == Some("\n") && self.grapheme_at(target_pos) == Some("\n")
&& line != "\n" // Allow landing on newline for empty lines && line != "\n"
// Allow landing on newline for empty lines
{ {
target_pos = target_pos.saturating_sub(1); // Don't land on the target_pos = target_pos.saturating_sub(1); // Don't land on the
// newline // newline
@@ -2155,7 +2166,7 @@ impl LineBuf {
MotionCmd(_, Motion::BeginningOfLine) => MotionKind::On(self.start_of_line()), MotionCmd(_, Motion::BeginningOfLine) => MotionKind::On(self.start_of_line()),
MotionCmd(count, Motion::EndOfLine) => { MotionCmd(count, Motion::EndOfLine) => {
let pos = if count == 1 { let pos = if count == 1 {
self.end_of_line() self.end_of_line()
} else if let Some((_, end)) = self.select_lines_down(count) { } else if let Some((_, end)) = self.select_lines_down(count) {
end end
} else { } else {
@@ -2228,14 +2239,15 @@ impl LineBuf {
}; };
let Some(line) = self.slice(start..end).map(|s| s.to_string()) else { let Some(line) = self.slice(start..end).map(|s| s.to_string()) else {
log::warn!("Failed to get line slice for motion, start: {start}, end: {end}"); log::warn!("Failed to get line slice for motion, start: {start}, end: {end}");
return MotionKind::Null; return MotionKind::Null;
}; };
let mut target_pos = self.grapheme_index_for_display_col(&line, target_col); let mut target_pos = self.grapheme_index_for_display_col(&line, target_col);
if self.cursor.exclusive if self.cursor.exclusive
&& line.ends_with("\n") && line.ends_with("\n")
&& self.grapheme_at(target_pos) == Some("\n") && self.grapheme_at(target_pos) == Some("\n")
&& line != "\n" // Allow landing on newline for empty lines && line != "\n"
// Allow landing on newline for empty lines
{ {
target_pos = target_pos.saturating_sub(1); // Don't land on the target_pos = target_pos.saturating_sub(1); // Don't land on the
// newline // newline
@@ -2247,7 +2259,6 @@ impl LineBuf {
_ => unreachable!(), _ => unreachable!(),
}; };
MotionKind::InclusiveWithTargetCol((start, end), target_pos) MotionKind::InclusiveWithTargetCol((start, end), target_pos)
} }
MotionCmd(count, Motion::LineDownCharwise) | MotionCmd(count, Motion::LineUpCharwise) => { MotionCmd(count, Motion::LineDownCharwise) | MotionCmd(count, Motion::LineUpCharwise) => {
@@ -2428,13 +2439,13 @@ impl LineBuf {
pub fn range_from_motion(&mut self, motion: &MotionKind) -> Option<(usize, usize)> { pub fn range_from_motion(&mut self, motion: &MotionKind) -> Option<(usize, usize)> {
let range = match motion { let range = match motion {
MotionKind::On(pos) => { MotionKind::On(pos) => {
let cursor_pos = self.cursor.get(); let cursor_pos = self.cursor.get();
if cursor_pos == *pos { if cursor_pos == *pos {
ordered(cursor_pos, pos + 1) // scary ordered(cursor_pos, pos + 1) // scary
} else { } else {
ordered(cursor_pos, *pos) ordered(cursor_pos, *pos)
} }
} }
MotionKind::Onto(pos) => { MotionKind::Onto(pos) => {
// For motions which include the character at the cursor during operations // For motions which include the character at the cursor during operations
// but exclude the character during movements // but exclude the character during movements
@@ -2478,29 +2489,32 @@ impl LineBuf {
) -> ShResult<()> { ) -> ShResult<()> {
match verb { match verb {
Verb::Delete | Verb::Yank | Verb::Change => { Verb::Delete | Verb::Yank | Verb::Change => {
log::debug!("Executing verb: {verb:?} with motion: {motion:?}"); log::debug!("Executing verb: {verb:?} with motion: {motion:?}");
let Some((mut start, mut end)) = self.range_from_motion(&motion) else { let Some((mut start, mut end)) = self.range_from_motion(&motion) else {
log::debug!("No range from motion, nothing to do"); log::debug!("No range from motion, nothing to do");
return Ok(()); return Ok(());
}; };
log::debug!("Initial range from motion: ({start}, {end})"); log::debug!("Initial range from motion: ({start}, {end})");
log::debug!("self.grapheme_indices().len(): {}", self.grapheme_indices().len()); log::debug!(
"self.grapheme_indices().len(): {}",
self.grapheme_indices().len()
);
let mut do_indent = false; let mut do_indent = false;
if verb == Verb::Change && (start,end) == self.this_line_exclusive() { if verb == Verb::Change && (start, end) == self.this_line_exclusive() {
do_indent = read_shopts(|o| o.prompt.auto_indent); do_indent = read_shopts(|o| o.prompt.auto_indent);
} }
let mut text = if verb == Verb::Yank { let mut text = if verb == Verb::Yank {
self self
.slice(start..end) .slice(start..end)
.map(|c| c.to_string()) .map(|c| c.to_string())
.unwrap_or_default() .unwrap_or_default()
} else if start == self.grapheme_indices().len() && end == self.grapheme_indices().len() { } else if start == self.grapheme_indices().len() && end == self.grapheme_indices().len() {
// user is in normal mode and pressed 'x' on the last char in the buffer // user is in normal mode and pressed 'x' on the last char in the buffer
let drained = self.drain(end.saturating_sub(1)..end); let drained = self.drain(end.saturating_sub(1)..end);
self.update_graphemes(); self.update_graphemes();
drained drained
} else { } else {
let drained = self.drain(start..end); let drained = self.drain(start..end);
self.update_graphemes(); self.update_graphemes();
@@ -2508,30 +2522,30 @@ impl LineBuf {
}; };
let is_linewise = matches!( let is_linewise = matches!(
motion, motion,
MotionKind::InclusiveWithTargetCol(..) | MotionKind::InclusiveWithTargetCol(..) | MotionKind::ExclusiveWithTargetCol(..)
MotionKind::ExclusiveWithTargetCol(..)
) || matches!(self.select_mode, Some(SelectMode::Line(_))); ) || matches!(self.select_mode, Some(SelectMode::Line(_)));
let register_content = if is_linewise { let register_content = if is_linewise {
if !text.ends_with('\n') && !text.is_empty() { if !text.ends_with('\n') && !text.is_empty() {
text.push('\n'); text.push('\n');
} }
RegisterContent::Line(text) RegisterContent::Line(text)
} else { } else {
RegisterContent::Span(text) RegisterContent::Span(text)
}; };
register.write_to_register(register_content); register.write_to_register(register_content);
self.cursor.set(start); self.cursor.set(start);
if do_indent { if do_indent {
self.calc_indent_level(); self.calc_indent_level();
let tabs = (0..self.auto_indent_level).map(|_| '\t'); let tabs = (0..self.auto_indent_level).map(|_| '\t');
for tab in tabs { for tab in tabs {
self.insert_at_cursor(tab); self.insert_at_cursor(tab);
self.cursor.add(1); self.cursor.add(1);
} }
} else if verb != Verb::Change } else if verb != Verb::Change
&& let MotionKind::InclusiveWithTargetCol((_,_), col) = motion { && let MotionKind::InclusiveWithTargetCol((_, _), col) = motion
self.cursor.add(col); {
} self.cursor.add(col);
}
} }
Verb::Rot13 => { Verb::Rot13 => {
let Some((start, end)) = self.range_from_motion(&motion) else { let Some((start, end)) = self.range_from_motion(&motion) else {
@@ -2682,7 +2696,7 @@ impl LineBuf {
self.buffer.replace_range(pos..pos + new.len(), &old); self.buffer.replace_range(pos..pos + new.len(), &old);
let new_cursor_pos = self.cursor.get(); let new_cursor_pos = self.cursor.get();
self.cursor.set(cursor_pos); self.cursor.set(cursor_pos);
let new_edit = Edit { let new_edit = Edit {
pos, pos,
cursor_pos: new_cursor_pos, cursor_pos: new_cursor_pos,
@@ -2701,17 +2715,17 @@ impl LineBuf {
if content.is_empty() { if content.is_empty() {
return Ok(()); return Ok(());
} }
if let Some(range) = self.select_range() { if let Some(range) = self.select_range() {
let register_text = self.drain_inclusive(range.0..=range.1); let register_text = self.drain_inclusive(range.0..=range.1);
write_register(None, RegisterContent::Span(register_text)); // swap deleted text into register write_register(None, RegisterContent::Span(register_text)); // swap deleted text into register
let text = content.as_str(); let text = content.as_str();
self.insert_str_at(range.0, text); self.insert_str_at(range.0, text);
self.cursor.set(range.0 + content.char_count()); self.cursor.set(range.0 + content.char_count());
self.select_range = None; self.select_range = None;
self.update_graphemes(); self.update_graphemes();
return Ok(()); return Ok(());
} }
match content { match content {
RegisterContent::Span(ref text) => { RegisterContent::Span(ref text) => {
let insert_idx = match anchor { let insert_idx = match anchor {
@@ -2726,7 +2740,9 @@ impl LineBuf {
Anchor::After => self.end_of_line(), Anchor::After => self.end_of_line(),
Anchor::Before => self.start_of_line(), Anchor::Before => self.start_of_line(),
}; };
let needs_newline = self.grapheme_before(insert_idx).is_some_and(|gr| gr != "\n"); let needs_newline = self
.grapheme_before(insert_idx)
.is_some_and(|gr| gr != "\n");
if needs_newline { if needs_newline {
let full = format!("\n{}", text); let full = format!("\n{}", text);
self.insert_str_at(insert_idx, &full); self.insert_str_at(insert_idx, &full);
@@ -2788,11 +2804,11 @@ impl LineBuf {
let Some((start, end)) = self.range_from_motion(&motion) else { let Some((start, end)) = self.range_from_motion(&motion) else {
return Ok(()); return Ok(());
}; };
let move_cursor = self.cursor.get() == start; let move_cursor = self.cursor.get() == start;
self.insert_at(start, '\t'); self.insert_at(start, '\t');
if move_cursor { if move_cursor {
self.cursor.add(1); self.cursor.add(1);
} }
let mut range_indices = self.grapheme_indices()[start..end].to_vec().into_iter(); let mut range_indices = self.grapheme_indices()[start..end].to_vec().into_iter();
while let Some(idx) = range_indices.next() { while let Some(idx) = range_indices.next() {
let gr = self.grapheme_at(idx).unwrap(); let gr = self.grapheme_at(idx).unwrap();
@@ -2822,7 +2838,7 @@ impl LineBuf {
if self.grapheme_at(start) == Some("\t") { if self.grapheme_at(start) == Some("\t") {
self.remove(start); self.remove(start);
} }
end = end.min(self.grapheme_indices().len().saturating_sub(1)); end = end.min(self.grapheme_indices().len().saturating_sub(1));
let mut range_indices = self.grapheme_indices()[start..end].to_vec().into_iter(); let mut range_indices = self.grapheme_indices()[start..end].to_vec().into_iter();
while let Some(idx) = range_indices.next() { while let Some(idx) = range_indices.next() {
let gr = self.grapheme_at(idx).unwrap(); let gr = self.grapheme_at(idx).unwrap();
@@ -2852,29 +2868,29 @@ impl LineBuf {
Verb::Equalize => todo!(), Verb::Equalize => todo!(),
Verb::InsertModeLineBreak(anchor) => { Verb::InsertModeLineBreak(anchor) => {
let (mut start, end) = self.this_line(); let (mut start, end) = self.this_line();
let auto_indent = read_shopts(|o| o.prompt.auto_indent); let auto_indent = read_shopts(|o| o.prompt.auto_indent);
if start == 0 && end == self.cursor.max { if start == 0 && end == self.cursor.max {
match anchor { match anchor {
Anchor::After => { Anchor::After => {
self.push('\n'); self.push('\n');
if auto_indent { if auto_indent {
self.calc_indent_level(); self.calc_indent_level();
let tabs = (0..self.auto_indent_level).map(|_| '\t'); let tabs = (0..self.auto_indent_level).map(|_| '\t');
for tab in tabs { for tab in tabs {
self.push(tab); self.push(tab);
} }
} }
self.cursor.set(self.cursor_max()); self.cursor.set(self.cursor_max());
return Ok(()); return Ok(());
} }
Anchor::Before => { Anchor::Before => {
if auto_indent { if auto_indent {
self.calc_indent_level(); self.calc_indent_level();
let tabs = (0..self.auto_indent_level).map(|_| '\t'); let tabs = (0..self.auto_indent_level).map(|_| '\t');
for tab in tabs { for tab in tabs {
self.insert_at(0, tab); self.insert_at(0, tab);
} }
} }
self.insert_at(0, '\n'); self.insert_at(0, '\n');
self.cursor.set(0); self.cursor.set(0);
return Ok(()); return Ok(());
@@ -2888,52 +2904,52 @@ impl LineBuf {
self.cursor.set(end); self.cursor.set(end);
self.insert_at_cursor('\n'); self.insert_at_cursor('\n');
self.cursor.add(1); self.cursor.add(1);
if auto_indent { if auto_indent {
self.calc_indent_level(); self.calc_indent_level();
let tabs = (0..self.auto_indent_level).map(|_| '\t'); let tabs = (0..self.auto_indent_level).map(|_| '\t');
for tab in tabs { for tab in tabs {
self.insert_at_cursor(tab); self.insert_at_cursor(tab);
self.cursor.add(1); self.cursor.add(1);
} }
} }
} }
Anchor::Before => { Anchor::Before => {
self.cursor.set(start); self.cursor.set(start);
self.insert_at_cursor('\n'); self.insert_at_cursor('\n');
self.cursor.add(1); self.cursor.add(1);
if auto_indent { if auto_indent {
self.calc_indent_level(); self.calc_indent_level();
let tabs = (0..self.auto_indent_level).map(|_| '\t'); let tabs = (0..self.auto_indent_level).map(|_| '\t');
for tab in tabs { for tab in tabs {
self.insert_at_cursor(tab); self.insert_at_cursor(tab);
self.cursor.add(1); self.cursor.add(1);
} }
} }
} }
} }
} }
Verb::AcceptLineOrNewline => { Verb::AcceptLineOrNewline => {
// If this verb has reached this function, it means we have incomplete input // If this verb has reached this function, it means we have incomplete input
// and therefore must insert a newline instead of accepting the input // and therefore must insert a newline instead of accepting the input
if self.cursor.exclusive { if self.cursor.exclusive {
// in this case we are in normal/visual mode, so we don't insert anything // in this case we are in normal/visual mode, so we don't insert anything
// and just move down a line // and just move down a line
let motion = self.eval_motion(None, MotionCmd(1, Motion::LineDownCharwise)); let motion = self.eval_motion(None, MotionCmd(1, Motion::LineDownCharwise));
self.apply_motion(motion); self.apply_motion(motion);
return Ok(()); return Ok(());
} }
let auto_indent = read_shopts(|o| o.prompt.auto_indent); let auto_indent = read_shopts(|o| o.prompt.auto_indent);
self.insert_at_cursor('\n'); self.insert_at_cursor('\n');
self.cursor.add(1); self.cursor.add(1);
if auto_indent { if auto_indent {
self.calc_indent_level(); self.calc_indent_level();
let tabs = (0..self.auto_indent_level).map(|_| '\t'); let tabs = (0..self.auto_indent_level).map(|_| '\t');
for tab in tabs { for tab in tabs {
self.insert_at_cursor(tab); self.insert_at_cursor(tab);
self.cursor.add(1); self.cursor.add(1);
} }
} }
} }
Verb::Complete Verb::Complete
| Verb::EndOfFile | Verb::EndOfFile
@@ -2951,7 +2967,11 @@ impl LineBuf {
pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> { pub fn exec_cmd(&mut self, cmd: ViCmd) -> ShResult<()> {
let clear_redos = !cmd.is_undo_op() || cmd.verb.as_ref().is_some_and(|v| v.1.is_edit()); let clear_redos = !cmd.is_undo_op() || cmd.verb.as_ref().is_some_and(|v| v.1.is_edit());
let is_char_insert = cmd.verb.as_ref().is_some_and(|v| v.1.is_char_insert()); let is_char_insert = cmd.verb.as_ref().is_some_and(|v| v.1.is_char_insert());
let is_line_motion = cmd.is_line_motion() || cmd.verb.as_ref().is_some_and(|v| v.1 == Verb::AcceptLineOrNewline); let is_line_motion = cmd.is_line_motion()
|| cmd
.verb
.as_ref()
.is_some_and(|v| v.1 == Verb::AcceptLineOrNewline);
let is_undo_op = cmd.is_undo_op(); let is_undo_op = cmd.is_undo_op();
let edit_is_merging = self.undo_stack.last().is_some_and(|edit| edit.merging); let edit_is_merging = self.undo_stack.last().is_some_and(|edit| edit.merging);
@@ -3024,13 +3044,14 @@ impl LineBuf {
self.apply_motion(motion_eval); self.apply_motion(motion_eval);
} }
if self.cursor.exclusive if self.cursor.exclusive
&& self.grapheme_at_cursor().is_some_and(|gr| gr == "\n") && self.grapheme_at_cursor().is_some_and(|gr| gr == "\n")
&& self.grapheme_before_cursor().is_some_and(|gr| gr != "\n") { && self.grapheme_before_cursor().is_some_and(|gr| gr != "\n")
// we landed on a newline, and we aren't inbetween two newlines. {
self.cursor.sub(1); // we landed on a newline, and we aren't inbetween two newlines.
self.update_select_range(); self.cursor.sub(1);
} self.update_select_range();
}
/* Done executing, do some cleanup */ /* Done executing, do some cleanup */
@@ -3070,10 +3091,10 @@ impl LineBuf {
let text = self let text = self
.hint .hint
.clone() .clone()
.map(|h| format!("\x1b[90m{h}\x1b[0m")) .map(|h| format!("\x1b[90m{h}\x1b[0m"))
.unwrap_or_default(); .unwrap_or_default();
text.replace("\n", "\n\x1b[90m") text.replace("\n", "\n\x1b[90m")
} }
} }
@@ -3085,20 +3106,29 @@ impl Display for LineBuf {
let start_byte = self.read_idx_byte_pos(start); let start_byte = self.read_idx_byte_pos(start);
let end_byte = self.read_idx_byte_pos(end).min(full_buf.len()); let end_byte = self.read_idx_byte_pos(end).min(full_buf.len());
match mode.anchor() { match mode.anchor() {
SelectAnchor::Start => { SelectAnchor::Start => {
let mut inclusive = start_byte..=end_byte; let mut inclusive = start_byte..=end_byte;
if *inclusive.end() == full_buf.len() { if *inclusive.end() == full_buf.len() {
inclusive = start_byte..=end_byte.saturating_sub(1); inclusive = start_byte..=end_byte.saturating_sub(1);
} }
let selected = format!("{}{}{}", markers::VISUAL_MODE_START, &full_buf[inclusive.clone()], markers::VISUAL_MODE_END) let selected = format!(
.replace("\n", format!("\n{}",markers::VISUAL_MODE_START).as_str()); "{}{}{}",
markers::VISUAL_MODE_START,
&full_buf[inclusive.clone()],
markers::VISUAL_MODE_END
)
.replace("\n", format!("\n{}", markers::VISUAL_MODE_START).as_str());
full_buf.replace_range(inclusive, &selected); full_buf.replace_range(inclusive, &selected);
} }
SelectAnchor::End => { SelectAnchor::End => {
let selected = format!("{}{}{}", markers::VISUAL_MODE_START, &full_buf[start_byte..end_byte], markers::VISUAL_MODE_END) let selected = format!(
.replace("\n", format!("\n{}",markers::VISUAL_MODE_START).as_str()); "{}{}{}",
markers::VISUAL_MODE_START,
&full_buf[start_byte..end_byte],
markers::VISUAL_MODE_END
)
.replace("\n", format!("\n{}", markers::VISUAL_MODE_START).as_str());
full_buf.replace_range(start_byte..end_byte, &selected); full_buf.replace_range(start_byte..end_byte, &selected);
} }
} }

View File

@@ -36,12 +36,12 @@ pub mod vimode;
pub mod markers { pub mod markers {
use super::Marker; use super::Marker;
/* /*
* These are invisible Unicode characters used to annotate * These are invisible Unicode characters used to annotate
* strings with various contextual metadata. * strings with various contextual metadata.
*/ */
/* Highlight Markers */ /* Highlight Markers */
// token-level (derived from token class) // token-level (derived from token class)
pub const COMMAND: Marker = '\u{e100}'; pub const COMMAND: Marker = '\u{e100}';
@@ -71,36 +71,36 @@ pub mod markers {
pub const ESCAPE: Marker = '\u{e116}'; pub const ESCAPE: Marker = '\u{e116}';
pub const GLOB: Marker = '\u{e117}'; pub const GLOB: Marker = '\u{e117}';
// other // other
pub const VISUAL_MODE_START: Marker = '\u{e118}'; pub const VISUAL_MODE_START: Marker = '\u{e118}';
pub const VISUAL_MODE_END: Marker = '\u{e119}'; pub const VISUAL_MODE_END: Marker = '\u{e119}';
pub const RESET: Marker = '\u{e11a}'; pub const RESET: Marker = '\u{e11a}';
pub const NULL: Marker = '\u{e11b}'; pub const NULL: Marker = '\u{e11b}';
/* Expansion Markers */ /* Expansion Markers */
/// Double quote '"' marker /// Double quote '"' marker
pub const DUB_QUOTE: Marker = '\u{e001}'; pub const DUB_QUOTE: Marker = '\u{e001}';
/// Single quote '\\'' marker /// Single quote '\\'' marker
pub const SNG_QUOTE: Marker = '\u{e002}'; pub const SNG_QUOTE: Marker = '\u{e002}';
/// Tilde sub marker /// Tilde sub marker
pub const TILDE_SUB: Marker = '\u{e003}'; pub const TILDE_SUB: Marker = '\u{e003}';
/// Input process sub marker /// Input process sub marker
pub const PROC_SUB_IN: Marker = '\u{e005}'; pub const PROC_SUB_IN: Marker = '\u{e005}';
/// Output process sub marker /// Output process sub marker
pub const PROC_SUB_OUT: Marker = '\u{e006}'; pub const PROC_SUB_OUT: Marker = '\u{e006}';
/// Marker for null expansion /// Marker for null expansion
/// This is used for when "$@" or "$*" are used in quotes and there are no /// This is used for when "$@" or "$*" are used in quotes and there are no
/// arguments Without this marker, it would be handled like an empty string, /// arguments Without this marker, it would be handled like an empty string,
/// which breaks some commands /// which breaks some commands
pub const NULL_EXPAND: Marker = '\u{e007}'; pub const NULL_EXPAND: Marker = '\u{e007}';
/// Explicit marker for argument separation /// Explicit marker for argument separation
/// This is used to join the arguments given by "$@", and preserves exact formatting /// This is used to join the arguments given by "$@", and preserves exact
/// of the original arguments, including quoting /// formatting of the original arguments, including quoting
pub const ARG_SEP: Marker = '\u{e008}'; pub const ARG_SEP: Marker = '\u{e008}';
pub const VI_SEQ_EXP: Marker = '\u{e009}'; pub const VI_SEQ_EXP: Marker = '\u{e009}';
pub const END_MARKERS: [Marker; 7] = [ pub const END_MARKERS: [Marker; 7] = [
VAR_SUB_END, VAR_SUB_END,
@@ -116,10 +116,10 @@ pub mod markers {
]; ];
pub const SUB_TOKEN: [Marker; 6] = [VAR_SUB, CMD_SUB, PROC_SUB, STRING_DQ, STRING_SQ, GLOB]; pub const SUB_TOKEN: [Marker; 6] = [VAR_SUB, CMD_SUB, PROC_SUB, STRING_DQ, STRING_SQ, GLOB];
pub const MISC: [Marker; 3] = [ESCAPE, VISUAL_MODE_START, VISUAL_MODE_END]; pub const MISC: [Marker; 3] = [ESCAPE, VISUAL_MODE_START, VISUAL_MODE_END];
pub fn is_marker(c: Marker) -> bool { pub fn is_marker(c: Marker) -> bool {
('\u{e000}'..'\u{efff}').contains(&c) ('\u{e000}'..'\u{efff}').contains(&c)
} }
} }
type Marker = char; type Marker = char;
@@ -135,66 +135,73 @@ pub enum ReadlineEvent {
} }
pub struct Prompt { pub struct Prompt {
ps1_expanded: String, ps1_expanded: String,
ps1_raw: String, ps1_raw: String,
psr_expanded: Option<String>, psr_expanded: Option<String>,
psr_raw: Option<String>, psr_raw: Option<String>,
} }
impl Prompt { impl Prompt {
const DEFAULT_PS1: &str = "\\e[0m\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m "; const DEFAULT_PS1: &str =
pub fn new() -> Self { "\\e[0m\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m ";
let Ok(ps1_raw) = env::var("PS1") else { pub fn new() -> Self {
return Self::default(); let Ok(ps1_raw) = env::var("PS1") else {
}; return Self::default();
let Ok(ps1_expanded) = expand_prompt(&ps1_raw) else { };
return Self::default(); let Ok(ps1_expanded) = expand_prompt(&ps1_raw) else {
}; return Self::default();
let psr_raw = env::var("PSR").ok(); };
let psr_expanded = psr_raw.clone().map(|r| expand_prompt(&r)).transpose().ok().flatten(); let psr_raw = env::var("PSR").ok();
Self { let psr_expanded = psr_raw
ps1_expanded, .clone()
ps1_raw, .map(|r| expand_prompt(&r))
psr_expanded, .transpose()
psr_raw, .ok()
} .flatten();
} Self {
ps1_expanded,
ps1_raw,
psr_expanded,
psr_raw,
}
}
pub fn get_ps1(&self) -> &str { pub fn get_ps1(&self) -> &str {
&self.ps1_expanded &self.ps1_expanded
} }
pub fn set_ps1(&mut self, ps1_raw: String) -> ShResult<()> { pub fn set_ps1(&mut self, ps1_raw: String) -> ShResult<()> {
self.ps1_expanded = expand_prompt(&ps1_raw)?; self.ps1_expanded = expand_prompt(&ps1_raw)?;
self.ps1_raw = ps1_raw; self.ps1_raw = ps1_raw;
Ok(()) Ok(())
} }
pub fn set_psr(&mut self, psr_raw: String) -> ShResult<()> { pub fn set_psr(&mut self, psr_raw: String) -> ShResult<()> {
self.psr_expanded = Some(expand_prompt(&psr_raw)?); self.psr_expanded = Some(expand_prompt(&psr_raw)?);
self.psr_raw = Some(psr_raw); self.psr_raw = Some(psr_raw);
Ok(()) Ok(())
} }
pub fn get_psr(&self) -> Option<&str> { pub fn get_psr(&self) -> Option<&str> {
self.psr_expanded.as_deref() self.psr_expanded.as_deref()
} }
pub fn refresh(&mut self) -> ShResult<()> { pub fn refresh(&mut self) -> ShResult<()> {
self.ps1_expanded = expand_prompt(&self.ps1_raw)?; self.ps1_expanded = expand_prompt(&self.ps1_raw)?;
if let Some(psr_raw) = &self.psr_raw { if let Some(psr_raw) = &self.psr_raw {
self.psr_expanded = Some(expand_prompt(psr_raw)?); self.psr_expanded = Some(expand_prompt(psr_raw)?);
} }
Ok(()) Ok(())
} }
} }
impl Default for Prompt { impl Default for Prompt {
fn default() -> Self { fn default() -> Self {
Self { Self {
ps1_expanded: expand_prompt(Self::DEFAULT_PS1).unwrap_or_else(|_| Self::DEFAULT_PS1.to_string()), ps1_expanded: expand_prompt(Self::DEFAULT_PS1)
ps1_raw: Self::DEFAULT_PS1.to_string(), .unwrap_or_else(|_| Self::DEFAULT_PS1.to_string()),
psr_expanded: None, ps1_raw: Self::DEFAULT_PS1.to_string(),
psr_raw: None, psr_expanded: None,
} psr_raw: None,
} }
}
} }
pub struct ShedVi { pub struct ShedVi {
@@ -232,7 +239,7 @@ impl ShedVi {
history: History::new()?, history: History::new()?,
needs_redraw: true, needs_redraw: true,
}; };
new.writer.flush_write("\n")?; // ensure we start on a new line, in case the previous command didn't end with a newline new.writer.flush_write("\n")?; // ensure we start on a new line, in case the previous command didn't end with a newline
new.print_line(false)?; new.print_line(false)?;
Ok(new) Ok(new)
} }
@@ -255,58 +262,55 @@ impl ShedVi {
self.needs_redraw = true; self.needs_redraw = true;
} }
/// Reset readline state for a new prompt /// Reset readline state for a new prompt
pub fn reset(&mut self, full_redraw: bool) -> ShResult<()> { pub fn reset(&mut self, full_redraw: bool) -> ShResult<()> {
// Clear old display before resetting state — old_layout must survive // Clear old display before resetting state — old_layout must survive
// so print_line can call clear_rows with the full multi-line layout // so print_line can call clear_rows with the full multi-line layout
self.prompt = Prompt::new(); self.prompt = Prompt::new();
self.editor = Default::default(); self.editor = Default::default();
self.mode = Box::new(ViInsert::new()); self.mode = Box::new(ViInsert::new());
self.needs_redraw = true; self.needs_redraw = true;
if full_redraw { if full_redraw {
self.old_layout = None; self.old_layout = None;
} }
self.history.pending = None; self.history.pending = None;
self.history.reset(); self.history.reset();
self.print_line(false) self.print_line(false)
} }
pub fn prompt(&self) -> &Prompt { pub fn prompt(&self) -> &Prompt {
&self.prompt &self.prompt
} }
pub fn prompt_mut(&mut self) -> &mut Prompt { pub fn prompt_mut(&mut self) -> &mut Prompt {
&mut self.prompt &mut self.prompt
} }
fn should_submit(&mut self) -> ShResult<bool> { fn should_submit(&mut self) -> ShResult<bool> {
if self.mode.report_mode() == ModeReport::Normal { if self.mode.report_mode() == ModeReport::Normal {
return Ok(true); return Ok(true);
} }
let input = Arc::new(self.editor.buffer.clone()); let input = Arc::new(self.editor.buffer.clone());
self.editor.calc_indent_level(); self.editor.calc_indent_level();
let lex_result1 = LexStream::new(Arc::clone(&input), LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<_>>>(); let lex_result1 =
let lex_result2 = LexStream::new(Arc::clone(&input), LexFlags::empty()).collect::<ShResult<Vec<_>>>(); LexStream::new(Arc::clone(&input), LexFlags::LEX_UNFINISHED).collect::<ShResult<Vec<_>>>();
let is_top_level = self.editor.auto_indent_level == 0; let lex_result2 =
LexStream::new(Arc::clone(&input), LexFlags::empty()).collect::<ShResult<Vec<_>>>();
let is_top_level = self.editor.auto_indent_level == 0;
let is_complete = match (lex_result1.is_err(), lex_result2.is_err()) { let is_complete = match (lex_result1.is_err(), lex_result2.is_err()) {
(true, true) => { (true, true) => {
return Err(lex_result2.unwrap_err()); return Err(lex_result2.unwrap_err());
} }
(true, false) => { (true, false) => {
return Err(lex_result1.unwrap_err()); return Err(lex_result1.unwrap_err());
} }
(false, true) => { (false, true) => false,
false (false, false) => true,
} };
(false, false) => {
true
}
};
Ok(is_complete && is_top_level) Ok(is_complete && is_top_level)
} }
/// Process any available input and return readline event /// Process any available input and return readline event
/// This is non-blocking - returns Pending if no complete line yet /// This is non-blocking - returns Pending if no complete line yet
@@ -362,8 +366,8 @@ impl ShedVi {
self.editor.set_hint(hint); self.editor.set_hint(hint);
} }
None => { None => {
self.writer.send_bell().ok(); self.writer.send_bell().ok();
}, }
} }
self.needs_redraw = true; self.needs_redraw = true;
@@ -385,9 +389,11 @@ impl ShedVi {
continue; continue;
} }
if cmd.is_submit_action() && (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete)) { if cmd.is_submit_action()
&& (self.should_submit()? || !read_shopts(|o| o.prompt.linebreak_on_incomplete))
{
self.editor.set_hint(None); self.editor.set_hint(None);
self.editor.cursor.set(self.editor.cursor_max()); // Move the cursor to the very end self.editor.cursor.set(self.editor.cursor_max()); // Move the cursor to the very end
self.print_line(true)?; // Redraw self.print_line(true)?; // Redraw
self.writer.flush_write("\n")?; self.writer.flush_write("\n")?;
let buf = self.editor.take_buf(); let buf = self.editor.take_buf();
@@ -407,13 +413,13 @@ impl ShedVi {
return Ok(ReadlineEvent::Eof); return Ok(ReadlineEvent::Eof);
} else { } else {
self.editor = LineBuf::new(); self.editor = LineBuf::new();
self.mode = Box::new(ViInsert::new()); self.mode = Box::new(ViInsert::new());
self.needs_redraw = true; self.needs_redraw = true;
continue; continue;
} }
} }
let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit()); let has_edit_verb = cmd.verb().is_some_and(|v| v.1.is_edit());
let before = self.editor.buffer.clone(); let before = self.editor.buffer.clone();
self.exec_cmd(cmd)?; self.exec_cmd(cmd)?;
@@ -424,8 +430,8 @@ impl ShedVi {
.history .history
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get())); .update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
} else if before == after && has_edit_verb { } else if before == after && has_edit_verb {
self.writer.send_bell().ok(); // bell on no-op commands with a verb (e.g., 'x' on empty line) self.writer.send_bell().ok(); // bell on no-op commands with a verb (e.g., 'x' on empty line)
} }
let hint = self.history.get_hint(); let hint = self.history.get_hint();
self.editor.set_hint(hint); self.editor.set_hint(hint);
@@ -462,21 +468,21 @@ impl ShedVi {
}; };
let entry = self.history.scroll(count); let entry = self.history.scroll(count);
if let Some(entry) = entry { if let Some(entry) = entry {
let editor = std::mem::take(&mut self.editor); let editor = std::mem::take(&mut self.editor);
self.editor.set_buffer(entry.command().to_string()); self.editor.set_buffer(entry.command().to_string());
if self.history.pending.is_none() { if self.history.pending.is_none() {
self.history.pending = Some(editor); self.history.pending = Some(editor);
} }
self.editor.set_hint(None); self.editor.set_hint(None);
self.editor.move_cursor_to_end(); self.editor.move_cursor_to_end();
} else if let Some(pending) = self.history.pending.take() { } else if let Some(pending) = self.history.pending.take() {
self.editor = pending; self.editor = pending;
} else { } else {
// If we are here it should mean we are on our pending command // If we are here it should mean we are on our pending command
// And the user tried to scroll history down // And the user tried to scroll history down
// Since there is no "future" history, we should just bell and do nothing // Since there is no "future" history, we should just bell and do nothing
self.writer.send_bell().ok(); self.writer.send_bell().ok();
} }
} }
pub fn should_accept_hint(&self, event: &KeyEvent) -> bool { pub fn should_accept_hint(&self, event: &KeyEvent) -> bool {
if self.editor.cursor_at_max() && self.editor.has_hint() { if self.editor.cursor_at_max() && self.editor.has_hint() {
@@ -512,7 +518,9 @@ impl ShedVi {
let line = self.editor.to_string(); let line = self.editor.to_string();
let hint = self.editor.get_hint_text(); let hint = self.editor.get_hint_text();
if crate::state::read_shopts(|s| s.prompt.highlight) { if crate::state::read_shopts(|s| s.prompt.highlight) {
self.highlighter.load_input(&line,self.editor.cursor_byte_pos()); self
.highlighter
.load_input(&line, self.editor.cursor_byte_pos());
self.highlighter.highlight(); self.highlighter.highlight();
let highlighted = self.highlighter.take(); let highlighted = self.highlighter.take();
format!("{highlighted}{hint}") format!("{highlighted}{hint}")
@@ -524,53 +532,84 @@ impl ShedVi {
pub fn print_line(&mut self, final_draw: bool) -> ShResult<()> { pub fn print_line(&mut self, final_draw: bool) -> ShResult<()> {
let line = self.line_text(); let line = self.line_text();
let new_layout = self.get_layout(&line); let new_layout = self.get_layout(&line);
let pending_seq = self.mode.pending_seq(); let pending_seq = self.mode.pending_seq();
let mut prompt_string_right = self.prompt.psr_expanded.clone(); let mut prompt_string_right = self.prompt.psr_expanded.clone();
if prompt_string_right.as_ref().is_some_and(|psr| psr.lines().count() > 1) { if prompt_string_right
log::warn!("PSR has multiple lines, truncating to one line"); .as_ref()
prompt_string_right = prompt_string_right.map(|psr| psr.lines().next().unwrap_or_default().to_string()); .is_some_and(|psr| psr.lines().count() > 1)
} {
log::warn!("PSR has multiple lines, truncating to one line");
let row0_used = self.prompt prompt_string_right =
.get_ps1() prompt_string_right.map(|psr| psr.lines().next().unwrap_or_default().to_string());
.lines() }
.next()
.map(|l| Layout::calc_pos(self.writer.t_cols, l, Pos { col: 0, row: 0 }, 0))
.map(|p| p.col)
.unwrap_or_default() as usize;
let one_line = new_layout.end.row == 0;
let row0_used = self
.prompt
.get_ps1()
.lines()
.next()
.map(|l| Layout::calc_pos(self.writer.t_cols, l, Pos { col: 0, row: 0 }, 0))
.map(|p| p.col)
.unwrap_or_default() as usize;
let one_line = new_layout.end.row == 0;
if let Some(layout) = self.old_layout.as_ref() { if let Some(layout) = self.old_layout.as_ref() {
self.writer.clear_rows(layout)?; self.writer.clear_rows(layout)?;
} }
self.writer.redraw(self.prompt.get_ps1(), &line, &new_layout)?; self
.writer
.redraw(self.prompt.get_ps1(), &line, &new_layout)?;
let seq_fits = pending_seq.as_ref().is_some_and(|seq| row0_used + 1 < self.writer.t_cols as usize - seq.width()); let seq_fits = pending_seq
let psr_fits = prompt_string_right.as_ref().is_some_and(|psr| new_layout.end.col as usize + 1 < (self.writer.t_cols as usize).saturating_sub(psr.width())); .as_ref()
.is_some_and(|seq| row0_used + 1 < self.writer.t_cols as usize - seq.width());
let psr_fits = prompt_string_right.as_ref().is_some_and(|psr| {
new_layout.end.col as usize + 1 < (self.writer.t_cols as usize).saturating_sub(psr.width())
});
if !final_draw && let Some(seq) = pending_seq && !seq.is_empty() && !(prompt_string_right.is_some() && one_line) && seq_fits { if !final_draw
let to_col = self.writer.t_cols - calc_str_width(&seq); && let Some(seq) = pending_seq
let up = new_layout.cursor.row; // rows to move up from cursor to top line of prompt && !seq.is_empty()
&& !(prompt_string_right.is_some() && one_line)
&& seq_fits
{
let to_col = self.writer.t_cols - calc_str_width(&seq);
let up = new_layout.cursor.row; // rows to move up from cursor to top line of prompt
let move_up = if up > 0 { format!("\x1b[{up}A") } else { String::new() }; let move_up = if up > 0 {
format!("\x1b[{up}A")
} else {
String::new()
};
// Save cursor, move up to top row, move right to column, write sequence, restore cursor // Save cursor, move up to top row, move right to column, write sequence,
self.writer.flush_write(&format!("\x1b7{move_up}\x1b[{to_col}G{seq}\x1b8"))?; // restore cursor
} else if !final_draw && let Some(psr) = prompt_string_right && psr_fits { self
let to_col = self.writer.t_cols - calc_str_width(&psr); .writer
let down = new_layout.end.row - new_layout.cursor.row; .flush_write(&format!("\x1b7{move_up}\x1b[{to_col}G{seq}\x1b8"))?;
let move_down = if down > 0 { format!("\x1b[{down}B") } else { String::new() }; } else if !final_draw
&& let Some(psr) = prompt_string_right
&& 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 {
format!("\x1b[{down}B")
} else {
String::new()
};
self.writer.flush_write(&format!("\x1b7{move_down}\x1b[{to_col}G{psr}\x1b8"))?; self
} .writer
.flush_write(&format!("\x1b7{move_down}\x1b[{to_col}G{psr}\x1b8"))?;
}
self.writer.flush_write(&self.mode.cursor_style())?; self.writer.flush_write(&self.mode.cursor_style())?;
self.old_layout = Some(new_layout); self.old_layout = Some(new_layout);
self.needs_redraw = false; self.needs_redraw = false;
Ok(()) Ok(())
} }
@@ -853,19 +892,22 @@ pub fn get_insertions(input: &str) -> Vec<(usize, Marker)> {
/// - Unimplemented features (comments, brace groups) /// - Unimplemented features (comments, brace groups)
pub fn marker_for(class: &TkRule) -> Option<Marker> { pub fn marker_for(class: &TkRule) -> Option<Marker> {
match class { match class {
TkRule::Pipe | TkRule::Pipe
TkRule::ErrPipe | | TkRule::ErrPipe
TkRule::And | | TkRule::And
TkRule::Or | | TkRule::Or
TkRule::Bg | | TkRule::Bg
TkRule::BraceGrpStart | | TkRule::BraceGrpStart
TkRule::BraceGrpEnd => { | TkRule::BraceGrpEnd => Some(markers::OPERATOR),
Some(markers::OPERATOR)
}
TkRule::Sep => Some(markers::CMD_SEP), TkRule::Sep => Some(markers::CMD_SEP),
TkRule::Redir => Some(markers::REDIRECT), TkRule::Redir => Some(markers::REDIRECT),
TkRule::Comment => Some(markers::COMMENT), TkRule::Comment => Some(markers::COMMENT),
TkRule::Expanded { exp: _ } | TkRule::EOI | TkRule::SOI | TkRule::Null | TkRule::Str | TkRule::CasePattern => None, TkRule::Expanded { exp: _ }
| TkRule::EOI
| TkRule::SOI
| TkRule::Null
| TkRule::Str
| TkRule::CasePattern => None,
} }
} }
@@ -880,9 +922,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
std::cmp::Ordering::Equal => { std::cmp::Ordering::Equal => {
let priority = |m: Marker| -> u8 { let priority = |m: Marker| -> u8 {
match m { match m {
markers::VISUAL_MODE_END markers::VISUAL_MODE_END | markers::VISUAL_MODE_START | markers::RESET => 0,
| markers::VISUAL_MODE_START
| markers::RESET => 0,
markers::VAR_SUB markers::VAR_SUB
| markers::VAR_SUB_END | markers::VAR_SUB_END
| markers::CMD_SUB | markers::CMD_SUB
@@ -911,9 +951,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
std::cmp::Ordering::Equal => { std::cmp::Ordering::Equal => {
let priority = |m: Marker| -> u8 { let priority = |m: Marker| -> u8 {
match m { match m {
markers::VISUAL_MODE_END markers::VISUAL_MODE_END | markers::VISUAL_MODE_START | markers::RESET => 0,
| markers::VISUAL_MODE_START
| markers::RESET => 0,
markers::VAR_SUB markers::VAR_SUB
| markers::VAR_SUB_END | markers::VAR_SUB_END
| markers::CMD_SUB | markers::CMD_SUB
@@ -926,7 +964,7 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
| markers::STRING_SQ_END | markers::STRING_SQ_END
| markers::SUBSH_END => 2, | markers::SUBSH_END => 2,
| markers::ARG => 3, // Lowest priority - processed first, overridden by sub-tokens markers::ARG => 3, // Lowest priority - processed first, overridden by sub-tokens
_ => 1, _ => 1,
} }
}; };
@@ -960,11 +998,11 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
insertions.push((token.span.start, markers::SUBSH)); insertions.push((token.span.start, markers::SUBSH));
return insertions; return insertions;
} else if token.class == TkRule::CasePattern { } else if token.class == TkRule::CasePattern {
insertions.push((token.span.end, markers::RESET)); insertions.push((token.span.end, markers::RESET));
insertions.push((token.span.end - 1, markers::CASE_PAT)); insertions.push((token.span.end - 1, markers::CASE_PAT));
insertions.push((token.span.start, markers::OPERATOR)); insertions.push((token.span.start, markers::OPERATOR));
return insertions; return insertions;
} }
let token_raw = token.span.as_str(); let token_raw = token.span.as_str();
let mut token_chars = token_raw.char_indices().peekable(); let mut token_chars = token_raw.char_indices().peekable();
@@ -1144,17 +1182,17 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
} }
} }
'*' | '?' if (!in_dub_qt && !in_sng_qt) => { '*' | '?' if (!in_dub_qt && !in_sng_qt) => {
let glob_ch = *ch; let glob_ch = *ch;
token_chars.next(); // consume the first glob char token_chars.next(); // consume the first glob char
if !in_context(markers::COMMAND, &insertions) { if !in_context(markers::COMMAND, &insertions) {
let offset = if glob_ch == '*' && token_chars.peek().is_some_and(|(_, c)| *c == '*') { let offset = if glob_ch == '*' && token_chars.peek().is_some_and(|(_, c)| *c == '*') {
// it's one of these probably: ./dir/**/*.txt // it's one of these probably: ./dir/**/*.txt
token_chars.next(); // consume the second * token_chars.next(); // consume the second *
2 2
} else { } else {
// just a regular glob char // just a regular glob char
1 1
}; };
insertions.push((span_start + index + offset, markers::RESET)); insertions.push((span_start + index + offset, markers::RESET));
insertions.push((span_start + index, markers::GLOB)); insertions.push((span_start + index, markers::GLOB));
} }

View File

@@ -101,35 +101,41 @@ pub fn get_win_size(fd: RawFd) -> (Col, Row) {
} }
fn enumerate_lines(s: &str, left_pad: usize) -> String { fn enumerate_lines(s: &str, left_pad: usize) -> String {
let total_lines = s.lines().count(); let total_lines = s.lines().count();
let max_num_len = total_lines.to_string().len(); let max_num_len = total_lines.to_string().len();
s.lines() s.lines()
.enumerate() .enumerate()
.fold(String::new(), |mut acc, (i, ln)| { .fold(String::new(), |mut acc, (i, ln)| {
if i == 0 { if i == 0 {
acc.push_str(ln); acc.push_str(ln);
acc.push('\n'); acc.push('\n');
} else { } else {
let num = (i + 1).to_string(); let num = (i + 1).to_string();
let num_pad = max_num_len - num.len(); let num_pad = max_num_len - num.len();
// " 2 | " — num + padding + " | " // " 2 | " — num + padding + " | "
let prefix_len = max_num_len + 3; // "N | " let prefix_len = max_num_len + 3; // "N | "
let trail_pad = left_pad.saturating_sub(prefix_len); let trail_pad = left_pad.saturating_sub(prefix_len);
if i == total_lines - 1 { if i == total_lines - 1 {
// Don't add a newline to the last line // Don't add a newline to the last line
write!(acc, "\x1b[0m\x1b[90m{}{num} |\x1b[0m {}{ln}", write!(
" ".repeat(num_pad), acc,
" ".repeat(trail_pad), "\x1b[0m\x1b[90m{}{num} |\x1b[0m {}{ln}",
).unwrap(); " ".repeat(num_pad),
} else { " ".repeat(trail_pad),
writeln!(acc, "\x1b[0m\x1b[90m{}{num} |\x1b[0m {}{ln}", )
" ".repeat(num_pad), .unwrap();
" ".repeat(trail_pad), } else {
).unwrap(); writeln!(
} acc,
} "\x1b[0m\x1b[90m{}{num} |\x1b[0m {}{ln}",
acc " ".repeat(num_pad),
}) " ".repeat(trail_pad),
)
.unwrap();
}
}
acc
})
} }
fn write_all(fd: RawFd, buf: &str) -> nix::Result<()> { fn write_all(fd: RawFd, buf: &str) -> nix::Result<()> {
@@ -171,8 +177,8 @@ fn ends_with_newline(s: &str) -> bool {
} }
pub fn calc_str_width(s: &str) -> u16 { pub fn calc_str_width(s: &str) -> u16 {
let mut esc_seq = 0; let mut esc_seq = 0;
s.graphemes(true).map(|g| width(g, &mut esc_seq)).sum() s.graphemes(true).map(|g| width(g, &mut esc_seq)).sum()
} }
// Big credit to rustyline for this // Big credit to rustyline for this

View File

@@ -1,6 +1,6 @@
use bitflags::bitflags; use bitflags::bitflags;
use super::register::{append_register, read_register, write_register, RegisterContent}; use super::register::{RegisterContent, append_register, read_register, write_register};
//TODO: write tests that take edit results and cursor positions from actual //TODO: write tests that take edit results and cursor positions from actual
// neovim edits and test them against the behavior of this editor // neovim edits and test them against the behavior of this editor
@@ -383,7 +383,12 @@ impl Motion {
pub fn is_linewise(&self) -> bool { pub fn is_linewise(&self) -> bool {
matches!( matches!(
self, self,
Self::WholeLineInclusive | Self::WholeLineExclusive | Self::LineUp | Self::LineDown | Self::ScreenLineDown | Self::ScreenLineUp Self::WholeLineInclusive
| Self::WholeLineExclusive
| Self::LineUp
| Self::LineDown
| Self::ScreenLineDown
| Self::ScreenLineUp
) )
} }
} }

View File

@@ -1020,15 +1020,13 @@ impl ViNormal {
impl ViMode for ViNormal { impl ViMode for ViNormal {
fn handle_key(&mut self, key: E) -> Option<ViCmd> { fn handle_key(&mut self, key: E) -> Option<ViCmd> {
let mut cmd = match key { let mut cmd = match key {
E(K::Char('V'), M::NONE) => { E(K::Char('V'), M::NONE) => Some(ViCmd {
Some(ViCmd { register: Default::default(),
register: Default::default(), verb: Some(VerbCmd(1, Verb::VisualModeLine)),
verb: Some(VerbCmd(1, Verb::VisualModeLine)), motion: None,
motion: None, raw_seq: "".into(),
raw_seq: "".into(), flags: self.flags(),
flags: self.flags(), }),
})
}
E(K::Char(ch), M::NONE) => self.try_parse(ch), E(K::Char(ch), M::NONE) => self.try_parse(ch),
E(K::Backspace, M::NONE) => Some(ViCmd { E(K::Backspace, M::NONE) => Some(ViCmd {
register: Default::default(), register: Default::default(),
@@ -1405,8 +1403,8 @@ impl ViVisual {
break 'motion_parse Some(MotionCmd(count, Motion::WholeLineInclusive)); break 'motion_parse Some(MotionCmd(count, Motion::WholeLineInclusive));
} }
('c', Some(VerbCmd(_, Verb::Change))) => { ('c', Some(VerbCmd(_, Verb::Change))) => {
break 'motion_parse Some(MotionCmd(count, Motion::WholeLineExclusive)); break 'motion_parse Some(MotionCmd(count, Motion::WholeLineExclusive));
} }
_ => {} _ => {}
} }
match ch { match ch {

View File

@@ -83,14 +83,14 @@ impl ShOpts {
} }
} }
pub fn display_opts(&mut self) -> ShResult<String> { pub fn display_opts(&mut self) -> ShResult<String> {
let output = [ let output = [
format!("core:\n{}", self.query("core")?.unwrap_or_default()), format!("core:\n{}", self.query("core")?.unwrap_or_default()),
format!("prompt:\n{}",self.query("prompt")?.unwrap_or_default()) format!("prompt:\n{}", self.query("prompt")?.unwrap_or_default()),
]; ];
Ok(output.join("\n")) Ok(output.join("\n"))
} }
pub fn set(&mut self, opt: &str, val: &str) -> ShResult<()> { pub fn set(&mut self, opt: &str, val: &str) -> ShResult<()> {
let mut query = opt.split('.'); let mut query = opt.split('.');
@@ -542,7 +542,10 @@ impl Display for ShOptPrompt {
output.push(format!("comp_limit = {}", self.comp_limit)); output.push(format!("comp_limit = {}", self.comp_limit));
output.push(format!("highlight = {}", self.highlight)); output.push(format!("highlight = {}", self.highlight));
output.push(format!("auto_indent = {}", self.auto_indent)); output.push(format!("auto_indent = {}", self.auto_indent));
output.push(format!("linebreak_on_incomplete = {}", self.linebreak_on_incomplete)); output.push(format!(
"linebreak_on_incomplete = {}",
self.linebreak_on_incomplete
));
let final_output = output.join("\n"); let final_output = output.join("\n");

View File

@@ -83,10 +83,10 @@ pub fn check_signals() -> ShResult<()> {
run_trap(Signal::SIGCHLD)?; run_trap(Signal::SIGCHLD)?;
wait_child()?; wait_child()?;
} }
if got_signal(Signal::SIGWINCH) { if got_signal(Signal::SIGWINCH) {
GOT_SIGWINCH.store(true, Ordering::SeqCst); GOT_SIGWINCH.store(true, Ordering::SeqCst);
run_trap(Signal::SIGWINCH)?; run_trap(Signal::SIGWINCH)?;
} }
for sig in MISC_SIGNALS { for sig in MISC_SIGNALS {
if got_signal(sig) { if got_signal(sig) {
@@ -157,7 +157,7 @@ pub fn hang_up(_: libc::c_int) {
SHOULD_QUIT.store(true, Ordering::SeqCst); SHOULD_QUIT.store(true, Ordering::SeqCst);
QUIT_CODE.store(1, Ordering::SeqCst); QUIT_CODE.store(1, Ordering::SeqCst);
write_jobs(|j| { write_jobs(|j| {
j.hang_up(); j.hang_up();
}); });
} }
@@ -286,7 +286,7 @@ pub fn child_exited(pid: Pid, status: WtStat) -> ShResult<()> {
if is_fg { if is_fg {
take_term()?; take_term()?;
} else { } else {
JOB_DONE.store(true, Ordering::SeqCst); JOB_DONE.store(true, Ordering::SeqCst);
let job_order = read_jobs(|j| j.order().to_vec()); let job_order = read_jobs(|j| j.order().to_vec());
let result = read_jobs(|j| j.query(JobID::Pgid(pgid)).cloned()); let result = read_jobs(|j| j.query(JobID::Pgid(pgid)).cloned());
if let Some(job) = result { if let Some(job) = result {

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@ use tempfile::TempDir;
use crate::prompt::readline::complete::Completer; use crate::prompt::readline::complete::Completer;
use crate::prompt::readline::markers; use crate::prompt::readline::markers;
use crate::state::{write_logic, write_vars, VarFlags}; use crate::state::{VarFlags, write_logic, write_vars};
use super::*; use super::*;
@@ -192,10 +192,12 @@ fn complete_filename_with_slash() {
// Should complete files in subdir/ // Should complete files in subdir/
if result.is_some() { if result.is_some() {
assert!(completer assert!(
.candidates completer
.iter() .candidates
.any(|c| c.contains("nested.txt"))); .iter()
.any(|c| c.contains("nested.txt"))
);
} }
} }
@@ -702,10 +704,12 @@ fn complete_special_characters_in_filename() {
if result.is_some() { if result.is_some() {
// Should handle special chars in filenames // Should handle special chars in filenames
assert!(completer assert!(
.candidates completer
.iter() .candidates
.any(|c| c.contains("file-with-dash") || c.contains("file_with_underscore"))); .iter()
.any(|c| c.contains("file-with-dash") || c.contains("file_with_underscore"))
);
} }
} }

View File

@@ -58,8 +58,8 @@ fn unclosed_squote() {
#[test] #[test]
fn unclosed_brc_grp() { fn unclosed_brc_grp() {
let input = "{ foo bar"; let input = "{ foo bar";
let tokens = LexStream::new(Arc::new(input.into()), LexFlags::empty()) let tokens =
.collect::<ShResult<Vec<_>>>(); LexStream::new(Arc::new(input.into()), LexFlags::empty()).collect::<ShResult<Vec<_>>>();
let Err(err) = tokens else { let Err(err) = tokens else {
panic!("Expected an error, got {:?}", tokens); panic!("Expected an error, got {:?}", tokens);

View File

@@ -9,7 +9,13 @@ use super::*;
#[test] #[test]
fn simple_expansion() { fn simple_expansion() {
let varsub = "$foo"; let varsub = "$foo";
write_vars(|v| v.set_var("foo", VarKind::Str("this is the value of the variable".into()), VarFlags::NONE)); write_vars(|v| {
v.set_var(
"foo",
VarKind::Str("this is the value of the variable".into()),
VarFlags::NONE,
)
});
let mut tokens: Vec<Tk> = LexStream::new(Arc::new(varsub.to_string()), LexFlags::empty()) let mut tokens: Vec<Tk> = LexStream::new(Arc::new(varsub.to_string()), LexFlags::empty())
.map(|tk| tk.unwrap()) .map(|tk| tk.unwrap())
@@ -308,7 +314,10 @@ fn dquote_escape_dollar() {
fn dquote_escape_backslash() { fn dquote_escape_backslash() {
// "\\" in double quotes should produce a single backslash // "\\" in double quotes should produce a single backslash
let result = unescape_str(r#""\\""#); let result = unescape_str(r#""\\""#);
let inner: String = result.chars().filter(|&c| c != markers::DUB_QUOTE).collect(); let inner: String = result
.chars()
.filter(|&c| c != markers::DUB_QUOTE)
.collect();
assert_eq!( assert_eq!(
inner, "\\", inner, "\\",
"Double backslash should produce single backslash" "Double backslash should produce single backslash"
@@ -319,7 +328,10 @@ fn dquote_escape_backslash() {
fn dquote_escape_quote() { fn dquote_escape_quote() {
// "\"" should produce a literal double quote // "\"" should produce a literal double quote
let result = unescape_str(r#""\"""#); let result = unescape_str(r#""\"""#);
let inner: String = result.chars().filter(|&c| c != markers::DUB_QUOTE).collect(); let inner: String = result
.chars()
.filter(|&c| c != markers::DUB_QUOTE)
.collect();
assert!( assert!(
inner.contains('"'), inner.contains('"'),
"Escaped quote should produce literal quote" "Escaped quote should produce literal quote"
@@ -330,7 +342,10 @@ fn dquote_escape_quote() {
fn dquote_escape_backtick() { fn dquote_escape_backtick() {
// "\`" should strip backslash, produce literal backtick // "\`" should strip backslash, produce literal backtick
let result = unescape_str(r#""\`""#); let result = unescape_str(r#""\`""#);
let inner: String = result.chars().filter(|&c| c != markers::DUB_QUOTE).collect(); let inner: String = result
.chars()
.filter(|&c| c != markers::DUB_QUOTE)
.collect();
assert_eq!( assert_eq!(
inner, "`", inner, "`",
"Escaped backtick should produce literal backtick" "Escaped backtick should produce literal backtick"
@@ -341,7 +356,10 @@ fn dquote_escape_backtick() {
fn dquote_escape_nonspecial_preserves_backslash() { fn dquote_escape_nonspecial_preserves_backslash() {
// "\a" inside double quotes should preserve the backslash (a is not special) // "\a" inside double quotes should preserve the backslash (a is not special)
let result = unescape_str(r#""\a""#); let result = unescape_str(r#""\a""#);
let inner: String = result.chars().filter(|&c| c != markers::DUB_QUOTE).collect(); let inner: String = result
.chars()
.filter(|&c| c != markers::DUB_QUOTE)
.collect();
assert_eq!( assert_eq!(
inner, "\\a", inner, "\\a",
"Backslash before non-special char should be preserved" "Backslash before non-special char should be preserved"
@@ -362,10 +380,16 @@ fn dquote_unescaped_dollar_expands() {
fn dquote_mixed_escapes() { fn dquote_mixed_escapes() {
// "hello \$world \\end" should have literal $, single backslash // "hello \$world \\end" should have literal $, single backslash
let result = unescape_str(r#""hello \$world \\end""#); let result = unescape_str(r#""hello \$world \\end""#);
assert!(!result.contains(markers::VAR_SUB), "Escaped $ should not expand"); assert!(
!result.contains(markers::VAR_SUB),
"Escaped $ should not expand"
);
assert!(result.contains('$'), "Literal $ should be in output"); assert!(result.contains('$'), "Literal $ should be in output");
// Should have exactly one backslash (from \\) // Should have exactly one backslash (from \\)
let inner: String = result.chars().filter(|&c| c != markers::DUB_QUOTE).collect(); let inner: String = result
.chars()
.filter(|&c| c != markers::DUB_QUOTE)
.collect();
let backslash_count = inner.chars().filter(|&c| c == '\\').count(); let backslash_count = inner.chars().filter(|&c| c == '\\').count();
assert_eq!(backslash_count, 1, "\\\\ should produce one backslash"); assert_eq!(backslash_count, 1, "\\\\ should produce one backslash");
} }

View File

@@ -4,8 +4,9 @@ use super::*;
use crate::expand::{expand_aliases, unescape_str}; use crate::expand::{expand_aliases, unescape_str};
use crate::libsh::error::{Note, ShErr, ShErrKind}; use crate::libsh::error::{Note, ShErr, ShErrKind};
use crate::parse::{ use crate::parse::{
NdRule, Node, ParseStream,
lex::{LexFlags, LexStream, Tk, TkRule}, lex::{LexFlags, LexStream, Tk, TkRule},
node_operation, NdRule, Node, ParseStream, node_operation,
}; };
use crate::state::{write_logic, write_vars}; use crate::state::{write_logic, write_vars};

View File

@@ -1,12 +1,19 @@
use std::collections::VecDeque; use std::collections::VecDeque;
use crate::{ use crate::{
expand::expand_prompt, libsh::{ expand::expand_prompt,
libsh::{
error::ShErr, error::ShErr,
term::{Style, Styled}, term::{Style, Styled},
}, prompt::readline::{ },
Prompt, ShedVi, history::History, keys::{KeyCode, KeyEvent, ModKeys}, linebuf::LineBuf, term::{KeyReader, LineWriter, raw_mode}, vimode::{ViInsert, ViMode, ViNormal} prompt::readline::{
} Prompt, ShedVi,
history::History,
keys::{KeyCode, KeyEvent, ModKeys},
linebuf::LineBuf,
term::{KeyReader, LineWriter, raw_mode},
vimode::{ViInsert, ViMode, ViNormal},
},
}; };
use pretty_assertions::assert_eq; use pretty_assertions::assert_eq;
@@ -251,9 +258,13 @@ fn linebuf_ascii_content() {
#[test] #[test]
fn expand_default_prompt() { fn expand_default_prompt() {
let prompt = expand_prompt("\\e[0m\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m ".into()).unwrap(); let prompt = expand_prompt(
"\\e[0m\\n\\e[1;0m\\u\\e[1;36m@\\e[1;31m\\h\\n\\e[1;36m\\W\\e[1;32m/\\n\\e[1;32m\\$\\e[0m "
.into(),
)
.unwrap();
insta::assert_debug_snapshot!(prompt) insta::assert_debug_snapshot!(prompt)
} }
#[test] #[test]

View File

@@ -1,8 +1,8 @@
use std::sync::Arc; use std::sync::Arc;
use crate::parse::{ use crate::parse::{
lex::{LexFlags, LexStream},
NdRule, Node, ParseStream, Redir, RedirType, NdRule, Node, ParseStream, Redir, RedirType,
lex::{LexFlags, LexStream},
}; };
use crate::procio::{IoFrame, IoMode, IoStack}; use crate::procio::{IoFrame, IoMode, IoStack};

View File

@@ -11,8 +11,8 @@ fn scopestack_new() {
// Should start with one global scope // Should start with one global scope
assert!(stack.var_exists("PATH") || !stack.var_exists("PATH")); // Just check assert!(stack.var_exists("PATH") || !stack.var_exists("PATH")); // Just check
// it doesn't // it doesn't
// panic // panic
} }
#[test] #[test]