tab completion and glob results are now properly escaped before being parsed

This commit is contained in:
2026-03-11 18:48:07 -04:00
parent bb3db444db
commit f279159873
3 changed files with 75 additions and 27 deletions

View File

@@ -86,6 +86,11 @@ impl Expander {
'outer: while let Some(ch) = chars.next() { 'outer: while let Some(ch) = chars.next() {
match ch { match ch {
markers::ESCAPE => {
if let Some(next_ch) = chars.next() {
cur_word.push(next_ch);
}
}
markers::DUB_QUOTE | markers::SNG_QUOTE | markers::SUBSH => { markers::DUB_QUOTE | markers::SNG_QUOTE | markers::SUBSH => {
while let Some(q_ch) = chars.next() { while let Some(q_ch) = chars.next() {
match q_ch { match q_ch {
@@ -634,8 +639,10 @@ pub fn expand_glob(raw: &str) -> ShResult<String> {
{ {
let entry = let entry =
entry.map_err(|_| ShErr::simple(ShErrKind::SyntaxErr, "Invalid filename found in glob"))?; 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(" ")) Ok(words.join(" "))
} }
@@ -989,6 +996,7 @@ pub fn unescape_str(raw: &str) -> String {
'~' if first_char => result.push(markers::TILDE_SUB), '~' if first_char => result.push(markers::TILDE_SUB),
'\\' => { '\\' => {
if let Some(next_ch) = chars.next() { if let Some(next_ch) = chars.next() {
result.push(markers::ESCAPE);
result.push(next_ch) result.push(next_ch)
} }
} }
@@ -1311,6 +1319,62 @@ pub fn unescape_str(raw: &str) -> String {
result 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 { pub fn unescape_math(raw: &str) -> String {
let mut chars = raw.chars().peekable(); let mut chars = raw.chars().peekable();
let mut result = String::new(); let mut result = String::new();
@@ -2858,7 +2922,8 @@ mod tests {
#[test] #[test]
fn unescape_backslash() { fn unescape_backslash() {
let result = unescape_str("hello\\nworld"); let result = unescape_str("hello\\nworld");
assert_eq!(result, "hellonworld"); let expected = format!("hello{}nworld", markers::ESCAPE);
assert_eq!(result, expected);
} }
#[test] #[test]

View File

@@ -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::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 crate::state::{AutoCmdKind, read_logic, read_shopts, source_rc, write_jobs, write_meta, write_shopts};
use clap::Parser; use clap::Parser;
use state::{read_vars, write_vars}; use state::write_vars;
#[derive(Parser, Debug)] #[derive(Parser, Debug)]
struct ShedArgs { struct ShedArgs {
@@ -64,20 +64,6 @@ struct ShedArgs {
login_shell: bool, 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 /// We need to make sure that even if we panic, our child processes get sighup
fn setup_panic_handler() { fn setup_panic_handler() {
let default_panic_hook = std::panic::take_hook(); let default_panic_hook = std::panic::take_hook();
@@ -112,7 +98,6 @@ fn setup_panic_handler() {
fn main() -> ExitCode { fn main() -> ExitCode {
yansi::enable(); yansi::enable();
env_logger::init(); env_logger::init();
kickstart_lazy_evals();
setup_panic_handler(); setup_panic_handler();
let mut args = ShedArgs::parse(); let mut args = ShedArgs::parse();

View File

@@ -8,21 +8,17 @@ use std::{
use nix::sys::signal::Signal; use nix::sys::signal::Signal;
use crate::{ use crate::{
builtin::complete::{CompFlags, CompOptFlags, CompOpts}, builtin::complete::{CompFlags, CompOptFlags, CompOpts}, expand::escape_str, libsh::{error::ShResult, guards::var_ctx_guard, sys::TTY_FILENO, utils::TkVecUtils}, parse::{
libsh::{error::ShResult, guards::var_ctx_guard, sys::TTY_FILENO, utils::TkVecUtils},
parse::{
execute::exec_input, execute::exec_input,
lex::{self, LexFlags, Tk, TkRule, ends_with_unescaped}, lex::{self, LexFlags, Tk, TkRule, ends_with_unescaped},
}, }, readline::{
readline::{
Marker, annotate_input_recursive, Marker, annotate_input_recursive,
keys::{KeyCode as C, KeyEvent as K, ModKeys as M}, keys::{KeyCode as C, KeyEvent as K, ModKeys as M},
linebuf::{ClampedUsize, LineBuf}, linebuf::{ClampedUsize, LineBuf},
markers::{self, is_marker}, markers::{self, is_marker},
term::{LineWriter, TermWriter, calc_str_width, get_win_size}, term::{LineWriter, TermWriter, calc_str_width, get_win_size},
vimode::{ViInsert, ViMode}, 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<String> { pub fn complete_signals(start: &str) -> Vec<String> {
@@ -1176,13 +1172,14 @@ impl Completer for FuzzyCompleter {
log::debug!("Getting completed line for candidate: {}", _candidate); log::debug!("Getting completed line for candidate: {}", _candidate);
let selected = self.selector.selected_candidate().unwrap_or_default(); let selected = self.selector.selected_candidate().unwrap_or_default();
let escaped = escape_str(&selected, false);
log::debug!("Selected candidate: {}", selected); log::debug!("Selected candidate: {}", selected);
let (start, end) = self.completer.token_span; let (start, end) = self.completer.token_span;
log::debug!("Token span: ({}, {})", start, end); log::debug!("Token span: ({}, {})", start, end);
let ret = format!( let ret = format!(
"{}{}{}", "{}{}{}",
&self.completer.original_input[..start], &self.completer.original_input[..start],
selected, escaped,
&self.completer.original_input[end..] &self.completer.original_input[end..]
); );
log::debug!("Completed line: {}", ret); log::debug!("Completed line: {}", ret);
@@ -1435,11 +1432,12 @@ impl SimpleCompleter {
} }
let selected = &self.candidates[self.selected_idx]; let selected = &self.candidates[self.selected_idx];
let escaped = escape_str(selected, false);
let (start, end) = self.token_span; let (start, end) = self.token_span;
format!( format!(
"{}{}{}", "{}{}{}",
&self.original_input[..start], &self.original_input[..start],
selected, escaped,
&self.original_input[end..] &self.original_input[end..]
) )
} }