Skip to content
Draft
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
26 changes: 23 additions & 3 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,25 @@ on:
jobs:
test:
name: Test
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
# Make sure we have all the target shells installed.
- run: sudo apt-get install -y bash dash fish zsh
- name: Install shells (Ubuntu)
if: runner.os == 'Linux'
run: sudo apt-get install -y bash dash fish zsh
- name: Install shells (Windows)
if: runner.os == 'Windows'
run: choco install git -y --no-progress # `git` includes `bash`.
# Record shell versions. Almost unbelievably, it's not possible to get the
# version of Dash from `dash` itself, so we skip it here. Since `sh` might
# be `dash`, we also do not try to get its version.
- name: Shell versions
- name: Shell versions (Ubuntu)
if: runner.os == 'Linux'
run: |
for shell in sh dash; do
for path in $(type -ap "$shell"); do
Expand All @@ -30,6 +39,17 @@ jobs:
printf "%10s @ %-30q: %s\n" "$shell" "$path" "$version"
done
done
- name: Shell versions (Windows)
if: runner.os == 'Windows'
shell: bash
run: |
for shell in bash; do
if command -v "$shell" &> /dev/null; then
path=$(command -v "$shell")
version=$("$shell" --version | head -n 1)
printf "%10s @ %-30s: %s\n" "$shell" "$path" "$version"
fi
done
# Test in debug mode first.
- run: cargo test
# Test in release mode too, to defend against, for example, use of
Expand Down
6 changes: 3 additions & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,9 @@ sh = []
bstr = { version = "1", optional = true }

[dev-dependencies]
criterion = { version = "^0.5.1", features = ["html_reports"] }
lenient_semver = "0.4.2"
semver = "1.0.23"
criterion = { version = "0.7", features = ["html_reports"] }
lenient_semver = "0.4"
semver = "1.0"
test-case = "3.3.1"

[[bench]]
Expand Down
5 changes: 5 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
.PHONY: test
test:
python3 test.py | bash -xe

.PHONY: check
check:
cargo check --all-targets --all-features
cargo check --all-targets --all-features --target aarch64-pc-windows-msvc
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,15 @@ It can take as input many different string and byte string types:
- [`&str`] and [`String`]
- [`&bstr::BStr`][`bstr::BStr`] and [`bstr::BString`]
- [`&[u8]`][`slice`] and [`Vec<u8>`]
- [`&OsStr`][`OsStr`] and [`OsString`] (on UNIX)
- [`&OsStr`][`OsStr`] and [`OsString`]
- [`&Path`][`Path`] and [`PathBuf`]

and produce output as (or push into) the following types:

- [`String`] (for shells that support it, i.e. not [`Sh`]/[`Dash`])
- [`bstr::BString`]
- [`Vec<u8>`]
- [`OsString`] (on UNIX)
- [`OsString`] (except for [`Sh`])

Inspired by the Haskell [shell-escape][] package.

Expand Down
4 changes: 3 additions & 1 deletion benches/bash.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use std::hint::black_box;

use criterion::{criterion_group, criterion_main, Criterion};

use shell_quote::Bash;

Expand Down
4 changes: 3 additions & 1 deletion benches/fish.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use std::hint::black_box;

use criterion::{criterion_group, criterion_main, Criterion};

use shell_quote::Fish;

Expand Down
4 changes: 3 additions & 1 deletion benches/sh.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use std::hint::black_box;

use criterion::{criterion_group, criterion_main, Criterion};

use shell_quote::Sh;

Expand Down
5 changes: 2 additions & 3 deletions src/bash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,12 +81,11 @@ impl QuoteInto<String> for Bash {
}
}

#[cfg(unix)]
impl QuoteInto<std::ffi::OsString> for Bash {
fn quote_into<'q, S: Into<Quotable<'q>>>(s: S, out: &mut std::ffi::OsString) {
use std::os::unix::ffi::OsStringExt;
let s = Self::quote_vec(s);
let s = std::ffi::OsString::from_vec(s);
// SAFETY: `Bash::quote_vec` produces UTF-8.
let s = unsafe { std::ffi::OsString::from_encoded_bytes_unchecked(s) };
out.push(s);
}
}
Expand Down
5 changes: 2 additions & 3 deletions src/fish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,12 +53,11 @@ impl QuoteInto<String> for Fish {
}
}

#[cfg(unix)]
impl QuoteInto<std::ffi::OsString> for Fish {
fn quote_into<'q, S: Into<Quotable<'q>>>(s: S, out: &mut std::ffi::OsString) {
use std::os::unix::ffi::OsStringExt;
let s = Self::quote_vec(s);
let s = std::ffi::OsString::from_vec(s);
// SAFETY: `Fish::quote_vec` produces UTF-8.
let s = unsafe { std::ffi::OsString::from_encoded_bytes_unchecked(s) };
out.push(s);
}
}
Expand Down
23 changes: 10 additions & 13 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,17 @@
doc = include_str!("../README.md")
)]

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

mod ascii;
mod bash;
mod fish;
mod sh;
mod utf8;

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

#[cfg(feature = "bash")]
pub use bash::Bash;
#[cfg(feature = "fish")]
Expand Down Expand Up @@ -87,7 +89,8 @@ impl<T: QuoteInto<OUT>, OUT: Default> Quote<OUT> for T {}

/// Extension trait for pushing shell quoted byte slices, e.g. `&[u8]`, [`&str`]
/// – anything that's [`Quotable`] – into container types like [`Vec<u8>`],
/// [`String`], [`OsString`] on Unix, and [`bstr::BString`] if it's enabled.
/// [`String`], [`OsString`] where possible, and [`bstr::BString`] if it's
/// enabled.
pub trait QuoteExt {
fn push_quoted<'q, Q, S>(&mut self, _q: Q, s: S)
where
Expand All @@ -109,7 +112,7 @@ impl<T: ?Sized> QuoteExt for T {

/// Extension trait for shell quoting many different owned and reference types,
/// e.g. `&[u8]`, [`&str`] – anything that's [`Quotable`] – into owned container
/// types like [`Vec<u8>`], [`String`], [`OsString`] on Unix, and
/// types like [`Vec<u8>`], [`String`], [`OsString`] where possible, and
/// [`bstr::BString`] if it's enabled.
pub trait QuoteRefExt<Output: Default> {
fn quoted<Q: Quote<Output>>(self, q: Q) -> Output;
Expand Down Expand Up @@ -176,19 +179,15 @@ impl<'a> From<&'a String> for Quotable<'a> {
}
}

#[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()
source.as_encoded_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()
source.as_encoded_bytes().into()
}
}

Expand All @@ -208,14 +207,12 @@ impl<'a> From<&'a bstr::BString> for Quotable<'a> {
}
}

#[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()
Expand Down
28 changes: 9 additions & 19 deletions tests/test_bash.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,66 +81,59 @@ mod bash_impl {
assert_eq!(buffer, b"$'-_=/,.+'");
}

#[cfg(unix)]
#[test_matrix(
(script_bytes, script_text),
("bash", "zsh")
)]
fn test_roundtrip(prepare: fn() -> (OsString, OsString), shell: &str) {
use std::os::unix::ffi::OsStringExt;
let (input, script) = prepare();
for bin in find_bins(shell) {
let output = invoke_shell(&bin, &script).unwrap();
let result = OsString::from_vec(output.stdout);
let result = unsafe { OsString::from_encoded_bytes_unchecked(output.stdout) };
assert_eq!(result, input);
}
}

#[cfg(unix)]
fn script_bytes() -> (OsString, OsString) {
use std::os::unix::ffi::{OsStrExt, OsStringExt};
// It doesn't seem possible to roundtrip NUL, probably because it is the
// string terminator character in C.
let input: OsString = OsString::from_vec((1..=u8::MAX).collect());
let input: OsString =
unsafe { OsString::from_encoded_bytes_unchecked((1..=u8::MAX).collect()) };
// NOTE: Do NOT use `echo` here; in most/all shells it interprets
// escapes with no way to disable that behaviour (unlike the `echo`
// builtin in Bash, for example, which accepts a `-E` flag). Using
// `printf %s` seems to do the right thing in most shells, i.e. it does
// not interpret the arguments in any way.
let mut script = b"printf %s ".to_vec();
Bash::quote_into_vec(input.as_bytes(), &mut script);
let script = OsString::from_vec(script);
Bash::quote_into_vec(input.as_encoded_bytes(), &mut script);
let script = unsafe { OsString::from_encoded_bytes_unchecked(script) };
(input, script)
}

#[cfg(unix)]
fn script_text() -> (OsString, OsString) {
use std::os::unix::ffi::OsStringExt;
// NOTE: Do NOT use `echo` here; in most/all shells it interprets
// escapes with no way to disable that behaviour (unlike the `echo`
// builtin in Bash, for example, which accepts a `-E` flag). Using
// `printf %s` seems to do the right thing in most shells, i.e. it does
// not interpret the arguments in any way.
let mut script = b"printf %s ".to_vec();
Bash::quote_into_vec(resources::UTF8_SAMPLE, &mut script);
let script = OsString::from_vec(script);
let script = unsafe { OsString::from_encoded_bytes_unchecked(script) };
(resources::UTF8_SAMPLE.into(), script)
}

#[cfg(unix)]
#[test_matrix(("bash", "zsh"))]
fn test_roundtrip_utf8_full(shell: &str) {
use std::os::unix::ffi::OsStringExt;
let utf8: Vec<_> = ('\x01'..=char::MAX).collect(); // Not including NUL.
for bin in find_bins(shell) {
// Chunk to avoid over-length arguments (see`getconf ARG_MAX`).
for chunk in utf8.chunks(2usize.pow(14)) {
let input: String = String::from_iter(chunk);
let mut script = b"printf %s ".to_vec();
Bash::quote_into_vec(&input, &mut script);
let script = OsString::from_vec(script);
let script = unsafe { OsString::from_encoded_bytes_unchecked(script) };
let output = invoke_shell(&bin, &script).unwrap();
let observed = OsString::from_vec(output.stdout);
let observed = unsafe { OsString::from_encoded_bytes_unchecked(output.stdout) };
assert_eq!(observed.into_string(), Ok(input));
}
}
Expand All @@ -160,12 +153,9 @@ mod bash_quote_ext {
assert_eq!(string, "Hello, $'World, Bob, !@#$%^&*(){}[]'");
}

#[cfg(unix)]
#[test]
fn test_os_string_push_quoted_with_bash() {
use std::ffi::OsString;

let mut buffer: OsString = "Hello, ".into();
let mut buffer: std::ffi::OsString = "Hello, ".into();
buffer.push_quoted(Bash, "World, Bob, !@#$%^&*(){}[]");
let string = buffer.into_string().unwrap(); // -> test failures are more readable.
assert_eq!(string, "Hello, $'World, Bob, !@#$%^&*(){}[]'");
Expand Down
Loading