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() {
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<String> {
{
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]

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::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();

View File

@@ -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<String> {
@@ -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..]
)
}