Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve UX, i.e. work with paths, OS strings, references, etc. #7

Merged
merged 1 commit into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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