Compare commits

..

1 Commits

Author SHA1 Message Date
3a5b56fcd3 more improvements to auto indent depth tracking
added test cases for the auto indent/dedent feature
2026-03-14 01:04:03 -04:00
17 changed files with 232 additions and 1555 deletions

View File

@@ -8,7 +8,7 @@ A Linux shell written in Rust. The name is a nod to the original Unix utilities
### Line Editor
`shed` includes a built-in `vim` emulator as its line editor, written from scratch. It aims to provide a more precise vim-like editing experience at the shell prompt than conventional `vi` mode implementations.
`shed` includes a built-in `vim` emulator as its line editor, written from scratch. It aims to provide a more precise vim-like editing experience at the shell prompt.
- **Normal mode** - motions (`w`, `b`, `e`, `f`, `t`, `%`, `0`, `$`, etc.), verbs (`d`, `c`, `y`, `p`, `r`, `x`, `~`, etc.), text objects (`iw`, `aw`, `i"`, `a{`, `is`, etc.), registers, `.` repeat, `;`/`,` repeat, and counts
- **Insert mode** - insert, append, replace, with Ctrl+W word deletion and undo/redo

View File

@@ -24,14 +24,13 @@ pub mod source;
pub mod test; // [[ ]] thing
pub mod trap;
pub mod varcmds;
pub mod seek;
pub const BUILTINS: [&str; 50] = [
pub const BUILTINS: [&str; 49] = [
"echo", "cd", "read", "export", "local", "pwd", "source", ".", "shift", "jobs", "fg", "bg",
"disown", "alias", "unalias", "return", "break", "continue", "exit", "shopt", "builtin",
"command", "trap", "pushd", "popd", "dirs", "exec", "eval", "true", "false", ":", "readonly",
"unset", "complete", "compgen", "map", "pop", "fpop", "push", "fpush", "rotate", "wait", "type",
"getopts", "keymap", "read_key", "autocmd", "ulimit", "umask", "seek"
"getopts", "keymap", "read_key", "autocmd", "ulimit", "umask",
];
pub fn true_builtin() -> ShResult<()> {

View File

@@ -1,253 +0,0 @@
use nix::{libc::STDOUT_FILENO, unistd::{Whence, lseek, write}};
use crate::{getopt::{Opt, OptSpec, get_opts_from_tokens}, libsh::error::{ShErr, ShErrKind, ShResult}, parse::{NdRule, Node, execute::prepare_argv}, procio::borrow_fd, state};
pub const LSEEK_OPTS: [OptSpec;2] = [
OptSpec {
opt: Opt::Short('c'),
takes_arg: false
},
OptSpec {
opt: Opt::Short('e'),
takes_arg: false
},
];
pub struct LseekOpts {
cursor_rel: bool,
end_rel: bool
}
pub fn seek(node: Node) -> ShResult<()> {
let NdRule::Command {
assignments: _,
argv,
} = node.class else { unreachable!() };
let (argv, opts) = get_opts_from_tokens(argv, &LSEEK_OPTS)?;
let lseek_opts = get_lseek_opts(opts)?;
let mut argv = prepare_argv(argv)?.into_iter();
argv.next(); // drop 'seek'
let Some(fd) = argv.next() else {
return Err(ShErr::simple(
ShErrKind::ExecFail,
"lseek: Missing required argument 'fd'",
));
};
let Ok(fd) = fd.0.parse::<u32>() else {
return Err(ShErr::at(
ShErrKind::ExecFail,
fd.1,
"Invalid file descriptor",
).with_note("file descriptors are integers"));
};
let Some(offset) = argv.next() else {
return Err(ShErr::simple(
ShErrKind::ExecFail,
"lseek: Missing required argument 'offset'",
));
};
let Ok(offset) = offset.0.parse::<i64>() else {
return Err(ShErr::at(
ShErrKind::ExecFail,
offset.1,
"Invalid offset",
).with_note("offset can be a positive or negative integer"));
};
let whence = if lseek_opts.cursor_rel {
Whence::SeekCur
} else if lseek_opts.end_rel {
Whence::SeekEnd
} else {
Whence::SeekSet
};
match lseek(fd as i32, offset, whence) {
Ok(new_offset) => {
let stdout = borrow_fd(STDOUT_FILENO);
let buf = new_offset.to_string() + "\n";
write(stdout, buf.as_bytes())?;
}
Err(e) => {
state::set_status(1);
return Err(e.into())
}
}
state::set_status(0);
Ok(())
}
pub fn get_lseek_opts(opts: Vec<Opt>) -> ShResult<LseekOpts> {
let mut lseek_opts = LseekOpts {
cursor_rel: false,
end_rel: false,
};
for opt in opts {
match opt {
Opt::Short('c') => lseek_opts.cursor_rel = true,
Opt::Short('e') => lseek_opts.end_rel = true,
_ => {
return Err(ShErr::simple(
ShErrKind::ExecFail,
format!("lseek: Unexpected flag '{opt}'"),
));
}
}
}
Ok(lseek_opts)
}
#[cfg(test)]
mod tests {
use crate::testutil::{TestGuard, test_input};
use pretty_assertions::assert_eq;
#[test]
fn seek_set_beginning() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("seek.txt");
std::fs::write(&path, "hello world\n").unwrap();
let g = TestGuard::new();
test_input(format!("exec 9<> {}", path.display())).unwrap();
test_input("seek 9 0").unwrap();
let out = g.read_output();
assert_eq!(out, "0\n");
}
#[test]
fn seek_set_offset() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("seek.txt");
std::fs::write(&path, "hello world\n").unwrap();
let g = TestGuard::new();
test_input(format!("exec 9<> {}", path.display())).unwrap();
test_input("seek 9 6").unwrap();
let out = g.read_output();
assert_eq!(out, "6\n");
}
#[test]
fn seek_then_read() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("seek.txt");
std::fs::write(&path, "hello world\n").unwrap();
let g = TestGuard::new();
test_input(format!("exec 9<> {}", path.display())).unwrap();
test_input("seek 9 6").unwrap();
// Clear the seek output
g.read_output();
test_input("read line <&9").unwrap();
let val = crate::state::read_vars(|v| v.get_var("line"));
assert_eq!(val, "world");
}
#[test]
fn seek_cur_relative() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("seek.txt");
std::fs::write(&path, "abcdefghij\n").unwrap();
let g = TestGuard::new();
test_input(format!("exec 9<> {}", path.display())).unwrap();
test_input("seek 9 3").unwrap();
test_input("seek -c 9 4").unwrap();
let out = g.read_output();
assert_eq!(out, "3\n7\n");
}
#[test]
fn seek_end() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("seek.txt");
std::fs::write(&path, "hello\n").unwrap(); // 6 bytes
let g = TestGuard::new();
test_input(format!("exec 9<> {}", path.display())).unwrap();
test_input("seek -e 9 0").unwrap();
let out = g.read_output();
assert_eq!(out, "6\n");
}
#[test]
fn seek_end_negative() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("seek.txt");
std::fs::write(&path, "hello\n").unwrap(); // 6 bytes
let g = TestGuard::new();
test_input(format!("exec 9<> {}", path.display())).unwrap();
test_input("seek -e 9 -2").unwrap();
let out = g.read_output();
assert_eq!(out, "4\n");
}
#[test]
fn seek_write_overwrite() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("seek.txt");
std::fs::write(&path, "hello world\n").unwrap();
let _g = TestGuard::new();
test_input(format!("exec 9<> {}", path.display())).unwrap();
test_input("seek 9 6").unwrap();
test_input("echo -n 'WORLD' >&9").unwrap();
let contents = std::fs::read_to_string(&path).unwrap();
assert_eq!(contents, "hello WORLD\n");
}
#[test]
fn seek_rewind_full_read() {
let dir = tempfile::TempDir::new().unwrap();
let path = dir.path().join("seek.txt");
std::fs::write(&path, "abc\n").unwrap();
let g = TestGuard::new();
test_input(format!("exec 9<> {}", path.display())).unwrap();
// Read moves cursor to EOF
test_input("read line <&9").unwrap();
// Rewind
test_input("seek 9 0").unwrap();
// Clear output from seek
g.read_output();
// Read again from beginning
test_input("read line <&9").unwrap();
let val = crate::state::read_vars(|v| v.get_var("line"));
assert_eq!(val, "abc");
}
#[test]
fn seek_bad_fd() {
let _g = TestGuard::new();
let result = test_input("seek 99 0");
assert!(result.is_err());
}
#[test]
fn seek_missing_args() {
let _g = TestGuard::new();
let result = test_input("seek");
assert!(result.is_err());
let result = test_input("seek 9");
assert!(result.is_err());
}
}

View File

@@ -40,23 +40,18 @@ impl Tk {
}
pub struct Expander {
flags: TkFlags,
raw: String,
}
impl Expander {
pub fn new(raw: Tk) -> ShResult<Self> {
let tk_raw = raw.span.as_str();
Self::from_raw(tk_raw, raw.flags)
let raw = raw.span.as_str();
Self::from_raw(raw)
}
pub fn from_raw(raw: &str, flags: TkFlags) -> ShResult<Self> {
pub fn from_raw(raw: &str) -> ShResult<Self> {
let raw = expand_braces_full(raw)?.join(" ");
let unescaped = if flags.contains(TkFlags::IS_HEREDOC) {
unescape_heredoc(&raw)
} else {
unescape_str(&raw)
};
Ok(Self { raw: unescaped, flags })
let unescaped = unescape_str(&raw);
Ok(Self { raw: unescaped })
}
pub fn expand(&mut self) -> ShResult<Vec<String>> {
let mut chars = self.raw.chars().peekable();
@@ -80,11 +75,7 @@ impl Expander {
self.raw.insert_str(0, "./");
}
if self.flags.contains(TkFlags::IS_HEREDOC) {
Ok(vec![self.raw.clone()])
} else {
Ok(self.split_words())
}
Ok(self.split_words())
}
pub fn split_words(&mut self) -> Vec<String> {
let mut words = vec![];
@@ -1163,25 +1154,6 @@ pub fn unescape_str(raw: &str) -> String {
}
}
}
'`' => {
result.push(markers::VAR_SUB);
result.push(markers::SUBSH);
while let Some(bt_ch) = chars.next() {
match bt_ch {
'\\' => {
result.push(bt_ch);
if let Some(next_ch) = chars.next() {
result.push(next_ch);
}
}
'`' => {
result.push(markers::SUBSH);
break;
}
_ => result.push(bt_ch),
}
}
}
'"' => {
result.push(markers::DUB_QUOTE);
break;
@@ -1346,25 +1318,6 @@ pub fn unescape_str(raw: &str) -> String {
result.push('$');
}
}
'`' => {
result.push(markers::VAR_SUB);
result.push(markers::SUBSH);
while let Some(bt_ch) = chars.next() {
match bt_ch {
'\\' => {
result.push(bt_ch);
if let Some(next_ch) = chars.next() {
result.push(next_ch);
}
}
'`' => {
result.push(markers::SUBSH);
break;
}
_ => result.push(bt_ch),
}
}
}
_ => result.push(ch),
}
first_char = false;
@@ -1373,96 +1326,6 @@ pub fn unescape_str(raw: &str) -> String {
result
}
/// Like unescape_str but for heredoc bodies. Only processes:
/// - $var / ${var} / $(cmd) substitution markers
/// - Backslash escapes (only before $, `, \, and newline)
/// Everything else (quotes, tildes, globs, process subs, etc.) is literal.
pub fn unescape_heredoc(raw: &str) -> String {
let mut chars = raw.chars().peekable();
let mut result = String::new();
while let Some(ch) = chars.next() {
match ch {
'\\' => {
match chars.peek() {
Some('$') | Some('`') | Some('\\') | Some('\n') => {
let next_ch = chars.next().unwrap();
if next_ch == '\n' {
// line continuation — discard both backslash and newline
continue;
}
result.push(markers::ESCAPE);
result.push(next_ch);
}
_ => {
// backslash is literal
result.push('\\');
}
}
}
'$' if chars.peek() == Some(&'(') => {
result.push(markers::VAR_SUB);
chars.next(); // consume '('
result.push(markers::SUBSH);
let mut paren_count = 1;
while let Some(subsh_ch) = chars.next() {
match subsh_ch {
'\\' => {
result.push(subsh_ch);
if let Some(next_ch) = chars.next() {
result.push(next_ch);
}
}
'(' => {
paren_count += 1;
result.push(subsh_ch);
}
')' => {
paren_count -= 1;
if paren_count == 0 {
result.push(markers::SUBSH);
break;
} else {
result.push(subsh_ch);
}
}
_ => result.push(subsh_ch),
}
}
}
'$' => {
result.push(markers::VAR_SUB);
if chars.peek() == Some(&'$') {
chars.next();
result.push('$');
}
}
'`' => {
result.push(markers::VAR_SUB);
result.push(markers::SUBSH);
while let Some(bt_ch) = chars.next() {
match bt_ch {
'\\' => {
result.push(bt_ch);
if let Some(next_ch) = chars.next() {
result.push(next_ch);
}
}
'`' => {
result.push(markers::SUBSH);
break;
}
_ => result.push(bt_ch),
}
}
}
_ => result.push(ch),
}
}
result
}
/// Opposite of unescape_str - escapes a string to be executed as literal text
/// Used for completion results, and glob filename matches.
pub fn escape_str(raw: &str, use_marker: bool) -> String {
@@ -3669,7 +3532,6 @@ mod tests {
let mut exp = Expander {
raw: "hello world\tfoo".to_string(),
flags: TkFlags::empty()
};
let words = exp.split_words();
assert_eq!(words, vec!["hello", "world", "foo"]);
@@ -3684,7 +3546,6 @@ mod tests {
let mut exp = Expander {
raw: "a:b:c".to_string(),
flags: TkFlags::empty()
};
let words = exp.split_words();
assert_eq!(words, vec!["a", "b", "c"]);
@@ -3699,7 +3560,6 @@ mod tests {
let mut exp = Expander {
raw: "hello world".to_string(),
flags: TkFlags::empty()
};
let words = exp.split_words();
assert_eq!(words, vec!["hello world"]);
@@ -3710,10 +3570,7 @@ mod tests {
let _guard = TestGuard::new();
let raw = format!("{}hello world{}", markers::DUB_QUOTE, markers::DUB_QUOTE);
let mut exp = Expander {
raw,
flags: TkFlags::empty()
};
let mut exp = Expander { raw };
let words = exp.split_words();
assert_eq!(words, vec!["hello world"]);
}
@@ -3725,10 +3582,7 @@ mod tests {
let _guard = TestGuard::new();
let raw = format!("hello{}world", unescape_str("\\ "));
let mut exp = Expander {
raw,
flags: TkFlags::empty()
};
let mut exp = Expander { raw };
let words = exp.split_words();
assert_eq!(words, vec!["hello world"]);
}
@@ -3738,10 +3592,7 @@ mod tests {
let _guard = TestGuard::new();
let raw = format!("hello{}world", unescape_str("\\\t"));
let mut exp = Expander {
raw,
flags: TkFlags::empty()
};
let mut exp = Expander { raw };
let words = exp.split_words();
assert_eq!(words, vec!["hello\tworld"]);
}
@@ -3754,10 +3605,7 @@ mod tests {
}
let raw = format!("a{}b:c", unescape_str("\\:"));
let mut exp = Expander {
raw,
flags: TkFlags::empty()
};
let mut exp = Expander { raw };
let words = exp.split_words();
assert_eq!(words, vec!["a:b", "c"]);
}

View File

@@ -95,16 +95,14 @@ pub fn sort_tks(
.into_iter()
.map(|t| t.expand())
.collect::<ShResult<Vec<_>>>()?
.into_iter()
.peekable();
.into_iter();
let mut opts = vec![];
let mut non_opts = vec![];
while let Some(token) = tokens_iter.next() {
if &token.to_string() == "--" {
non_opts.push(token);
non_opts.extend(tokens_iter);
break;
non_opts.extend(tokens_iter);
break;
}
let parsed_opts = Opt::parse(&token.to_string());

View File

@@ -201,7 +201,6 @@ impl ShErr {
pub fn is_flow_control(&self) -> bool {
self.kind.is_flow_control()
}
/// Promotes a shell error from a simple error to an error that blames a span
pub fn promote(mut self, span: Span) -> Self {
if self.notes.is_empty() {
return self;
@@ -209,9 +208,7 @@ impl ShErr {
let first = self.notes[0].clone();
if self.notes.len() > 1 {
self.notes = self.notes[1..].to_vec();
} else {
self.notes = vec![];
}
}
self.labeled(span, first)
}

View File

@@ -147,9 +147,11 @@ impl RawModeGuard {
let orig = ORIG_TERMIOS
.with(|cell| cell.borrow().clone())
.expect("with_cooked_mode called before raw_mode()");
tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &orig).ok();
tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &orig)
.expect("Failed to restore cooked mode");
let res = f();
tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &current).ok();
tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &current)
.expect("Failed to restore raw mode");
res
}
}

View File

@@ -2,14 +2,6 @@ use std::sync::LazyLock;
use crate::prelude::*;
/// Minimum fd number for shell-internal file descriptors.
const MIN_INTERNAL_FD: RawFd = 10;
pub static TTY_FILENO: LazyLock<RawFd> = LazyLock::new(|| {
let fd = open("/dev/tty", OFlag::O_RDWR, Mode::empty()).expect("Failed to open /dev/tty");
// Move the tty fd above the user-accessible range so that
// `exec 3>&-` and friends don't collide with shell internals.
let high = fcntl(fd, FcntlArg::F_DUPFD_CLOEXEC(MIN_INTERNAL_FD)).expect("Failed to dup /dev/tty high");
close(fd).ok();
high
open("/dev/tty", OFlag::O_RDWR, Mode::empty()).expect("Failed to open /dev/tty")
});

View File

@@ -8,7 +8,28 @@ use ariadne::Fmt;
use crate::{
builtin::{
alias::{alias, unalias}, arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate}, autocmd::autocmd, cd::cd, complete::{compgen_builtin, complete_builtin}, dirstack::{dirs, popd, pushd}, echo::echo, eval, exec, flowctl::flowctl, getopts::getopts, intro, jobctl::{self, JobBehavior, continue_job, disown, jobs}, keymap, seek::seek, map, pwd::pwd, read::{self, read_builtin}, resource::{ulimit, umask_builtin}, shift::shift, shopt::shopt, source::source, test::double_bracket_test, trap::{TrapTarget, trap}, varcmds::{export, local, readonly, unset}
alias::{alias, unalias},
arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate},
autocmd::autocmd,
cd::cd,
complete::{compgen_builtin, complete_builtin},
dirstack::{dirs, popd, pushd},
echo::echo,
eval, exec,
flowctl::flowctl,
getopts::getopts,
intro,
jobctl::{self, JobBehavior, continue_job, disown, jobs},
keymap, map,
pwd::pwd,
read::{self, read_builtin},
resource::{ulimit, umask_builtin},
shift::shift,
shopt::shopt,
source::source,
test::double_bracket_test,
trap::{TrapTarget, trap},
varcmds::{export, local, readonly, unset},
},
expand::{expand_aliases, expand_case_pattern, glob_to_regex},
jobs::{ChildProc, JobStack, attach_tty, dispatch_job},
@@ -319,19 +340,24 @@ impl Dispatcher {
};
let mut elem_iter = elements.into_iter();
let mut skip = false;
while let Some(element) = elem_iter.next() {
let ConjunctNode { cmd, operator } = element;
if !skip {
self.dispatch_node(*cmd)?;
}
self.dispatch_node(*cmd)?;
let status = state::get_status();
skip = match operator {
ConjunctOp::And => status != 0,
ConjunctOp::Or => status == 0,
match operator {
ConjunctOp::And => {
if status != 0 {
break;
}
}
ConjunctOp::Or => {
if status == 0 {
break;
}
}
ConjunctOp::Null => break,
};
}
}
Ok(())
}
@@ -351,7 +377,7 @@ impl Dispatcher {
};
let body_span = body.get_span();
let body = body_span.as_str().to_string();
let name = name.span.as_str().strip_suffix("()").unwrap_or(name.span.as_str());
let name = name.span.as_str().strip_suffix("()").unwrap();
if KEYWORDS.contains(&name) {
return Err(ShErr::at(
@@ -862,10 +888,7 @@ impl Dispatcher {
if fork_builtins {
log::trace!("Forking builtin: {}", cmd_raw);
let guard = self.io_stack.pop_frame().redirect()?;
if cmd_raw.as_str() == "exec" {
guard.persist();
}
let _guard = self.io_stack.pop_frame().redirect()?;
self.run_fork(&cmd_raw, |s| {
if let Err(e) = s.dispatch_builtin(cmd) {
e.print_error();
@@ -990,7 +1013,6 @@ impl Dispatcher {
"autocmd" => autocmd(cmd),
"ulimit" => ulimit(cmd),
"umask" => umask_builtin(cmd),
"seek" => seek(cmd),
"true" | ":" => {
state::set_status(0);
Ok(())

View File

@@ -267,12 +267,20 @@ bitflags! {
const ASSIGN = 0b0000000001000000;
const BUILTIN = 0b0000000010000000;
const IS_PROCSUB = 0b0000000100000000;
const IS_HEREDOC = 0b0000001000000000;
const LIT_HEREDOC = 0b0000010000000000;
const TAB_HEREDOC = 0b0000100000000000;
}
}
pub struct LexStream {
source: Arc<String>,
pub cursor: usize,
pub name: String,
quote_state: QuoteState,
brc_grp_depth: usize,
brc_grp_start: Option<usize>,
case_depth: usize,
flags: LexFlags,
}
bitflags! {
#[derive(Debug, Clone, Copy)]
pub struct LexFlags: u32 {
@@ -314,19 +322,6 @@ pub fn clean_input(input: &str) -> String {
output
}
pub struct LexStream {
source: Arc<String>,
pub cursor: usize,
pub name: String,
quote_state: QuoteState,
brc_grp_depth: usize,
brc_grp_start: Option<usize>,
case_depth: usize,
heredoc_skip: Option<usize>,
flags: LexFlags,
}
impl LexStream {
pub fn new(source: Arc<String>, flags: LexFlags) -> Self {
let flags = flags | LexFlags::FRESH | LexFlags::NEXT_IS_CMD;
@@ -338,7 +333,6 @@ impl LexStream {
quote_state: QuoteState::default(),
brc_grp_depth: 0,
brc_grp_start: None,
heredoc_skip: None,
case_depth: 0,
}
}
@@ -399,7 +393,7 @@ impl LexStream {
}
pub fn read_redir(&mut self) -> Option<ShResult<Tk>> {
assert!(self.cursor <= self.source.len());
let slice = self.slice(self.cursor..)?.to_string();
let slice = self.slice(self.cursor..)?;
let mut pos = self.cursor;
let mut chars = slice.chars().peekable();
let mut tk = Tk::default();
@@ -411,51 +405,37 @@ impl LexStream {
return None; // It's a process sub
}
pos += 1;
if let Some('|') = chars.peek() {
// noclobber force '>|'
chars.next();
pos += 1;
tk = self.get_token(self.cursor..pos, TkRule::Redir);
break
}
if let Some('>') = chars.peek() {
chars.next();
pos += 1;
}
let Some('&') = chars.peek() else {
if let Some('&') = chars.peek() {
chars.next();
pos += 1;
let mut found_fd = false;
while chars.peek().is_some_and(|ch| ch.is_ascii_digit()) {
chars.next();
found_fd = true;
pos += 1;
}
if !found_fd && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
let span_start = self.cursor;
self.cursor = pos;
return Some(Err(ShErr::at(
ShErrKind::ParseErr,
Span::new(span_start..pos, self.source.clone()),
"Invalid redirection",
)));
} else {
tk = self.get_token(self.cursor..pos, TkRule::Redir);
break;
}
} else {
tk = self.get_token(self.cursor..pos, TkRule::Redir);
break;
};
chars.next();
pos += 1;
let mut found_fd = false;
if chars.peek().is_some_and(|ch| *ch == '-') {
chars.next();
found_fd = true;
pos += 1;
} else {
while chars.peek().is_some_and(|ch| ch.is_ascii_digit()) {
chars.next();
found_fd = true;
pos += 1;
}
}
if !found_fd && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
let span_start = self.cursor;
self.cursor = pos;
return Some(Err(ShErr::at(
ShErrKind::ParseErr,
Span::new(span_start..pos, self.source.clone()),
"Invalid redirection",
)));
} else {
tk = self.get_token(self.cursor..pos, TkRule::Redir);
break;
}
}
}
'<' => {
if chars.peek() == Some(&'(') {
@@ -463,94 +443,14 @@ impl LexStream {
}
pos += 1;
match chars.peek() {
Some('<') => {
chars.next();
pos += 1;
match chars.peek() {
Some('<') => {
chars.next();
pos += 1;
}
Some(ch) => {
let mut ch = *ch;
while is_field_sep(ch) {
let Some(next_ch) = chars.next() else {
// Incomplete input — fall through to emit << as Redir
break;
};
pos += next_ch.len_utf8();
ch = next_ch;
}
if is_field_sep(ch) {
// Ran out of input while skipping whitespace — fall through
} else {
let saved_cursor = self.cursor;
match self.read_heredoc(pos) {
Ok(Some(heredoc_tk)) => {
// cursor is set to after the delimiter word;
// heredoc_skip is set to after the body
pos = self.cursor;
self.cursor = saved_cursor;
tk = heredoc_tk;
break;
}
Ok(None) => {
// Incomplete heredoc — restore cursor and fall through
self.cursor = saved_cursor;
}
Err(e) => return Some(Err(e)),
}
}
}
_ => {
// No delimiter yet — input is incomplete
// Fall through to emit the << as a Redir token
}
}
}
Some('>') => {
chars.next();
pos += 1;
tk = self.get_token(self.cursor..pos, TkRule::Redir);
break;
}
Some('&') => {
chars.next();
pos += 1;
let mut found_fd = false;
if chars.peek().is_some_and(|ch| *ch == '-') {
chars.next();
found_fd = true;
pos += 1;
} else {
while chars.peek().is_some_and(|ch| ch.is_ascii_digit()) {
chars.next();
found_fd = true;
pos += 1;
}
}
if !found_fd && !self.flags.contains(LexFlags::LEX_UNFINISHED) {
let span_start = self.cursor;
self.cursor = pos;
return Some(Err(ShErr::at(
ShErrKind::ParseErr,
Span::new(span_start..pos, self.source.clone()),
"Invalid redirection",
)));
} else {
tk = self.get_token(self.cursor..pos, TkRule::Redir);
break;
}
}
_ => {}
}
for _ in 0..2 {
if let Some('<') = chars.peek() {
chars.next();
pos += 1;
} else {
break;
}
}
tk = self.get_token(self.cursor..pos, TkRule::Redir);
break;
}
@@ -574,130 +474,6 @@ impl LexStream {
self.cursor = pos;
Some(Ok(tk))
}
pub fn read_heredoc(&mut self, mut pos: usize) -> ShResult<Option<Tk>> {
let slice = self.slice(pos..).unwrap_or_default().to_string();
let mut chars = slice.chars();
let mut delim = String::new();
let mut flags = TkFlags::empty();
let mut first_char = true;
// Parse the delimiter word, stripping quotes
while let Some(ch) = chars.next() {
match ch {
'-' if first_char => {
pos += 1;
flags |= TkFlags::TAB_HEREDOC;
}
'\"' => {
pos += 1;
self.quote_state.toggle_double();
flags |= TkFlags::LIT_HEREDOC;
}
'\'' => {
pos += 1;
self.quote_state.toggle_single();
flags |= TkFlags::LIT_HEREDOC;
}
_ if self.quote_state.in_quote() => {
pos += ch.len_utf8();
delim.push(ch);
}
ch if is_hard_sep(ch) => {
break;
}
ch => {
pos += ch.len_utf8();
delim.push(ch);
}
}
first_char = false;
}
// pos is now right after the delimiter word — this is where
// the cursor should return so the rest of the line gets lexed
let cursor_after_delim = pos;
// Re-slice from cursor_after_delim so iterator and pos are in sync
// (the old chars iterator consumed the hard_sep without advancing pos)
let rest = self.slice(cursor_after_delim..).unwrap_or_default().to_string();
let mut chars = rest.chars();
// Scan forward to the newline (or use heredoc_skip from a previous heredoc)
let body_start = if let Some(skip) = self.heredoc_skip {
// A previous heredoc on this line already read its body;
// our body starts where that one ended
let skip_offset = skip - cursor_after_delim;
for _ in 0..skip_offset {
chars.next();
}
skip
} else {
// Skip the rest of the current line to find where the body begins
let mut scan = pos;
let mut found_newline = false;
while let Some(ch) = chars.next() {
scan += ch.len_utf8();
if ch == '\n' {
found_newline = true;
break;
}
}
if !found_newline {
if self.flags.contains(LexFlags::LEX_UNFINISHED) {
return Ok(None);
} else {
return Err(ShErr::at(
ShErrKind::ParseErr,
Span::new(pos..pos, self.source.clone()),
"Heredoc delimiter not found",
));
}
}
scan
};
pos = body_start;
let start = pos;
// Read lines until we find one that matches the delimiter exactly
let mut line = String::new();
let mut line_start = pos;
while let Some(ch) = chars.next() {
pos += ch.len_utf8();
if ch == '\n' {
let trimmed = line.trim_end_matches('\r');
if trimmed == delim {
let mut tk = self.get_token(start..line_start, TkRule::Redir);
tk.flags |= TkFlags::IS_HEREDOC | flags;
self.heredoc_skip = Some(pos);
self.cursor = cursor_after_delim;
return Ok(Some(tk));
}
line.clear();
line_start = pos;
} else {
line.push(ch);
}
}
// Check the last line (no trailing newline)
let trimmed = line.trim_end_matches('\r');
if trimmed == delim {
let mut tk = self.get_token(start..line_start, TkRule::Redir);
tk.flags |= TkFlags::IS_HEREDOC | flags;
self.heredoc_skip = Some(pos);
self.cursor = cursor_after_delim;
return Ok(Some(tk));
}
if !self.flags.contains(LexFlags::LEX_UNFINISHED) {
Err(ShErr::at(
ShErrKind::ParseErr,
Span::new(start..pos, self.source.clone()),
format!("Heredoc delimiter '{}' not found", delim),
))
} else {
Ok(None)
}
}
pub fn read_string(&mut self) -> ShResult<Tk> {
assert!(self.cursor <= self.source.len());
let slice = self.slice_from_cursor().unwrap().to_string();
@@ -875,16 +651,6 @@ impl LexStream {
));
}
}
'(' if can_be_subshell && chars.peek() == Some(&')') => {
// standalone "()" — function definition marker
pos += 2;
chars.next();
let mut tk = self.get_token(self.cursor..pos, TkRule::Str);
tk.mark(TkFlags::KEYWORD);
self.cursor = pos;
self.set_next_is_cmd(true);
return Ok(tk);
}
'(' if self.next_is_cmd() && can_be_subshell => {
pos += 1;
let mut paren_count = 1;
@@ -1105,18 +871,10 @@ impl Iterator for LexStream {
let token = match get_char(&self.source, self.cursor).unwrap() {
'\r' | '\n' | ';' => {
let ch = get_char(&self.source, self.cursor).unwrap();
let ch_idx = self.cursor;
self.cursor += 1;
self.set_next_is_cmd(true);
// If a heredoc was parsed on this line, skip past the body
// Only on newline — ';' is a command separator within the same line
if (ch == '\n' || ch == '\r')
&& let Some(skip) = self.heredoc_skip.take() {
self.cursor = skip;
}
while let Some(ch) = get_char(&self.source, self.cursor) {
match ch {
'\\' if get_char(&self.source, self.cursor + 1) == Some('\n') => {

View File

@@ -12,7 +12,7 @@ use crate::{
},
parse::lex::clean_input,
prelude::*,
procio::IoMode, state::read_shopts,
procio::IoMode,
};
pub mod execute;
@@ -280,17 +280,12 @@ bitflags! {
pub struct Redir {
pub io_mode: IoMode,
pub class: RedirType,
pub span: Option<Span>
}
impl Redir {
pub fn new(io_mode: IoMode, class: RedirType) -> Self {
Self { io_mode, class, span: None }
Self { io_mode, class }
}
pub fn with_span(mut self, span: Span) -> Self {
self.span = Some(span);
self
}
}
#[derive(Default, Debug)]
@@ -298,7 +293,6 @@ pub struct RedirBldr {
pub io_mode: Option<IoMode>,
pub class: Option<RedirType>,
pub tgt_fd: Option<RawFd>,
pub span: Option<Span>,
}
impl RedirBldr {
@@ -306,41 +300,48 @@ impl RedirBldr {
Default::default()
}
pub fn with_io_mode(self, io_mode: IoMode) -> Self {
Self {
io_mode: Some(io_mode),
..self
}
let Self {
io_mode: _,
class,
tgt_fd,
} = self;
Self {
io_mode: Some(io_mode),
class,
tgt_fd,
}
}
pub fn with_class(self, class: RedirType) -> Self {
Self {
class: Some(class),
..self
}
let Self {
io_mode,
class: _,
tgt_fd,
} = self;
Self {
io_mode,
class: Some(class),
tgt_fd,
}
}
pub fn with_tgt(self, tgt_fd: RawFd) -> Self {
Self {
tgt_fd: Some(tgt_fd),
..self
}
let Self {
io_mode,
class,
tgt_fd: _,
} = self;
Self {
io_mode,
class,
tgt_fd: Some(tgt_fd),
}
}
pub fn with_span(self, span: Span) -> Self {
Self {
span: Some(span),
..self
}
}
pub fn build(self) -> Redir {
let new = Redir::new(self.io_mode.unwrap(), self.class.unwrap());
if let Some(span) = self.span {
new.with_span(span)
} else {
new
}
Redir::new(self.io_mode.unwrap(), self.class.unwrap())
}
}
impl FromStr for RedirBldr {
type Err = ShErr;
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut chars = s.chars().peekable();
let mut src_fd = String::new();
@@ -354,24 +355,16 @@ impl FromStr for RedirBldr {
if let Some('>') = chars.peek() {
chars.next();
redir = redir.with_class(RedirType::Append);
} else if let Some('|') = chars.peek() {
chars.next();
redir = redir.with_class(RedirType::OutputForce);
}
}
}
'<' => {
redir = redir.with_class(RedirType::Input);
let mut count = 0;
if chars.peek() == Some(&'>') {
chars.next(); // consume the '>'
redir = redir.with_class(RedirType::ReadWrite);
} else {
while count < 2 && matches!(chars.peek(), Some('<')) {
chars.next();
count += 1;
}
}
while count < 2 && matches!(chars.peek(), Some('<')) {
chars.next();
count += 1;
}
redir = match count {
1 => redir.with_class(RedirType::HereDoc),
@@ -380,23 +373,15 @@ impl FromStr for RedirBldr {
};
}
'&' => {
if chars.peek() == Some(&'-') {
chars.next();
src_fd.push('-');
} else {
while let Some(next_ch) = chars.next() {
if next_ch.is_ascii_digit() {
src_fd.push(next_ch)
} else {
break;
}
}
}
while let Some(next_ch) = chars.next() {
if next_ch.is_ascii_digit() {
src_fd.push(next_ch)
} else {
break;
}
}
if src_fd.is_empty() {
return Err(ShErr::simple(
ShErrKind::ParseErr,
format!("Invalid character '{}' in redirection operator", ch),
));
return Err(());
}
}
_ if ch.is_ascii_digit() && tgt_fd.is_empty() => {
@@ -410,27 +395,19 @@ impl FromStr for RedirBldr {
}
}
}
_ => return Err(ShErr::simple(
ShErrKind::ParseErr,
format!("Invalid character '{}' in redirection operator", ch),
)),
_ => return Err(()),
}
}
// FIXME: I am 99.999999999% sure that tgt_fd and src_fd are backwards here
let tgt_fd = tgt_fd
.parse::<i32>()
.unwrap_or_else(|_| match redir.class.unwrap() {
RedirType::Input |
RedirType::ReadWrite |
RedirType::HereDoc |
RedirType::HereString => 0,
RedirType::Input | RedirType::HereDoc | RedirType::HereString => 0,
_ => 1,
});
redir = redir.with_tgt(tgt_fd);
if src_fd.as_str() == "-" {
let io_mode = IoMode::Close { tgt_fd };
redir = redir.with_io_mode(io_mode);
} else if let Ok(src_fd) = src_fd.parse::<i32>() {
if let Ok(src_fd) = src_fd.parse::<i32>() {
let io_mode = IoMode::fd(tgt_fd, src_fd);
redir = redir.with_io_mode(io_mode);
}
@@ -438,28 +415,6 @@ impl FromStr for RedirBldr {
}
}
impl TryFrom<Tk> for RedirBldr {
type Error = ShErr;
fn try_from(tk: Tk) -> Result<Self, Self::Error> {
let span = tk.span.clone();
if tk.flags.contains(TkFlags::IS_HEREDOC) {
let flags = tk.flags;
Ok(RedirBldr {
io_mode: Some(IoMode::buffer(0, tk.to_string(), flags)?),
class: Some(RedirType::HereDoc),
tgt_fd: Some(0),
span: Some(span)
})
} else {
match Self::from_str(tk.as_str()) {
Ok(bldr) => Ok(bldr.with_span(span)),
Err(e) => Err(e.promote(span)),
}
}
}
}
#[derive(PartialEq, Clone, Copy, Debug)]
pub enum RedirType {
Null, // Default
@@ -467,12 +422,9 @@ pub enum RedirType {
PipeAnd, // |&, redirs stderr and stdout
Input, // <
Output, // >
OutputForce,// >|
Append, // >>
HereDoc, // <<
IndentHereDoc, // <<-, strips leading tabs
HereString, // <<<
ReadWrite, // <>, fd is opened for reading and writing
}
#[derive(Clone, Debug)]
@@ -885,26 +837,13 @@ impl ParseStream {
let mut node_tks: Vec<Tk> = vec![];
let body;
// Two forms: "name()" as one token, or "name" followed by "()" as separate tokens
let spaced_form = !is_func_name(self.peek_tk())
&& self.peek_tk().is_some_and(|tk| tk.flags.contains(TkFlags::IS_CMD))
&& is_func_parens(self.tokens.get(1));
if !is_func_name(self.peek_tk()) && !spaced_form {
if !is_func_name(self.peek_tk()) {
return Ok(None);
}
let name_tk = self.next_tk().unwrap();
node_tks.push(name_tk.clone());
let name = name_tk.clone();
let name_raw = if spaced_form {
// Consume the "()" token
let parens_tk = self.next_tk().unwrap();
node_tks.push(parens_tk);
name.to_string()
} else {
name.to_string()
};
let name_raw = name.to_string();
let mut src = name_tk.span.span_source().clone();
src.rename(name_raw.clone());
let color = next_color();
@@ -1099,65 +1038,36 @@ impl ParseStream {
};
Ok(Some(node))
}
fn build_redir<F: FnMut() -> Option<Tk>>(
redir_tk: &Tk,
mut next: F,
node_tks: &mut Vec<Tk>,
context: LabelCtx,
) -> ShResult<Redir> {
let redir_bldr = RedirBldr::try_from(redir_tk.clone()).unwrap();
let next_tk = if redir_bldr.io_mode.is_none() { next() } else { None };
if redir_bldr.io_mode.is_some() {
return Ok(redir_bldr.build());
}
let Some(redir_type) = redir_bldr.class else {
return Err(parse_err_full(
"Malformed redirection operator",
&redir_tk.span,
context.clone(),
));
};
match redir_type {
RedirType::HereString => {
if next_tk.as_ref().is_none_or(|tk| tk.class == TkRule::EOI) {
return Err(ShErr::at(
ShErrKind::ParseErr,
next_tk.unwrap_or(redir_tk.clone()).span.clone(),
"Expected a string after this redirection",
));
}
let mut string = next_tk
.unwrap()
.expand()?
.get_words()
.join(" ");
string.push('\n');
let io_mode = IoMode::buffer(redir_bldr.tgt_fd.unwrap_or(0), string, redir_tk.flags)?;
Ok(redir_bldr.with_io_mode(io_mode).build())
}
_ => {
if next_tk.as_ref().is_none_or(|tk| tk.class == TkRule::EOI) {
return Err(ShErr::at(
ShErrKind::ParseErr,
redir_tk.span.clone(),
"Expected a filename after this redirection",
));
}
let path_tk = next_tk.unwrap();
node_tks.push(path_tk.clone());
let pathbuf = PathBuf::from(path_tk.span.as_str());
let io_mode = IoMode::file(redir_bldr.tgt_fd.unwrap(), pathbuf, redir_type);
Ok(redir_bldr.with_io_mode(io_mode).build())
}
}
}
fn parse_redir(&mut self, redirs: &mut Vec<Redir>, node_tks: &mut Vec<Tk>) -> ShResult<()> {
while self.check_redir() {
let tk = self.next_tk().unwrap();
node_tks.push(tk.clone());
let ctx = self.context.clone();
let redir = Self::build_redir(&tk, || self.next_tk(), node_tks, ctx)?;
redirs.push(redir);
let redir_bldr = tk.span.as_str().parse::<RedirBldr>().unwrap();
if redir_bldr.io_mode.is_none() {
let path_tk = self.next_tk();
if path_tk.clone().is_none_or(|tk| tk.class == TkRule::EOI) {
return Err(ShErr::at(
ShErrKind::ParseErr,
tk.span.clone(),
"Expected a filename after this redirection",
));
};
let path_tk = path_tk.unwrap();
node_tks.push(path_tk.clone());
let redir_class = redir_bldr.class.unwrap();
let pathbuf = PathBuf::from(path_tk.span.as_str());
let io_mode = IoMode::file(redir_bldr.tgt_fd.unwrap(), pathbuf, redir_class);
let redir_bldr = redir_bldr.with_io_mode(io_mode);
let redir = redir_bldr.build();
redirs.push(redir);
} else {
// io_mode is already set (e.g., for fd redirections like 2>&1)
let redir = redir_bldr.build();
redirs.push(redir);
}
}
Ok(())
}
@@ -1721,9 +1631,33 @@ impl ParseStream {
}
TkRule::Redir => {
node_tks.push(tk.clone());
let ctx = self.context.clone();
let redir = Self::build_redir(tk, || tk_iter.next().cloned(), &mut node_tks, ctx)?;
redirs.push(redir);
let redir_bldr = tk.span.as_str().parse::<RedirBldr>().unwrap();
if redir_bldr.io_mode.is_none() {
let path_tk = tk_iter.next();
if path_tk.is_none_or(|tk| tk.class == TkRule::EOI) {
self.panic_mode(&mut node_tks);
return Err(ShErr::at(
ShErrKind::ParseErr,
tk.span.clone(),
"Expected a filename after this redirection",
));
};
let path_tk = path_tk.unwrap();
node_tks.push(path_tk.clone());
let redir_class = redir_bldr.class.unwrap();
let pathbuf = PathBuf::from(path_tk.span.as_str());
let io_mode = IoMode::file(redir_bldr.tgt_fd.unwrap(), pathbuf, redir_class);
let redir_bldr = redir_bldr.with_io_mode(io_mode);
let redir = redir_bldr.build();
redirs.push(redir);
} else {
// io_mode is already set (e.g., for fd redirections like 2>&1)
let redir = redir_bldr.build();
redirs.push(redir);
}
}
_ => unimplemented!("Unexpected token rule `{:?}` in parse_cmd()", tk.class),
}
@@ -1882,36 +1816,13 @@ pub fn get_redir_file<P: AsRef<Path>>(class: RedirType, path: P) -> ShResult<Fil
let path = path.as_ref();
let result = match class {
RedirType::Input => OpenOptions::new().read(true).open(Path::new(&path)),
RedirType::Output => {
if read_shopts(|o| o.core.noclobber) && path.is_file() {
return Err(ShErr::simple(
ShErrKind::ExecFail,
format!("shopt core.noclobber is set, refusing to overwrite existing file `{}`", path.display()),
));
}
OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)
},
RedirType::ReadWrite => {
OpenOptions::new()
.write(true)
.read(true)
.create(true)
.truncate(false)
.open(path)
}
RedirType::OutputForce => {
OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path)
}
RedirType::Output => OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(path),
RedirType::Append => OpenOptions::new().create(true).append(true).open(path),
_ => unimplemented!("Unimplemented redir type: {:?}", class),
_ => unimplemented!(),
};
Ok(result?)
}
@@ -1935,12 +1846,6 @@ fn is_func_name(tk: Option<&Tk>) -> bool {
})
}
fn is_func_parens(tk: Option<&Tk>) -> bool {
tk.is_some_and(|tk| {
tk.flags.contains(TkFlags::KEYWORD) && tk.span.as_str() == "()"
})
}
/// Perform an operation on the child nodes of a given node
///
/// # Parameters
@@ -2689,247 +2594,4 @@ pub mod tests {
let input = "{ echo bar case foo in bar) echo fizz ;; buzz) echo buzz ;; esac }";
assert!(get_ast(input).is_err());
}
// ===================== Heredocs =====================
#[test]
fn parse_basic_heredoc() {
let input = "cat <<EOF\nhello world\nEOF";
let expected = &mut [NdKind::Conjunction, NdKind::Pipeline, NdKind::Command].into_iter();
let ast = get_ast(input).unwrap();
let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) {
panic!("{}", e);
}
}
#[test]
fn parse_heredoc_with_tab_strip() {
let input = "cat <<-EOF\n\t\thello\n\t\tworld\nEOF";
let expected = &mut [NdKind::Conjunction, NdKind::Pipeline, NdKind::Command].into_iter();
let ast = get_ast(input).unwrap();
let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) {
panic!("{}", e);
}
}
#[test]
fn parse_literal_heredoc() {
let input = "cat <<'EOF'\nhello $world\nEOF";
let expected = &mut [NdKind::Conjunction, NdKind::Pipeline, NdKind::Command].into_iter();
let ast = get_ast(input).unwrap();
let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) {
panic!("{}", e);
}
}
#[test]
fn parse_herestring() {
let input = "cat <<< \"hello world\"";
let expected = &mut [NdKind::Conjunction, NdKind::Pipeline, NdKind::Command].into_iter();
let ast = get_ast(input).unwrap();
let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) {
panic!("{}", e);
}
}
#[test]
fn parse_heredoc_in_pipeline() {
let input = "cat <<EOF | grep hello\nhello world\ngoodbye world\nEOF";
let expected = &mut [
NdKind::Conjunction,
NdKind::Pipeline,
NdKind::Command,
NdKind::Command,
]
.into_iter();
let ast = get_ast(input).unwrap();
let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) {
panic!("{}", e);
}
}
#[test]
fn parse_heredoc_in_conjunction() {
let input = "cat <<EOF && echo done\nhello\nEOF";
let expected = &mut [
NdKind::Conjunction,
NdKind::Pipeline,
NdKind::Command,
NdKind::Pipeline,
NdKind::Command,
]
.into_iter();
let ast = get_ast(input).unwrap();
let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) {
panic!("{}", e);
}
}
#[test]
fn parse_heredoc_double_quoted_delimiter() {
let input = "cat <<\"EOF\"\nhello $world\nEOF";
let expected = &mut [NdKind::Conjunction, NdKind::Pipeline, NdKind::Command].into_iter();
let ast = get_ast(input).unwrap();
let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) {
panic!("{}", e);
}
}
#[test]
fn parse_heredoc_empty_body() {
let input = "cat <<EOF\nEOF";
let expected = &mut [NdKind::Conjunction, NdKind::Pipeline, NdKind::Command].into_iter();
let ast = get_ast(input).unwrap();
let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) {
panic!("{}", e);
}
}
#[test]
fn parse_heredoc_multiword_delimiter() {
// delimiter should only be the first word
let input = "cat <<DELIM\nsome content\nDELIM";
let expected = &mut [NdKind::Conjunction, NdKind::Pipeline, NdKind::Command].into_iter();
let ast = get_ast(input).unwrap();
let mut node = ast[0].clone();
if let Err(e) = node.assert_structure(expected) {
panic!("{}", e);
}
}
#[test]
fn parse_two_heredocs_on_one_line() {
let input = "cat <<A; cat <<B\nfoo\nA\nbar\nB";
let ast = get_ast(input).unwrap();
assert_eq!(ast.len(), 2);
}
// ===================== Heredoc Execution =====================
use crate::testutil::{TestGuard, test_input};
use crate::state::{VarFlags, VarKind, write_vars};
#[test]
fn heredoc_basic_output() {
let guard = TestGuard::new();
test_input("cat <<EOF\nhello world\nEOF".to_string()).unwrap();
let out = guard.read_output();
assert_eq!(out, "hello world\n");
}
#[test]
fn heredoc_multiline_output() {
let guard = TestGuard::new();
test_input("cat <<EOF\nline one\nline two\nline three\nEOF".to_string()).unwrap();
let out = guard.read_output();
assert_eq!(out, "line one\nline two\nline three\n");
}
#[test]
fn heredoc_variable_expansion() {
let guard = TestGuard::new();
write_vars(|v| v.set_var("NAME", VarKind::Str("world".into()), VarFlags::NONE)).unwrap();
test_input("cat <<EOF\nhello $NAME\nEOF".to_string()).unwrap();
let out = guard.read_output();
assert_eq!(out, "hello world\n");
}
#[test]
fn heredoc_literal_no_expansion() {
let guard = TestGuard::new();
write_vars(|v| v.set_var("NAME", VarKind::Str("world".into()), VarFlags::NONE)).unwrap();
test_input("cat <<'EOF'\nhello $NAME\nEOF".to_string()).unwrap();
let out = guard.read_output();
assert_eq!(out, "hello $NAME\n");
}
#[test]
fn heredoc_tab_stripping() {
let guard = TestGuard::new();
test_input("cat <<-EOF\n\t\thello\n\t\tworld\nEOF".to_string()).unwrap();
let out = guard.read_output();
assert_eq!(out, "hello\nworld\n");
}
#[test]
fn heredoc_tab_stripping_uneven() {
let guard = TestGuard::new();
test_input("cat <<-EOF\n\t\t\thello\n\tworld\nEOF".to_string()).unwrap();
let out = guard.read_output();
assert_eq!(out, "\t\thello\nworld\n");
}
#[test]
fn heredoc_empty_body() {
let guard = TestGuard::new();
test_input("cat <<EOF\nEOF".to_string()).unwrap();
let out = guard.read_output();
assert_eq!(out, "");
}
#[test]
fn heredoc_in_pipeline() {
let guard = TestGuard::new();
test_input("cat <<EOF | grep hello\nhello world\ngoodbye world\nEOF".to_string()).unwrap();
let out = guard.read_output();
assert_eq!(out, "hello world\n");
}
#[test]
fn herestring_basic() {
let guard = TestGuard::new();
test_input("cat <<< \"hello world\"".to_string()).unwrap();
let out = guard.read_output();
assert_eq!(out, "hello world\n");
}
#[test]
fn herestring_variable_expansion() {
let guard = TestGuard::new();
write_vars(|v| v.set_var("MSG", VarKind::Str("hi there".into()), VarFlags::NONE)).unwrap();
test_input("cat <<< $MSG".to_string()).unwrap();
let out = guard.read_output();
assert_eq!(out, "hi there\n");
}
#[test]
fn heredoc_double_quoted_delimiter_is_literal() {
let guard = TestGuard::new();
write_vars(|v| v.set_var("X", VarKind::Str("val".into()), VarFlags::NONE)).unwrap();
test_input("cat <<\"EOF\"\nhello $X\nEOF".to_string()).unwrap();
let out = guard.read_output();
assert_eq!(out, "hello $X\n");
}
#[test]
fn heredoc_preserves_blank_lines() {
let guard = TestGuard::new();
test_input("cat <<EOF\nfirst\n\nsecond\nEOF".to_string()).unwrap();
let out = guard.read_output();
assert_eq!(out, "first\n\nsecond\n");
}
#[test]
fn heredoc_tab_strip_preserves_empty_lines() {
let guard = TestGuard::new();
test_input("cat <<-EOF\n\thello\n\n\tworld\nEOF".to_string()).unwrap();
let out = guard.read_output();
assert_eq!(out, "hello\n\nworld\n");
}
#[test]
fn heredoc_two_on_one_line() {
let guard = TestGuard::new();
test_input("cat <<A; cat <<B\nfoo\nA\nbar\nB".to_string()).unwrap();
let out = guard.read_output();
assert_eq!(out, "foo\nbar\n");
}
}

View File

@@ -19,7 +19,7 @@ pub use std::os::unix::io::{AsRawFd, BorrowedFd, FromRawFd, IntoRawFd, OwnedFd,
pub use bitflags::bitflags;
pub use nix::{
errno::Errno,
fcntl::{FcntlArg, OFlag, fcntl, open},
fcntl::{OFlag, open},
libc::{self, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO},
sys::{
signal::{self, SigHandler, SigSet, SigmaskHow, Signal, kill, killpg, pthread_sigmask, signal},

View File

@@ -8,26 +8,15 @@ use crate::{
expand::Expander,
libsh::{
error::{ShErr, ShErrKind, ShResult},
sys::TTY_FILENO,
utils::RedirVecUtils,
},
parse::{Redir, RedirType, get_redir_file, lex::TkFlags},
prelude::*, state,
parse::{Redir, RedirType, get_redir_file},
prelude::*,
};
// Credit to fish-shell for many of the implementation ideas present in this
// module https://fishshell.com/
/// Minimum fd number for shell-internal file descriptors.
/// User-visible fds (0-9) are kept clear so `exec 3>&-` etc. work as expected.
const MIN_INTERNAL_FD: RawFd = 10;
/// Like `dup()`, but places the new fd at `MIN_INTERNAL_FD` or above so it
/// doesn't collide with user-managed fds.
fn dup_high(fd: RawFd) -> nix::Result<RawFd> {
fcntl(fd, FcntlArg::F_DUPFD_CLOEXEC(MIN_INTERNAL_FD))
}
#[derive(Clone, Debug)]
pub enum IoMode {
Fd {
@@ -48,9 +37,8 @@ pub enum IoMode {
pipe: Arc<OwnedFd>,
},
Buffer {
tgt_fd: RawFd,
buf: String,
flags: TkFlags, // so we can see if its a heredoc or not
pipe: Arc<OwnedFd>,
},
Close {
tgt_fd: RawFd,
@@ -91,28 +79,19 @@ impl IoMode {
if let IoMode::File { tgt_fd, path, mode } = self {
let path_raw = path.as_os_str().to_str().unwrap_or_default().to_string();
let expanded_path = Expander::from_raw(&path_raw, TkFlags::empty())?.expand()?.join(" "); // should just be one string, will have to find some way to handle a return of multiple paths
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
let expanded_pathbuf = PathBuf::from(expanded_path);
let file = get_redir_file(mode, expanded_pathbuf)?;
// Move the opened fd above the user-accessible range so it never
// collides with the target fd (e.g. `3>/tmp/foo` where open() returns 3,
// causing dup2(3,3) to be a no-op and then OwnedFd drop closes it).
let raw = file.as_raw_fd();
let high = fcntl(raw, FcntlArg::F_DUPFD_CLOEXEC(MIN_INTERNAL_FD))
.map_err(ShErr::from)?;
drop(file); // closes the original low fd
self = IoMode::OpenedFile {
tgt_fd,
file: Arc::new(unsafe { OwnedFd::from_raw_fd(high) }),
file: Arc::new(OwnedFd::from(file)),
}
}
Ok(self)
}
pub fn buffer(tgt_fd: RawFd, buf: String, flags: TkFlags) -> ShResult<Self> {
Ok(Self::Buffer { tgt_fd, buf, flags })
}
pub fn get_pipes() -> (Self, Self) {
let (rpipe, wpipe) = nix::unistd::pipe2(OFlag::O_CLOEXEC).unwrap();
(
@@ -227,102 +206,23 @@ impl<'e> IoFrame {
)
}
pub fn save(&'e mut self) {
let saved_in = dup_high(STDIN_FILENO).unwrap();
let saved_out = dup_high(STDOUT_FILENO).unwrap();
let saved_err = dup_high(STDERR_FILENO).unwrap();
let saved_in = dup(STDIN_FILENO).unwrap();
let saved_out = dup(STDOUT_FILENO).unwrap();
let saved_err = dup(STDERR_FILENO).unwrap();
self.saved_io = Some(IoGroup(saved_in, saved_out, saved_err));
}
pub fn redirect(mut self) -> ShResult<RedirGuard> {
self.save();
if let Err(e) = self.apply_redirs() {
// Restore saved fds before propagating the error so they don't leak.
self.restore().ok();
return Err(e);
}
Ok(RedirGuard::new(self))
}
fn apply_redirs(&mut self) -> ShResult<()> {
for redir in &mut self.redirs {
let io_mode = &mut redir.io_mode;
match io_mode {
IoMode::Close { tgt_fd } => {
if *tgt_fd == *TTY_FILENO {
// Don't let user close the shell's tty fd.
continue;
}
close(*tgt_fd).ok();
continue;
}
IoMode::File { .. } => {
match io_mode.clone().open_file() {
Ok(file) => *io_mode = file,
Err(e) => {
if let Some(span) = redir.span.as_ref() {
return Err(e.promote(span.clone()));
}
return Err(e)
}
}
}
IoMode::Buffer { tgt_fd, buf, flags } => {
let (rpipe, wpipe) = nix::unistd::pipe()?;
let mut text = if flags.contains(TkFlags::LIT_HEREDOC) {
buf.clone()
} else {
let words = Expander::from_raw(buf, *flags)?.expand()?;
if flags.contains(TkFlags::IS_HEREDOC) {
words.into_iter().next().unwrap_or_default()
} else {
let ifs = state::get_separator();
words.join(&ifs).trim().to_string() + "\n"
}
};
if flags.contains(TkFlags::TAB_HEREDOC) {
let lines = text.lines();
let mut min_tabs = usize::MAX;
for line in lines {
if line.is_empty() { continue; }
let line_len = line.len();
let after_strip = line.trim_start_matches('\t').len();
let delta = line_len - after_strip;
min_tabs = min_tabs.min(delta);
}
if min_tabs == usize::MAX {
// let's avoid possibly allocating a string with 18 quintillion tabs
min_tabs = 0;
}
if min_tabs > 0 {
let stripped = text.lines()
.fold(vec![], |mut acc, ln| {
if ln.is_empty() {
acc.push("");
return acc;
}
let stripped_ln = ln.strip_prefix(&"\t".repeat(min_tabs)).unwrap();
acc.push(stripped_ln);
acc
})
.join("\n");
text = stripped + "\n";
}
}
write(wpipe, text.as_bytes())?;
*io_mode = IoMode::Pipe { tgt_fd: *tgt_fd, pipe: rpipe.into() };
}
_ => {}
}
if let IoMode::File { .. } = io_mode {
*io_mode = io_mode.clone().open_file()?;
};
let tgt_fd = io_mode.tgt_fd();
let src_fd = io_mode.src_fd();
if let Err(e) = dup2(src_fd, tgt_fd) {
if let Some(span) = redir.span.as_ref() {
return Err(ShErr::from(e).promote(span.clone()));
} else {
return Err(e.into());
}
}
dup2(src_fd, tgt_fd)?;
}
Ok(())
Ok(RedirGuard::new(self))
}
pub fn restore(&mut self) -> ShResult<()> {
if let Some(saved) = self.saved_io.take() {
@@ -434,8 +334,6 @@ pub fn borrow_fd<'f>(fd: i32) -> BorrowedFd<'f> {
}
type PipeFrames = Map<PipeGenerator, fn((Option<Redir>, Option<Redir>)) -> IoFrame>;
/// An iterator that lazily creates a specific number of pipes.
pub struct PipeGenerator {
num_cmds: usize,
cursor: usize,

View File

@@ -1430,8 +1430,6 @@ pub fn annotate_input(input: &str) -> String {
for tk in tokens.into_iter().rev() {
let insertions = annotate_token(tk);
for (pos, marker) in insertions {
log::info!("pos: {pos}, marker: {marker:?}");
log::info!("before: {annotated:?}");
let pos = pos.max(0).min(annotated.len());
annotated.insert(pos, marker);
}
@@ -1613,12 +1611,6 @@ pub fn annotate_token(token: Tk) -> Vec<(usize, Marker)> {
let mut insertions: Vec<(usize, Marker)> = vec![];
// Heredoc tokens have spans covering the body content far from the <<
// operator, which breaks position tracking after marker insertions
if token.flags.contains(TkFlags::IS_HEREDOC) {
return insertions;
}
if token.class != TkRule::Str
&& let Some(marker) = marker_for(&token.class)
{

View File

@@ -2,15 +2,10 @@
use std::os::fd::AsRawFd;
use crate::{
readline::{Prompt, ShedVi, annotate_input},
readline::{Prompt, ShedVi},
testutil::TestGuard,
};
fn assert_annotated(input: &str, expected: &str) {
let result = annotate_input(input);
assert_eq!(result, expected, "\nInput: {input:?}");
}
/// Tests for our vim logic emulation. Each test consists of an initial text, a sequence of keys to feed, and the expected final text and cursor position.
macro_rules! vi_test {
{ $($name:ident: $input:expr => $op:expr => $expected_text:expr,$expected_cursor:expr);* } => {
@@ -31,202 +26,6 @@ macro_rules! vi_test {
};
}
// ===================== Annotation Tests =====================
#[test]
fn annotate_simple_command() {
assert_annotated("echo hello",
"\u{e101}echo\u{e11a} \u{e102}hello\u{e11a}");
}
#[test]
fn annotate_pipeline() {
assert_annotated("ls | grep foo",
"\u{e100}ls\u{e11a} \u{e104}|\u{e11a} \u{e100}grep\u{e11a} \u{e102}foo\u{e11a}");
}
#[test]
fn annotate_conjunction() {
assert_annotated("echo foo && echo bar",
"\u{e101}echo\u{e11a} \u{e102}foo\u{e11a} \u{e104}&&\u{e11a} \u{e101}echo\u{e11a} \u{e102}bar\u{e11a}");
}
#[test]
fn annotate_redirect_output() {
assert_annotated("echo hello > file.txt",
"\u{e101}echo\u{e11a} \u{e102}hello\u{e11a} \u{e105}>\u{e11a} \u{e102}file.txt\u{e11a}");
}
#[test]
fn annotate_redirect_append() {
assert_annotated("echo hello >> file.txt",
"\u{e101}echo\u{e11a} \u{e102}hello\u{e11a} \u{e105}>>\u{e11a} \u{e102}file.txt\u{e11a}");
}
#[test]
fn annotate_redirect_input() {
assert_annotated("cat < file.txt",
"\u{e100}cat\u{e11a} \u{e105}<\u{e11a} \u{e102}file.txt\u{e11a}");
}
#[test]
fn annotate_fd_redirect() {
assert_annotated("cmd 2>&1",
"\u{e100}cmd\u{e11a} \u{e105}2>&1\u{e11a}");
}
#[test]
fn annotate_variable_sub() {
assert_annotated("echo $HOME",
"\u{e101}echo\u{e11a} \u{e102}\u{e10c}$HOME\u{e10d}\u{e11a}");
}
#[test]
fn annotate_variable_brace_sub() {
assert_annotated("echo ${HOME}",
"\u{e101}echo\u{e11a} \u{e102}\u{e10c}${HOME}\u{e10d}\u{e11a}");
}
#[test]
fn annotate_command_sub() {
assert_annotated("echo $(ls)",
"\u{e101}echo\u{e11a} \u{e102}\u{e10e}$(ls)\u{e10f}\u{e11a}");
}
#[test]
fn annotate_single_quoted_string() {
assert_annotated("echo 'hello world'",
"\u{e101}echo\u{e11a} \u{e102}\u{e114}'hello world'\u{e115}\u{e11a}");
}
#[test]
fn annotate_double_quoted_string() {
assert_annotated("echo \"hello world\"",
"\u{e101}echo\u{e11a} \u{e102}\u{e112}\"hello world\"\u{e113}\u{e11a}");
}
#[test]
fn annotate_assignment() {
assert_annotated("FOO=bar",
"\u{e107}FOO=bar\u{e11a}");
}
#[test]
fn annotate_assignment_with_command() {
assert_annotated("FOO=bar echo hello",
"\u{e107}FOO=bar\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}");
}
#[test]
fn annotate_if_statement() {
assert_annotated("if true; then echo yes; fi",
"\u{e103}if\u{e11a} \u{e101}true\u{e11a}\u{e108}; \u{e11a}\u{e103}then\u{e11a} \u{e101}echo\u{e11a} \u{e102}yes\u{e11a}\u{e108}; \u{e11a}\u{e103}fi\u{e11a}");
}
#[test]
fn annotate_for_loop() {
assert_annotated("for i in a b c; do echo $i; done",
"\u{e103}for\u{e11a} \u{e102}i\u{e11a} \u{e103}in\u{e11a} \u{e102}a\u{e11a} \u{e102}b\u{e11a} \u{e102}c\u{e11a}\u{e108}; \u{e11a}\u{e103}do\u{e11a} \u{e101}echo\u{e11a} \u{e102}\u{e10c}$i\u{e10d}\u{e11a}\u{e108}; \u{e11a}\u{e103}done\u{e11a}");
}
#[test]
fn annotate_while_loop() {
assert_annotated("while true; do echo hello; done",
"\u{e103}while\u{e11a} \u{e101}true\u{e11a}\u{e108}; \u{e11a}\u{e103}do\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}\u{e108}; \u{e11a}\u{e103}done\u{e11a}");
}
#[test]
fn annotate_case_statement() {
assert_annotated("case foo in bar) echo bar;; esac",
"\u{e103}case\u{e11a} \u{e102}foo\u{e11a} \u{e103}in\u{e11a} \u{e104}bar\u{e109})\u{e11a} \u{e101}echo\u{e11a} \u{e102}bar\u{e11a}\u{e108};; \u{e11a}\u{e103}esac\u{e11a}");
}
#[test]
fn annotate_brace_group() {
assert_annotated("{ echo hello; }",
"\u{e104}{\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}\u{e108}; \u{e11a}\u{e104}}\u{e11a}");
}
#[test]
fn annotate_comment() {
assert_annotated("echo hello # this is a comment",
"\u{e101}echo\u{e11a} \u{e102}hello\u{e11a} \u{e106}# this is a comment\u{e11a}");
}
#[test]
fn annotate_semicolon_sep() {
assert_annotated("echo foo; echo bar",
"\u{e101}echo\u{e11a} \u{e102}foo\u{e11a}\u{e108}; \u{e11a}\u{e101}echo\u{e11a} \u{e102}bar\u{e11a}");
}
#[test]
fn annotate_escaped_char() {
assert_annotated("echo hello\\ world",
"\u{e101}echo\u{e11a} \u{e102}hello\\ world\u{e11a}");
}
#[test]
fn annotate_glob() {
assert_annotated("ls *.txt",
"\u{e100}ls\u{e11a} \u{e102}\u{e117}*\u{e11a}.txt\u{e11a}");
}
#[test]
fn annotate_heredoc_operator() {
assert_annotated("cat <<EOF",
"\u{e100}cat\u{e11a} \u{e105}<<\u{e11a}\u{e102}EOF\u{e11a}");
}
#[test]
fn annotate_herestring_operator() {
assert_annotated("cat <<< hello",
"\u{e100}cat\u{e11a} \u{e105}<<<\u{e11a} \u{e102}hello\u{e11a}");
}
#[test]
fn annotate_nested_command_sub() {
assert_annotated("echo $(echo $(ls))",
"\u{e101}echo\u{e11a} \u{e102}\u{e10e}$(echo $(ls))\u{e10f}\u{e11a}");
}
#[test]
fn annotate_var_in_double_quotes() {
assert_annotated("echo \"hello $USER\"",
"\u{e101}echo\u{e11a} \u{e102}\u{e112}\"hello \u{e10c}$USER\u{e10d}\"\u{e113}\u{e11a}");
}
#[test]
fn annotate_func_def() {
assert_annotated("foo() { echo hello; }",
"\u{e103}foo()\u{e11a} \u{e104}{\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}\u{e108}; \u{e11a}\u{e104}}\u{e11a}");
}
#[test]
fn annotate_negate() {
assert_annotated("! echo hello",
"\u{e104}!\u{e11a} \u{e101}echo\u{e11a} \u{e102}hello\u{e11a}");
}
#[test]
fn annotate_or_conjunction() {
assert_annotated("false || echo fallback",
"\u{e101}false\u{e11a} \u{e104}||\u{e11a} \u{e101}echo\u{e11a} \u{e102}fallback\u{e11a}");
}
#[test]
fn annotate_complex_pipeline() {
assert_annotated("cat file.txt | grep pattern | wc -l",
"\u{e100}cat\u{e11a} \u{e102}file.txt\u{e11a} \u{e104}|\u{e11a} \u{e100}grep\u{e11a} \u{e102}pattern\u{e11a} \u{e104}|\u{e11a} \u{e100}wc\u{e11a} \u{e102}-l\u{e11a}");
}
#[test]
fn annotate_multiple_redirects() {
assert_annotated("cmd > out.txt 2> err.txt",
"\u{e100}cmd\u{e11a} \u{e105}>\u{e11a} \u{e102}out.txt\u{e11a} \u{e105}2>\u{e11a} \u{e102}err.txt\u{e11a}");
}
// ===================== Vi Tests =====================
fn test_vi(initial: &str) -> (ShedVi, TestGuard) {
let g = TestGuard::new();
let prompt = Prompt::default();

View File

@@ -146,7 +146,6 @@ pub struct ShOptCore {
pub bell_enabled: bool,
pub max_recurse_depth: usize,
pub xpg_echo: bool,
pub noclobber: bool,
}
impl ShOptCore {
@@ -239,15 +238,6 @@ impl ShOptCore {
};
self.xpg_echo = val;
}
"noclobber" => {
let Ok(val) = val.parse::<bool>() else {
return Err(ShErr::simple(
ShErrKind::SyntaxErr,
"shopt: expected 'true' or 'false' for noclobber value",
));
};
self.noclobber = val;
}
_ => {
return Err(ShErr::simple(
ShErrKind::SyntaxErr,
@@ -314,11 +304,6 @@ impl ShOptCore {
output.push_str(&format!("{}", self.xpg_echo));
Ok(Some(output))
}
"noclobber" => {
let mut output = String::from("Prevent > from overwriting existing files (use >| to override)\n");
output.push_str(&format!("{}", self.noclobber));
Ok(Some(output))
}
_ => Err(ShErr::simple(
ShErrKind::SyntaxErr,
format!("shopt: Unexpected 'core' option '{query}'"),
@@ -342,7 +327,6 @@ impl Display for ShOptCore {
output.push(format!("bell_enabled = {}", self.bell_enabled));
output.push(format!("max_recurse_depth = {}", self.max_recurse_depth));
output.push(format!("xpg_echo = {}", self.xpg_echo));
output.push(format!("noclobber = {}", self.noclobber));
let final_output = output.join("\n");
@@ -362,7 +346,6 @@ impl Default for ShOptCore {
bell_enabled: true,
max_recurse_depth: 1000,
xpg_echo: false,
noclobber: false,
}
}
}
@@ -606,7 +589,6 @@ mod tests {
bell_enabled,
max_recurse_depth,
xpg_echo,
noclobber,
} = ShOptCore::default();
// If a field is added to the struct, this destructure fails to compile.
let _ = (
@@ -619,7 +601,6 @@ mod tests {
bell_enabled,
max_recurse_depth,
xpg_echo,
noclobber,
);
}

View File

@@ -1330,15 +1330,6 @@ impl VarTab {
.get(&ShellParam::Status)
.map(|s| s.to_string())
.unwrap_or("0".into()),
ShellParam::AllArgsStr => {
let ifs = get_separator();
self
.params
.get(&ShellParam::AllArgs)
.map(|s| s.replace(markers::ARG_SEP, &ifs).to_string())
.unwrap_or_default()
}
_ => self
.params
.get(&param)
@@ -1851,15 +1842,6 @@ pub fn change_dir<P: AsRef<Path>>(dir: P) -> ShResult<()> {
Ok(())
}
pub fn get_separator() -> String {
env::var("IFS")
.unwrap_or(String::from(" "))
.chars()
.next()
.unwrap()
.to_string()
}
pub fn get_status() -> i32 {
read_vars(|v| v.get_param(ShellParam::Status))
.parse::<i32>()