implemented '<>' redirects, and the 'seek' builtin

'seek' is a wrapper around the lseek() syscall

added noclobber to core shopts and implemented '>|' redirection syntax

properly implemented fd close syntax

fixed saved fds being leaked into exec'd programs
This commit is contained in:
2026-03-14 20:04:20 -04:00
parent 5173e1908d
commit 9bd9c66b92
11 changed files with 576 additions and 162 deletions

View File

@@ -24,13 +24,14 @@ pub mod source;
pub mod test; // [[ ]] thing
pub mod trap;
pub mod varcmds;
pub mod seek;
pub const BUILTINS: [&str; 49] = [
pub const BUILTINS: [&str; 50] = [
"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",
"getopts", "keymap", "read_key", "autocmd", "ulimit", "umask", "seek"
];
pub fn true_builtin() -> ShResult<()> {

253
src/builtin/seek.rs Normal file
View File

@@ -0,0 +1,253 @@
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

@@ -201,6 +201,7 @@ 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;
@@ -208,6 +209,8 @@ 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,11 +147,9 @@ 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)
.expect("Failed to restore cooked mode");
tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &orig).ok();
let res = f();
tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &current)
.expect("Failed to restore raw mode");
tcsetattr(borrow_fd(*TTY_FILENO), termios::SetArg::TCSANOW, &current).ok();
res
}
}

View File

@@ -2,6 +2,14 @@ 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(|| {
open("/dev/tty", OFlag::O_RDWR, Mode::empty()).expect("Failed to open /dev/tty")
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
});

View File

@@ -8,28 +8,7 @@ use ariadne::Fmt;
use crate::{
builtin::{
alias::{alias, unalias},
arrops::{arr_fpop, arr_fpush, arr_pop, arr_push, arr_rotate},
autocmd::autocmd,
cd::cd,
complete::{compgen_builtin, complete_builtin},
dirstack::{dirs, popd, pushd},
echo::echo,
eval, exec,
flowctl::flowctl,
getopts::getopts,
intro,
jobctl::{self, JobBehavior, continue_job, disown, jobs},
keymap, map,
pwd::pwd,
read::{self, read_builtin},
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, 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}
},
expand::{expand_aliases, expand_case_pattern, glob_to_regex},
jobs::{ChildProc, JobStack, attach_tty, dispatch_job},
@@ -888,7 +867,10 @@ impl Dispatcher {
if fork_builtins {
log::trace!("Forking builtin: {}", cmd_raw);
let _guard = self.io_stack.pop_frame().redirect()?;
let guard = self.io_stack.pop_frame().redirect()?;
if cmd_raw.as_str() == "exec" {
guard.persist();
}
self.run_fork(&cmd_raw, |s| {
if let Err(e) = s.dispatch_builtin(cmd) {
e.print_error();
@@ -1013,6 +995,7 @@ impl Dispatcher {
"autocmd" => autocmd(cmd),
"ulimit" => ulimit(cmd),
"umask" => umask_builtin(cmd),
"seek" => seek(cmd),
"true" | ":" => {
state::set_status(0);
Ok(())

View File

@@ -411,20 +411,38 @@ 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;
}
if let Some('&') = chars.peek() {
let Some('&') = chars.peek() 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;
@@ -438,10 +456,6 @@ impl LexStream {
tk = self.get_token(self.cursor..pos, TkRule::Redir);
break;
}
} else {
tk = self.get_token(self.cursor..pos, TkRule::Redir);
break;
}
}
'<' => {
if chars.peek() == Some(&'(') {
@@ -449,7 +463,8 @@ impl LexStream {
}
pos += 1;
if let Some('<') = chars.peek() {
match chars.peek() {
Some('<') => {
chars.next();
pos += 1;
@@ -497,6 +512,44 @@ impl LexStream {
}
}
}
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;
}
}
_ => {}
}
tk = self.get_token(self.cursor..pos, TkRule::Redir);
break;
@@ -1049,11 +1102,10 @@ impl Iterator for LexStream {
// 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' {
if let Some(skip) = self.heredoc_skip.take() {
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 {

View File

@@ -12,7 +12,7 @@ use crate::{
},
parse::lex::clean_input,
prelude::*,
procio::IoMode,
procio::IoMode, state::read_shopts,
};
pub mod execute;
@@ -280,11 +280,16 @@ 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 }
Self { io_mode, class, span: None }
}
pub fn with_span(mut self, span: Span) -> Self {
self.span = Some(span);
self
}
}
@@ -293,6 +298,7 @@ pub struct RedirBldr {
pub io_mode: Option<IoMode>,
pub class: Option<RedirType>,
pub tgt_fd: Option<RawFd>,
pub span: Option<Span>,
}
impl RedirBldr {
@@ -300,43 +306,36 @@ impl RedirBldr {
Default::default()
}
pub fn with_io_mode(self, io_mode: IoMode) -> Self {
let Self {
io_mode: _,
class,
tgt_fd,
} = self;
Self {
io_mode: Some(io_mode),
class,
tgt_fd,
..self
}
}
pub fn with_class(self, class: RedirType) -> Self {
let Self {
io_mode,
class: _,
tgt_fd,
} = self;
Self {
io_mode,
class: Some(class),
tgt_fd,
..self
}
}
pub fn with_tgt(self, tgt_fd: RawFd) -> Self {
let Self {
io_mode,
class,
tgt_fd: _,
} = self;
Self {
io_mode,
class,
tgt_fd: Some(tgt_fd),
..self
}
}
pub fn with_span(self, span: Span) -> Self {
Self {
span: Some(span),
..self
}
}
pub fn build(self) -> Redir {
Redir::new(self.io_mode.unwrap(), self.class.unwrap())
let new = Redir::new(self.io_mode.unwrap(), self.class.unwrap());
if let Some(span) = self.span {
new.with_span(span)
} else {
new
}
}
}
@@ -355,16 +354,24 @@ 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;
}
}
redir = match count {
1 => redir.with_class(RedirType::HereDoc),
@@ -373,6 +380,10 @@ 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)
@@ -380,6 +391,7 @@ impl FromStr for RedirBldr {
break;
}
}
}
if src_fd.is_empty() {
return Err(ShErr::simple(
ShErrKind::ParseErr,
@@ -405,15 +417,20 @@ impl FromStr for RedirBldr {
}
}
// 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::HereDoc | RedirType::HereString => 0,
RedirType::Input |
RedirType::ReadWrite |
RedirType::HereDoc |
RedirType::HereString => 0,
_ => 1,
});
redir = redir.with_tgt(tgt_fd);
if let Ok(src_fd) = src_fd.parse::<i32>() {
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>() {
let io_mode = IoMode::fd(tgt_fd, src_fd);
redir = redir.with_io_mode(io_mode);
}
@@ -424,6 +441,7 @@ 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;
let mut heredoc_body = if flags.contains(TkFlags::LIT_HEREDOC) {
@@ -466,10 +484,14 @@ impl TryFrom<Tk> for RedirBldr {
Ok(RedirBldr {
io_mode: Some(IoMode::loaded_pipe(0, heredoc_body.as_bytes())?),
class: Some(RedirType::HereDoc),
tgt_fd: Some(0)
tgt_fd: Some(0),
span: Some(span)
})
} else {
Self::from_str(tk.as_str())
match Self::from_str(tk.as_str()) {
Ok(bldr) => Ok(bldr.with_span(span)),
Err(e) => Err(e.promote(span)),
}
}
}
}
@@ -481,10 +503,12 @@ 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)]
@@ -1881,11 +1905,34 @@ 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 => OpenOptions::new()
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),
.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::Append => OpenOptions::new().create(true).append(true).open(path),
_ => unimplemented!("Unimplemented redir type: {:?}", class),
};

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::{OFlag, open},
fcntl::{FcntlArg, OFlag, fcntl, open},
libc::{self, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO},
sys::{
signal::{self, SigHandler, SigSet, SigmaskHow, Signal, kill, killpg, pthread_sigmask, signal},

View File

@@ -8,6 +8,7 @@ use crate::{
expand::Expander,
libsh::{
error::{ShErr, ShErrKind, ShResult},
sys::TTY_FILENO,
utils::RedirVecUtils,
},
parse::{Redir, RedirType, get_redir_file, lex::TkFlags},
@@ -17,6 +18,16 @@ use crate::{
// 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 {
@@ -84,9 +95,16 @@ impl IoMode {
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(OwnedFd::from(file)),
file: Arc::new(unsafe { OwnedFd::from_raw_fd(high) }),
}
}
Ok(self)
@@ -210,23 +228,53 @@ impl<'e> IoFrame {
)
}
pub fn save(&'e mut self) {
let saved_in = dup(STDIN_FILENO).unwrap();
let saved_out = dup(STDOUT_FILENO).unwrap();
let saved_err = dup(STDERR_FILENO).unwrap();
let saved_in = dup_high(STDIN_FILENO).unwrap();
let saved_out = dup_high(STDOUT_FILENO).unwrap();
let saved_err = dup_high(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;
if let IoMode::Close { tgt_fd } = io_mode {
if *tgt_fd == *TTY_FILENO {
// Don't let user close the shell's tty fd.
continue;
}
close(*tgt_fd).ok();
continue;
}
if let IoMode::File { .. } = io_mode {
*io_mode = io_mode.clone().open_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)
}
}
};
let tgt_fd = io_mode.tgt_fd();
let src_fd = io_mode.src_fd();
dup2(src_fd, tgt_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());
}
Ok(RedirGuard::new(self))
}
}
Ok(())
}
pub fn restore(&mut self) -> ShResult<()> {
if let Some(saved) = self.saved_io.take() {
@@ -338,6 +386,8 @@ 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

@@ -146,6 +146,7 @@ pub struct ShOptCore {
pub bell_enabled: bool,
pub max_recurse_depth: usize,
pub xpg_echo: bool,
pub noclobber: bool,
}
impl ShOptCore {
@@ -238,6 +239,15 @@ 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,
@@ -304,6 +314,11 @@ 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}'"),
@@ -327,6 +342,7 @@ 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");
@@ -346,6 +362,7 @@ impl Default for ShOptCore {
bell_enabled: true,
max_recurse_depth: 1000,
xpg_echo: false,
noclobber: false,
}
}
}
@@ -589,6 +606,7 @@ 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 _ = (
@@ -601,6 +619,7 @@ mod tests {
bell_enabled,
max_recurse_depth,
xpg_echo,
noclobber,
);
}