From 4cbd53a59f9df93b55b6a84ad7396268242b968b Mon Sep 17 00:00:00 2001 From: Gavin Panella Date: Sun, 8 Jun 2025 09:48:08 +0200 Subject: [PATCH 1/5] Enable some more paranoid Clippy options --- src/bash.rs | 2 +- src/fish.rs | 2 +- src/lib.rs | 17 +++++++++++++++++ src/sh.rs | 2 +- 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/bash.rs b/src/bash.rs index 63ba917..c216549 100644 --- a/src/bash.rs +++ b/src/bash.rs @@ -77,7 +77,7 @@ impl QuoteInto> for Bash { impl QuoteInto for Bash { fn quote_into<'q, S: Into>>(s: S, out: &mut String) { - Self::quote_into_vec(s, unsafe { out.as_mut_vec() }) + Self::quote_into_vec(s, unsafe { out.as_mut_vec() }); } } diff --git a/src/fish.rs b/src/fish.rs index 647cbcd..88adb50 100644 --- a/src/fish.rs +++ b/src/fish.rs @@ -49,7 +49,7 @@ impl QuoteInto> for Fish { impl QuoteInto for Fish { fn quote_into<'q, S: Into>>(s: S, out: &mut String) { - Self::quote_into_vec(s, unsafe { out.as_mut_vec() }) + Self::quote_into_vec(s, unsafe { out.as_mut_vec() }); } } diff --git a/src/lib.rs b/src/lib.rs index a1504ec..f7cbf69 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -35,6 +35,23 @@ ), doc = include_str!("../README.md") )] +#![warn(clippy::unused_result_ok)] +#![warn(clippy::pedantic)] +#![allow( + clippy::default_trait_access, + clippy::enum_glob_use, + clippy::items_after_statements, + clippy::map_unwrap_or, + clippy::match_same_arms, + clippy::missing_errors_doc, + clippy::must_use_candidate, + clippy::needless_pass_by_value, + clippy::redundant_closure_for_method_calls, + clippy::struct_field_names, + clippy::too_many_lines, + clippy::unnecessary_debug_formatting, + clippy::unused_async +)] use std::ffi::{OsStr, OsString}; use std::path::{Path, PathBuf}; diff --git a/src/sh.rs b/src/sh.rs index 5086943..f591c7a 100644 --- a/src/sh.rs +++ b/src/sh.rs @@ -73,7 +73,7 @@ use crate::{ascii::Char, Quotable, QuoteInto}; /// > ## Double Quotes /// > /// > Enclosing characters within double quotes preserves the literal meaning -/// > of all characters except dollarsign ($), backquote (`), and backslash +/// > of all characters except dollarsign ($), backquote (\`), and backslash /// > (\). The backslash inside double quotes is historically weird, and /// > serves to quote only the following characters: /// > From 8d3eb259725b30eb068fb3bf64712fc0112ad6a1 Mon Sep 17 00:00:00 2001 From: Gavin Panella Date: Sun, 8 Jun 2025 23:41:24 +0200 Subject: [PATCH 2/5] Add PowerShell/`pwsh` support --- .clippy.toml | 1 + .vscode/settings.json | 2 + Cargo.toml | 3 +- README.md | 16 ++-- src/lib.rs | 9 +- src/pwsh.rs | 152 ++++++++++++++++++++++++++++++++ tests/test_pwsh.rs | 198 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 373 insertions(+), 8 deletions(-) create mode 100644 .clippy.toml create mode 100644 src/pwsh.rs create mode 100644 tests/test_pwsh.rs diff --git a/.clippy.toml b/.clippy.toml new file mode 100644 index 0000000..5520930 --- /dev/null +++ b/.clippy.toml @@ -0,0 +1 @@ +doc-valid-idents = ["PowerShell"] diff --git a/.vscode/settings.json b/.vscode/settings.json index 309558e..7a79cb7 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -10,8 +10,10 @@ "dtolnay", "execing", "getconf", + "idents", "metacharacters", "POSIX", + "pwsh", "Rustfmt", "sout", "UXXXXXXXX" diff --git a/Cargo.toml b/Cargo.toml index a19f692..b45563f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,9 +13,10 @@ version = "0.7.2" include = ["LICENSE", "README.md", "src/**/*.rs"] [features] -default = ["bstr", "bash", "sh", "fish"] +default = ["bstr", "bash", "pwsh", "sh", "fish"] bash = [] fish = [] +pwsh = [] sh = [] [dependencies] diff --git a/README.md b/README.md index 6025cc5..187891d 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ [`Dash`]: https://docs.rs/shell-quote/latest/shell_quote/struct.Dash.html [`Bash`]: https://docs.rs/shell-quote/latest/shell_quote/struct.Bash.html [`Fish`]: https://docs.rs/shell-quote/latest/shell_quote/struct.Fish.html +[`Pwsh`]: https://docs.rs/shell-quote/latest/shell_quote/struct.Pwsh.html [`Zsh`]: https://docs.rs/shell-quote/latest/shell_quote/struct.Zsh.html [`QuoteRefExt`]: https://docs.rs/shell-quote/latest/shell_quote/trait.QuoteRefExt.html [`QuoteRefExt::quoted`]: https://docs.rs/shell-quote/latest/shell_quote/trait.QuoteRefExt.html#tymethod.quoted @@ -37,11 +38,12 @@ metacharacters, function calls, or other syntax. This is frequently not as simple as wrapping a string in quotes. This package implements escaping for [GNU Bash][gnu-bash], [Z Shell][z-shell], -[fish][], and `/bin/sh`-like shells including [Dash][dash]. +[fish][], [PowerShell][pwsh], and `/bin/sh`-like shells including [Dash][dash]. [dash]: https://en.wikipedia.org/wiki/Almquist_shell#dash [gnu-bash]: https://www.gnu.org/software/bash/ [z-shell]: https://zsh.sourceforge.io/ +[pwsh]: https://learn.microsoft.com/powershell/ [fish]: https://fishshell.com/ It can take as input many different string and byte string types: @@ -66,29 +68,31 @@ Inspired by the Haskell [shell-escape][] package. ## Examples When quoting using raw bytes it can be convenient to call [`Sh`]'s, [`Dash`]'s, -[`Bash`]'s, [`Fish`]'s, and [`Zsh`]'s associated functions directly: +[`Bash`]'s, [`Fish`]'s, [`Pwsh`]'s, and [`Zsh`]'s associated functions directly: ```rust -use shell_quote::{Bash, Dash, Fish, Sh, Zsh}; +use shell_quote::{Bash, Dash, Fish, Pwsh, Sh, Zsh}; // No quoting is necessary for simple strings. assert_eq!(Sh::quote_vec("foobar"), b"foobar"); assert_eq!(Dash::quote_vec("foobar"), b"foobar"); // `Dash` is an alias for `Sh` assert_eq!(Bash::quote_vec("foobar"), b"foobar"); assert_eq!(Zsh::quote_vec("foobar"), b"foobar"); // `Zsh` is an alias for `Bash` assert_eq!(Fish::quote_vec("foobar"), b"foobar"); +assert_eq!(Pwsh::quote_vec("foobar"), b"'foobar'"); // In all shells, quoting is necessary for strings with spaces. assert_eq!(Sh::quote_vec("foo bar"), b"foo' bar'"); assert_eq!(Dash::quote_vec("foo bar"), b"foo' bar'"); assert_eq!(Bash::quote_vec("foo bar"), b"$'foo bar'"); assert_eq!(Zsh::quote_vec("foo bar"), b"$'foo bar'"); assert_eq!(Fish::quote_vec("foo bar"), b"foo' bar'"); +assert_eq!(Pwsh::quote_vec("foo bar"), b"'foo bar'"); ``` It's also possible to use the extension trait [`QuoteRefExt`] which provides a [`quoted`][`QuoteRefExt::quoted`] function: ```rust -use shell_quote::{Bash, Sh, Fish, QuoteRefExt}; +use shell_quote::{Bash, Sh, Fish, Pwsh, QuoteRefExt}; let quoted: String = "foo bar".quoted(Bash); assert_eq!(quoted, "$'foo bar'"); let quoted: Vec = "foo bar".quoted(Sh); @@ -159,7 +163,8 @@ assert_eq!(&data_iso_8859_1_quoted, b"$'caf\\xE9'"); // ISO-8859-1: 1 byte, hex [`Sh`] can serve as a lowest common denominator for Bash, Z Shell, and `/bin/sh`-like shells like Dash. However, fish's quoting rules are different -enough that you must use [`Fish`] for fish scripts. +enough that you must use [`Fish`] for fish scripts, and PowerShells's rules are +different enough again that you must use [`Pwsh`] for PowerShell scripts. Note that using [`Sh`] as a lowest common denominator brings with it other issues; read its documentation carefully to understand the limitations. @@ -171,6 +176,7 @@ The following are all enabled by default: - `bstr`: Support [`bstr::BStr`] and [`bstr::BString`]. - `bash`: Support [Bash][gnu-bash] and [Z Shell][z-shell]. - `fish`: Support [fish][]. +- `pwsh`: Support [PowerShell][pwsh]. - `sh`: Support `/bin/sh`-like shells including [Dash][dash]. To limit support to specific shells, you must disable this crate's default diff --git a/src/lib.rs b/src/lib.rs index f7cbf69..011be6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,7 @@ //! [`Dash`]: `Dash` //! [`Bash`]: `Bash` //! [`Fish`]: `Fish` +//! [`Pwsh`]: `Pwsh` //! [`Zsh`]: `Zsh` //! //! [`QuoteRefExt`]: `QuoteRefExt` @@ -31,6 +32,7 @@ feature = "bstr", feature = "bash", feature = "fish", + feature = "pwsh", feature = "sh", ), doc = include_str!("../README.md") @@ -59,6 +61,7 @@ use std::path::{Path, PathBuf}; mod ascii; mod bash; mod fish; +mod pwsh; mod sh; mod utf8; @@ -66,6 +69,8 @@ mod utf8; pub use bash::Bash; #[cfg(feature = "fish")] pub use fish::Fish; +#[cfg(feature = "pwsh")] +pub use pwsh::Pwsh; #[cfg(feature = "sh")] pub use sh::Sh; @@ -152,12 +157,12 @@ where /// [`PathBuf`]/[`Path`] didn't work in a natural way. pub enum Quotable<'a> { #[cfg_attr( - not(any(feature = "bash", feature = "fish", feature = "sh")), + not(any(feature = "bash", feature = "fish", feature = "pwsh", feature = "sh")), allow(unused) )] Bytes(&'a [u8]), #[cfg_attr( - not(any(feature = "bash", feature = "fish", feature = "sh")), + not(any(feature = "bash", feature = "fish", feature = "pwsh", feature = "sh")), allow(unused) )] Text(&'a str), diff --git a/src/pwsh.rs b/src/pwsh.rs new file mode 100644 index 0000000..f56e1a1 --- /dev/null +++ b/src/pwsh.rs @@ -0,0 +1,152 @@ +#![cfg(feature = "pwsh")] + +use crate::{Quotable, QuoteInto}; + +/// Quote byte strings for use with [PowerShell][], `pwsh`. +/// +/// [PowerShell]: https://en.wikipedia.org/wiki/PowerShell +/// +/// In the [Windows PowerShell Language Specification 3.0][] [§ 2.3.5.2 String +/// literals][], several quoting styles are described. This module renders +/// `verbatim-string-literal` only. +/// +/// [Windows PowerShell Language Specification 3.0]: +/// https://learn.microsoft.com/en-us/powershell/scripting/lang-spec/chapter-01 +/// [§ 2.3.5.2 String literals]: +/// https://learn.microsoft.com/en-us/powershell/scripting/lang-spec/chapter-02?view=powershell-7.5#2352-string-literals +/// +/// # ⚠️ Warning +/// +/// PowerShell strings must be Unicode according to the specification. This +/// means one cannot include arbitrary binary data in a PowerShell string. +/// Hence, for now, there is no [`QuoteInto`] implementation for +/// [`Pwsh`]. +/// +/// If you're only using Unicode, a workaround is to instead quote into a +/// [`Vec`] and convert that into a string using [`String::from_utf8`]. The +/// key difference is that `from_utf8` returns a [`Result`] which the caller +/// must deal with. +/// +#[derive(Debug, Clone, Copy)] +pub struct Pwsh; + +impl QuoteInto> for Pwsh { + fn quote_into<'q, S: Into>>(s: S, out: &mut Vec) { + Self::quote_into_vec(s, out); + } +} + +#[cfg(unix)] +impl QuoteInto for Pwsh { + fn quote_into<'q, S: Into>>(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); + out.push(s); + } +} + +#[cfg(feature = "bstr")] +impl QuoteInto for Pwsh { + fn quote_into<'q, S: Into>>(s: S, out: &mut bstr::BString) { + let s = Self::quote_vec(s); + out.extend(s); + } +} + +impl Pwsh { + /// Quote a string of bytes into a new `Vec`. + /// + /// This will return one of the following: + /// - The string as-is, if no quoting is necessary. + /// - A string containing single-quoted sections, like `'foo bar'`. + /// + /// See [`quote_into_vec`][`Self::quote_into_vec`] for a variant that + /// extends an existing `Vec` instead of allocating a new one. + /// + /// # Examples + /// + /// ``` + /// # use shell_quote::Pwsh; + /// assert_eq!(Pwsh::quote_vec("foobar"), b"'foobar'"); + /// assert_eq!(Pwsh::quote_vec("foo bar"), b"'foo bar'"); + /// ``` + /// + pub fn quote_vec<'a, S: Into>>(s: S) -> Vec { + let mut sout = Vec::new(); + Self::quote_into_vec(s, &mut sout); + sout + } + + /// Quote a string of bytes into an existing `Vec`. + /// + /// See [`quote_vec`][`Self::quote_vec`] for more details. + /// + /// # Examples + /// + /// ``` + /// # use shell_quote::Pwsh; + /// let mut buf = Vec::with_capacity(128); + /// Pwsh::quote_into_vec("foobar", &mut buf); + /// buf.push(b' '); // Add a space. + /// Pwsh::quote_into_vec("foo bar", &mut buf); + /// assert_eq!(buf, b"'foobar' 'foo bar'"); // Invalid PowerShell; see note below. + /// ``` + /// + /// ⚠️ Note that *when pushing multiple items* no attempt is made to create + /// syntactically valid PowerShell. In particular, string literals are + /// concatenated with the `+` operator, not by juxtaposition. It's up to the + /// caller to add the necessary `+` operators. + /// + pub fn quote_into_vec<'a, S: Into>>(s: S, sout: &mut Vec) { + let bytes = match s.into() { + Quotable::Bytes(bytes) => bytes, + Quotable::Text(s) => s.as_bytes(), + }; + sout.push(b'\''); + let mut last: [u8; 2] = [0x00, 0x00]; // Used to track UTF-8 sequences. + let sout = bytes.iter().fold(sout, |sout, ch| { + match *ch { + // Escape multi-byte single quotes by doubling them. + 0xE2 if last == [0x00, 0x00] => last = [0x00, 0xE2], // Start of a UTF-8 sequence. + 0x80 if last == [0x00, 0xE2] => last = [0xE2, 0x80], // Continuation byte of a UTF-8 sequence. + 0x98 if last == [0xE2, 0x80] => { + sout.extend("\u{2018}\u{2018}".as_bytes()); // Left single quotation mark (U+2018). + last = [0x00, 0x00]; // Reset last byte tracker. + } + 0x99 if last == [0xE2, 0x80] => { + sout.extend("\u{2019}\u{2019}".as_bytes()); // Right single quotation mark (U+2019). + last = [0x00, 0x00]; // Reset last byte tracker. + } + 0x9A if last == [0xE2, 0x80] => { + sout.extend("\u{201A}\u{201A}".as_bytes()); // Single low-9 quotation mark (U+201A). + last = [0x00, 0x00]; // Reset last byte tracker. + } + 0x9B if last == [0xE2, 0x80] => { + sout.extend("\u{201B}\u{201B}".as_bytes()); // Single high-reversed-9 quotation mark (U+201B). + last = [0x00, 0x00]; // Reset last byte tracker. + } + + // We're in a UTF-8 sequence, but not one we're interested in. + _ if last != [0x00, 0x00] => { + sout.extend(last.iter().skip_while(|b| **b == 0x00)); + sout.push(*ch); + last = [0x00, 0x00]; // Reset last byte tracker. + } + + // Escape single quotes by doubling them. + b'\'' => { + // Single quotation mark (U+0027). + sout.extend(b"''"); + last = [0x00, 0x00]; // Reset last byte tracker. + } + _ => { + sout.push(*ch); // Otherwise, just copy the byte. + last = [0x00, 0x00]; // Reset last byte tracker. + } + } + sout + }); + sout.push(b'\''); + } +} diff --git a/tests/test_pwsh.rs b/tests/test_pwsh.rs new file mode 100644 index 0000000..28d6b4b --- /dev/null +++ b/tests/test_pwsh.rs @@ -0,0 +1,198 @@ +#![cfg(all(unix, feature = "pwsh"))] + +mod resources; +mod util; + +// -- impl Pwsh --------------------------------------------------------------- + +mod pwsh_impl { + use std::ffi::{OsStr, OsString}; + use std::{io::Result, path::Path, process::Output}; + + use super::resources; + use super::util::{find_bins, invoke_shell}; + use shell_quote::Pwsh; + use test_case::test_matrix; + + #[test] + fn test_lowercase_ascii() { + assert_eq!( + Pwsh::quote_vec("abcdefghijklmnopqrstuvwxyz"), + b"'abcdefghijklmnopqrstuvwxyz'" + ); + } + + #[test] + fn test_uppercase_ascii() { + assert_eq!( + Pwsh::quote_vec("ABCDEFGHIJKLMNOPQRSTUVWXYZ"), + b"'ABCDEFGHIJKLMNOPQRSTUVWXYZ'" + ); + } + + #[test] + fn test_numbers() { + assert_eq!(Pwsh::quote_vec("0123456789"), b"'0123456789'"); + } + + #[test] + fn test_punctuation() { + assert_eq!(Pwsh::quote_vec("-_=/,.+"), b"'-_=/,.+'"); + assert_eq!(Pwsh::quote_vec("Hello \r\n"), b"'Hello \r\n'"); + } + + #[test] + fn test_empty_string() { + assert_eq!(Pwsh::quote_vec(""), b"''"); + } + + #[test] + fn test_basic_escapes() { + assert_eq!(Pwsh::quote_vec(r#"woo'wah""#), br#"'woo''wah"'"#); + } + + #[test] + fn test_control_characters() { + assert_eq!(Pwsh::quote_vec("\x07"), b"'\x07'"); + assert_eq!(Pwsh::quote_vec("\x00"), b"'\x00'"); + assert_eq!(Pwsh::quote_vec("\x06"), b"'\x06'"); + assert_eq!(Pwsh::quote_vec("\x7F"), b"'\x7F'"); + assert_eq!(Pwsh::quote_vec("\x1B"), b"'\x1B'"); + } + + #[test] + fn test_quote_into_plain() { + let mut buffer = Vec::new(); + Pwsh::quote_into_vec("hello", &mut buffer); + assert_eq!(buffer, b"'hello'"); + } + + #[test] + fn test_quote_into_with_escapes() { + let mut buffer = Vec::new(); + Pwsh::quote_into_vec("-_=/,.+", &mut buffer); + assert_eq!(buffer, b"'-_=/,.+'"); + } + + type InvokeShell = fn(&Path, &OsStr) -> Result; + + #[cfg(unix)] + #[test_matrix( + (script_bytes, + script_text), + (("pwsh", invoke_shell),) + )] + fn test_roundtrip(prepare: fn() -> (OsString, OsString), (shell, invoke): (&str, InvokeShell)) { + use std::os::unix::ffi::OsStringExt; + let (input, script) = prepare(); + for bin in find_bins(shell) { + let output = invoke(&bin, &script).unwrap(); + let observed = OsString::from_vec(output.stdout); + assert_eq!(observed, input); + } + } + + #[cfg(unix)] + fn script_bytes() -> (OsString, OsString) { + use std::os::unix::ffi::{OsStrExt, OsStringExt}; + // Strings in PowerShell must be Unicode, so NUL and all high bytes + // (>7f) are forbidden and either cause an error or result in the Unicode + // Replacement Character � (U+FFFD). + let input: OsString = OsString::from_vec((1..=0x7F).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"[Console]::Write(".to_vec(); + Pwsh::quote_into_vec(input.as_bytes(), &mut script); + script.push(b')'); + let script = OsString::from_vec(script); + (input, script) + } + + #[cfg(unix)] + fn script_text() -> (OsString, OsString) { + use std::os::unix::ffi::OsStringExt; + let mut script = b"[Console]::Write(".to_vec(); + Pwsh::quote_into_vec(resources::UTF8_SAMPLE, &mut script); + script.push(b')'); + let input: OsString = resources::UTF8_SAMPLE.into(); + let script = OsString::from_vec(script); + (input, script) + } + + #[cfg(unix)] + #[test_matrix( + (("pwsh", invoke_shell),) + )] + fn test_roundtrip_utf8_full((shell, invoke): (&str, InvokeShell)) { + 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"[Console]::Write(".to_vec(); + Pwsh::quote_into_vec(&input, &mut script); + script.push(b')'); + let script = OsString::from_vec(script); + let output = invoke(&bin, &script).unwrap(); + let observed = OsString::from_vec(output.stdout); + assert_eq!(observed.into_string(), Ok(input)); + } + } + } +} + +// -- QuoteExt ---------------------------------------------------------------- + +mod pwsh_quote_ext { + use std::ffi::OsString; + + use shell_quote::{Pwsh, QuoteExt}; + + #[test] + fn test_vec_push_quoted() { + let mut buffer = Vec::from(b"Hello, "); + buffer.push_quoted(Pwsh, "World, Bob, !@#$%^&*(){}[]"); + let string = String::from_utf8(buffer).unwrap(); // -> test failures are more readable. + assert_eq!(string, "Hello, 'World, Bob, !@#$%^&*(){}[]'"); + } + + #[cfg(unix)] + #[test] + fn test_os_string_push_quoted() { + let mut buffer: OsString = "Hello, ".into(); + buffer.push_quoted(Pwsh, "World, Bob, !@#$%^&*(){}[]"); + let string = buffer.into_string().unwrap(); // -> test failures are more readable. + assert_eq!(string, "Hello, 'World, Bob, !@#$%^&*(){}[]'"); + } + + #[cfg(feature = "bstr")] + #[test] + fn test_bstring_push_quoted() { + let mut string: bstr::BString = "Hello, ".into(); + string.push_quoted(Pwsh, "World, Bob, !@#$%^&*(){}[]"); + assert_eq!(string, "Hello, 'World, Bob, !@#$%^&*(){}[]'"); + } + + #[cfg(feature = "bstr")] + #[test] + fn test_bstring_push_quoted_roundtrip() { + use super::util::{find_bins, invoke_shell}; + use bstr::{BString, ByteSlice}; + let mut script: BString = "printf %s ".into(); + // Strings in PowerShell must be Unicode, so NUL and all high bytes + // (>7f) are forbidden and either cause an error or result in the Unicode + // Replacement Character � (U+FFFD). + let string: Vec<_> = (1..=0x7F).collect(); + script.push_quoted(Pwsh, &string); + let script = script.to_os_str().unwrap(); + // Test with every version of `pwsh` we find on `PATH`. + for bin in find_bins("pwsh") { + let output = invoke_shell(&bin, script).unwrap(); + assert_eq!(output.stdout, string); + } + } +} From c0916c948fa81521f27cbfb4e3b23b8c911e70c0 Mon Sep 17 00:00:00 2001 From: Gavin Panella Date: Mon, 9 Jun 2025 00:00:58 +0200 Subject: [PATCH 3/5] Add bench for `Pwsh` --- Cargo.toml | 13 +++++++++---- benches/pwsh.rs | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) create mode 100644 benches/pwsh.rs diff --git a/Cargo.toml b/Cargo.toml index b45563f..1274086 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -34,11 +34,16 @@ harness = false required-features = ["bash"] [[bench]] -name = "sh" +name = "fish" harness = false -required-features = ["sh"] +required-features = ["fish"] [[bench]] -name = "fish" +name = "pwsh" harness = false -required-features = ["fish"] +required-features = ["pwsh"] + +[[bench]] +name = "sh" +harness = false +required-features = ["sh"] diff --git a/benches/pwsh.rs b/benches/pwsh.rs new file mode 100644 index 0000000..b4cf926 --- /dev/null +++ b/benches/pwsh.rs @@ -0,0 +1,38 @@ +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +use shell_quote::Pwsh; + +fn criterion_benchmark(c: &mut Criterion) { + let empty_string = ""; + c.bench_function("pwsh escape empty", |b| { + b.iter(|| Pwsh::quote_vec(black_box(empty_string))) + }); + + let alphanumeric_short = "abcdefghijklmnopqrstuvwxyz0123456789"; + c.bench_function("pwsh escape a-z", |b| { + b.iter(|| Pwsh::quote_vec(black_box(alphanumeric_short))) + }); + + let alphanumeric_long = alphanumeric_short.repeat(1000); + c.bench_function("pwsh escape a-z long", |b| { + b.iter(|| Pwsh::quote_vec(black_box(&alphanumeric_long))) + }); + + let bytes_short = (1..=255u8).map(char::from).collect::(); + c.bench_function("pwsh escape bytes", |b| { + b.iter(|| Pwsh::quote_vec(black_box(&bytes_short))) + }); + + let bytes_long = bytes_short.repeat(1000); + c.bench_function("pwsh escape bytes long", |b| { + b.iter(|| Pwsh::quote_vec(black_box(&bytes_long))) + }); + + let utf8 = ('\x01'..=char::MAX).collect::(); + c.bench_function("pwsh escape utf-8", |b| { + b.iter(|| Pwsh::quote_vec(black_box(&utf8))) + }); +} + +criterion_group!(benches, criterion_benchmark); +criterion_main!(benches); From 92a47d1747bd52dff6e6ca507878a4a6b0394f3c Mon Sep 17 00:00:00 2001 From: Gavin Panella Date: Mon, 9 Jun 2025 13:40:24 +0200 Subject: [PATCH 4/5] Upgrade dependencies --- Cargo.toml | 4 ++-- benches/bash.rs | 4 +++- benches/fish.rs | 4 +++- benches/pwsh.rs | 4 +++- benches/sh.rs | 4 +++- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1274086..b384b80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,9 +23,9 @@ sh = [] bstr = { version = "1", optional = true } [dev-dependencies] -criterion = { version = "^0.5.1", features = ["html_reports"] } +criterion = { version = "^0.6.0", features = ["html_reports"] } lenient_semver = "0.4.2" -semver = "1.0.23" +semver = "1.0.26" test-case = "3.3.1" [[bench]] diff --git a/benches/bash.rs b/benches/bash.rs index e0cf0c6..6474c33 100644 --- a/benches/bash.rs +++ b/benches/bash.rs @@ -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; diff --git a/benches/fish.rs b/benches/fish.rs index dc50d93..ae6612e 100644 --- a/benches/fish.rs +++ b/benches/fish.rs @@ -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; diff --git a/benches/pwsh.rs b/benches/pwsh.rs index b4cf926..e961846 100644 --- a/benches/pwsh.rs +++ b/benches/pwsh.rs @@ -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::Pwsh; diff --git a/benches/sh.rs b/benches/sh.rs index 2ff96df..fb263ce 100644 --- a/benches/sh.rs +++ b/benches/sh.rs @@ -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; From c20fd22b406d719b4bc389dbf12c59a2819fe3ac Mon Sep 17 00:00:00 2001 From: Gavin Panella Date: Mon, 9 Jun 2025 13:54:16 +0200 Subject: [PATCH 5/5] Add `pwsh` to all-feature-combos test helper --- test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.py b/test.py index 669ab39..9935df5 100644 --- a/test.py +++ b/test.py @@ -1,6 +1,6 @@ from itertools import combinations -features = "bstr", "bash", "fish", "sh" +features = "bstr", "bash", "fish", "pwsh", "sh" def power_set(input): for length in range(0, len(input) + 1):