From f279159873c718c603cd576ce924a35769b47658 Mon Sep 17 00:00:00 2001 From: pagedmov Date: Wed, 11 Mar 2026 18:48:07 -0400 Subject: [PATCH] tab completion and glob results are now properly escaped before being parsed --- src/expand.rs | 69 ++++++++++++++++++++++++++++++++++++++-- src/main.rs | 17 +--------- src/readline/complete.rs | 16 ++++------ 3 files changed, 75 insertions(+), 27 deletions(-) diff --git a/src/expand.rs b/src/expand.rs index aa01f26..46a51c2 100644 --- a/src/expand.rs +++ b/src/expand.rs @@ -86,6 +86,11 @@ impl Expander { 'outer: while let Some(ch) = chars.next() { match ch { + markers::ESCAPE => { + if let Some(next_ch) = chars.next() { + cur_word.push(next_ch); + } + } markers::DUB_QUOTE | markers::SNG_QUOTE | markers::SUBSH => { while let Some(q_ch) = chars.next() { match q_ch { @@ -634,8 +639,10 @@ pub fn expand_glob(raw: &str) -> ShResult { { let entry = entry.map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid filename found in glob"))?; + let entry_raw = entry.to_str().ok_or_else(|| ShErr::simple(ShErrKind::SyntaxErr, "Non-UTF8 filename found in glob"))?; + let escaped = escape_str(entry_raw, true); - words.push(entry.to_str().unwrap().to_string()) + words.push(escaped) } Ok(words.join(" ")) } @@ -989,6 +996,7 @@ pub fn unescape_str(raw: &str) -> String { '~' if first_char => result.push(markers::TILDE_SUB), '\\' => { if let Some(next_ch) = chars.next() { + result.push(markers::ESCAPE); result.push(next_ch) } } @@ -1311,6 +1319,62 @@ pub fn unescape_str(raw: &str) -> String { result } +/// Opposite of unescape_str - escapes a string to be executed as literal text +/// Used for completion results, and glob filename matches. +pub fn escape_str(raw: &str, use_marker: bool) -> String { + let mut result = String::new(); + let mut chars = raw.chars(); + + while let Some(ch) = chars.next() { + match ch { + '\''| + '"' | + '\\' | + '|' | + '&' | + ';' | + '(' | + ')' | + '<' | + '>' | + '$' | + '*' | + '!' | + '`' | + '{' | + '?' | + '[' | + '#' | + ' ' | + '\t'| + '\n' => { + if use_marker { + result.push(markers::ESCAPE); + } else { + result.push('\\'); + } + result.push(ch); + continue; + } + '~' if result.is_empty() => { + if use_marker { + result.push(markers::ESCAPE); + } else { + result.push('\\'); + } + result.push(ch); + continue; + } + _ => { + result.push(ch); + continue; + } + } + } + + result +} + pub fn unescape_math(raw: &str) -> String { let mut chars = raw.chars().peekable(); let mut result = String::new(); @@ -2858,7 +2922,8 @@ mod tests { #[test] fn unescape_backslash() { let result = unescape_str("hello\\nworld"); - assert_eq!(result, "hellonworld"); + let expected = format!("hello{}nworld", markers::ESCAPE); + assert_eq!(result, expected); } #[test] diff --git a/src/main.rs b/src/main.rs index ffdcf03..cbf7cf3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -42,7 +42,7 @@ use crate::readline::{Prompt, ReadlineEvent, ShedVi}; use crate::signal::{GOT_SIGWINCH, JOB_DONE, QUIT_CODE, check_signals, sig_setup, signals_pending}; use crate::state::{AutoCmdKind, read_logic, read_shopts, source_rc, write_jobs, write_meta, write_shopts}; use clap::Parser; -use state::{read_vars, write_vars}; +use state::write_vars; #[derive(Parser, Debug)] struct ShedArgs { @@ -64,20 +64,6 @@ struct ShedArgs { login_shell: bool, } -/// Force evaluation of lazily-initialized values early in shell startup. -/// -/// In particular, this ensures that the variable table is initialized, which -/// populates environment variables from the system. If this initialization is -/// deferred too long, features like prompt expansion may fail due to missing -/// environment variables. -/// -/// This function triggers initialization by calling `read_vars` with a no-op -/// closure, which forces access to the variable table and causes its `LazyLock` -/// constructor to run. -fn kickstart_lazy_evals() { - read_vars(|_| {}); -} - /// We need to make sure that even if we panic, our child processes get sighup fn setup_panic_handler() { let default_panic_hook = std::panic::take_hook(); @@ -112,7 +98,6 @@ fn setup_panic_handler() { fn main() -> ExitCode { yansi::enable(); env_logger::init(); - kickstart_lazy_evals(); setup_panic_handler(); let mut args = ShedArgs::parse(); diff --git a/src/readline/complete.rs b/src/readline/complete.rs index c0a05e8..b716379 100644 --- a/src/readline/complete.rs +++ b/src/readline/complete.rs @@ -8,21 +8,17 @@ use std::{ use nix::sys::signal::Signal; use crate::{ - builtin::complete::{CompFlags, CompOptFlags, CompOpts}, - libsh::{error::ShResult, guards::var_ctx_guard, sys::TTY_FILENO, utils::TkVecUtils}, - parse::{ + builtin::complete::{CompFlags, CompOptFlags, CompOpts}, expand::escape_str, libsh::{error::ShResult, guards::var_ctx_guard, sys::TTY_FILENO, utils::TkVecUtils}, parse::{ execute::exec_input, lex::{self, LexFlags, Tk, TkRule, ends_with_unescaped}, - }, - readline::{ + }, readline::{ Marker, annotate_input_recursive, keys::{KeyCode as C, KeyEvent as K, ModKeys as M}, linebuf::{ClampedUsize, LineBuf}, markers::{self, is_marker}, term::{LineWriter, TermWriter, calc_str_width, get_win_size}, vimode::{ViInsert, ViMode}, - }, - state::{VarFlags, VarKind, read_jobs, read_logic, read_meta, read_shopts, read_vars, write_vars}, + }, state::{VarFlags, VarKind, read_jobs, read_logic, read_meta, read_shopts, read_vars, write_vars} }; pub fn complete_signals(start: &str) -> Vec { @@ -1176,13 +1172,14 @@ impl Completer for FuzzyCompleter { log::debug!("Getting completed line for candidate: {}", _candidate); let selected = self.selector.selected_candidate().unwrap_or_default(); + let escaped = escape_str(&selected, false); log::debug!("Selected candidate: {}", selected); let (start, end) = self.completer.token_span; log::debug!("Token span: ({}, {})", start, end); let ret = format!( "{}{}{}", &self.completer.original_input[..start], - selected, + escaped, &self.completer.original_input[end..] ); log::debug!("Completed line: {}", ret); @@ -1435,11 +1432,12 @@ impl SimpleCompleter { } let selected = &self.candidates[self.selected_idx]; + let escaped = escape_str(selected, false); let (start, end) = self.token_span; format!( "{}{}{}", &self.original_input[..start], - selected, + escaped, &self.original_input[end..] ) }