1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161
use crate::{ascii::Char, quoter::QuoterSealed, util::u8_to_hex, Quotable, Quoter};
/// Quote byte strings for use with fish.
///
/// The documentation on [quoting][] and [escaping characters][] in fish is
/// confusing at first, especially when coming from a Bourne-like shell, but
/// essentially we have to be able to move and and out of a quoted string
/// context. For example, the escape sequence `\t` for a tab _must_ be outside
/// of quotes, single or double, to be recognised as a tab character by fish:
///
/// ```fish
/// echo 'foo'\t'bar'
/// ```
///
/// This emphasises the importance of using the correct quoting module for the
/// target shell.
///
/// [quoting]: https://fishshell.com/docs/current/language.html#quotes
/// [escaping characters]:
/// https://fishshell.com/docs/current/language.html#escaping-characters
#[derive(Debug, Clone, Copy)]
pub struct Fish;
impl Quoter for Fish {}
/// Expose [`Quoter`] implementation as default impl too, for convenience.
impl QuoterSealed for Fish {
fn quote<'a, S: ?Sized + Into<Quotable<'a>>>(s: S) -> Vec<u8> {
Self::quote(s)
}
fn quote_into<'a, S: ?Sized + Into<Quotable<'a>>>(s: S, sout: &mut Vec<u8>) {
Self::quote_into(s, sout)
}
}
impl Fish {
/// Quote a string of bytes into a new `Vec<u8>`.
///
/// This will return one of the following:
/// - The string as-is, if no escaping is necessary.
/// - An escaped string, like `'foo \'bar'`, `\a'ABC'`
///
/// See [`quote_into`](#method.quote_into) for a variant that extends an
/// existing `Vec` instead of allocating a new one.
///
/// # Examples
///
/// ```
/// # use shell_quote::{Fish, Quoter};
/// assert_eq!(Fish::quote("foobar"), b"foobar");
/// assert_eq!(Fish::quote("foo 'bar"), b"'foo \\'bar'");
/// ```
pub fn quote<'a, S: ?Sized + Into<Quotable<'a>>>(s: S) -> Vec<u8> {
let sin: Quotable<'a> = s.into();
match escape_prepare(sin.bytes) {
Prepared::Empty => vec![b'\'', b'\''],
Prepared::Inert => sin.bytes.into(),
Prepared::Escape(esc) => {
let mut sout = Vec::with_capacity(esc.len() + 2);
escape_chars(esc, &mut sout); // Do the work.
sout
}
}
}
/// Quote a string of bytes into an existing `Vec<u8>`.
///
/// See [`quote`](#method.quote) for more details.
///
/// # Examples
///
/// ```
/// # use shell_quote::{Fish, Quoter};
/// let mut buf = Vec::with_capacity(128);
/// Fish::quote_into("foobar", &mut buf);
/// buf.push(b' '); // Add a space.
/// Fish::quote_into("foo 'bar", &mut buf);
/// assert_eq!(buf, b"foobar 'foo \\'bar'");
/// ```
///
pub fn quote_into<'a, S: ?Sized + Into<Quotable<'a>>>(s: S, sout: &mut Vec<u8>) {
let sin: Quotable<'a> = s.into();
match escape_prepare(sin.bytes) {
Prepared::Empty => sout.extend(b"''"),
Prepared::Inert => sout.extend(sin.bytes),
Prepared::Escape(esc) => {
sout.reserve(esc.len() + 2);
escape_chars(esc, sout); // Do the work.
}
}
}
}
// ----------------------------------------------------------------------------
enum Prepared {
Empty,
Inert,
Escape(Vec<Char>),
}
fn escape_prepare(sin: &[u8]) -> Prepared {
let esc: Vec<_> = sin.iter().map(Char::from).collect();
// An optimisation: if the string is not empty and contains only "safe"
// characters we can avoid further work.
if esc.is_empty() {
Prepared::Empty
} else if esc.iter().all(Char::is_inert) {
Prepared::Inert
} else {
Prepared::Escape(esc)
}
}
fn escape_chars(esc: Vec<Char>, sout: &mut Vec<u8>) {
let mut inside_quotes_now = false;
let mut push_literal = |inside_quotes: bool, literal: &[u8]| {
if inside_quotes_now == inside_quotes {
sout.extend(literal)
} else {
sout.push(b'\'');
inside_quotes_now = inside_quotes;
sout.extend(literal);
}
};
for mode in esc {
use Char::*;
let mut tmp = b"\\x00".to_owned();
match mode {
Bell => push_literal(false, b"\\a"),
Backspace => push_literal(false, b"\\b"),
Escape => push_literal(false, b"\\e"),
FormFeed => push_literal(false, b"\\f"),
NewLine => push_literal(true, b"\n"), // No need to escape newlines in fish
CarriageReturn => push_literal(false, b"\\r"),
HorizontalTab => push_literal(false, b"\\t"),
VerticalTab => push_literal(false, b"\\v"),
Control(ch) => {
tmp[2..].copy_from_slice(&u8_to_hex(ch));
push_literal(false, &tmp[..])
}
Backslash => push_literal(true, b"\\\\"),
SingleQuote => push_literal(true, b"\\'"),
DoubleQuote => push_literal(true, b"\""),
Delete => push_literal(false, b"\\x7F"),
PrintableInert(ch) => push_literal(true, &ch.to_le_bytes()),
Printable(ch) => push_literal(true, &ch.to_le_bytes()),
Extended(ch) => {
tmp[2..].copy_from_slice(&u8_to_hex(ch));
push_literal(false, &tmp[..])
}
}
}
if inside_quotes_now {
if sout.last() == Some(&b'\'') {
sout.pop(); // Remove trailing quote.
} else {
sout.push(b'\'');
}
}
}