diff options
| author | Jonas Maier <jonas@x77.dev> | 2026-05-04 23:28:17 +0200 |
|---|---|---|
| committer | Jonas Maier <jonas@x77.dev> | 2026-05-04 23:28:17 +0200 |
| commit | e56e2d1f9206102068c402a0cadbe62284816586 (patch) | |
| tree | 350b4667b0f14648b10d70f30f682a7ecfbe603e | |
| parent | 44dfa6dd66ed6734754262f7b27f6eff53c068bf (diff) | |
| download | pish-e56e2d1f9206102068c402a0cadbe62284816586.tar.gz | |
completion now should not introduce strings that do not parse as strings
| -rw-r--r-- | src/completion.rs | 49 | ||||
| -rw-r--r-- | src/main.rs | 5 | ||||
| -rw-r--r-- | src/parse/mod.rs | 99 |
3 files changed, 124 insertions, 29 deletions
diff --git a/src/completion.rs b/src/completion.rs index c628bd2..0e24d6d 100644 --- a/src/completion.rs +++ b/src/completion.rs @@ -1,4 +1,4 @@ -use crate::parse; +use crate::parse::{self, CompletionContext}; use crate::{BString, Session}; use std::collections::HashMap; use std::ffi::OsStr; @@ -9,15 +9,21 @@ use std::sync::{Arc, Mutex}; use std::{env, fs}; pub struct Suggestion { + /// display string that is shown in the possibilities pub display: BString, + + /// *escaped* bytes that can be directly appended into terminal. pub delta: BString, } fn _path_completion( - mut prefix: BString, + cc: CompletionContext, filter: &dyn Fn(&DirEntry) -> bool, ) -> std::io::Result<Vec<Suggestion>> { + let delim = cc.delim; + let mut prefix = cc.partial; let mut partial_entry = BString::new(); + while let Some(c) = prefix.last().cloned() { if c == b'/' { break; @@ -40,12 +46,14 @@ fn _path_completion( } let name = entry.file_name().as_bytes().to_vec(); if name.starts_with(&partial_entry) { - let mut delta = name[partial_entry.len()..].to_vec(); + let mut delta = BString::new(); + delim.escape(&name[partial_entry.len()..], &mut delta); let is_dir = entry.metadata().map(|m| m.is_dir()).unwrap_or(false); if is_dir { delta.push(b'/'); } else { + delim.write_closing_delimiter(&mut delta); delta.push(b' '); } @@ -59,8 +67,8 @@ fn _path_completion( Ok(sugs) } -pub fn path_completion(prefix: BString) -> Vec<Suggestion> { - match _path_completion(prefix, &|_| true) { +pub fn path_completion(cc: CompletionContext) -> Vec<Suggestion> { + match _path_completion(cc, &|_| true) { Ok(suggestions) => suggestions, Err(err) => { println!("path completion failed: {err:?}\r"); @@ -69,8 +77,8 @@ pub fn path_completion(prefix: BString) -> Vec<Suggestion> { } } -pub fn path_exe_completion(prefix: BString) -> Vec<Suggestion> { - match _path_completion(prefix, &|d| is_executable(&d.path())) { +pub fn path_exe_completion(cc: CompletionContext) -> Vec<Suggestion> { + match _path_completion(cc, &|d| is_executable(&d.path())) { Ok(suggestions) => suggestions, Err(err) => { println!("path completion failed: {err:?}\r"); @@ -134,7 +142,7 @@ pub fn populate_path_cache(session: Arc<Mutex<Session>>) { session.lock().unwrap().path_cache = PathCache { binaries }; } -pub fn command_completion(session: Arc<Mutex<Session>>, prefix: BString) -> Vec<Suggestion> { +pub fn command_completion(session: Arc<Mutex<Session>>, cc: CompletionContext) -> Vec<Suggestion> { let se = session.lock().unwrap(); let mut out = Vec::new(); for fun in se @@ -143,18 +151,19 @@ pub fn command_completion(session: Arc<Mutex<Session>>, prefix: BString) -> Vec< .chain(se.builtins.keys()) .chain(se.path_cache.binaries.keys()) { - if fun.starts_with(&prefix) { + if fun.starts_with(&cc.partial) { + let mut delta = BString::new(); + cc.delim.escape(&fun[cc.partial.len()..], &mut delta); + cc.delim.write_closing_delimiter(&mut delta); + delta.push(b' '); + out.push(Suggestion { display: fun.to_vec(), - delta: fun[prefix.len()..].to_vec(), + delta, }) } } - for s in out.iter_mut() { - s.delta.push(b' '); - } - out } @@ -180,10 +189,12 @@ pub fn completion(session: Arc<Mutex<Session>>, cmd: &[u8]) -> CompletionResult &mut crate::run::Executor::new_for_completion(session.clone()), ); + let kind = comp.kind.clone(); + let mut suggestions = match comp.kind { - parse::CompletionKind::Command => command_completion(session.clone(), comp.partial), - parse::CompletionKind::PathCommand => path_exe_completion(comp.partial), - parse::CompletionKind::Argument => path_completion(comp.partial), + parse::CompletionKind::Command => command_completion(session.clone(), comp), + parse::CompletionKind::PathCommand => path_exe_completion(comp), + parse::CompletionKind::Argument => path_completion(comp), parse::CompletionKind::Variable => variable_completion(session.clone(), comp.partial), parse::CompletionKind::None => return CompletionResult::empty(), }; @@ -193,7 +204,7 @@ pub fn completion(session: Arc<Mutex<Session>>, cmd: &[u8]) -> CompletionResult if suggestions.is_empty() { return CompletionResult { - kind: comp.kind, + kind, ..CompletionResult::empty() }; } @@ -214,7 +225,7 @@ pub fn completion(session: Arc<Mutex<Session>>, cmd: &[u8]) -> CompletionResult let shared_prefix = shared_prefix.to_vec(); CompletionResult { - kind: comp.kind, + kind, suggestions, shared_prefix, } diff --git a/src/main.rs b/src/main.rs index a54f4fc..e10a8e3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -374,7 +374,10 @@ impl Session { } fn exec_rc_file(se: Arc<Mutex<Session>>) { - let _ = run::source(se, basedir::config_dir().join(".pishrc").as_os_str().as_bytes()); + let _ = run::source( + se, + basedir::config_dir().join(".pishrc").as_os_str().as_bytes(), + ); } fn event_loop() { diff --git a/src/parse/mod.rs b/src/parse/mod.rs index e061d67..02012dc 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -98,14 +98,14 @@ pub fn pipes<const N: usize>(cmds: [Command<PreExpansion>; N]) -> Ast<PreExpansi pub fn estr(x: &[u8]) -> ExpString { ExpString { parts: vec![StringPart::Boring(x.to_vec())], - delim: b' ', + delim: StringDelimiter::None, } } pub fn str<const N: usize>(parts: [StringPart; N]) -> ExpString { ExpString { parts: parts.to_vec(), - delim: b' ', + delim: StringDelimiter::None, } } @@ -480,7 +480,7 @@ impl StringPart { /// `"hi ${var} $(cmd) "` gets mapped to `[Boring("hi "), Var("var"), String(" "), Cmd(...), Boring(" ")]` pub struct ExpString { parts: Vec<StringPart>, - delim: u8, + delim: StringDelimiter, } impl ExpString { @@ -559,8 +559,8 @@ impl Parse for VarName { } } -#[derive(Clone, Debug)] -enum StringDelimiter { +#[derive(Clone, Debug, PartialEq)] +pub enum StringDelimiter { /// no delimiter, i.e. when parsing a simple command like `echo foo` None, @@ -593,6 +593,18 @@ enum StringDelimiter { StrictCustom(BString), } +trait PushAll { + fn push_all(&mut self, other: &bstr); +} + +impl PushAll for BString { + fn push_all(&mut self, other: &bstr) { + for &c in other { + self.push(c); + } + } +} + /// gets the largest ident this slice starts with, might be empty fn peek_ident(b: &[u8]) -> &[u8] { if b.is_empty() || !b[0].is_ascii_alphabetic() { @@ -698,6 +710,61 @@ impl StringDelimiter { fn is_none(&self) -> bool { matches!(self, Self::None) } + + /// assuming that `s` will be placed in the middle of some specifically delimited string + pub fn escape(&self, mut s: &bstr, out: &mut BString) { + while !s.is_empty() { + let first = s[0]; + match self { + StringDelimiter::None => { + if matches!(first, b' ' | b'$' | b'\\' | b'\'' | b'"') { + out.push(b'\\'); + } + } + StringDelimiter::Interp | StringDelimiter::InterpCustom(_) => { + if matches!(first, b'$' | b'\\' | b'"') { + out.push(b'\\'); + } + } + StringDelimiter::Strict => { + if first == b'\'' { + out.push_all(b"'\\'"); + } + } + StringDelimiter::StrictCustom(delim) => { + if s.starts_with(b"'''") && s[3..].starts_with(delim) { + out.push_all(b"'''"); + out.push_all(delim); + out.push_all(b"\\'\\'\\'"); + out.push_all(delim); + out.push_all(b"''"); + out.push_all(delim); + out.push_all(b"'''"); + s = &s[3 + delim.len()..]; + continue; + } + } + } + out.push(first); + s = &s[1..]; + } + } + + pub fn write_closing_delimiter(&self, out: &mut BString) { + match self { + StringDelimiter::None => (), + StringDelimiter::Interp => out.push(b'"'), + StringDelimiter::Strict => out.push(b'\''), + StringDelimiter::InterpCustom(delim) => { + out.push_all(b"\"\"\""); + out.push_all(&delim); + } + StringDelimiter::StrictCustom(delim) => { + out.push_all(b"'''"); + out.push_all(&delim); + } + } + } } fn parse_escape_code(b: &mut Cursor<'_>) -> Result<Option<u8>> { @@ -752,11 +819,15 @@ impl Parse for ExpString { let mut already_parsed = false; + let mut last_delim = StringDelimiter::None; + 'outer: loop { let Some(delim) = StringDelimiter::try_begin(b) else { break; }; + last_delim = delim.clone(); + already_parsed = true; while !delim.try_end(b) { @@ -879,8 +950,11 @@ impl Parse for ExpString { } } - if b.is_completion() || already_parsed { - Ok(Self { parts, delim: b' ' }) + if already_parsed { + Ok(Self { + parts, + delim: last_delim, + }) } else { Err(ParseError::NotAString) } @@ -978,7 +1052,7 @@ pub fn do_parse(x: &[u8]) -> Res<Ast<PreExpansion>, (ParseError, &[u8])> { } } -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Clone)] pub enum CompletionKind { Command, PathCommand, @@ -990,6 +1064,7 @@ pub enum CompletionKind { pub struct CompletionContext { pub kind: CompletionKind, pub partial: BString, + pub delim: StringDelimiter, } impl CompletionContext { @@ -997,6 +1072,7 @@ impl CompletionContext { Self { kind: CompletionKind::None, partial: BString::new(), + delim: StringDelimiter::None, } } } @@ -1029,6 +1105,7 @@ impl ExpString { CompletionContext { kind: CompletionKind::Variable, partial: var.name.name.clone(), + delim: self.delim.clone(), } } else if let Some(StringPart::Cmd(cmd)) = self.parts.last() && !cmd.already_complete @@ -1038,7 +1115,11 @@ impl ExpString { if s.contains(&b'/') && kind == CompletionKind::Command { kind = CompletionKind::PathCommand; } - CompletionContext { kind, partial: s } + CompletionContext { + kind, + partial: s, + delim: self.delim.clone(), + } } else { CompletionContext::none() } |
