#![cfg_attr(not(feature = "std"), no_std)]
extern crate alloc;
use alloc::vec::Vec;
use alloc::borrow::Cow;
use alloc::string::String;
#[cfg(test)]
use alloc::vec;
#[cfg(test)]
use alloc::borrow::ToOwned;
pub mod bytes;
#[cfg(all(doc, not(doctest)))]
#[path = "quoting_warning.md"]
pub mod quoting_warning;
pub struct Shlex<'a>(bytes::Shlex<'a>);
impl<'a> Shlex<'a> {
pub fn new(in_str: &'a str) -> Self {
Self(bytes::Shlex::new(in_str.as_bytes()))
}
}
impl<'a> Iterator for Shlex<'a> {
type Item = String;
fn next(&mut self) -> Option<String> {
self.0.next().map(|byte_word| {
unsafe { String::from_utf8_unchecked(byte_word) }
})
}
}
impl<'a> core::ops::Deref for Shlex<'a> {
type Target = bytes::Shlex<'a>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl<'a> core::ops::DerefMut for Shlex<'a> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
pub fn split(in_str: &str) -> Option<Vec<String>> {
let mut shl = Shlex::new(in_str);
let res = shl.by_ref().collect();
if shl.had_error { None } else { Some(res) }
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum QuoteError {
Nul,
}
impl core::fmt::Display for QuoteError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
QuoteError::Nul => f.write_str("cannot shell-quote string containing nul byte"),
}
}
}
#[cfg(feature = "std")]
impl std::error::Error for QuoteError {}
#[derive(Default, Debug, Clone)]
pub struct Quoter {
inner: bytes::Quoter,
}
impl Quoter {
#[inline]
pub fn new() -> Self {
Self::default()
}
#[inline]
pub fn allow_nul(mut self, allow: bool) -> Self {
self.inner = self.inner.allow_nul(allow);
self
}
pub fn join<'a, I: IntoIterator<Item = &'a str>>(&self, words: I) -> Result<String, QuoteError> {
self.inner.join(words.into_iter().map(|s| s.as_bytes()))
.map(|bytes| unsafe { String::from_utf8_unchecked(bytes) })
}
pub fn quote<'a>(&self, in_str: &'a str) -> Result<Cow<'a, str>, QuoteError> {
Ok(match self.inner.quote(in_str.as_bytes())? {
Cow::Borrowed(out) => {
unsafe { core::str::from_utf8_unchecked(out) }.into()
}
Cow::Owned(out) => {
unsafe { String::from_utf8_unchecked(out) }.into()
}
})
}
}
impl From<bytes::Quoter> for Quoter {
fn from(inner: bytes::Quoter) -> Quoter {
Quoter { inner }
}
}
impl From<Quoter> for bytes::Quoter {
fn from(quoter: Quoter) -> bytes::Quoter {
quoter.inner
}
}
#[deprecated(since = "1.3.0", note = "replace with `try_join(words)?` to avoid nul byte danger")]
pub fn join<'a, I: IntoIterator<Item = &'a str>>(words: I) -> String {
Quoter::new().allow_nul(true).join(words).unwrap()
}
pub fn try_join<'a, I: IntoIterator<Item = &'a str>>(words: I) -> Result<String, QuoteError> {
Quoter::new().join(words)
}
#[deprecated(since = "1.3.0", note = "replace with `try_quote(str)?` to avoid nul byte danger")]
pub fn quote(in_str: &str) -> Cow<str> {
Quoter::new().allow_nul(true).quote(in_str).unwrap()
}
pub fn try_quote(in_str: &str) -> Result<Cow<str>, QuoteError> {
Quoter::new().quote(in_str)
}
#[cfg(test)]
static SPLIT_TEST_ITEMS: &'static [(&'static str, Option<&'static [&'static str]>)] = &[
("foo$baz", Some(&["foo$baz"])),
("foo baz", Some(&["foo", "baz"])),
("foo\"bar\"baz", Some(&["foobarbaz"])),
("foo \"bar\"baz", Some(&["foo", "barbaz"])),
(" foo \nbar", Some(&["foo", "bar"])),
("foo\\\nbar", Some(&["foobar"])),
("\"foo\\\nbar\"", Some(&["foobar"])),
("'baz\\$b'", Some(&["baz\\$b"])),
("'baz\\\''", None),
("\\", None),
("\"\\", None),
("'\\", None),
("\"", None),
("'", None),
("foo #bar\nbaz", Some(&["foo", "baz"])),
("foo #bar", Some(&["foo"])),
("foo#bar", Some(&["foo#bar"])),
("foo\"#bar", None),
("'\\n'", Some(&["\\n"])),
("'\\\\n'", Some(&["\\\\n"])),
];
#[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("\nfoo\nbar");
while let Some(word) = sh.next() {
if word == "bar" {
assert_eq!(sh.line_no, 3);
}
}
}
#[test]
#[cfg_attr(not(feature = "std"), allow(unreachable_code, unused_mut))]
fn test_quote() {
let tests = r#"
<> => <''>
<foobar> => <foobar>
<foo bar> => <'foo bar'>
<"foo bar'"> => <"\"foo bar'\"">
<'foo bar'> => <"'foo bar'">
<"> => <'"'>
<"'> => <"\"'">
<hello!world> => <'hello!world'>
<'hello!world> => <"'hello"'!world'>
<'hello!> => <"'hello"'!'>
<hello ^ world> => <'hello ''^ world'>
<hello^> => <hello'^'>
<!world'> => <'!world'"'">
<{a, b}> => <'{a, b}'>
<NL> => <'NL'>
<^> => <'^'>
<foo^bar> => <foo'^bar'>
<NLx^> => <'NLx''^'>
<NL^x> => <'NL''^x'>
<NL ^x> => <'NL ''^x'>
<{a,b}> => <'{a,b}'>
<a,b> => <'a,b'>
<a..b => <a..b>
<'$> => <"'"'$'>
<"^> => <'"''^'>
"#;
let mut ok = true;
for test in tests.trim().split('\n') {
let parts: Vec<String> = test
.replace("NL", "\n")
.split("=>")
.map(|part| part.trim().trim_start_matches('<').trim_end_matches('>').to_owned())
.collect();
assert!(parts.len() == 2);
let unquoted = &*parts[0];
let quoted_expected = &*parts[1];
let quoted_actual = try_quote(&parts[0]).unwrap();
if quoted_expected != quoted_actual {
#[cfg(not(feature = "std"))]
panic!("FAIL: for input <{}>, expected <{}>, got <{}>",
unquoted, quoted_expected, quoted_actual);
#[cfg(feature = "std")]
println!("FAIL: for input <{}>, expected <{}>, got <{}>",
unquoted, quoted_expected, quoted_actual);
ok = false;
}
}
assert!(ok);
}
#[test]
#[allow(deprecated)]
fn test_join() {
assert_eq!(join(vec![]), "");
assert_eq!(join(vec![""]), "''");
assert_eq!(join(vec!["a", "b"]), "a b");
assert_eq!(join(vec!["foo bar", "baz"]), "'foo bar' baz");
}
#[test]
fn test_fallible() {
assert_eq!(try_join(vec!["\0"]), Err(QuoteError::Nul));
assert_eq!(try_quote("\0"), Err(QuoteError::Nul));
}