shell_quote/
fish.rs

1#![cfg(feature = "fish")]
2
3use crate::{Quotable, QuoteInto};
4
5/// Quote byte strings for use with fish.
6///
7/// # ⚠️ Warning
8///
9/// Prior to version 3.6.2, fish did not correctly handle some Unicode code
10/// points encoded as UTF-8. From the [version 3.6.2 release notes][]:
11///
12/// > fish uses certain Unicode non-characters internally for marking wildcards
13/// > and expansions. It incorrectly allowed these markers to be read on command
14/// > substitution output, rather than transforming them into a safe internal
15/// > representation.
16///
17/// [version 3.6.2 release notes]:
18///   https://github.com/fish-shell/fish-shell/releases/tag/3.6.2
19///
20/// At present this crate has **no workaround** for this issue. Please use fish
21/// 3.6.2 or later.
22///
23/// # Notes
24///
25/// The documentation on [quoting][] and [escaping characters][] in fish is
26/// confusing at first, especially when coming from a Bourne-like shell, but
27/// essentially we have to be able to move and and out of a quoted string
28/// context. For example, the escape sequence `\t` for a tab _must_ be outside
29/// of quotes, single or double, to be recognised as a tab character by fish:
30///
31/// ```fish
32/// echo 'foo'\t'bar'
33/// ```
34///
35/// This emphasises the importance of using the correct quoting module for the
36/// target shell.
37///
38/// [quoting]: https://fishshell.com/docs/current/language.html#quotes
39/// [escaping characters]:
40///     https://fishshell.com/docs/current/language.html#escaping-characters
41#[derive(Debug, Clone, Copy)]
42pub struct Fish;
43
44impl QuoteInto<Vec<u8>> for Fish {
45    fn quote_into<'q, S: Into<Quotable<'q>>>(s: S, out: &mut Vec<u8>) {
46        Self::quote_into_vec(s, out);
47    }
48}
49
50impl QuoteInto<String> for Fish {
51    fn quote_into<'q, S: Into<Quotable<'q>>>(s: S, out: &mut String) {
52        Self::quote_into_vec(s, unsafe { out.as_mut_vec() })
53    }
54}
55
56#[cfg(unix)]
57impl QuoteInto<std::ffi::OsString> for Fish {
58    fn quote_into<'q, S: Into<Quotable<'q>>>(s: S, out: &mut std::ffi::OsString) {
59        use std::os::unix::ffi::OsStringExt;
60        let s = Self::quote_vec(s);
61        let s = std::ffi::OsString::from_vec(s);
62        out.push(s);
63    }
64}
65
66#[cfg(feature = "bstr")]
67impl QuoteInto<bstr::BString> for Fish {
68    fn quote_into<'q, S: Into<Quotable<'q>>>(s: S, out: &mut bstr::BString) {
69        let s = Self::quote_vec(s);
70        out.extend(s);
71    }
72}
73
74impl Fish {
75    /// Quote a string of bytes into a new `Vec<u8>`.
76    ///
77    /// This will return one of the following:
78    /// - The string as-is, if no escaping is necessary.
79    /// - An escaped string, like `'foo \'bar'`, `\a'ABC'`
80    ///
81    /// See [`quote_into_vec`][`Self::quote_into_vec`] for a variant that
82    /// extends an existing `Vec` instead of allocating a new one.
83    ///
84    /// # Examples
85    ///
86    /// ```
87    /// # use shell_quote::Fish;
88    /// assert_eq!(Fish::quote_vec("foobar"), b"foobar");
89    /// assert_eq!(Fish::quote_vec("foo 'bar"), b"foo' \\'bar'");
90    /// ```
91    pub fn quote_vec<'a, S: Into<Quotable<'a>>>(s: S) -> Vec<u8> {
92        match s.into() {
93            Quotable::Bytes(bytes) => match bytes::escape_prepare(bytes) {
94                bytes::Prepared::Empty => vec![b'\'', b'\''],
95                bytes::Prepared::Inert => bytes.into(),
96                bytes::Prepared::Escape(esc) => {
97                    let mut sout = Vec::new();
98                    bytes::escape_chars(esc, &mut sout);
99                    sout
100                }
101            },
102            Quotable::Text(text) => match text::escape_prepare(text) {
103                text::Prepared::Empty => vec![b'\'', b'\''],
104                text::Prepared::Inert => text.into(),
105                text::Prepared::Escape(esc) => {
106                    let mut sout = Vec::new();
107                    text::escape_chars(esc, &mut sout);
108                    sout
109                }
110            },
111        }
112    }
113
114    /// Quote a string of bytes into an existing `Vec<u8>`.
115    ///
116    /// See [`quote_vec`][`Self::quote_vec`] for more details.
117    ///
118    /// # Examples
119    ///
120    /// ```
121    /// # use shell_quote::Fish;
122    /// let mut buf = Vec::with_capacity(128);
123    /// Fish::quote_into_vec("foobar", &mut buf);
124    /// buf.push(b' ');  // Add a space.
125    /// Fish::quote_into_vec("foo 'bar", &mut buf);
126    /// assert_eq!(buf, b"foobar foo' \\'bar'");
127    /// ```
128    ///
129    pub fn quote_into_vec<'a, S: Into<Quotable<'a>>>(s: S, sout: &mut Vec<u8>) {
130        match s.into() {
131            Quotable::Bytes(bytes) => match bytes::escape_prepare(bytes) {
132                bytes::Prepared::Empty => sout.extend(b"''"),
133                bytes::Prepared::Inert => sout.extend(bytes),
134                bytes::Prepared::Escape(esc) => bytes::escape_chars(esc, sout),
135            },
136            Quotable::Text(text) => match text::escape_prepare(text) {
137                text::Prepared::Empty => sout.extend(b"''"),
138                text::Prepared::Inert => sout.extend(text.as_bytes()),
139                text::Prepared::Escape(esc) => text::escape_chars(esc, sout),
140            },
141        }
142    }
143}
144
145// ----------------------------------------------------------------------------
146
147mod bytes {
148    use super::u8_to_hex_escape_uppercase_x;
149    use crate::ascii::Char;
150
151    pub enum Prepared {
152        Empty,
153        Inert,
154        Escape(Vec<Char>),
155    }
156
157    pub fn escape_prepare(sin: &[u8]) -> Prepared {
158        let esc: Vec<_> = sin.iter().map(Char::from).collect();
159        // An optimisation: if the string is not empty and contains only "safe"
160        // characters we can avoid further work.
161        if esc.is_empty() {
162            Prepared::Empty
163        } else if esc.iter().all(Char::is_inert) {
164            Prepared::Inert
165        } else {
166            Prepared::Escape(esc)
167        }
168    }
169
170    pub fn escape_chars(esc: Vec<Char>, sout: &mut Vec<u8>) {
171        #[derive(PartialEq)]
172        enum QuoteStyle {
173            Inside,
174            Outside,
175            Whatever,
176        }
177        use QuoteStyle::*;
178
179        let mut inside_quotes_now = false;
180        let mut push_literal = |style: QuoteStyle, literal: &[u8]| {
181            match (inside_quotes_now, style) {
182                (true, Outside) => {
183                    sout.push(b'\'');
184                    inside_quotes_now = false;
185                }
186                (false, Inside) => {
187                    sout.push(b'\'');
188                    inside_quotes_now = true;
189                }
190                _ => (),
191            }
192            sout.extend(literal);
193        };
194        for mode in esc {
195            use Char::*;
196            match mode {
197                Bell => push_literal(Outside, b"\\a"),
198                Backspace => push_literal(Outside, b"\\b"),
199                Escape => push_literal(Outside, b"\\e"),
200                FormFeed => push_literal(Outside, b"\\f"),
201                NewLine => push_literal(Outside, b"\\n"),
202                CarriageReturn => push_literal(Outside, b"\\r"),
203                HorizontalTab => push_literal(Outside, b"\\t"),
204                VerticalTab => push_literal(Outside, b"\\v"),
205                Control(ch) => push_literal(Outside, &u8_to_hex_escape_uppercase_x(ch)),
206                Backslash => push_literal(Whatever, b"\\\\"),
207                SingleQuote => push_literal(Whatever, b"\\'"),
208                DoubleQuote => push_literal(Inside, b"\""),
209                Delete => push_literal(Outside, b"\\X7F"),
210                PrintableInert(ch) => push_literal(Whatever, &ch.to_le_bytes()),
211                Printable(ch) => push_literal(Inside, &ch.to_le_bytes()),
212                Extended(ch) => push_literal(Outside, &u8_to_hex_escape_uppercase_x(ch)),
213            }
214        }
215        if inside_quotes_now {
216            sout.push(b'\'');
217        }
218    }
219}
220
221// ----------------------------------------------------------------------------
222
223mod text {
224    use super::u8_to_hex_escape_uppercase_x;
225    use crate::utf8::Char;
226
227    pub enum Prepared {
228        Empty,
229        Inert,
230        Escape(Vec<Char>),
231    }
232
233    pub fn escape_prepare(sin: &str) -> Prepared {
234        let esc: Vec<_> = sin.chars().map(Char::from).collect();
235        // An optimisation: if the string is not empty and contains only "safe"
236        // characters we can avoid further work.
237        if esc.is_empty() {
238            Prepared::Empty
239        } else if esc.iter().all(Char::is_inert) {
240            Prepared::Inert
241        } else {
242            Prepared::Escape(esc)
243        }
244    }
245
246    pub fn escape_chars(esc: Vec<Char>, sout: &mut Vec<u8>) {
247        #[derive(PartialEq)]
248        enum QuoteStyle {
249            Inside,
250            Outside,
251            Whatever,
252        }
253        use QuoteStyle::*;
254
255        let mut inside_quotes_now = false;
256        let mut push_literal = |style: QuoteStyle, literal: &[u8]| {
257            match (inside_quotes_now, style) {
258                (true, Outside) => {
259                    sout.push(b'\'');
260                    inside_quotes_now = false;
261                }
262                (false, Inside) => {
263                    sout.push(b'\'');
264                    inside_quotes_now = true;
265                }
266                _ => (),
267            }
268            sout.extend(literal);
269        };
270        let buf = &mut [0u8; 4];
271        for mode in esc {
272            use Char::*;
273            match mode {
274                Bell => push_literal(Outside, b"\\a"),
275                Backspace => push_literal(Outside, b"\\b"),
276                Escape => push_literal(Outside, b"\\e"),
277                FormFeed => push_literal(Outside, b"\\f"),
278                NewLine => push_literal(Outside, b"\\n"),
279                CarriageReturn => push_literal(Outside, b"\\r"),
280                HorizontalTab => push_literal(Outside, b"\\t"),
281                VerticalTab => push_literal(Outside, b"\\v"),
282                Control(ch) => push_literal(Outside, &u8_to_hex_escape_uppercase_x(ch)),
283                Backslash => push_literal(Whatever, b"\\\\"),
284                SingleQuote => push_literal(Whatever, b"\\'"),
285                DoubleQuote => push_literal(Inside, b"\""),
286                Delete => push_literal(Outside, b"\\X7F"),
287                PrintableInert(ch) => push_literal(Whatever, &ch.to_le_bytes()),
288                Printable(ch) => push_literal(Inside, &ch.to_le_bytes()),
289                Utf8(char) => push_literal(Inside, char.encode_utf8(buf).as_bytes()),
290            }
291        }
292        if inside_quotes_now {
293            sout.push(b'\'');
294        }
295    }
296}
297
298// ----------------------------------------------------------------------------
299
300/// Escape a byte as a 4-byte hex escape sequence _with uppercase "X"_.
301///
302/// The `\\XHH` format (backslash, a literal "X", two hex characters) is
303/// understood by fish. The `\\xHH` format is _also_ understood, but until fish
304/// 3.6.0 it had a weirdness. From the [release notes][]:
305///
306/// > The `\\x` and `\\X` escape syntax is now equivalent. `\\xAB` previously
307/// > behaved the same as `\\XAB`, except that it would error if the value “AB”
308/// > was larger than “7f” (127 in decimal, the highest ASCII value).
309///
310/// [release notes]: https://github.com/fish-shell/fish-shell/releases/tag/3.6.0
311///
312#[inline]
313fn u8_to_hex_escape_uppercase_x(ch: u8) -> [u8; 4] {
314    const HEX_DIGITS: &[u8] = b"0123456789ABCDEF";
315    [
316        b'\\',
317        b'X',
318        HEX_DIGITS[(ch >> 4) as usize],
319        HEX_DIGITS[(ch & 0xF) as usize],
320    ]
321}
322
323#[cfg(test)]
324#[test]
325fn test_u8_to_hex_escape_uppercase_x() {
326    for ch in u8::MIN..=u8::MAX {
327        let expected = format!("\\X{ch:02X}");
328        let observed = u8_to_hex_escape_uppercase_x(ch);
329        let observed = std::str::from_utf8(&observed).unwrap();
330        assert_eq!(observed, &expected);
331    }
332}