tab completion and glob results are now properly escaped before being parsed
This commit is contained in:
@@ -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]
|
||||
|
||||
17
src/main.rs
17
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();
|
||||
|
||||
@@ -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..]
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user