shell_quote/bash.rs
1#![cfg(feature = "bash")]
2
3use crate::{Quotable, QuoteInto};
4
5/// Quote byte strings for use with Bash, the GNU Bourne-Again Shell.
6///
7/// # Compatibility
8///
9/// Quoted/escaped strings produced by [`Bash`] work in both Bash and Z Shell.
10///
11/// # ⚠️ Warning regarding `NUL`
12///
13/// It is _possible_ to encode NUL in a Bash string, but Bash appears to then
14/// truncate the rest of the string after that point **or** sometimes it filters
15/// the NUL out. It's not yet clear to me when/why each behaviour is chosen.
16///
17/// If you're quoting UTF-8 content this may not be a problem since there is
18/// only one code point – the null character itself – that will ever produce a
19/// NUL byte. To avoid this problem entirely, consider using [Modified
20/// UTF-8][modified-utf-8] so that the NUL byte can never appear in a valid byte
21/// stream.
22///
23/// [modified-utf-8]: https://en.wikipedia.org/wiki/UTF-8#Modified_UTF-8
24///
25/// # ⚠️ Warning regarding `%` and job control in Bash
26///
27/// A word beginning with `%` **cannot be made safe** to use as a **command
28/// name** in Bash, by this crate or by any other means. In command position
29/// Bash reads such a word as a [job specification][job-control]: `%1` on its
30/// own is shorthand for `fg %1`.
31///
32/// Quoting does not prevent it, because Bash tests the word's _value_, after
33/// expansion and quote removal. Every one of these runs `fg`:
34///
35/// ```bash
36/// %1 '%1' "%1" $'%1' \%1 ""%1
37/// ```
38///
39/// Nor is it conditional on job control being enabled. Bash rewrites the word
40/// either way; all that changes is which complaint you get, `fg: no job
41/// control` or `fg: %1: no such job`. It behaves this same way with job control
42/// off, with `set -m`, with `set +m`, under `--posix`, and in Bash 3.2 as
43/// shipped by Apple. Put an executable named `%1` on `PATH` and Bash will not
44/// run it, though it will run it happily by an explicit path.
45///
46/// It is the first character that matters, and only the first: `%1`, `%foo`,
47/// `%`, `%%`, `%+`, `%-`, and `%?x` are all intercepted, while `a%1` is an
48/// ordinary command name.
49///
50/// Z Shell differs: it tests the literal token, so there quoting _does_ prevent
51/// it, and all of the above but the first are ordinary command names. This
52/// crate quotes `%` unconditionally, which is what Z Shell needs, and is at
53/// worst harmless in Bash.
54///
55/// Keep it in proportion, though: this bites only in command position. As an
56/// **argument** – which is what this crate is mostly used for – `%` is an
57/// ordinary character in both shells, and `printf %s $'%1'` prints `%1` as you
58/// would expect.
59///
60/// If you are interpolating an untrusted string into command position, quoting
61/// is not sufficient protection in any case; prefer to invoke a known command
62/// and pass the string as an argument.
63///
64/// [job-control]:
65/// https://www.gnu.org/software/bash/manual/html_node/Job-Control-Basics.html
66///
67/// # Notes
68///
69/// From bash(1):
70///
71/// Words of the form $'string' are treated specially. The word expands to
72/// string, with backslash-escaped characters replaced as specified by the
73/// ANSI C standard. Backslash escape sequences, if present, are decoded as
74/// follows:
75///
76/// ```text
77/// \a alert (bell)
78/// \b backspace
79/// \e an escape character
80/// \f form feed
81/// \n new line
82/// \r carriage return
83/// \t horizontal tab
84/// \v vertical tab
85/// \\ backslash
86/// \' single quote
87/// \nnn the eight-bit character whose value is the
88/// octal value nnn (one to three digits)
89/// \xHH the eight-bit character whose value is the
90/// hexadecimal value HH (one or two hex digits)
91/// \cx a control-x character
92/// ```
93///
94/// Bash allows, in newer versions, for non-ASCII Unicode characters with
95/// `\uHHHH` and `\UXXXXXXXX` syntax inside these [ANSI C quoted
96/// strings][ansi-c-quoting], but we avoid this and work only with bytes. Part
97/// of the problem is that it's not clear how Bash then works with these
98/// strings. Does it encode these characters into bytes according to the user's
99/// current locale? Are strings in Bash now natively Unicode?
100///
101/// For now it's up to the caller to figure out encoding. A significant use case
102/// for this code is to quote filenames into scripts, and on *nix variants I
103/// understand that filenames are essentially arrays of bytes, even if the OS
104/// adds some normalisation and case-insensitivity on top.
105///
106/// [ansi-c-quoting]:
107/// https://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html
108///
109#[derive(Debug, Clone, Copy)]
110pub struct Bash;
111
112// ----------------------------------------------------------------------------
113
114impl QuoteInto<Vec<u8>> for Bash {
115 fn quote_into<'q, S: Into<Quotable<'q>>>(s: S, out: &mut Vec<u8>) {
116 Self::quote_into_vec(s, out);
117 }
118}
119
120impl QuoteInto<String> for Bash {
121 fn quote_into<'q, S: Into<Quotable<'q>>>(s: S, out: &mut String) {
122 Self::quote_into_vec(s, unsafe { out.as_mut_vec() })
123 }
124}
125
126#[cfg(unix)]
127impl QuoteInto<std::ffi::OsString> for Bash {
128 fn quote_into<'q, S: Into<Quotable<'q>>>(s: S, out: &mut std::ffi::OsString) {
129 use std::os::unix::ffi::OsStringExt;
130 let s = Self::quote_vec(s);
131 let s = std::ffi::OsString::from_vec(s);
132 out.push(s);
133 }
134}
135
136#[cfg(feature = "bstr")]
137impl QuoteInto<bstr::BString> for Bash {
138 fn quote_into<'q, S: Into<Quotable<'q>>>(s: S, out: &mut bstr::BString) {
139 let s = Self::quote_vec(s);
140 out.extend(s);
141 }
142}
143
144// ----------------------------------------------------------------------------
145
146impl Bash {
147 /// Quote a string of bytes into a new `Vec<u8>`.
148 ///
149 /// This will return one of the following:
150 /// - The string as-is, if no escaping is necessary.
151 /// - An [ANSI-C escaped string][ansi-c-quoting], like `$'foo\nbar'`.
152 ///
153 /// See [`quote_into_vec`][`Self::quote_into_vec`] for a variant that
154 /// extends an existing `Vec` instead of allocating a new one.
155 ///
156 /// # Examples
157 ///
158 /// ```
159 /// # use shell_quote::Bash;
160 /// assert_eq!(Bash::quote_vec("foobar"), b"foobar");
161 /// assert_eq!(Bash::quote_vec("foo bar"), b"$'foo bar'");
162 /// ```
163 ///
164 /// [ansi-c-quoting]:
165 /// https://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html
166 ///
167 pub fn quote_vec<'a, S: Into<Quotable<'a>>>(s: S) -> Vec<u8> {
168 // Here, previously, in the `Escape` cases, an optimisation
169 // precalculated the required capacity of the output `Vec` to avoid
170 // reallocations later on, but benchmarks showed that it was slower. It
171 // _may_ have lowered maximum RAM required, but that was not measured.
172 match s.into() {
173 Quotable::Bytes(bytes) => match bytes::escape_prepare(bytes) {
174 bytes::Prepared::Empty => vec![b'\'', b'\''],
175 bytes::Prepared::Inert => bytes.into(),
176 bytes::Prepared::Escape(esc) => {
177 let mut sout = Vec::new();
178 bytes::escape_chars(esc, &mut sout);
179 sout
180 }
181 },
182 Quotable::Text(text) => match text::escape_prepare(text) {
183 text::Prepared::Empty => vec![b'\'', b'\''],
184 text::Prepared::Inert => text.into(),
185 text::Prepared::Escape(esc) => {
186 let mut sout = Vec::new();
187 text::escape_chars(esc, &mut sout);
188 sout
189 }
190 },
191 }
192 }
193
194 /// Quote a string of bytes into an existing `Vec<u8>`.
195 ///
196 /// See [`quote_vec`][`Self::quote_vec`] for more details.
197 ///
198 /// # Examples
199 ///
200 /// ```
201 /// # use shell_quote::Bash;
202 /// let mut buf = Vec::with_capacity(128);
203 /// Bash::quote_into_vec("foobar", &mut buf);
204 /// buf.push(b' '); // Add a space.
205 /// Bash::quote_into_vec("foo bar", &mut buf);
206 /// assert_eq!(buf, b"foobar $'foo bar'");
207 /// ```
208 ///
209 pub fn quote_into_vec<'a, S: Into<Quotable<'a>>>(s: S, sout: &mut Vec<u8>) {
210 // Here, previously, in the `Escape` cases, an optimisation
211 // precalculated the required capacity of the output `Vec` to avoid
212 // reallocations later on, but benchmarks showed that it was slower. It
213 // _may_ have lowered maximum RAM required, but that was not measured.
214 match s.into() {
215 Quotable::Bytes(bytes) => match bytes::escape_prepare(bytes) {
216 bytes::Prepared::Empty => sout.extend(b"''"),
217 bytes::Prepared::Inert => sout.extend(bytes),
218 bytes::Prepared::Escape(esc) => bytes::escape_chars(esc, sout),
219 },
220 Quotable::Text(text) => match text::escape_prepare(text) {
221 text::Prepared::Empty => sout.extend(b"''"),
222 text::Prepared::Inert => sout.extend(text.as_bytes()),
223 text::Prepared::Escape(esc) => text::escape_chars(esc, sout),
224 },
225 }
226 }
227}
228
229// ----------------------------------------------------------------------------
230
231mod bytes {
232 use super::u8_to_hex_escape;
233 use crate::ascii::Char;
234
235 pub enum Prepared {
236 Empty,
237 Inert,
238 Escape(Vec<Char>),
239 }
240
241 pub fn escape_prepare(sin: &[u8]) -> Prepared {
242 let esc: Vec<_> = sin.iter().map(Char::from).collect();
243 // An optimisation: if the string is not empty and contains only "safe"
244 // characters we can avoid further work.
245 if esc.is_empty() {
246 Prepared::Empty
247 } else if esc.iter().all(Char::is_inert) {
248 Prepared::Inert
249 } else {
250 Prepared::Escape(esc)
251 }
252 }
253
254 pub fn escape_chars(esc: Vec<Char>, sout: &mut Vec<u8>) {
255 // Push a Bash-style $'...' quoted string into `sout`.
256 sout.extend(b"$'");
257 for mode in esc {
258 use Char::*;
259 match mode {
260 Bell => sout.extend(b"\\a"),
261 Backspace => sout.extend(b"\\b"),
262 Escape => sout.extend(b"\\e"),
263 FormFeed => sout.extend(b"\\f"),
264 NewLine => sout.extend(b"\\n"),
265 CarriageReturn => sout.extend(b"\\r"),
266 HorizontalTab => sout.extend(b"\\t"),
267 VerticalTab => sout.extend(b"\\v"),
268 Control(ch) => sout.extend(&u8_to_hex_escape(ch)),
269 Backslash => sout.extend(b"\\\\"),
270 SingleQuote => sout.extend(b"\\'"),
271 DoubleQuote => sout.extend(b"\""),
272 Delete => sout.extend(b"\\x7F"),
273 PrintableInert(ch) => sout.push(ch),
274 Printable(ch) => sout.push(ch),
275 Extended(ch) => sout.extend(&u8_to_hex_escape(ch)),
276 }
277 }
278 sout.push(b'\'');
279 }
280}
281
282// ----------------------------------------------------------------------------
283
284mod text {
285 use super::u8_to_hex_escape;
286 use crate::utf8::Char;
287
288 pub enum Prepared {
289 Empty,
290 Inert,
291 Escape(Vec<Char>),
292 }
293
294 pub fn escape_prepare(sin: &str) -> Prepared {
295 let esc: Vec<_> = sin.chars().map(Char::from).collect();
296 // An optimisation: if the string is not empty and contains only "safe"
297 // characters we can avoid further work.
298 if esc.is_empty() {
299 Prepared::Empty
300 } else if esc.iter().all(Char::is_inert) {
301 Prepared::Inert
302 } else {
303 Prepared::Escape(esc)
304 }
305 }
306
307 pub fn escape_chars(esc: Vec<Char>, sout: &mut Vec<u8>) {
308 // Push a Bash-style $'...' quoted string into `sout`.
309 sout.extend(b"$'");
310 let buf = &mut [0u8; 4];
311 for mode in esc {
312 use Char::*;
313 match mode {
314 Bell => sout.extend(b"\\a"),
315 Backspace => sout.extend(b"\\b"),
316 Escape => sout.extend(b"\\e"),
317 FormFeed => sout.extend(b"\\f"),
318 NewLine => sout.extend(b"\\n"),
319 CarriageReturn => sout.extend(b"\\r"),
320 HorizontalTab => sout.extend(b"\\t"),
321 VerticalTab => sout.extend(b"\\v"),
322 Control(ch) => sout.extend(&u8_to_hex_escape(ch)),
323 Backslash => sout.extend(b"\\\\"),
324 SingleQuote => sout.extend(b"\\'"),
325 DoubleQuote => sout.extend(b"\""),
326 Delete => sout.extend(b"\\x7F"),
327 PrintableInert(ch) => sout.push(ch),
328 Printable(ch) => sout.push(ch),
329 Utf8(ch) => sout.extend(ch.encode_utf8(buf).as_bytes()),
330 }
331 }
332 sout.push(b'\'');
333 }
334}
335
336// ----------------------------------------------------------------------------
337
338/// Escape a byte as a 4-byte hex escape sequence.
339///
340/// The `\\xHH` format (backslash, a literal "x", two hex characters) is
341/// understood by many shells.
342#[inline]
343fn u8_to_hex_escape(ch: u8) -> [u8; 4] {
344 const HEX_DIGITS: &[u8] = b"0123456789ABCDEF";
345 [
346 b'\\',
347 b'x',
348 HEX_DIGITS[(ch >> 4) as usize],
349 HEX_DIGITS[(ch & 0xF) as usize],
350 ]
351}
352
353#[cfg(test)]
354#[test]
355fn test_u8_to_hex_escape() {
356 for ch in u8::MIN..=u8::MAX {
357 let expected = format!("\\x{ch:02X}");
358 let observed = u8_to_hex_escape(ch);
359 let observed = std::str::from_utf8(&observed).unwrap();
360 assert_eq!(observed, &expected);
361 }
362}