diff --git a/src/execute/mod.rs b/src/execute/mod.rs index 9e55459..43c4e06 100644 --- a/src/execute/mod.rs +++ b/src/execute/mod.rs @@ -1,6 +1,4 @@ -use std::os::fd::AsRawFd; - -use crate::{expand::{arithmetic::expand_arith_string, tilde::expand_tilde_string, vars::{expand_string, expand_var}}, prelude::*}; +use crate::{expand::{arithmetic::expand_arith_string, tilde::expand_tilde_string, vars::expand_string}, prelude::*}; use shellenv::jobs::{ChildProc, JobBldr}; pub mod shellcmd; @@ -23,6 +21,7 @@ pub fn exec_input>(input: S, shenv: &mut ShEnv) -> ShResult<()> let parse_time = std::time::Instant::now(); let syn_tree = Parser::new(token_stream,shenv).parse()?; + log!(TRACE,syn_tree); log!(INFO, "Parsing done in {:?}", parse_time.elapsed()); if !shenv.ctx().flags().contains(ExecFlags::IN_FUNC) { shenv.save_io()?; @@ -79,8 +78,6 @@ fn exec_list(list: Vec<(Option, Node)>, shenv: &mut ShEnv) -> ShResult while let Some(cmd_info) = list.fpop() { let guard = cmd_info.0; let cmd = cmd_info.1; - let span = cmd.span(); - let cmd_raw = cmd.as_raw(shenv); if let Some(guard) = guard { let code = shenv.get_code(); @@ -122,7 +119,9 @@ fn dispatch_command(mut node: Node, shenv: &mut ShEnv) -> ShResult<()> { let mut is_subsh = false; let mut is_assign = false; if let NdRule::Command { ref mut argv, redirs: _ } = node.rule_mut() { - *argv = expand_argv(argv.to_vec(), shenv)?; + if !shenv.ctx().flags().contains(ExecFlags::NO_EXPAND) { + *argv = expand_argv(argv.to_vec(), shenv)?; + } let cmd = argv.first().unwrap().as_raw(shenv); if shenv.logic().get_function(&cmd).is_some() { is_func = true; @@ -130,7 +129,9 @@ fn dispatch_command(mut node: Node, shenv: &mut ShEnv) -> ShResult<()> { is_builtin = true; } } else if let NdRule::Subshell { body: _, ref mut argv, redirs: _ } = node.rule_mut() { - *argv = expand_argv(argv.to_vec(), shenv)?; + if !shenv.ctx().flags().contains(ExecFlags::NO_EXPAND) { + *argv = expand_argv(argv.to_vec(), shenv)?; + } is_subsh = true; } else if let NdRule::Assignment { assignments: _, cmd: _ } = node.rule() { is_assign = true; @@ -508,5 +509,6 @@ fn prep_execve(argv: Vec, shenv: &mut ShEnv) -> (Vec, Vec envp.push(formatted); } log!(TRACE, argv_s); + log!(DEBUG, argv_s); (argv_s, envp) } diff --git a/src/expand/alias.rs b/src/expand/alias.rs index 5f674f0..c434550 100644 --- a/src/expand/alias.rs +++ b/src/expand/alias.rs @@ -48,6 +48,15 @@ pub fn expand_aliases(tokens: Vec, shenv: &mut ShEnv) -> Vec { is_command = true; processed.push(token.clone()); } + TkRule::Case | TkRule::For => { + processed.push(token.clone()); + while let Some(token) = stream.next() { + processed.push(token.clone()); + if token.rule() == TkRule::Sep { + break + } + } + } TkRule::Ident if is_command => { is_command = false; let mut alias_tokens = expand_alias(token.clone(), shenv); diff --git a/src/filefilefile.txt b/src/filefilefile.txt new file mode 100644 index 0000000..323fae0 --- /dev/null +++ b/src/filefilefile.txt @@ -0,0 +1 @@ +foobar diff --git a/src/libsh/sys.rs b/src/libsh/sys.rs index 1f331bb..b4f9cea 100644 --- a/src/libsh/sys.rs +++ b/src/libsh/sys.rs @@ -1,4 +1,4 @@ -use std::{fmt::Display, os::fd::AsRawFd}; +use std::{fmt::Display, os::{fd::AsRawFd, unix::fs::PermissionsExt}}; use nix::sys::termios; @@ -6,10 +6,42 @@ use crate::prelude::*; pub const SIG_EXIT_OFFSET: i32 = 128; +pub fn get_path_cmds() -> ShResult> { + let mut cmds = vec![]; + let path_var = std::env::var("PATH")?; + let paths = path_var.split(':'); + + for path in paths { + let path = PathBuf::from(&path); + if path.is_dir() { + let path_files = std::fs::read_dir(&path)?; + for file in path_files { + let file_path = file?.path(); + if file_path.is_file() { + if let Ok(meta) = std::fs::metadata(&file_path) { + let perms = meta.permissions(); + if perms.mode() & 0o111 != 0 { + let file_name = file_path.file_name().unwrap(); + cmds.push(file_name.to_str().unwrap().to_string()) + } + } + } + } + } + } + + Ok(cmds) +} + pub fn get_bin_path(command: &str, shenv: &ShEnv) -> Option { let env = shenv.vars().env(); let path_var = env.get("PATH")?; let mut paths = path_var.split(':'); + + let script_check = PathBuf::from(command); + if script_check.is_file() { + return Some(script_check) + } while let Some(raw_path) = paths.next() { let mut path = PathBuf::from(raw_path); path.push(command); diff --git a/src/main.rs b/src/main.rs index 3e1feb2..eeaedb2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,4 +1,4 @@ -#![allow(unused_unsafe)] +#![allow(static_mut_refs,unused_unsafe)] pub mod libsh; pub mod shellenv; @@ -20,6 +20,14 @@ use crate::prelude::*; pub static mut SAVED_TERMIOS: Option> = None; +bitflags! { + pub struct FernFlags: u32 { + const NO_RC = 0b000001; + const NO_HIST = 0b000010; + const INTERACTIVE = 0b000100; + } +} + pub fn save_termios() { unsafe { SAVED_TERMIOS = Some(if isatty(std::io::stdin().as_raw_fd()).unwrap() { @@ -34,6 +42,8 @@ pub fn save_termios() { } pub fn get_saved_termios() -> Option { unsafe { + // This is only used when the shell exits so it's fine + // SAVED_TERMIOS is only mutated once at the start as well SAVED_TERMIOS.clone().flatten() } } @@ -44,22 +54,68 @@ fn set_termios() { termios::tcsetattr(std::io::stdin(), nix::sys::termios::SetArg::TCSANOW, &termios).unwrap(); } } +fn parse_args(shenv: &mut ShEnv) { + let mut args = std::env::args().skip(1); + let mut script_path: Option = None; + let mut command: Option = None; + let mut flags = FernFlags::empty(); + + log!(DEBUG, args); + while let Some(mut arg) = args.next() { + log!(DEBUG, arg); + if arg.starts_with("--") { + arg = arg.strip_prefix("--").unwrap().to_string(); + match arg.as_str() { + "no-rc" => flags |= FernFlags::NO_RC, + "no-hist" => flags |= FernFlags::NO_HIST, + _ => eprintln!("Warning - Unrecognized option: {arg}") + } + } else if arg.starts_with('-') { + arg = arg.strip_prefix('-').unwrap().to_string(); + match arg.as_str() { + "c" => command = args.next(), + _ => eprintln!("Warning - Unrecognized option: {arg}") + } + } else { + let path_check = PathBuf::from(&arg); + if path_check.is_file() { + script_path = Some(path_check); + } + } + } + + if !flags.contains(FernFlags::NO_RC) { + let _ = shenv.source_rc().eprint(); + } + + if let Some(cmd) = command { + let input = clean_string(cmd); + let _ = exec_input(input, shenv).eprint(); + + } else if let Some(script) = script_path { + let _ = shenv.source_file(script).eprint(); + + } else { + interactive(shenv); + } +} pub fn main() { sig_setup(); save_termios(); set_termios(); let mut shenv = ShEnv::new(); - if let Err(e) = shenv.source_rc() { - eprintln!("Error sourcing rc file: {}", e.to_string()); - } + parse_args(&mut shenv); +} + +fn interactive(shenv: &mut ShEnv) { loop { log!(TRACE, "Entered loop"); - match prompt::read_line(&mut shenv) { + match prompt::read_line(shenv) { Ok(line) => { shenv.meta_mut().start_timer(); - let _ = exec_input(line, &mut shenv).eprint(); + let _ = exec_input(line, shenv).eprint(); } Err(e) => { eprintln!("{}",e); diff --git a/src/parse/lex.rs b/src/parse/lex.rs index 23387a6..f94db0f 100644 --- a/src/parse/lex.rs +++ b/src/parse/lex.rs @@ -71,7 +71,7 @@ impl<'a> Lexer<'a> { rule = TkRule::Ident // If we are in a command right now, after this we are in arguments - } else if self.is_command && rule != TkRule::Whitespace && !KEYWORDS.contains(&rule) { + } else if self.is_command && !matches!(rule, TkRule::Comment | TkRule::Whitespace) && !KEYWORDS.contains(&rule) { self.is_command = false; } // If we see a separator like && or ;, we are now in a command again @@ -295,7 +295,7 @@ impl TkRule { } tkrule_def!(Comment, |input: &str| { - let mut chars = input.chars(); + let mut chars = input.chars().peekable(); let mut len = 0; if let Some('#') = chars.next() { @@ -304,6 +304,14 @@ tkrule_def!(Comment, |input: &str| { let chlen = ch.len_utf8(); len += chlen; if ch == '\n' { + while let Some(ch) = chars.peek() { + if *ch == '\n' { + len += 1; + chars.next(); + } else { + break + } + } break } } @@ -743,31 +751,53 @@ tkrule_def!(SQuote, |input: &str| { // Double quoted strings let mut chars = input.chars(); let mut len = 0; - let mut quoted = false; + let mut quote_count = 0; while let Some(ch) = chars.next() { match ch { '\\' => { - chars.next(); - len += 2; + len += 1; + if let Some(ch) = chars.next() { + let chlen = ch.len_utf8(); + len += chlen; + } } - '\'' if !quoted => { + '\'' => { let chlen = ch.len_utf8(); len += chlen; - quoted = true; + quote_count += 1; } - '\'' if quoted => { + ' ' | '\t' | ';' | '\n' if quote_count % 2 == 0 => { + if quote_count > 0 { + if quote_count % 2 == 0 { + return Some(len) + } else { + return None + } + } else { + return None + } + } + _ => { let chlen = ch.len_utf8(); len += chlen; - return Some(len) } - _ if !quoted => { - return None - } - _ => len += 1 } } - None + match len { + 0 => None, + _ => { + if quote_count > 0 { + if quote_count % 2 == 0 { + return Some(len) + } else { + return None + } + } else { + return None + } + } + } }); tkrule_def!(DQuote, |input: &str| { diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 0dd296a..4d72e6c 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -158,18 +158,18 @@ impl<'a> Parser<'a> { pub fn parse(mut self) -> ShResult { log!(TRACE, "Starting parse"); - let mut lists = VecDeque::new(); + let mut lists = vec![]; let token_slice = &*self.token_stream; // Get the Main rule if let Some(mut node) = Main::try_match(token_slice,self.shenv)? { // Extract the inner lists if let NdRule::Main { ref mut cmd_lists } = node.rule_mut() { while let Some(node) = cmd_lists.pop() { - lists.bpush(node) + lists.push(node) } } } - while let Some(node) = lists.bpop() { + while let Some(node) = lists.pop() { // Push inner command lists to self.ast self.ast.push_node(node); } diff --git a/src/prelude.rs b/src/prelude.rs index 1edf234..8891dbb 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -98,6 +98,7 @@ pub use crate::{ }, sys::{ self, + get_path_cmds, get_bin_path, sh_quit, read_to_string, diff --git a/src/prompt/comp.rs b/src/prompt/comp.rs new file mode 100644 index 0000000..1b22e3e --- /dev/null +++ b/src/prompt/comp.rs @@ -0,0 +1,86 @@ +use rustyline::completion::{Candidate, Completer}; + +use crate::{expand::cmdsub::expand_cmdsub_string, parse::lex::KEYWORDS, prelude::*}; + +use super::readline::SynHelper; + +impl<'a> Completer for SynHelper<'a> { + type Candidate = String; + fn complete( &self, line: &str, pos: usize, ctx: &rustyline::Context<'_>,) -> rustyline::Result<(usize, Vec)> { + let mut shenv = self.shenv.clone(); + let mut comps = vec![]; + shenv.new_input(line); + let mut token_stream = Lexer::new(line.to_string(), &mut shenv).lex(); + if let Some(comp_token) = token_stream.pop() { + let raw = comp_token.as_raw(&mut shenv); + let is_cmd = if let Some(token) = token_stream.pop() { + match token.rule() { + TkRule::Sep => true, + _ if KEYWORDS.contains(&token.rule()) => true, + _ => false + } + } else { + true + }; + if let TkRule::Ident | TkRule::Whitespace = comp_token.rule() { + if is_cmd { + let cmds = shenv.meta().path_cmds(); + comps.extend(cmds.iter().map(|cmd| cmd.to_string())); + comps.retain(|cmd| cmd.starts_with(&raw)); + if !comps.is_empty() && comps.len() > 1 { + if get_bin_path("fzf", &self.shenv).is_some() { + if let Some(mut selection) = fzf_comp(&comps, &mut shenv) { + while selection.starts_with(&raw) { + selection = selection.strip_prefix(&raw).unwrap().to_string(); + } + comps = vec![selection]; + } + } + } else if let Some(mut comp) = comps.pop() { + while comp.starts_with(&raw) { + comp = comp.strip_prefix(&raw).unwrap().to_string(); + } + comps = vec![comp]; + } + return Ok((pos,comps)) + } else { + let (start, matches) = self.file_comp.complete(line, pos, ctx)?; + comps.extend(matches.iter().map(|c| c.display().to_string())); + + if !comps.is_empty() && comps.len() > 1 { + if get_bin_path("fzf", &self.shenv).is_some() { + if let Some(selection) = fzf_comp(&comps, &mut shenv) { + return Ok((start, vec![selection])) + } else { + return Ok((start, comps)) + } + } else { + return Ok((start, comps)) + } + } else if let Some(comp) = comps.pop() { + // Slice off the already typed bit + return Ok((start, vec![comp])) + } + } + } + } + Ok((pos,comps)) + } +} + +pub fn fzf_comp(comps: &[String], shenv: &mut ShEnv) -> Option { + // All of the fzf wrapper libraries suck + // So we gotta do this now + let echo_args = comps.join("\n"); + let echo = format!("echo \"{echo_args}\""); + let fzf = "fzf --height=~30% --layout=reverse --border --border-label=completion"; + let command = format!("{echo} | {fzf}"); + + shenv.ctx_mut().set_flag(ExecFlags::NO_EXPAND); // Prevent any pesky shell injections with filenames like '$(rm -rf /)' + let selection = expand_cmdsub_string(&command, shenv).ok()?; + if selection.is_empty() { + None + } else { + Some(selection.trim().to_string()) + } +} diff --git a/src/prompt/mod.rs b/src/prompt/mod.rs index d120a9d..0e86940 100644 --- a/src/prompt/mod.rs +++ b/src/prompt/mod.rs @@ -5,6 +5,7 @@ use rustyline::{config::Configurer, history::{DefaultHistory, History}, ColorMod pub mod readline; pub mod highlight; pub mod validate; +pub mod comp; fn init_rl<'a>(shenv: &'a mut ShEnv) -> Editor, DefaultHistory> { let hist_path = std::env::var("FERN_HIST").unwrap_or_default(); diff --git a/src/prompt/readline.rs b/src/prompt/readline.rs index ae841cc..59c3f35 100644 --- a/src/prompt/readline.rs +++ b/src/prompt/readline.rs @@ -1,11 +1,10 @@ -use rustyline::{completion::{Completer, FilenameCompleter}, hint::{Hint, Hinter}, history::{History, SearchDirection}, Helper}; +use rustyline::{completion::{Candidate, Completer, FilenameCompleter}, hint::{Hint, Hinter}, history::{History, SearchDirection}, Helper}; use crate::prelude::*; pub struct SynHelper<'a> { - file_comp: FilenameCompleter, + pub file_comp: FilenameCompleter, pub shenv: &'a mut ShEnv, - pub commands: Vec } impl<'a> Helper for SynHelper<'a> {} @@ -15,7 +14,6 @@ impl<'a> SynHelper<'a> { Self { file_comp: FilenameCompleter::new(), shenv, - commands: vec![] } } @@ -35,12 +33,6 @@ impl<'a> SynHelper<'a> { -impl<'a> Completer for SynHelper<'a> { - type Candidate = String; - fn complete( &self, line: &str, pos: usize, ctx: &rustyline::Context<'_>,) -> rustyline::Result<(usize, Vec)> { - Ok((0,vec![])) - } -} pub struct SynHint { text: String, diff --git a/src/shellenv/exec_ctx.rs b/src/shellenv/exec_ctx.rs index 37f9dd7..4a4e3ea 100644 --- a/src/shellenv/exec_ctx.rs +++ b/src/shellenv/exec_ctx.rs @@ -3,8 +3,9 @@ use crate::prelude::*; bitflags! { #[derive(Copy,Clone,Debug,PartialEq,PartialOrd)] pub struct ExecFlags: u32 { - const NO_FORK = 0x00000001; - const IN_FUNC = 0x00000010; + const NO_FORK = 0b00000001; + const IN_FUNC = 0b00000010; + const NO_EXPAND = 0b00000100; } } diff --git a/src/shellenv/meta.rs b/src/shellenv/meta.rs index 88ef225..4b99ef1 100644 --- a/src/shellenv/meta.rs +++ b/src/shellenv/meta.rs @@ -5,13 +5,16 @@ use crate::prelude::*; pub struct MetaTab { timer_start: Instant, last_runtime: Option, + path_cmds: Vec // Used for command completion } impl MetaTab { pub fn new() -> Self { + let path_cmds = get_path_cmds().unwrap_or_default(); Self { timer_start: Instant::now(), last_runtime: None, + path_cmds } } pub fn start_timer(&mut self) { @@ -23,4 +26,7 @@ impl MetaTab { pub fn get_runtime(&self) -> Option { self.last_runtime } + pub fn path_cmds(&self) -> &[String] { + &self.path_cmds + } }