diff --git a/src/builtin/alias.rs b/src/builtin/alias.rs index 2dd3d75..2c270d3 100644 --- a/src/builtin/alias.rs +++ b/src/builtin/alias.rs @@ -18,7 +18,7 @@ pub fn alias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult< unreachable!() }; - let (argv, io_frame) = 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() { // Display the environment variables @@ -54,7 +54,6 @@ pub fn alias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult< write_logic(|l| l.insert_alias(name, body)); } } - io_frame.unwrap().restore()?; state::set_status(0); Ok(()) } @@ -68,7 +67,7 @@ pub fn unalias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResul unreachable!() }; - let (argv, io_frame) = 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() { // Display the environment variables @@ -97,7 +96,6 @@ pub fn unalias(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResul write_logic(|l| l.remove_alias(&arg)) } } - io_frame.unwrap().restore()?; state::set_status(0); Ok(()) } diff --git a/src/builtin/echo.rs b/src/builtin/echo.rs index 24b4866..2a230a7 100644 --- a/src/builtin/echo.rs +++ b/src/builtin/echo.rs @@ -1,18 +1,15 @@ use std::sync::LazyLock; use crate::{ - builtin::setup_builtin, expand::expand_prompt, getopt::{Opt, OptSet, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node}, prelude::*, procio::{IoStack, borrow_fd}, state + builtin::setup_builtin, expand::expand_prompt, getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node}, prelude::*, procio::{IoStack, borrow_fd}, state }; -pub static ECHO_OPTS: LazyLock = LazyLock::new(|| { - [ - Opt::Short('n'), - Opt::Short('E'), - Opt::Short('e'), - Opt::Short('p'), - ] - .into() -}); +pub const ECHO_OPTS: [OptSpec;4] = [ + OptSpec { opt: Opt::Short('n'), takes_arg: false }, + OptSpec { opt: Opt::Short('E'), takes_arg: false }, + OptSpec { opt: Opt::Short('e'), takes_arg: false }, + OptSpec { opt: Opt::Short('p'), takes_arg: false }, +]; bitflags! { pub struct EchoFlags: u32 { @@ -33,9 +30,9 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<( unreachable!() }; assert!(!argv.is_empty()); - let (argv, opts) = get_opts_from_tokens(argv); + let (argv, opts) = get_opts_from_tokens(argv, &ECHO_OPTS); let flags = get_echo_flags(opts).blame(blame)?; - let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; let output_channel = if flags.contains(EchoFlags::USE_STDERR) { borrow_fd(STDERR_FILENO) @@ -57,7 +54,6 @@ pub fn echo(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<( write(output_channel, echo_output.as_bytes())?; - io_frame.unwrap().restore()?; state::set_status(0); Ok(()) } @@ -178,24 +174,21 @@ pub fn prepare_echo_args(argv: Vec, use_escape: bool, use_prompt: bool) Ok(prepared_args) } -pub fn get_echo_flags(mut opts: Vec) -> ShResult { +pub fn get_echo_flags(opts: Vec) -> ShResult { let mut flags = EchoFlags::empty(); - while let Some(opt) = opts.pop() { - if !ECHO_OPTS.contains(&opt) { - return Err(ShErr::simple( - ShErrKind::ExecFail, - format!("echo: Unexpected flag '{opt}'"), - )); - } - let Opt::Short(opt) = opt else { unreachable!() }; - + for opt in opts { match opt { - 'n' => flags |= EchoFlags::NO_NEWLINE, - 'r' => flags |= EchoFlags::USE_STDERR, - 'e' => flags |= EchoFlags::USE_ESCAPE, - 'p' => flags |= EchoFlags::USE_PROMPT, - _ => unreachable!(), + Opt::Short('n') => flags |= EchoFlags::NO_NEWLINE, + Opt::Short('r') => flags |= EchoFlags::USE_STDERR, + Opt::Short('e') => flags |= EchoFlags::USE_ESCAPE, + Opt::Short('p') => flags |= EchoFlags::USE_PROMPT, + _ => { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("echo: Unexpected flag '{opt}'"), + )); + } } } diff --git a/src/builtin/export.rs b/src/builtin/export.rs index c2b398f..8030f6d 100644 --- a/src/builtin/export.rs +++ b/src/builtin/export.rs @@ -18,7 +18,7 @@ pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult unreachable!() }; - let (argv, io_frame) = 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() { // Display the environment variables @@ -42,7 +42,6 @@ pub fn export(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult } } } - io_frame.unwrap().restore()?; state::set_status(0); Ok(()) } diff --git a/src/builtin/jobctl.rs b/src/builtin/jobctl.rs index c7a647e..b18c496 100644 --- a/src/builtin/jobctl.rs +++ b/src/builtin/jobctl.rs @@ -143,7 +143,7 @@ pub fn jobs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<( unreachable!() }; - let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; let mut flags = JobCmdFlags::empty(); for (arg, span) in argv { @@ -175,7 +175,6 @@ pub fn jobs(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<( } } write_jobs(|j| j.print_jobs(flags))?; - io_frame.unwrap().restore()?; state::set_status(0); Ok(()) diff --git a/src/builtin/mod.rs b/src/builtin/mod.rs index dbc044f..c3931e3 100644 --- a/src/builtin/mod.rs +++ b/src/builtin/mod.rs @@ -4,11 +4,9 @@ use crate::{ jobs::{ChildProc, JobBldr}, libsh::error::ShResult, parse::{ - execute::prepare_argv, - lex::{Span, Tk}, - Redir, + Redir, execute::prepare_argv, lex::{Span, Tk} }, - procio::{IoFrame, IoStack}, + procio::{IoFrame, IoStack, RedirGuard}, }; pub mod alias; @@ -21,12 +19,15 @@ pub mod pwd; pub mod shift; pub mod shopt; pub mod source; -pub mod test; -pub mod zoltraak; // [[ ]] thing +pub mod test; // [[ ]] thing +pub mod read; +pub mod zoltraak; -pub const BUILTINS: [&str; 19] = [ - "echo", "cd", "export", "pwd", "source", "shift", "jobs", "fg", "bg", "alias", "unalias", - "return", "break", "continue", "exit", "zoltraak", "shopt", "builtin", "command", +pub const BUILTINS: [&str; 20] = [ + "echo", "cd", "read", "export", "pwd", "source", + "shift", "jobs", "fg", "bg", "alias", "unalias", + "return", "break", "continue", "exit", "zoltraak", + "shopt", "builtin", "command", ]; /// Sets up a builtin command @@ -56,7 +57,7 @@ pub const BUILTINS: [&str; 19] = [ /// * If redirections are given, the second field of the resulting tuple will /// *always* be `Some()` /// * If no redirections are given, the second field will *always* be `None` -type SetupReturns = ShResult<(Vec<(String, Span)>, Option)>; +type SetupReturns = ShResult<(Vec<(String, Span)>, Option)>; pub fn setup_builtin( argv: Vec, job: &mut JobBldr, @@ -74,16 +75,16 @@ pub fn setup_builtin( let child = ChildProc::new(Pid::this(), Some(&cmd_name), Some(child_pgid))?; job.push_child(child); - let io_frame = if let Some((io_stack, redirs)) = io_mode { + let guard = if let Some((io_stack, redirs)) = io_mode { io_stack.append_to_frame(redirs); - let mut io_frame = io_stack.pop_frame(); - io_frame.redirect()?; - Some(io_frame) + let io_frame = io_stack.pop_frame(); + let guard = io_frame.redirect()?; + Some(guard) } else { None }; // We return the io_frame because the caller needs to also call // io_frame.restore() - Ok((argv, io_frame)) + Ok((argv, guard)) } diff --git a/src/builtin/pwd.rs b/src/builtin/pwd.rs index ebf9525..3cc4acf 100644 --- a/src/builtin/pwd.rs +++ b/src/builtin/pwd.rs @@ -18,7 +18,7 @@ pub fn pwd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<() unreachable!() }; - let (_, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (_, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; let stdout = borrow_fd(STDOUT_FILENO); @@ -26,7 +26,6 @@ pub fn pwd(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<() curr_dir.push('\n'); write(stdout, curr_dir.as_bytes())?; - io_frame.unwrap().restore().unwrap(); state::set_status(0); Ok(()) } diff --git a/src/builtin/read.rs b/src/builtin/read.rs new file mode 100644 index 0000000..1a5c80d --- /dev/null +++ b/src/builtin/read.rs @@ -0,0 +1,187 @@ +use bitflags::bitflags; +use nix::{errno::Errno, libc::{STDIN_FILENO, STDOUT_FILENO}, unistd::{isatty, read, write}}; + +use crate::{builtin::setup_builtin, getopt::{Opt, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node}, procio::{IoStack, borrow_fd}, prompt::readline::term::RawModeGuard, state::{self, VarFlags, read_vars, write_vars}}; + +pub const READ_OPTS: [OptSpec;7] = [ + OptSpec { opt: Opt::Short('r'), takes_arg: false }, // don't allow backslash escapes + OptSpec { opt: Opt::Short('s'), takes_arg: false }, // don't echo input + OptSpec { opt: Opt::Short('a'), takes_arg: false }, // read into array + OptSpec { opt: Opt::Short('n'), takes_arg: false }, // read only N characters + OptSpec { opt: Opt::Short('t'), takes_arg: false }, // timeout + OptSpec { opt: Opt::Short('p'), takes_arg: true }, // prompt + OptSpec { opt: Opt::Short('d'), takes_arg: true }, // read until delimiter +]; + +bitflags! { + pub struct ReadFlags: u32 { + const NO_ESCAPES = 0b000001; + const NO_ECHO = 0b000010; + const ARRAY = 0b000100; + const N_CHARS = 0b001000; + const TIMEOUT = 0b010000; + } +} + +pub struct ReadOpts { + prompt: Option, + delim: u8, // byte representation of the delimiter character + flags: ReadFlags, +} + +pub fn read_builtin(node: Node, _io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult<()> { + let blame = node.get_span().clone(); + let NdRule::Command { + assignments: _, + argv + } = node.class else { + unreachable!() + }; + + let (argv, opts) = get_opts_from_tokens(argv, &READ_OPTS); + let read_opts = get_read_flags(opts).blame(blame.clone())?; + let (argv, _) = setup_builtin(argv, job, None).blame(blame.clone())?; + + if let Some(prompt) = read_opts.prompt { + write(borrow_fd(STDOUT_FILENO), prompt.as_bytes())?; + } + + let input = if isatty(STDIN_FILENO)? { + // Restore default terminal settings + RawModeGuard::with_cooked_mode(|| { + let mut input: Vec = vec![]; + let mut escaped = false; + loop { + let mut buf = [0u8;1]; + match read(STDIN_FILENO, &mut buf) { + Ok(0) => { + state::set_status(1); + let str_result = String::from_utf8(input.clone()).map_err(|e| ShErr::simple( + ShErrKind::ExecFail, + format!("read: Input was not valid UTF-8: {e}"), + ))?; + return Ok(str_result); // EOF + } + Ok(_) => { + if buf[0] == read_opts.delim { + if read_opts.flags.contains(ReadFlags::NO_ESCAPES) && escaped { + input.push(buf[0]); + } else { + // Delimiter reached, stop reading + break; + } + } + else if read_opts.flags.contains(ReadFlags::NO_ESCAPES) + && buf[0] == b'\\' { + escaped = true; + } else { + input.push(buf[0]); + } + } + Err(Errno::EINTR) => continue, + Err(e) => return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("read: Failed to read from stdin: {e}"), + )), + } + } + + state::set_status(0); + let str_result = String::from_utf8(input.clone()).map_err(|e| ShErr::simple( + ShErrKind::ExecFail, + format!("read: Input was not valid UTF-8: {e}"), + ))?; + Ok(str_result) + }).blame(blame)? + } else { + let mut input: Vec = vec![]; + loop { + let mut buf = [0u8;1]; + match read(STDIN_FILENO, &mut buf) { + Ok(0) => { + state::set_status(1); + break; // EOF + } + Ok(_) => { + if buf[0] == read_opts.delim { + break; // Delimiter reached, stop reading + } + input.push(buf[0]); + } + Err(Errno::EINTR) => continue, + Err(e) => return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("read: Failed to read from stdin: {e}"), + )), + } + } + String::from_utf8(input).map_err(|e| ShErr::simple( + ShErrKind::ExecFail, + format!("read: Input was not valid UTF-8: {e}"), + ))? + }; + + if argv.is_empty() { + write_vars(|v| { + v.set_var("REPLY", &input, VarFlags::NONE); + }); + } else { + // get our field separator + let mut field_sep = read_vars(|v| v.get_var("IFS")); + if field_sep.is_empty() { field_sep = " ".to_string() } + let mut remaining = input; + + for (i, arg) in argv.iter().enumerate() { + if i == argv.len() - 1 { + // Last arg, stuff the rest of the input into it + write_vars(|v| v.set_var(&arg.0, &remaining, VarFlags::NONE)); + break; + } + + // trim leading IFS characters + let trimmed = remaining.trim_start_matches(|c: char| field_sep.contains(c)); + + if let Some(idx) = trimmed.find(|c: char| field_sep.contains(c)) { + // We found a field separator, split at the char index + let (field, rest) = trimmed.split_at(idx); + write_vars(|v| v.set_var(&arg.0, field, VarFlags::NONE)); + + // note that this doesn't account for consecutive IFS characters, which is what that trim above is for + remaining = rest.to_string(); + } else { + write_vars(|v| v.set_var(&arg.0, trimmed, VarFlags::NONE)); + remaining.clear(); + } + } + } + + Ok(()) +} + +pub fn get_read_flags(opts: Vec) -> ShResult { + let mut read_opts = ReadOpts { + prompt: None, + delim: b'\n', + flags: ReadFlags::empty(), + }; + + for opt in opts { + match opt { + Opt::Short('r') => read_opts.flags |= ReadFlags::NO_ESCAPES, + Opt::Short('s') => read_opts.flags |= ReadFlags::NO_ECHO, + Opt::Short('a') => read_opts.flags |= ReadFlags::ARRAY, + Opt::Short('n') => read_opts.flags |= ReadFlags::N_CHARS, + Opt::Short('t') => read_opts.flags |= ReadFlags::TIMEOUT, + Opt::ShortWithArg('p', prompt) => read_opts.prompt = Some(prompt), + Opt::ShortWithArg('d', delim) => read_opts.delim = delim.chars().map(|c| c as u8).next().unwrap_or(b'\n'), + _ => { + return Err(ShErr::simple( + ShErrKind::ExecFail, + format!("read: Unexpected flag '{opt}'"), + )); + } + } + } + + Ok(read_opts) +} diff --git a/src/builtin/shopt.rs b/src/builtin/shopt.rs index 1ac37f8..96dc4f6 100644 --- a/src/builtin/shopt.rs +++ b/src/builtin/shopt.rs @@ -18,10 +18,8 @@ pub fn shopt(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult< unreachable!() }; - let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; - let mut io_frame = io_frame.unwrap(); - io_frame.redirect()?; for (arg, span) in argv { let Some(mut output) = write_shopts(|s| s.query(&arg)).blame(span)? else { continue; @@ -31,11 +29,9 @@ pub fn shopt(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResult< output.push('\n'); if let Err(e) = write(output_channel, output.as_bytes()) { - io_frame.restore()?; return Err(e.into()); } } - io_frame.restore()?; Ok(()) } diff --git a/src/builtin/zoltraak.rs b/src/builtin/zoltraak.rs index fc53d22..ca0a095 100644 --- a/src/builtin/zoltraak.rs +++ b/src/builtin/zoltraak.rs @@ -1,28 +1,16 @@ use std::{os::unix::fs::OpenOptionsExt, sync::LazyLock}; use crate::{ - getopt::{get_opts_from_tokens, Opt, OptSet}, + getopt::{Opt, OptSet, OptSpec, get_opts_from_tokens}, jobs::JobBldr, libsh::error::{Note, ShErr, ShErrKind, ShResult, ShResultExt}, parse::{NdRule, Node}, prelude::*, - procio::{borrow_fd, IoStack}, + procio::{IoStack, borrow_fd}, }; use super::setup_builtin; -pub static ZOLTRAAK_OPTS: LazyLock = LazyLock::new(|| { - [ - Opt::Long("dry-run".into()), - Opt::Long("confirm".into()), - Opt::Long("no-preserve-root".into()), - Opt::Short('r'), - Opt::Short('f'), - Opt::Short('v'), - ] - .into() -}); - bitflags! { #[derive(Clone,Copy,Debug,PartialEq,Eq)] struct ZoltFlags: u32 { @@ -49,37 +37,59 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu else { unreachable!() }; + let zolt_opts = [ + OptSpec { opt: Opt::Long("dry-run".into()), takes_arg: false }, + OptSpec { opt: Opt::Long("confirm".into()), takes_arg: false }, + OptSpec { opt: Opt::Long("no-preserve-root".into()), takes_arg: false }, + OptSpec { opt: Opt::Short('r'), takes_arg: false }, + OptSpec { opt: Opt::Short('f'), takes_arg: false }, + OptSpec { opt: Opt::Short('v'), takes_arg: false } + ]; let mut flags = ZoltFlags::empty(); - let (argv, opts) = get_opts_from_tokens(argv); + let (argv, opts) = get_opts_from_tokens(argv, &zolt_opts); for opt in opts { - if !ZOLTRAAK_OPTS.contains(&opt) { - return Err(ShErr::simple( - ShErrKind::SyntaxErr, - format!("zoltraak: unrecognized option '{opt}'"), - )); - } match opt { Opt::Long(flag) => match flag.as_str() { "no-preserve-root" => flags |= ZoltFlags::NO_PRESERVE_ROOT, "confirm" => flags |= ZoltFlags::CONFIRM, "dry-run" => flags |= ZoltFlags::DRY, - _ => unreachable!(), + _ => { + return Err(ShErr::simple( + ShErrKind::SyntaxErr, + format!("zoltraak: unrecognized option '{flag}'"), + )); + } }, Opt::Short(flag) => match flag { 'r' => flags |= ZoltFlags::RECURSIVE, 'f' => flags |= ZoltFlags::FORCE, 'v' => flags |= ZoltFlags::VERBOSE, - _ => unreachable!(), + _ => { + return Err(ShErr::simple( + ShErrKind::SyntaxErr, + format!("zoltraak: unrecognized option '{flag}'"), + )); + } }, + Opt::LongWithArg(flag, _) => { + return Err(ShErr::simple( + ShErrKind::SyntaxErr, + format!("zoltraak: unrecognized option '{flag}'"), + )); + } + Opt::ShortWithArg(flag, _) => { + return Err(ShErr::simple( + ShErrKind::SyntaxErr, + format!("zoltraak: unrecognized option '{flag}'"), + )); + } } } - let (argv, io_frame) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; + let (argv, _guard) = setup_builtin(argv, job, Some((io_stack, node.redirs)))?; - let mut io_frame = io_frame.unwrap(); - io_frame.redirect()?; for (arg, span) in argv { if &arg == "/" && !flags.contains(ZoltFlags::NO_PRESERVE_ROOT) { @@ -95,12 +105,10 @@ pub fn zoltraak(node: Node, io_stack: &mut IoStack, job: &mut JobBldr) -> ShResu ); } if let Err(e) = annihilate(&arg, flags).blame(span) { - io_frame.restore()?; return Err(e); } } - io_frame.restore()?; Ok(()) } diff --git a/src/expand.rs b/src/expand.rs index afc69b5..1db45c3 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -9,7 +9,7 @@ use crate::libsh::error::{ShErr, ShErrKind, ShResult}; use crate::parse::execute::exec_input; use crate::parse::lex::{is_field_sep, is_hard_sep, LexFlags, LexStream, Tk, TkFlags, TkRule}; use crate::parse::{Redir, RedirType}; -use crate::prelude::*; +use crate::{jobs, prelude::*}; use crate::procio::{IoBuf, IoFrame, IoMode, IoStack}; use crate::state::{LogTab, VarFlags, read_jobs, read_logic, read_vars, write_jobs, write_meta, write_vars}; @@ -29,14 +29,13 @@ pub const SUBSH: char = '\u{fdd4}'; pub const PROC_SUB_IN: char = '\u{fdd5}'; /// Output process sub marker pub const PROC_SUB_OUT: char = '\u{fdd6}'; +/// Marker for null expansion +/// 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, which breaks some commands +pub const NULL_EXPAND: char = '\u{fdd7}'; impl Tk { /// Create a new expanded token - /// - /// params - /// tokens: A vector of raw tokens lexed from the expansion result - /// span: The span of the original token that is being expanded - /// flags: some TkFlags pub fn expand(self) -> ShResult { let flags = self.flags; let span = self.span.clone(); @@ -59,11 +58,14 @@ pub struct Expander { impl Expander { pub fn new(raw: Tk) -> ShResult { - let mut raw = raw.span.as_str().to_string(); - raw = expand_braces_full(&raw)?.join(" "); - let unescaped = unescape_str(&raw); - Ok(Self { raw: unescaped }) + let raw = raw.span.as_str(); + Self::from_raw(&raw) } + pub fn from_raw(raw: &str) -> ShResult { + let raw = expand_braces_full(raw)?.join(" "); + let unescaped = unescape_str(&raw); + Ok(Self { raw: unescaped }) + } pub fn expand(&mut self) -> ShResult> { let mut chars = self.raw.chars().peekable(); self.raw = expand_raw(&mut chars)?; @@ -77,24 +79,40 @@ impl Expander { let mut words = vec![]; let mut chars = self.raw.chars(); let mut cur_word = String::new(); + let mut was_quoted = false; 'outer: while let Some(ch) = chars.next() { match ch { DUB_QUOTE | SNG_QUOTE | SUBSH => { while let Some(q_ch) = chars.next() { match q_ch { - _ if q_ch == ch => continue 'outer, // Isn't rust cool + _ if q_ch == ch => { + was_quoted = true; + continue 'outer; // Isn't rust cool + } _ => cur_word.push(q_ch), } } } _ if is_field_sep(ch) => { - words.push(mem::take(&mut cur_word)); + if cur_word.is_empty() && !was_quoted { + cur_word.clear(); + } else { + words.push(mem::take(&mut cur_word)); + } + was_quoted = false; } _ => cur_word.push(ch), } } - words.push(cur_word); + + if words.is_empty() && (cur_word.is_empty() && !was_quoted) { + return words; + } else { + words.push(cur_word); + } + + words.retain(|w| w != &NULL_EXPAND.to_string()); words } } @@ -208,17 +226,16 @@ fn expand_one_brace(word: &str) -> ShResult> { /// Extract prefix, inner, and suffix from a brace expression. /// "pre{a,b}post" -> Some(("pre", "a,b", "post")) fn get_brace_parts(word: &str) -> Option<(String, String, String)> { - let mut chars = word.chars().enumerate().peekable(); + let mut chars = word.chars().peekable(); let mut prefix = String::new(); let mut cur_quote: Option = None; - let mut brace_start = None; // Find the opening brace - while let Some((i, ch)) = chars.next() { + while let Some(ch) = chars.next() { match ch { '\\' => { prefix.push(ch); - if let Some((_, next)) = chars.next() { + if let Some(next) = chars.next() { prefix.push(next); } } @@ -229,25 +246,22 @@ fn get_brace_parts(word: &str) -> Option<(String, String, String)> { '"' if cur_quote.is_none() => { cur_quote = Some('"'); prefix.push(ch); } '"' if cur_quote == Some('"') => { cur_quote = None; prefix.push(ch); } '{' if cur_quote.is_none() => { - brace_start = Some(i); break; } _ => prefix.push(ch), } } - let brace_start = brace_start?; - // Find matching closing brace let mut depth = 1; let mut inner = String::new(); cur_quote = None; - while let Some((_, ch)) = chars.next() { + while let Some(ch) = chars.next() { match ch { '\\' => { inner.push(ch); - if let Some((_, next)) = chars.next() { + if let Some(next) = chars.next() { inner.push(next); } } @@ -275,7 +289,7 @@ fn get_brace_parts(word: &str) -> Option<(String, String, String)> { } // Collect suffix - let suffix: String = chars.map(|(_, c)| c).collect(); + let suffix: String = chars.collect(); Some((prefix, inner, suffix)) } @@ -492,6 +506,11 @@ pub fn expand_var(chars: &mut Peekable>) -> ShResult { chars.next(); let parameter = format!("{ch}"); let val = read_vars(|v| v.get_var(¶meter)); + + if (ch == '@' || ch == '*') && val.is_empty() { + return Ok(NULL_EXPAND.to_string()); + } + flog!(DEBUG, val); return Ok(val); } @@ -815,7 +834,7 @@ pub fn expand_cmd_sub(raw: &str) -> ShResult { }; // Reclaim terminal foreground in case child changed it - crate::jobs::take_term()?; + jobs::take_term()?; match status { WtStat::Exited(_, _) => { @@ -995,6 +1014,77 @@ pub fn unescape_str(raw: &str) -> String { } } } + '$' if chars.peek() == Some(&'\'') => { + chars.next(); + result.push(SNG_QUOTE); + while let Some(q_ch) = chars.next() { + match q_ch { + '\'' => { + result.push(SNG_QUOTE); + break; + } + '\\' => { + if let Some(esc) = chars.next() { + match esc { + 'n' => result.push('\n'), + 't' => result.push('\t'), + 'r' => result.push('\r'), + '\'' => result.push('\''), + '\\' => result.push('\\'), + 'a' => result.push('\x07'), + 'b' => result.push('\x08'), + 'e' | 'E' => result.push('\x1b'), + 'v' => result.push('\x0b'), + 'x' => { + let mut hex = String::new(); + if let Some(h1) = chars.next() { + hex.push(h1); + } else { + result.push_str("\\x"); + continue; + } + if let Some(h2) = chars.next() { + hex.push(h2); + } else { + result.push_str(&format!("\\x{hex}")); + continue; + } + if let Ok(byte) = u8::from_str_radix(&hex, 16) { + result.push(byte as char); + } else { + result.push_str(&format!("\\x{hex}")); + continue; + } + } + 'o' => { + let mut oct = String::new(); + for _ in 0..3 { + if let Some(o) = chars.peek() { + if o.is_digit(8) { + oct.push(*o); + chars.next(); + } else { + break; + } + } else { + break; + } + } + if let Ok(byte) = u8::from_str_radix(&oct, 8) { + result.push(byte as char); + } else { + result.push_str(&format!("\\o{oct}")); + continue; + } + } + _ => result.push(esc), + } + } + } + _ => result.push(q_ch), + } + } + } '$' => { result.push(VAR_SUB); if chars.peek() == Some(&'$') { diff --git a/src/getopt.rs b/src/getopt.rs index d2232ef..51725ec 100644 --- a/src/getopt.rs +++ b/src/getopt.rs @@ -9,7 +9,14 @@ pub type OptSet = Arc<[Opt]>; #[derive(Clone, PartialEq, Eq, Debug)] pub enum Opt { Long(String), + LongWithArg(String,String), Short(char), + ShortWithArg(char,String), +} + +pub struct OptSpec { + pub opt: Opt, + pub takes_arg: bool, } impl Opt { @@ -34,6 +41,8 @@ impl Display for Opt { match self { Self::Long(opt) => write!(f, "--{}", opt), Self::Short(opt) => write!(f, "-{}", opt), + Self::LongWithArg(opt, arg) => write!(f, "--{} {}", opt, arg), + Self::ShortWithArg(opt, arg) => write!(f, "-{} {}", opt, arg) } } } @@ -58,7 +67,7 @@ pub fn get_opts(words: Vec) -> (Vec, Vec) { (non_opts, opts) } -pub fn get_opts_from_tokens(tokens: Vec) -> (Vec, Vec) { +pub fn get_opts_from_tokens(tokens: Vec, opt_specs: &[OptSpec]) -> (Vec, Vec) { let mut tokens_iter = tokens.into_iter(); let mut opts = vec![]; let mut non_opts = vec![]; @@ -69,10 +78,37 @@ pub fn get_opts_from_tokens(tokens: Vec) -> (Vec, Vec) { break; } let parsed_opts = Opt::parse(&token.to_string()); + if parsed_opts.is_empty() { non_opts.push(token) } else { - opts.extend(parsed_opts); + for opt in parsed_opts { + let mut pushed = false; + for opt_spec in opt_specs { + if opt_spec.opt == opt { + if opt_spec.takes_arg { + let arg = tokens_iter.next() + .map(|t| t.to_string()) + .unwrap_or_default(); + + let opt = match opt { + Opt::Long(ref opt) => Opt::LongWithArg(opt.to_string(), arg), + Opt::Short(opt) => Opt::ShortWithArg(opt, arg), + _ => unreachable!(), + }; + opts.push(opt); + pushed = true; + } else { + opts.push(opt.clone()); + pushed = true; + } + } + } + if !pushed { + non_opts.push(token.clone()); + log::warn!("Unexpected flag '{opt}'"); + } + } } } (non_opts, opts) diff --git a/src/main.rs b/src/main.rs index 2809556..d16e80f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,6 +42,9 @@ use state::{read_vars, write_vars}; struct FernArgs { script: Option, + #[arg(short)] + command: Option, + #[arg(trailing_var_arg = true)] script_args: Vec, @@ -74,6 +77,8 @@ fn main() -> ExitCode { if let Err(e) = if let Some(path) = args.script { run_script(path, args.script_args) + } else if let Some(cmd) = args.command { + exec_input(cmd, None, false) } else { fern_interactive() } { diff --git a/src/parse/execute.rs b/src/parse/execute.rs index cac051f..b483f6c 100644 --- a/src/parse/execute.rs +++ b/src/parse/execute.rs @@ -2,18 +2,7 @@ use std::collections::{HashSet, VecDeque}; use crate::{ builtin::{ - alias::{alias, unalias}, - cd::cd, - echo::echo, - export::export, - flowctl::flowctl, - jobctl::{JobBehavior, continue_job, jobs}, - pwd::pwd, - shift::shift, - shopt::shopt, - source::source, - test::double_bracket_test, - zoltraak::zoltraak, + alias::{alias, unalias}, cd::cd, echo::echo, export::export, flowctl::flowctl, jobctl::{JobBehavior, continue_job, jobs}, pwd::pwd, read::read_builtin, shift::shift, shopt::shopt, source::source, test::double_bracket_test, zoltraak::zoltraak }, expand::expand_aliases, jobs::{ChildProc, JobBldr, JobStack, dispatch_job}, @@ -314,12 +303,13 @@ impl Dispatcher { let NdRule::BraceGrp { body } = brc_grp.class else { unreachable!() }; - let mut io_frame = self.io_stack.pop_frame(); - io_frame.extend(brc_grp.redirs); + self.io_stack.append_to_frame(brc_grp.redirs); + let _guard = self.io_stack + .pop_frame() + .redirect()?; for node in body { let blame = node.get_span(); - self.io_stack.push_frame(io_frame.clone()); self.dispatch_node(node).try_blame(blame)?; } @@ -335,6 +325,9 @@ impl Dispatcher { }; self.io_stack.append_to_frame(case_stmt.redirs); + let _guard = self.io_stack + .pop_frame() + .redirect()?; let exp_pattern = pattern.clone().expand()?; let pattern_raw = exp_pattern @@ -372,16 +365,13 @@ impl Dispatcher { } }; - let io_frame = self.io_stack.pop_frame(); - let (mut cond_frame, mut body_frame) = io_frame.split_frame(); - let (in_redirs, out_redirs) = loop_stmt.redirs.split_by_channel(); - cond_frame.extend(in_redirs); - body_frame.extend(out_redirs); + self.io_stack.append_to_frame(loop_stmt.redirs); + let _guard = self.io_stack + .pop_frame() + .redirect()?; let CondNode { cond, body } = cond_node; 'outer: loop { - self.io_stack.push(cond_frame.clone()); - if let Err(e) = self.dispatch_node(*cond.clone()) { state::set_status(1); return Err(e); @@ -389,7 +379,6 @@ impl Dispatcher { let status = state::get_status(); if keep_going(kind, status) { - self.io_stack.push(body_frame.clone()); for node in &body { if let Err(e) = self.dispatch_node(node.clone()) { match e.kind() { @@ -401,7 +390,9 @@ impl Dispatcher { state::set_status(*code); continue 'outer; } - _ => return Err(e), + _ => { + return Err(e); + } } } } @@ -421,15 +412,15 @@ impl Dispatcher { vars.iter().map(|v| v.to_string()).collect() ); - let io_frame = self.io_stack.pop_frame(); - let (_, mut body_frame) = io_frame.split_frame(); - let (_, out_redirs) = for_stmt.redirs.split_by_channel(); - body_frame.extend(out_redirs); + self.io_stack.append_to_frame(for_stmt.redirs); + let _guard = self.io_stack + .pop_frame() + .redirect()?; 'outer: for chunk in arr.chunks(vars.len()) { let empty = Tk::default(); let chunk_iter = vars.iter().zip( - chunk.iter().chain(std::iter::repeat(&empty)), // Or however you define an empty token + chunk.iter().chain(std::iter::repeat(&empty)), ); for (var, val) in chunk_iter { @@ -438,7 +429,6 @@ impl Dispatcher { } for node in body.clone() { - self.io_stack.push(body_frame.clone()); if let Err(e) = self.dispatch_node(node) { match e.kind() { ShErrKind::LoopBreak(code) => { @@ -465,17 +455,15 @@ impl Dispatcher { else { unreachable!(); }; - // Pop the current frame and split it - let io_frame = self.io_stack.pop_frame(); - let (mut cond_frame, mut body_frame) = io_frame.split_frame(); - let (in_redirs, out_redirs) = if_stmt.redirs.split_by_channel(); - cond_frame.extend(in_redirs); // Condition gets input redirs - body_frame.extend(out_redirs); // Body gets output redirs + + self.io_stack.append_to_frame(if_stmt.redirs); + let _guard = self.io_stack + .pop_frame() + .redirect()?; let mut matched = false; for node in cond_nodes { let CondNode { cond, body } = node; - self.io_stack.push(cond_frame.clone()); if let Err(e) = self.dispatch_node(*cond) { state::set_status(1); @@ -486,7 +474,6 @@ impl Dispatcher { 0 => { matched = true; for body_node in body { - self.io_stack.push(body_frame.clone()); self.dispatch_node(body_node)?; } break; // Don't check remaining elif conditions @@ -497,7 +484,6 @@ impl Dispatcher { if !matched && !else_block.is_empty() { for node in else_block { - self.io_stack.push(body_frame.clone()); self.dispatch_node(node)?; } } @@ -576,6 +562,7 @@ impl Dispatcher { "exit" => flowctl(cmd, ShErrKind::CleanExit(0)), "zoltraak" => zoltraak(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), _ => unimplemented!( "Have not yet added support for builtin '{}'", cmd_raw.span.as_str() @@ -613,9 +600,11 @@ impl Dispatcher { log::info!("expanded argv: {:?}", exec_args.argv.iter().map(|s| s.to_str().unwrap()).collect::>()); } - let io_frame = self.io_stack.pop_frame(); + let _guard = self.io_stack + .pop_frame() + .redirect()?; + run_fork( - io_frame, Some(exec_args), self.job_stack.curr_job_mut().unwrap(), def_child_action, @@ -683,19 +672,18 @@ pub fn prepare_argv(argv: Vec) -> ShResult> { } pub fn run_fork( - io_frame: IoFrame, exec_args: Option, job: &mut JobBldr, child_action: C, parent_action: P, ) -> ShResult<()> where - C: Fn(IoFrame, Option), + C: Fn(Option), P: Fn(&mut JobBldr, Option<&str>, Pid) -> ShResult<()>, { match unsafe { fork()? } { ForkResult::Child => { - child_action(io_frame, exec_args); + child_action(exec_args); exit(0); // Just in case } ForkResult::Parent { child } => { @@ -710,10 +698,7 @@ where } /// The default behavior for the child process after forking -pub fn def_child_action(mut io_frame: IoFrame, exec_args: Option) { - if let Err(e) = io_frame.redirect() { - eprintln!("{e}"); - } +pub fn def_child_action(exec_args: Option) { let exec_args = exec_args.unwrap(); let cmd = &exec_args.cmd.0; let span = exec_args.cmd.1; diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 6267b52..aed0173 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -1150,6 +1150,7 @@ impl ParseStream { let cond_node: CondNode; let mut node_tks = vec![]; + let mut redirs = vec![]; if (!self.check_keyword("while") && !self.check_keyword("until")) || !self.next_tk_is_some() { return Ok(None); @@ -1204,6 +1205,9 @@ impl ParseStream { )); } node_tks.push(self.next_tk().unwrap()); + + self.parse_redir(&mut redirs, &mut node_tks)?; + self.assert_separator(&mut node_tks)?; cond_node = CondNode { @@ -1216,7 +1220,7 @@ impl ParseStream { cond_node, }, flags: NdFlags::empty(), - redirs: vec![], + redirs, tokens: node_tks, }; Ok(Some(loop_node)) diff --git a/src/procio.rs b/src/procio.rs index d9f5bb8..39fcaf0 100644 --- a/src/procio.rs +++ b/src/procio.rs @@ -4,12 +4,10 @@ use std::{ }; use crate::{ - libsh::{ + expand::Expander, libsh::{ error::{ShErr, ShErrKind, ShResult}, utils::RedirVecUtils, - }, - parse::{get_redir_file, Redir, RedirType}, - prelude::*, + }, parse::{Redir, RedirType, get_redir_file}, prelude::* }; // Credit to fish-shell for many of the implementation ideas present in this @@ -72,7 +70,19 @@ impl IoMode { } pub fn open_file(mut self) -> ShResult { if let IoMode::File { tgt_fd, path, mode } = self { - let file = get_redir_file(mode, path)?; + 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 multiple + + let expanded_pathbuf = PathBuf::from(expanded_path); + + let file = get_redir_file(mode, expanded_pathbuf)?; self = IoMode::OpenedFile { tgt_fd, file: Arc::new(OwnedFd::from(file)), @@ -145,6 +155,13 @@ impl IoBuf { } } +pub struct RedirGuard(IoFrame); +impl Drop for RedirGuard { + fn drop(&mut self) { + self.0.restore().ok(); + } +} + /// A struct wrapping three fildescs representing `stdin`, `stdout`, and /// `stderr` respectively #[derive(Debug, Clone)] @@ -199,7 +216,7 @@ impl<'e> IoFrame { let saved_err = dup(STDERR_FILENO).unwrap(); self.saved_io = Some(IoGroup(saved_in, saved_out, saved_err)); } - pub fn redirect(&mut self) -> ShResult<()> { + pub fn redirect(mut self) -> ShResult { self.save(); for redir in &mut self.redirs { let io_mode = &mut redir.io_mode; @@ -212,7 +229,7 @@ impl<'e> IoFrame { let src_fd = io_mode.src_fd(); dup2(src_fd, tgt_fd)?; } - Ok(()) + Ok(RedirGuard(self)) } pub fn restore(&mut self) -> ShResult<()> { if let Some(saved) = self.saved_io.take() { diff --git a/src/prompt/readline/term.rs b/src/prompt/readline/term.rs index 4722911..26e6128 100644 --- a/src/prompt/readline/term.rs +++ b/src/prompt/readline/term.rs @@ -10,14 +10,14 @@ use nix::{ errno::Errno, libc::{self, STDIN_FILENO}, poll::{self, PollFlags, PollTimeout}, - sys::termios, + sys::termios::{self, tcgetattr, tcsetattr}, unistd::isatty, }; use unicode_segmentation::UnicodeSegmentation; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; use vte::{Parser, Perform}; -use crate::prelude::*; +use crate::{prelude::*, procio::borrow_fd}; use crate::{ libsh::error::{ShErr, ShErrKind, ShResult}, prompt::readline::keys::{KeyCode, ModKeys}, @@ -282,6 +282,18 @@ impl RawModeGuard { result } } + + pub fn with_cooked_mode(f: F) -> R + where F: FnOnce() -> R { + let raw = tcgetattr(borrow_fd(STDIN_FILENO)).expect("Failed to get terminal attributes"); + let mut cooked = raw.clone(); + cooked.local_flags |= termios::LocalFlags::ICANON | termios::LocalFlags::ECHO; + cooked.input_flags |= termios::InputFlags::ICRNL; + tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &cooked).expect("Failed to set cooked mode"); + let res = f(); + tcsetattr(borrow_fd(STDIN_FILENO), termios::SetArg::TCSANOW, &raw).expect("Failed to restore raw mode"); + res + } } impl Drop for RawModeGuard {