Skip to content

Commit

Permalink
Improve UX, i.e. work with paths, OS strings, references, etc.
Browse files Browse the repository at this point in the history
  • Loading branch information
allenap committed Nov 27, 2023
1 parent 75e7a00 commit d49a870
Show file tree
Hide file tree
Showing 8 changed files with 400 additions and 52 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"cSpell.language": "en,en-GB",
"cSpell.words": [
"bindir",
"bstr",
"Clippy",
"dtolnay",
"execing",
Expand Down
4 changes: 4 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ readme = "README.md"
repository = "https://github.com/allenap/shell-quote"
version = "0.4.0"

[features]
default = ["bstr"]

[dependencies]
bstr = { version = "1", optional = true }

[dev-dependencies]
criterion = { version = "^0.5.1", features = ["html_reports"] }
Expand Down
19 changes: 11 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,25 +21,28 @@ some limitations and caveats.
When quoting using raw bytes it can be convenient to call [`Bash`]'s and
[`Sh`]'s associated functions directly:

```
```rust
use shell_quote::{Bash, Sh};
assert_eq!(Bash::quote("foobar"), b"foobar");
assert_eq!(Sh::quote("foobar"), b"foobar");
assert_eq!(Bash::quote("foo bar"), b"$'foo bar'");
assert_eq!(Sh::quote("foo bar"), b"'foo bar'");
```

It's also possible to use the extension trait [`QuoteExt`]:
It's also possible to use the extension trait [`QuoteRefExt`] which provides a
[`quoted`] function:

```
use shell_quote::{Bash, Sh, QuoteExt};
assert_eq!(Vec::quoted(Bash, "foo bar"), b"$'foo bar'");
assert_eq!(Vec::quoted(Sh, "foo bar"), b"'foo bar'");
```rust
use shell_quote::{Bash, Sh, QuoteRefExt};
let quoted: Vec<u8> = "foo bar".quoted(Bash);
assert_eq!(quoted, b"$'foo bar'");
let quoted: Vec<u8> = "foo bar".quoted(Sh);
assert_eq!(quoted, b"'foo bar'");
```

or, to construct something more elaborate:
Or the extension trait [`QuoteExt`] for pushing quoted strings into a buffer:

```
```rust
use shell_quote::{Bash, QuoteExt};
let mut script: String = "echo ".into();
script.push_quoted(Bash, "foo bar");
Expand Down
22 changes: 11 additions & 11 deletions src/bash.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{ascii::Char, quoter::QuoterSealed, Quoter};
use crate::{ascii::Char, quoter::QuoterSealed, Quotable, Quoter};

/// Quote byte strings for use with Bash, the GNU Bourne-Again Shell.
///
Expand Down Expand Up @@ -68,10 +68,10 @@ impl Quoter for Bash {}

/// Expose [`Quoter`] implementation as default impl too, for convenience.
impl QuoterSealed for Bash {
fn quote<S: ?Sized + AsRef<[u8]>>(s: &S) -> Vec<u8> {
fn quote<'a, S: ?Sized + Into<Quotable<'a>>>(s: S) -> Vec<u8> {
Self::quote(s)
}
fn quote_into<S: ?Sized + AsRef<[u8]>>(s: &S, sout: &mut Vec<u8>) {
fn quote_into<'a, S: ?Sized + Into<Quotable<'a>>>(s: S, sout: &mut Vec<u8>) {
Self::quote_into(s, sout)
}
}
Expand All @@ -97,11 +97,11 @@ impl Bash {
/// [ansi-c-quoting]:
/// https://www.gnu.org/software/bash/manual/html_node/ANSI_002dC-Quoting.html
///
pub fn quote<S: ?Sized + AsRef<[u8]>>(s: &S) -> Vec<u8> {
let sin = s.as_ref();
match escape_prepare(sin) {
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.into(),
Prepared::Inert => sin.bytes.into(),
Prepared::Escape(esc) => {
// This may be a pointless optimisation, but calculate the
// memory needed to avoid reallocations as we construct the
Expand Down Expand Up @@ -130,11 +130,11 @@ impl Bash {
/// assert_eq!(buf, b"foobar $'foo bar'");
/// ```
///
pub fn quote_into<S: ?Sized + AsRef<[u8]>>(s: &S, sout: &mut Vec<u8>) {
let sin = s.as_ref();
match escape_prepare(sin) {
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),
Prepared::Inert => sout.extend(sin.bytes),
Prepared::Escape(esc) => {
// This may be a pointless optimisation, but calculate the
// memory needed to avoid reallocations as we construct the
Expand Down
178 changes: 156 additions & 22 deletions src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#![doc = include_str!("../README.md")]

use std::ffi::{OsStr, OsString};
use std::path::{Path, PathBuf};

mod ascii;
mod bash;
Expand All @@ -9,65 +10,110 @@ mod sh;
pub use bash::Bash;
pub use sh::Sh;

/// Extension trait for shell quoting byte slices (e.g. `&[u8]`, `&str`) into
/// byte container types like [`Vec<u8>`], [`String`] (and [`OsString`] on
/// Unix).
/// Extension trait for pushing shell quoted byte slices, e.g. `&[u8]`, [`&str`]
/// – anything that's [`Quotable`] – into byte container types like [`Vec<u8>`],
/// [`String`], [`OsString`] on Unix, and [`bstr::BString`] if it's enabled
pub trait QuoteExt {
fn push_quoted<Q: Quoter, S: ?Sized + AsRef<[u8]>>(&mut self, q: Q, s: &S);
fn quoted<Q: Quoter, S: ?Sized + AsRef<[u8]>>(q: Q, s: &S) -> Self;
fn push_quoted<'a, Q: Quoter, S: ?Sized + Into<Quotable<'a>>>(&mut self, q: Q, s: S);
}

impl QuoteExt for Vec<u8> {
fn push_quoted<Q: Quoter, S: ?Sized + AsRef<[u8]>>(&mut self, _q: Q, s: &S) {
fn push_quoted<'a, Q: Quoter, S: ?Sized + Into<Quotable<'a>>>(&mut self, _q: Q, s: S) {
Q::quote_into(s, self);
}

fn quoted<Q: Quoter, S: ?Sized + AsRef<[u8]>>(_q: Q, s: &S) -> Self {
Q::quote(s)
}
}

#[cfg(unix)]
impl QuoteExt for OsString {
fn push_quoted<Q: Quoter, S: ?Sized + AsRef<[u8]>>(&mut self, _q: Q, s: &S) {
fn push_quoted<'a, Q: Quoter, S: ?Sized + Into<Quotable<'a>>>(&mut self, _q: Q, s: S) {
use std::os::unix::ffi::OsStrExt;
let quoted = Q::quote(s);
self.push(OsStr::from_bytes(&quoted))
}
}

fn quoted<Q: Quoter, S: ?Sized + AsRef<[u8]>>(_q: Q, s: &S) -> Self {
use std::os::unix::ffi::OsStringExt;
let quoted = Q::quote(s);
OsString::from_vec(quoted)
#[cfg(feature = "bstr")]
impl QuoteExt for bstr::BString {
fn push_quoted<'a, Q: Quoter, S: ?Sized + Into<Quotable<'a>>>(&mut self, _q: Q, s: S) {
Q::quote_into(s, self)
}
}

impl QuoteExt for String {
fn push_quoted<Q: Quoter, S: ?Sized + AsRef<[u8]>>(&mut self, _q: Q, s: &S) {
fn push_quoted<'a, Q: Quoter, S: ?Sized + Into<Quotable<'a>>>(&mut self, _q: Q, s: S) {
let quoted = Q::quote(s);
// SAFETY: `quoted` is valid UTF-8 (ASCII, in truth) because it was
// generated by a `quote` implementation from this crate –
// enforced because `Quoter` is sealed.
let quoted = unsafe { std::str::from_utf8_unchecked(&quoted) };
self.push_str(quoted);
}
}

fn quoted<Q: Quoter, S: ?Sized + AsRef<[u8]>>(_q: Q, s: &S) -> Self {
let quoted = Q::quote(s);
// ----------------------------------------------------------------------------

/// Extension trait for shell quoting many different owned and reference types,
/// e.g. `&[u8]`, [`&str`] – anything that's [`Quotable`] – into owned types
/// like [`Vec<u8>`], [`String`], [`OsString`] on Unix, and [`bstr::BString`] if
/// it's enabled.
pub trait QuoteRefExt<Output> {
fn quoted<Q: Quoter>(self, q: Q) -> Output;
}

impl<'a, S> QuoteRefExt<Vec<u8>> for S
where
S: ?Sized + Into<Quotable<'a>>,
{
fn quoted<Q: Quoter>(self, _q: Q) -> Vec<u8> {
Q::quote(self)
}
}

#[cfg(unix)]
impl<'a, S> QuoteRefExt<OsString> for S
where
S: ?Sized + Into<Quotable<'a>>,
{
fn quoted<Q: Quoter>(self, _q: Q) -> OsString {
use std::os::unix::ffi::OsStringExt;
let quoted = Q::quote(self);
OsString::from_vec(quoted)
}
}

#[cfg(feature = "bstr")]
impl<'a, S> QuoteRefExt<bstr::BString> for S
where
S: ?Sized + Into<Quotable<'a>>,
{
fn quoted<Q: Quoter>(self, _q: Q) -> bstr::BString {
let quoted = Q::quote(self);
bstr::BString::from(quoted)
}
}

impl<'a, S> QuoteRefExt<String> for S
where
S: ?Sized + Into<Quotable<'a>>,
{
fn quoted<Q: Quoter>(self, _q: Q) -> String {
let quoted = Q::quote(self);
// SAFETY: `quoted` is valid UTF-8 (ASCII, in truth) because it was
// generated by a `quote` implementation from this crate –
// enforced because `Quoter` is sealed.
unsafe { String::from_utf8_unchecked(quoted) }
}
}

// ----------------------------------------------------------------------------

pub(crate) mod quoter {
pub trait QuoterSealed {
/// Quote/escape a string of bytes into a new `Vec<u8>`.
fn quote<S: ?Sized + AsRef<[u8]>>(s: &S) -> Vec<u8>;
/// Quote/escape a string of bytes into a new [`Vec<u8>`].
fn quote<'a, S: ?Sized + Into<super::Quotable<'a>>>(s: S) -> Vec<u8>;

/// Quote/escape a string of bytes into an existing `Vec<u8>`.
fn quote_into<S: ?Sized + AsRef<[u8]>>(s: &S, sout: &mut Vec<u8>);
/// Quote/escape a string of bytes into an existing [`Vec<u8>`].
fn quote_into<'a, S: ?Sized + Into<super::Quotable<'a>>>(s: S, sout: &mut Vec<u8>);
}
}

Expand All @@ -78,3 +124,91 @@ pub(crate) mod quoter {
/// that quoted bytes are valid UTF-8, and that is only possible if the
/// implementation is known and tested in advance.
pub trait Quoter: quoter::QuoterSealed {}

// ----------------------------------------------------------------------------

/// A string of bytes that can be quoted/escaped.
///
/// This is used by many methods in this crate as a generic `Into<Quotable>`
/// constraint. Why not accept [`AsRef<[u8]>`] instead? The ergonomics of that
/// approach were not so good. For example, quoting [`OsString`]/[`OsStr`] and
/// [`PathBuf`]/[`Path`] didn't work in a natural way.
pub struct Quotable<'a> {
pub(crate) bytes: &'a [u8],
}

impl<'a> From<&'a [u8]> for Quotable<'a> {
fn from(source: &'a [u8]) -> Quotable<'a> {
Quotable { bytes: source }
}
}

impl<'a, const N: usize> From<&'a [u8; N]> for Quotable<'a> {
fn from(source: &'a [u8; N]) -> Quotable<'a> {
Quotable { bytes: &source[..] }
}
}

impl<'a> From<&'a Vec<u8>> for Quotable<'a> {
fn from(source: &'a Vec<u8>) -> Quotable<'a> {
Quotable { bytes: source }
}
}

impl<'a> From<&'a str> for Quotable<'a> {
fn from(source: &'a str) -> Quotable<'a> {
source.as_bytes().into()
}
}

impl<'a> From<&'a String> for Quotable<'a> {
fn from(source: &'a String) -> Quotable<'a> {
source.as_bytes().into()
}
}

#[cfg(unix)]
impl<'a> From<&'a OsStr> for Quotable<'a> {
fn from(source: &'a OsStr) -> Quotable<'a> {
use std::os::unix::ffi::OsStrExt;
source.as_bytes().into()
}
}

#[cfg(unix)]
impl<'a> From<&'a OsString> for Quotable<'a> {
fn from(source: &'a OsString) -> Quotable<'a> {
use std::os::unix::ffi::OsStrExt;
source.as_bytes().into()
}
}

#[cfg(feature = "bstr")]
impl<'a> From<&'a bstr::BStr> for Quotable<'a> {
fn from(source: &'a bstr::BStr) -> Quotable<'a> {
let bytes: &[u8] = source.as_ref();
bytes.into()
}
}

#[cfg(feature = "bstr")]
impl<'a> From<&'a bstr::BString> for Quotable<'a> {
fn from(source: &'a bstr::BString) -> Quotable<'a> {
let bytes: &[u8] = source.as_ref();
bytes.into()
}
}

#[cfg(unix)]
impl<'a> From<&'a Path> for Quotable<'a> {
fn from(source: &'a Path) -> Quotable<'a> {
source.as_os_str().into()
}
}

#[cfg(unix)]
impl<'a> From<&'a PathBuf> for Quotable<'a> {
fn from(source: &'a PathBuf) -> Quotable<'a> {
source.as_os_str().into()
}
}
Loading

0 comments on commit d49a870

Please sign in to comment.