cargo / shlex / audit
cargo : shlex @ 1.3.0
PE Patrick Elsen signed 2026-05-27 published 2026-05-27

src/bytes.rs

577 lines · rust · 2 line annotations

// Copyright 2015 Nicholas Allegra (comex).// Licensed under the Apache License, Version 2.0 <https://www.apache.org/licenses/LICENSE-2.0> or// the MIT license <https://opensource.org/licenses/MIT>, at your option. This file may not be// copied, modified, or distributed except according to those terms.//! [`Shlex`] and friends for byte strings.//!//! This is used internally by the [outer module](crate), and may be more//! convenient if you are working with byte slices (`[u8]`) or types that are//! wrappers around bytes, such as [`OsStr`](std::ffi::OsStr)://!//! ```rust//! #[cfg(unix)] {//!     use shlex::bytes::quote;//!     use std::ffi::OsStr;//!     use std::os::unix::ffi::OsStrExt;//!//!     // `\x80` is invalid in UTF-8.//!     let os_str = OsStr::from_bytes(b"a\x80b c");//!     assert_eq!(quote(os_str.as_bytes()), &b"'a\x80b c'"[..]);//! }//! ```//!//! (On Windows, `OsStr` uses 16 bit wide characters so this will not work.)extern crate alloc;use alloc::vec::Vec;use alloc::borrow::Cow;#[cfg(test)]use alloc::vec;#[cfg(test)]use alloc::borrow::ToOwned;#[cfg(all(doc, not(doctest)))]use crate::{self as shlex, quoting_warning};use super::QuoteError;/// An iterator that takes an input byte string and splits it into the words using the same syntax as/// the POSIX shell.pub struct Shlex<'a> {    in_iter: core::slice::Iter<'a, u8>,    /// The number of newlines read so far, plus one.    pub line_no: usize,    /// An input string is erroneous if it ends while inside a quotation or right after an    /// unescaped backslash.  Since Iterator does not have a mechanism to return an error, if that    /// happens, Shlex just throws out the last token, ends the iteration, and sets 'had_error' to    /// true; best to check it after you're done iterating.    pub had_error: bool,}impl<'a> Shlex<'a> {    pub fn new(in_bytes: &'a [u8]) -> Self {        Shlex {            in_iter: in_bytes.iter(),            line_no: 1,            had_error: false,        }    }
    fn parse_word(&mut self, mut ch: u8) -> Option<Vec<u8>> {        let mut result: Vec<u8> = Vec::new();        loop {            match ch as char {                '"' => if let Err(()) = self.parse_double(&mut result) {                    self.had_error = true;                    return None;                },                '\'' => if let Err(()) = self.parse_single(&mut result) {                    self.had_error = true;                    return None;                },                '\\' => if let Some(ch2) = self.next_char() {                    if ch2 != '\n' as u8 { result.push(ch2); }                } else {                    self.had_error = true;                    return None;                },                ' ' | '\t' | '\n' => { break; },                _ => { result.push(ch as u8); },            }            if let Some(ch2) = self.next_char() { ch = ch2; } else { break; }        }        Some(result)    }    fn parse_double(&mut self, result: &mut Vec<u8>) -> Result<(), ()> {        loop {            if let Some(ch2) = self.next_char() {                match ch2 as char {                    '\\' => {                        if let Some(ch3) = self.next_char() {                            match ch3 as char {                                // \$ => $                                '$' | '`' | '"' | '\\' => { result.push(ch3); },                                // \<newline> => nothing                                '\n' => {},                                // \x => =x                                _ => { result.push('\\' as u8); result.push(ch3); }                            }                        } else {                            return Err(());                        }                    },                    '"' => { return Ok(()); },                    _ => { result.push(ch2); },                }            } else {                return Err(());            }        }    }    fn parse_single(&mut self, result: &mut Vec<u8>) -> Result<(), ()> {        loop {            if let Some(ch2) = self.next_char() {                match ch2 as char {                    '\'' => { return Ok(()); },                    _ => { result.push(ch2); },                }            } else {                return Err(());            }        }    }    fn next_char(&mut self) -> Option<u8> {        let res = self.in_iter.next().copied();        if res == Some(b'\n') { self.line_no += 1; }        res    }}impl<'a> Iterator for Shlex<'a> {    type Item = Vec<u8>;    fn next(&mut self) -> Option<Self::Item> {        if let Some(mut ch) = self.next_char() {            // skip initial whitespace            loop {                match ch as char {                    ' ' | '\t' | '\n' => {},                    '#' => {                        while let Some(ch2) = self.next_char() {                            if ch2 as char == '\n' { break; }                        }                    },                    _ => { break; }                }                if let Some(ch2) = self.next_char() { ch = ch2; } else { return None; }            }            self.parse_word(ch)        } else { // no initial character            None        }    }}
Line 60–156

Shlex iterator implements POSIX-shell word splitting with single/double quote and backslash escapes. Errors (unclosed quote, trailing backslash) set had_error and end iteration. Implementation is straightforward byte-by-byte; no indexing arithmetic that could overflow.

/// Convenience function that consumes the whole byte string at once.  Returns None if the input was/// erroneous.pub fn split(in_bytes: &[u8]) -> Option<Vec<Vec<u8>>> {    let mut shl = Shlex::new(in_bytes);    let res = shl.by_ref().collect();    if shl.had_error { None } else { Some(res) }}/// A more configurable interface to quote strings.  If you only want the default settings you can/// use the convenience functions [`try_quote`] and [`try_join`].////// The string equivalent is [`shlex::Quoter`].#[derive(Default, Debug, Clone)]pub struct Quoter {    allow_nul: bool,    // TODO: more options}impl Quoter {    /// Create a new [`Quoter`] with default settings.    #[inline]    pub fn new() -> Self {        Self::default()    }    /// Set whether to allow [nul bytes](quoting_warning#nul-bytes).  By default they are not    /// allowed and will result in an error of [`QuoteError::Nul`].    #[inline]    pub fn allow_nul(mut self, allow: bool) -> Self {        self.allow_nul = allow;        self    }    /// Convenience function that consumes an iterable of words and turns it into a single byte string,    /// quoting words when necessary. Consecutive words will be separated by a single space.    pub fn join<'a, I: IntoIterator<Item = &'a [u8]>>(&self, words: I) -> Result<Vec<u8>, QuoteError> {        Ok(words.into_iter()            .map(|word| self.quote(word))            .collect::<Result<Vec<Cow<[u8]>>, QuoteError>>()?            .join(&b' '))    }    /// Given a single word, return a byte string suitable to encode it as a shell argument.    ///    /// If given valid UTF-8, this will never produce invalid UTF-8. This is because it only    /// ever inserts valid ASCII characters before or after existing ASCII characters (or    /// returns two single quotes if the input was an empty string). It will never modify a    /// multibyte UTF-8 character.
    pub fn quote<'a>(&self, mut in_bytes: &'a [u8]) -> Result<Cow<'a, [u8]>, QuoteError> {        if in_bytes.is_empty() {            // Empty string.  Special case that isn't meaningful as only part of a word.            return Ok(b"''"[..].into());        }        if !self.allow_nul && in_bytes.iter().any(|&b| b == b'\0') {            return Err(QuoteError::Nul);        }        let mut out: Vec<u8> = Vec::new();        while !in_bytes.is_empty() {            // Pick a quoting strategy for some prefix of the input.  Normally this will cover the            // entire input, but in some case we might need to divide the input into multiple chunks            // that are quoted differently.            let (cur_len, strategy) = quoting_strategy(in_bytes);            if cur_len == in_bytes.len() && strategy == QuotingStrategy::Unquoted && out.is_empty() {                // Entire string can be represented unquoted.  Reuse the allocation.                return Ok(in_bytes.into());            }            let (cur_chunk, rest) = in_bytes.split_at(cur_len);            assert!(rest.len() < in_bytes.len()); // no infinite loop            in_bytes = rest;            append_quoted_chunk(&mut out, cur_chunk, strategy);        }        Ok(out.into())    }
Line 206–230

quote operates on bytes and only inserts ASCII delimiters (single/double quote, backslash) around existing bytes. Multi-byte UTF-8 codepoints are preserved because the byte-by-byte logic only inspects bytes < 0x80 for special handling. Implements quoting; tests cover empty, ASCII, and invalid UTF-8 inputs.

}#[derive(PartialEq)]enum QuotingStrategy {    /// No quotes and no backslash escapes.  (If backslash escapes would be necessary, we use a    /// different strategy instead.)    Unquoted,    /// Single quoted.    SingleQuoted,    /// Double quotes, potentially with backslash escapes.    DoubleQuoted,    // TODO: add $'xxx' and "$(printf 'xxx')" styles}/// Is this ASCII byte okay to emit unquoted?const fn unquoted_ok(c: u8) -> bool {    match c as char {        // Allowed characters:        '+' | '-' | '.' | '/' | ':' | '@' | ']' | '_' |        '0'..='9' | 'A'..='Z' | 'a'..='z'        => true,        // Non-allowed characters:        // From POSIX https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html        // "The application shall quote the following characters if they are to represent themselves:"        '|' | '&' | ';' | '<' | '>' | '(' | ')' | '$' | '`' | '\\' | '"' | '\'' | ' ' | '\t' | '\n' |        // "and the following may need to be quoted under certain circumstances[..]:"        '*' | '?' | '[' | '#' | '~' | '=' | '%' |        // Brace expansion.  These ought to be in the POSIX list but aren't yet;        // see: https://www.austingroupbugs.net/view.php?id=1193        '{' | '}' |        // Also quote comma, just to be safe in the extremely odd case that the user of this crate        // is intentionally placing a quoted string inside a brace expansion, e.g.:        //     format!("echo foo{{a,b,{}}}" | shlex::quote(some_str))        ',' |        // '\r' is allowed in a word by all real shells I tested, but is treated as a word        // separator by Python `shlex` | and might be translated to '\n' in interactive mode.        '\r' |        // '!' and '^' are treated specially in interactive mode; see quoting_warning.        '!' | '^' |        // Nul bytes and control characters.        '\x00' ..= '\x1f' | '\x7f'        => false,        '\u{80}' ..= '\u{10ffff}' => {            // This is unreachable since `unquoted_ok` is only called for 0..128.            // Non-ASCII bytes are handled separately in `quoting_strategy`.            // Can't call unreachable!() from `const fn` on old Rust, so...            unquoted_ok(c)        },    }    // Note: The logic cited above for quoting comma might suggest that `..` should also be quoted,    // it as a special case of brace expansion).  But it's not necessary.  There are three cases:    //    // 1. The user wants comma-based brace expansion, but the untrusted string being `quote`d    //    contains `..`, so they get something like `{foo,bar,3..5}`.    //  => That's safe; both Bash and Zsh expand this to `foo bar 3..5` rather than    //     `foo bar 3 4 5`.  The presence of commas disables sequence expression expansion.    //    // 2. The user wants comma-based brace expansion where the contents of the braces are a    //    variable number of `quote`d strings and nothing else.  There happens to be exactly    //    one string and it contains `..`, so they get something like `{3..5}`.    //  => Then this will expand as a sequence expression, which is unintended.  But I don't mind,    //     because any such code is already buggy.  Suppose the untrusted string *didn't* contain    //     `,` or `..`, resulting in shell input like `{foo}`.  Then the shell would interpret it    //     as the literal string `{foo}` rather than brace-expanding it into `foo`.    //    // 3. The user wants a sequence expression and wants to supply an untrusted string as one of    //    the endpoints or the increment.    //  => Well, that's just silly, since the endpoints can only be numbers or single letters.}/// Optimized version of `unquoted_ok`.fn unquoted_ok_fast(c: u8) -> bool {    const UNQUOTED_OK_MASK: u128 = {        // Make a mask of all bytes in 0..<0x80 that pass.        let mut c = 0u8;        let mut mask = 0u128;        while c < 0x80 {            if unquoted_ok(c) {                mask |= 1u128 << c;            }            c += 1;        }        mask    };    ((UNQUOTED_OK_MASK >> c) & 1) != 0}/// Is this ASCII byte okay to emit in single quotes?fn single_quoted_ok(c: u8) -> bool {    match c {        // No single quotes in single quotes.        b'\'' => false,        // To work around a Bash bug, ^ is only allowed right after an opening single quote; see        // quoting_warning.        b'^' => false,        // Backslashes in single quotes are literal according to POSIX, but Fish treats them as an        // escape character.  Ban them.  Fish doesn't aim to be POSIX-compatible, but we *can*        // achieve Fish compatibility using double quotes, so we might as well.        b'\\' => false,        _ => true    }}/// Is this ASCII byte okay to emit in double quotes?fn double_quoted_ok(c: u8) -> bool {    match c {        // Work around Python `shlex` bug where parsing "\`" and "\$" doesn't strip the        // backslash, even though POSIX requires it.        b'`' | b'$' => false,        // '!' and '^' are treated specially in interactive mode; see quoting_warning.        b'!' | b'^' => false,        _ => true    }}/// Given an input, return a quoting strategy that can cover some prefix of the string, along with/// the size of that prefix.////// Precondition: input size is nonzero.  (Empty strings are handled by the caller.)/// Postcondition: returned size is nonzero.#[cfg_attr(manual_codegen_check, inline(never))]fn quoting_strategy(in_bytes: &[u8]) -> (usize, QuotingStrategy) {    const UNQUOTED_OK: u8 = 1;    const SINGLE_QUOTED_OK: u8 = 2;    const DOUBLE_QUOTED_OK: u8 = 4;    let mut prev_ok = SINGLE_QUOTED_OK | DOUBLE_QUOTED_OK | UNQUOTED_OK;    let mut i = 0;    if in_bytes[0] == b'^' {        // To work around a Bash bug, ^ is only allowed right after an opening single quote; see        // quoting_warning.        prev_ok = SINGLE_QUOTED_OK;        i = 1;    }    while i < in_bytes.len() {        let c = in_bytes[i];        let mut cur_ok = prev_ok;        if c >= 0x80 {            // Normally, non-ASCII characters shouldn't require quoting, but see quoting_warning.md            // about \xa0.  For now, just treat all non-ASCII characters as requiring quotes.  This            // also ensures things are safe in the off-chance that you're in a legacy 8-bit locale that            // has additional characters satisfying `isblank`.            cur_ok &= !UNQUOTED_OK;        } else {            if !unquoted_ok_fast(c) {                cur_ok &= !UNQUOTED_OK;            }            if !single_quoted_ok(c){                cur_ok &= !SINGLE_QUOTED_OK;            }            if !double_quoted_ok(c) {                cur_ok &= !DOUBLE_QUOTED_OK;            }        }        if cur_ok == 0 {            // There are no quoting strategies that would work for both the previous characters and            // this one.  So we have to end the chunk before this character.  The caller will call            // `quoting_strategy` again to handle the rest of the string.            break;        }        prev_ok = cur_ok;        i += 1;    }    // Pick the best allowed strategy.    let strategy = if prev_ok & UNQUOTED_OK != 0 {        QuotingStrategy::Unquoted    } else if prev_ok & SINGLE_QUOTED_OK != 0 {        QuotingStrategy::SingleQuoted    } else if prev_ok & DOUBLE_QUOTED_OK != 0 {        QuotingStrategy::DoubleQuoted    } else {        unreachable!()    };    debug_assert!(i > 0);    (i, strategy)}fn append_quoted_chunk(out: &mut Vec<u8>, cur_chunk: &[u8], strategy: QuotingStrategy) {    match strategy {        QuotingStrategy::Unquoted => {            out.extend_from_slice(cur_chunk);        },        QuotingStrategy::SingleQuoted => {            out.reserve(cur_chunk.len() + 2);            out.push(b'\'');            out.extend_from_slice(cur_chunk);            out.push(b'\'');        },        QuotingStrategy::DoubleQuoted => {            out.reserve(cur_chunk.len() + 2);            out.push(b'"');            for &c in cur_chunk.into_iter() {                if let b'$' | b'`' | b'"' | b'\\' = c {                    // Add a preceding backslash.                    // Note: We shouldn't actually get here for $ and ` because they don't pass                    // `double_quoted_ok`.                    out.push(b'\\');                }                // Add the character itself.                out.push(c);            }            out.push(b'"');        },    }}/// Convenience function that consumes an iterable of words and turns it into a single byte string,/// quoting words when necessary. Consecutive words will be separated by a single space.////// Uses default settings except that nul bytes are passed through, which [may be/// dangerous](quoting_warning#nul-bytes), leading to this function being deprecated.////// Equivalent to [`Quoter::new().allow_nul(true).join(words).unwrap()`](Quoter).////// (That configuration never returns `Err`, so this function does not panic.)////// The string equivalent is [shlex::join].#[deprecated(since = "1.3.0", note = "replace with `try_join(words)?` to avoid nul byte danger")]pub fn join<'a, I: IntoIterator<Item = &'a [u8]>>(words: I) -> Vec<u8> {    Quoter::new().allow_nul(true).join(words).unwrap()}/// Convenience function that consumes an iterable of words and turns it into a single byte string,/// quoting words when necessary. Consecutive words will be separated by a single space.////// Uses default settings.  The only error that can be returned is [`QuoteError::Nul`].////// Equivalent to [`Quoter::new().join(words)`](Quoter).////// The string equivalent is [shlex::try_join].pub fn try_join<'a, I: IntoIterator<Item = &'a [u8]>>(words: I) -> Result<Vec<u8>, QuoteError> {    Quoter::new().join(words)}/// Given a single word, return a string suitable to encode it as a shell argument.////// Uses default settings except that nul bytes are passed through, which [may be/// dangerous](quoting_warning#nul-bytes), leading to this function being deprecated.////// Equivalent to [`Quoter::new().allow_nul(true).quote(in_bytes).unwrap()`](Quoter).////// (That configuration never returns `Err`, so this function does not panic.)////// The string equivalent is [shlex::quote].#[deprecated(since = "1.3.0", note = "replace with `try_quote(str)?` to avoid nul byte danger")]pub fn quote(in_bytes: &[u8]) -> Cow<[u8]> {    Quoter::new().allow_nul(true).quote(in_bytes).unwrap()}/// Given a single word, return a string suitable to encode it as a shell argument.////// Uses default settings.  The only error that can be returned is [`QuoteError::Nul`].////// Equivalent to [`Quoter::new().quote(in_bytes)`](Quoter).////// (That configuration never returns `Err`, so this function does not panic.)////// The string equivalent is [shlex::try_quote].pub fn try_quote(in_bytes: &[u8]) -> Result<Cow<[u8]>, QuoteError> {    Quoter::new().quote(in_bytes)}#[cfg(test)]const INVALID_UTF8: &[u8] = b"\xa1";#[cfg(test)]const INVALID_UTF8_SINGLEQUOTED: &[u8] = b"'\xa1'";#[test]#[allow(invalid_from_utf8)]fn test_invalid_utf8() {    // Check that our test string is actually invalid UTF-8.    assert!(core::str::from_utf8(INVALID_UTF8).is_err());}#[cfg(test)]static SPLIT_TEST_ITEMS: &'static [(&'static [u8], Option<&'static [&'static [u8]]>)] = &[    (b"foo$baz", Some(&[b"foo$baz"])),    (b"foo baz", Some(&[b"foo", b"baz"])),    (b"foo\"bar\"baz", Some(&[b"foobarbaz"])),    (b"foo \"bar\"baz", Some(&[b"foo", b"barbaz"])),    (b"   foo \nbar", Some(&[b"foo", b"bar"])),    (b"foo\\\nbar", Some(&[b"foobar"])),    (b"\"foo\\\nbar\"", Some(&[b"foobar"])),    (b"'baz\\$b'", Some(&[b"baz\\$b"])),    (b"'baz\\\''", None),    (b"\\", None),    (b"\"\\", None),    (b"'\\", None),    (b"\"", None),    (b"'", None),    (b"foo #bar\nbaz", Some(&[b"foo", b"baz"])),    (b"foo #bar", Some(&[b"foo"])),    (b"foo#bar", Some(&[b"foo#bar"])),    (b"foo\"#bar", None),    (b"'\\n'", Some(&[b"\\n"])),    (b"'\\\\n'", Some(&[b"\\\\n"])),    (INVALID_UTF8, Some(&[INVALID_UTF8])),];#[test]fn test_split() {    for &(input, output) in SPLIT_TEST_ITEMS {        assert_eq!(split(input), output.map(|o| o.iter().map(|&x| x.to_owned()).collect()));    }}#[test]fn test_lineno() {    let mut sh = Shlex::new(b"\nfoo\nbar");    while let Some(word) = sh.next() {        if word == b"bar" {            assert_eq!(sh.line_no, 3);        }    }}#[test]#[allow(deprecated)]fn test_quote() {    // Validate behavior with invalid UTF-8:    assert_eq!(quote(INVALID_UTF8), INVALID_UTF8_SINGLEQUOTED);    // Replicate a few tests from lib.rs.  No need to replicate all of them.    assert_eq!(quote(b""), &b"''"[..]);    assert_eq!(quote(b"foobar"), &b"foobar"[..]);    assert_eq!(quote(b"foo bar"), &b"'foo bar'"[..]);    assert_eq!(quote(b"'\""), &b"\"'\\\"\""[..]);    assert_eq!(quote(b""), &b"''"[..]);}#[test]#[allow(deprecated)]fn test_join() {    // Validate behavior with invalid UTF-8:    assert_eq!(join(vec![INVALID_UTF8]), INVALID_UTF8_SINGLEQUOTED);    // Replicate a few tests from lib.rs.  No need to replicate all of them.    assert_eq!(join(vec![]), &b""[..]);    assert_eq!(join(vec![&b""[..]]), b"''");}