#![allow(non_camel_case_types)] use std::sync::{Arc, Mutex}; use std::{env::*, fs::OpenOptions, path::PathBuf}; use pish_derive::FromArgs; use super::{Builtin, BuiltinError as Error, BuiltinResult as Result}; use crate::parse::CmdDisplay; use crate::run::{AliasAge, AliasBody}; use crate::*; #[allow(unused)] pub enum ArgParseError<'a> { LeftoverArg(&'a [u8]), MissingArg(&'static str), MissingArgValue(&'static str), ArgValueParseError(&'static str, String), } pub trait ArgParse: Sized { fn parse<'a>(args: &'a [BString]) -> std::result::Result>; } fn read_args(args: &[BString], w: &mut dyn Write) -> std::result::Result { let err = match T::parse(args) { Ok(t) => return Ok(t), Err(e) => e, }; match err { ArgParseError::LeftoverArg(items) => { w.write_all(b"leftover argument: ")?; w.write_all(items)?; w.write_all(b"\n")?; } ArgParseError::MissingArg(arg) => { writeln!(w, "argument `{arg}` is missing")?; } ArgParseError::MissingArgValue(arg) => { writeln!(w, "argument `{arg}` is missing its value")?; } ArgParseError::ArgValueParseError(arg, err) => { writeln!(w, "failed to parse value of `{arg}`: {err}")?; } } Err(Error::Exit(-2)) } #[derive(Copy, Clone)] pub struct cd; impl Builtin for cd { fn name(&self) -> &str { "cd" } fn io( &self, se: Arc>, args: &[BString], _stdin: &mut dyn Read, stdout: &mut dyn Write, ) -> Result { let mut dir = match current_dir() { Ok(path) => path.as_os_str().as_bytes().to_vec(), Err(_) => vec![b'.'], }; std::mem::swap(&mut dir, &mut se.lock().unwrap().prev_path); let target_path: BString = match args.first().map(|v| &v[..]) { Some(b"-") => dir, Some(path) => path.to_vec(), None => { if let Some(home) = std::env::var_os("HOME") { home.into_encoded_bytes() } else { writeln!(stdout, "$HOME not set")?; return Err(Error::Exit(-1)); } } }; if set_current_dir(OsStr::from_bytes(&target_path)).is_err() { write!(stdout, "failed to cd into ")?; stdout.write_all(&target_path)?; writeln!(stdout, "\n")?; Err(Error::Exit(1)) } else { Ok(()) } } } #[derive(Copy, Clone)] pub struct clear; impl Builtin for clear { fn name(&self) -> &str { "clear" } fn io( &self, _session: Arc>, _args: &[BString], _stdin: &mut dyn Read, stdout: &mut dyn Write, ) -> Result { stdout.write_all(b"\x1B[2J\x1B[1;1H")?; Ok(()) } } #[derive(Copy, Clone)] pub struct Sink { name: &'static str, append: bool, } impl Builtin for Sink { fn name(&self) -> &str { self.name } fn io( &self, _session: Arc>, args: &[BString], stdin: &mut dyn Read, _stdout: &mut dyn Write, ) -> Result { let Some(path) = args.first() else { return Err(Error::Exit(1)); }; let path = PathBuf::from(OsStr::from_bytes(path)); let mut file = OpenOptions::new() .write(true) .create(true) .append(self.append) .open(path)?; std::io::copy(stdin, &mut file)?; Ok(()) } } pub const fn sink(name: &'static str, append: bool) -> Sink { Sink { name, append } } #[derive(Copy, Clone)] pub struct from; impl Builtin for from { fn name(&self) -> &str { "from" } fn io( &self, _session: Arc>, args: &[BString], _stdin: &mut dyn Read, stdout: &mut dyn Write, ) -> Result { let Some(path) = args.first() else { return Err(Error::Exit(1)); }; let path = PathBuf::from(OsStr::from_bytes(path)); let mut file = OpenOptions::new().read(true).open(path)?; std::io::copy(&mut file, stdout)?; Ok(()) } } #[derive(Copy, Clone)] pub struct _type; impl Builtin for _type { fn name(&self) -> &str { "type" } fn io( &self, session: Arc>, args: &[BString], _stdin: &mut dyn Read, stdout: &mut dyn Write, ) -> Result { let session = session.lock().unwrap(); for arg in args { if let Some((_, a)) = session.aliases.get(arg, AliasAge::MAX) { stdout.write_all(&arg)?; stdout.write_all(b" is aliased to ")?; stdout.write_all(&a.unparsed)?; stdout.write_all(b"\n")?; continue; } let kind = super::get_command_kind(&session, &arg[..]); let kind_str = match kind { run::CommandKind::Builtin(_) => "builtin", run::CommandKind::Fun(_) => "function", run::CommandKind::Path(_) => "command (if it exists)", }; writeln!(stdout, "{} is {}", String::from_utf8_lossy(arg), kind_str)?; } Ok(()) } } #[derive(Copy, Clone)] pub struct builtins; impl Builtin for builtins { fn name(&self) -> &str { "builtins" } fn io( &self, _session: Arc>, _args: &[BString], _stdin: &mut dyn Read, stdout: &mut dyn Write, ) -> Result { let mut bs = super::BUILTINS.to_vec(); bs.sort_by_key(|b| b.name()); for b in bs.into_iter() { write!(stdout, "{} ", b.name())?; } writeln!(stdout)?; Ok(()) } } #[derive(FromArgs, Debug)] struct HistoryArgs { /// displays only local shell session history local: bool, /// displays only history of current directory here: bool, /// only shows if cwd perfectly matches strict: bool, at: Option, // TODO: temporal control, i.e. before & after } fn display_history>(it: T, stdout: &mut dyn Write) -> Result { let now = crate::date::DateTime::now(); for entry in it { let delta = now.relative_to(&entry.time); for _ in 0..crate::date::DateTime::longest_reasonable_delta() - delta.len() { stdout.write_all(b" ")?; } stdout.write_all(delta.as_bytes())?; stdout.write_all(b" ")?; stdout.write_all(&entry.cmd)?; stdout.write_all(b"\n")?; } Ok(()) } #[derive(Copy, Clone)] pub struct history; impl Builtin for history { fn name(&self) -> &str { "history" } fn io( &self, session: Arc>, args: &[BString], _stdin: &mut dyn Read, stdout: &mut dyn Write, ) -> Result { let args: HistoryArgs = read_args(args, stdout)?; let path_prefix = if args.here { Some(current_dir()?.as_os_str().as_bytes().to_vec()) } else { args.at.map(|path| path.as_os_str().as_bytes().to_vec()) }; let min_time = None; let max_time = None; if args.local { let hist = session.lock().unwrap().history.clone(); display_history( crate::history::local_history_filter( hist, min_time, max_time, path_prefix.as_deref(), args.strict, ), stdout, ) } else { let Ok(hist) = crate::history::HistoryQueryer::new() else { writeln!(stdout, "error opening global history file")?; return Err(Error::Exit(-1)); }; let Ok(it) = hist.query(min_time, max_time, path_prefix.as_deref(), args.strict) else { writeln!(stdout, "error querying global history")?; return Err(Error::Exit(-1)); }; display_history(it, stdout) } } } #[derive(Copy, Clone)] pub struct escape; impl Builtin for escape { fn name(&self) -> &str { "escape" } fn io( &self, _session: Arc>, args: &[BString], _stdin: &mut dyn Read, stdout: &mut dyn Write, ) -> Result { for arg in args.iter() { let escaped = arg.escape_ascii().to_string(); stdout.write_all(escaped.as_bytes())?; stdout.write_all(b" ")?; } Ok(()) } } #[derive(Copy, Clone)] pub struct parse; impl Builtin for parse { fn name(&self) -> &str { "parse" } fn io( &self, _session: Arc>, args: &[BString], _stdin: &mut dyn Read, stdout: &mut dyn Write, ) -> Result { for arg in args { match crate::parse::do_parse(arg) { Ok(parsed) => { write!(stdout, "ok ")?; parsed.cdisplay(stdout)?; writeln!(stdout)?; } Err(err) => { writeln!(stdout, "err {:?} ", err.0)?; stdout.write_all(&err.1)?; } } } Ok(()) } } #[derive(Copy, Clone)] pub struct completion; impl Builtin for completion { fn name(&self) -> &str { "completion" } fn io( &self, session: Arc>, args: &[BString], _stdin: &mut dyn Read, stdout: &mut dyn Write, ) -> Result { for arg in args { let c = crate::completion(session.clone(), arg); write!(stdout, "{:?} ", c.kind)?; stdout.write_all(&c.shared_prefix)?; for s in c.suggestions { stdout.write_all(b" ")?; stdout.write_all(&s.display)?; } stdout.write_all(b"\n")?; } Ok(()) } } #[derive(Copy, Clone)] pub struct null; impl Builtin for null { fn name(&self) -> &str { "null" } fn io( &self, _session: Arc>, _args: &[BString], stdin: &mut dyn Read, _stdout: &mut dyn Write, ) -> Result { io::copy(stdin, &mut io::sink())?; Ok(()) } } #[derive(Copy, Clone)] pub struct var; impl Builtin for var { fn name(&self) -> &str { "$" } fn io( &self, session: Arc>, args: &[BString], _stdin: &mut dyn Read, stdout: &mut dyn Write, ) -> Result { if args.is_empty() { return Err(Error::Exit(-1)); } else if args.len() > 1 { return Err(Error::Exit(-2)); } let se = session.lock().unwrap(); if let Some(val) = se.vars.lookup(&args[0]) { stdout.write_all(val.as_ref())?; Ok(()) } else { Err(Error::Exit(1)) } } } #[derive(Copy, Clone)] pub struct alias; impl Builtin for alias { fn name(&self) -> &str { "alias" } fn io( &self, session: Arc>, mut args: &[BString], _stdin: &mut dyn Read, stdout: &mut dyn Write, ) -> Result { let Some(alias_name) = args.first() else { writeln!(stdout, "alias: expected alias name")?; return Err(Error::Exit(-1)); }; args = &args[1..]; if let Some(b"=") = args.first().map(|x| x.as_slice()) { args = &args[1..]; } if args.is_empty() { writeln!(stdout, "alias: expected alias definition")?; return Err(Error::Exit(-1)); }; let mut parse_fail = false; let mut alias_args = Vec::new(); for arg in args { match as crate::parse::Parse>::parse_from_bytes(&arg[..]) { Ok(mut parsed) => { alias_args.append(&mut parsed); } Err(err) => { writeln!(stdout, "alias: unparseable argument ({err:?}): ",)?; stdout.write_all(&arg)?; parse_fail = true; } } } if parse_fail { return Err(Error::Exit(-1)); } session.lock().unwrap().aliases.insert( alias_name.clone(), AliasBody { unparsed: args.iter().fold(BString::new(), |mut a, b| { if !a.is_empty() { a.push(b' '); } a.push_all(&b); a }), parsed: alias_args, }, ); Ok(()) } } #[derive(Copy, Clone)] pub struct unalias; impl Builtin for unalias { fn name(&self) -> &str { "unalias" } fn io( &self, session: Arc>, args: &[BString], _stdin: &mut dyn Read, stdout: &mut dyn Write, ) -> Result { if args.len() != 1 { writeln!(stdout, "unalias: expecting exactly one argument")?; return Err(Error::Exit(-1)); } session.lock().unwrap().aliases.remove(&args[0]); Ok(()) } } #[derive(Copy, Clone)] pub struct terminfo; impl Builtin for terminfo { fn name(&self) -> &str { "terminfo" } fn io( &self, _session: Arc>, _args: &[BString], _stdin: &mut dyn Read, f: &mut dyn Write, ) -> Result { let ti = crate::ansi::ti(); writeln!(f, "# Booleans")?; for k in ti.booleans.iter() { writeln!(f, "{k}")?; } writeln!(f)?; writeln!(f, "# Numbers")?; for (k, v) in ti.numbers.iter() { writeln!(f, "{k} {v}")?; } writeln!(f)?; writeln!(f, "# Strings")?; for (k, v) in ti.strings.iter() { writeln!(f, "{k} {}", v.escape_ascii())?; } writeln!(f)?; Ok(()) } } #[derive(Clone)] pub struct bind { key: Option>, } impl bind { pub const fn new() -> Self { Self { key: None } } fn is_interactive(args: &[BString]) -> bool { matches!( args.first().map(|x| &x[..]), Some(b"i" | b"interactive" | b"del" | b"delete") ) } } impl Builtin for bind { fn name(&self) -> &str { "bind" } fn special(&mut self, _session: Arc>, args: &[BString]) { if Self::is_interactive(args) { let raw = crate::raw::ScopedRawMode::on_fd(0); raw.enable(); self.key = crate::ansi::read(false); raw.disable(); } } fn io( &self, session: Arc>, args: &[BString], _stdin: &mut dyn Read, stdout: &mut dyn Write, ) -> Result { let mut usage = || { writeln!( stdout, "usage: bind | bind ti NAMED_KEYBIND COMMAND | bind key KEY COMMAND | bind [i|interactive] | bind [del|delete]" )?; Err(Error::Exit(1)) }; if args.is_empty() { let mut dump = |map: &HashMap>, category: &str| -> std::io::Result<()> { let mut entries: Vec<_> = map.iter().collect(); entries.sort_by_key(|x| x.0); for (key, cmd) in entries.iter() { write!(stdout, "bind {category} {} ", key.escape_ascii())?; stdout.write_all(&cmd.cmd)?; for arg in cmd.args.iter() { stdout.write_all(b" ")?; stdout.write_all(arg)?; } writeln!(stdout)?; } Ok(()) }; let se = session.lock().unwrap(); dump(&se.ti_keybinds, "ti ")?; dump(&se.ascii_keybinds, "key")?; return Ok(()); } let kind = &args[0]; let mut se = session.lock().unwrap(); let bind0 = |map: &mut HashMap, key: &[u8], cmd: &[BString]| { map.insert( key.to_vec(), crate::parse::Command { cmd: cmd[0].clone(), args: cmd[1..].to_vec(), }, ); }; let bind1 = |map: &mut HashMap<_, _>| { bind0(map, &args[1], &args[2..]); }; if Self::is_interactive(args) { let Some(key) = self.key.as_ref() else { writeln!(stdout, "no input available.")?; return Err(Error::Exit(-1)); }; if kind.starts_with(b"del") { match key { ansi::KbInput::Escape(e) => { se.ti_keybinds.remove(e.keys[0].as_bytes()); writeln!( stdout, "deleting keybind ti {}", e.keys[0].as_bytes().escape_ascii() )?; } _ => { se.ascii_keybinds.remove(key.as_bytes()); writeln!( stdout, "deleting keybind key {}", key.as_bytes().escape_ascii() )?; } } } else { let x = match key { ansi::KbInput::Escape(e) => { bind0(&mut se.ti_keybinds, e.keys[0].as_bytes(), &args[1..]); format!("ti {}", e.keys[0]) } _ => { bind0(&mut se.ascii_keybinds, key.as_bytes(), &args[1..]); format!("key {}", key.as_bytes().escape_ascii()) } }; // print what gets bound write!(stdout, "bind {x} ")?; for arg in args.iter().skip(1) { stdout.write_all(&arg[..])?; write!(stdout, " ")?; } writeln!(stdout)?; } return Ok(()); } match &kind[..] { b"ti" => bind1(&mut se.ti_keybinds), b"key" => bind1(&mut se.ascii_keybinds), _ => return usage(), } Ok(()) } } #[derive(Clone)] pub struct exit; impl Builtin for exit { fn name(&self) -> &str { "exit" } fn special(&mut self, _session: Arc>, args: &[BString]) { fn parse_exit_code(x: Option<&BString>) -> i32 { let Some(arg) = x else { return 0; }; let Ok(arg) = String::from_utf8(arg.clone()) else { return 1; }; let Ok(num) = arg.parse() else { return 1; }; num } println!("bye!\r"); std::process::exit(parse_exit_code(args.first())); } fn io( &self, _session: Arc>, _args: &[BString], _stdin: &mut dyn Read, _stdout: &mut dyn Write, ) -> Result { Ok(()) } } /// control terminal #[derive(Copy, Clone)] pub struct ct; impl Builtin for ct { fn name(&self) -> &str { "ct" } fn io( &self, session: Arc>, args: &[BString], _stdin: &mut dyn Read, _stdout: &mut dyn Write, ) -> Result { let Some(arg) = args.first() else { return Err(Error::Exit(-1)); }; let mut se = session.lock().unwrap(); match &arg[..] { b"cursor_begin" => se.move_to_begin(), b"cursor_end" => se.move_to_end(), b"cursor_right" => se.cursor_right(), b"cursor_left" => se.cursor_left(), b"cursor_right_word" => se.cursor_right_word(), b"cursor_left_word" => se.cursor_left_word(), b"prompt_clear" => se.prompt_clear(), b"screen_clear" => { drop(se); Session::screen_clear(session); } b"history_previous" => se.history_up(), b"history_next" => se.history_down(), b"prompt_del_left" => se.del_left(), b"prompt_del_right" => se.del_right(), b"prompt_del_left_word" => se.del_left_word(), b"prompt_del_right_word" => se.del_right_word(), b"prompt_del_left_or_previous" => se.del_left_or_previous(), b"prompt_pipe_previous" => se.prompt_pipe_previous(), b"complete" => { drop(se); Session::complete(session) } b"try_submit_command" => { drop(se); Session::try_submit_command(session); } _ => return Err(Error::Exit(-2)), } Ok(()) } } #[derive(Copy, Clone)] pub struct source; impl Builtin for source { fn name(&self) -> &str { "source" } fn io( &self, session: Arc>, args: &[BString], _stdin: &mut dyn Read, _stdout: &mut dyn Write, ) -> Result { for file in args.iter() { // TODO: useful error propagation (?) super::source(session.clone(), file).map_err(|_| Error::Exit(-1))?; } Ok(()) } } #[derive(Copy, Clone)] pub struct Break; impl Builtin for Break { fn name(&self) -> &str { "break" } fn io( &self, _session: Arc>, _args: &[BString], _stdin: &mut dyn Read, _stdout: &mut dyn Write, ) -> Result { Err(Error::Break) } } #[derive(Copy, Clone)] pub struct Continue; impl Builtin for Continue { fn name(&self) -> &str { "continue" } fn io( &self, _session: Arc>, _args: &[BString], _stdin: &mut dyn Read, _stdout: &mut dyn Write, ) -> Result { Err(Error::Continue) } } #[derive(Copy, Clone)] pub struct Here; impl Builtin for Here { fn name(&self) -> &str { "here" } fn io( &self, _session: Arc>, args: &[BString], _stdin: &mut dyn Read, stdout: &mut dyn Write, ) -> Result { if args.is_empty() { return Err(Error::Exit(1)); } stdout.write_all(&args[0])?; Ok(()) } } #[derive(Copy, Clone)] pub struct logo; impl Builtin for logo { fn name(&self) -> &str { "logo" } fn io( &self, _session: Arc>, _args: &[BString], _stdin: &mut dyn Read, stdout: &mut dyn Write, ) -> Result { stdout.write_all(&crate::icon())?; Ok(()) } } #[derive(Copy, Clone)] pub struct export; impl Builtin for export { fn name(&self) -> &str { "export" } fn io( &self, session: Arc>, args: &[BString], _stdin: &mut dyn Read, _stdout: &mut dyn Write, ) -> Result { let mut session = session.lock().unwrap(); for v in args.iter() { session.vars.allow_export(v, true); } Ok(()) } } #[cfg(debug_assertions)] mod dbg { use super::*; #[derive(Copy, Clone)] pub struct debug; impl Builtin for debug { fn name(&self) -> &str { "debug" } fn io( &self, session: Arc>, args: &[BString], _stdin: &mut dyn Read, stdout: &mut dyn Write, ) -> Result { let mut se = session.lock().unwrap(); for arg in args { match &arg[..] { b"keys" | b"keystrokes" => se.debug_keystrokes = !se.debug_keystrokes, _ => { stdout.write_all(b"debug: unknown option ")?; stdout.write_all(&arg)?; } } } Ok(()) } } /// restart shell #[derive(Copy, Clone)] pub struct re; impl Builtin for re { fn name(&self) -> &str { "re" } fn special(&mut self, session: Arc>, _args: &[BString]) { session.lock().unwrap().raw_disable(); crate::reload::begin_reload(); session.lock().unwrap().raw_enable(); // something went wrong, let's restore raw mode } fn io( &self, _session: Arc>, _args: &[BString], _stdin: &mut dyn Read, _stdout: &mut dyn Write, ) -> Result { Ok(()) } } } #[cfg(debug_assertions)] pub use dbg::{debug, re};