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

Parse --shell option as shell command #436

Merged
merged 3 commits into from
Oct 17, 2021
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
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
rust_decimal = "1.15"
rand = "0.8"
shell-words = "1.0"

[target.'cfg(not(windows))'.dependencies]
libc = "0.2"
Expand Down
12 changes: 6 additions & 6 deletions src/benchmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::benchmark_result::BenchmarkResult;
use crate::command::Command;
use crate::format::{format_duration, format_duration_unit};
use crate::min_max::{max, min};
use crate::options::{CmdFailureAction, HyperfineOptions, OutputStyleOption};
use crate::options::{CmdFailureAction, HyperfineOptions, OutputStyleOption, Shell};
use crate::outlier_detection::{modified_zscores, OUTLIER_THRESHOLD};
use crate::progress_bar::get_progress_bar;
use crate::shell::execute_and_time;
Expand Down Expand Up @@ -45,7 +45,7 @@ fn subtract_shell_spawning_time(time: Second, shell_spawning_time: Second) -> Se

/// Run the given shell command and measure the execution time
pub fn time_shell_command(
shell: &str,
shell: &Shell,
command: &Command<'_>,
show_output: bool,
failure_action: CmdFailureAction,
Expand Down Expand Up @@ -98,7 +98,7 @@ pub fn time_shell_command(

/// Measure the average shell spawning time
pub fn mean_shell_spawning_time(
shell: &str,
shell: &Shell,
style: OutputStyleOption,
show_output: bool,
) -> io::Result<TimingResult> {
Expand Down Expand Up @@ -168,7 +168,7 @@ pub fn mean_shell_spawning_time(
}

fn run_intermediate_command(
shell: &str,
shell: &Shell,
command: &Option<Command<'_>>,
show_output: bool,
error_output: &'static str,
Expand All @@ -187,7 +187,7 @@ fn run_intermediate_command(

/// Run the command specified by `--prepare`.
fn run_preparation_command(
shell: &str,
shell: &Shell,
command: &Option<Command<'_>>,
show_output: bool,
) -> io::Result<TimingResult> {
Expand All @@ -199,7 +199,7 @@ fn run_preparation_command(

/// Run the command specified by `--cleanup`.
fn run_cleanup_command(
shell: &str,
shell: &Shell,
command: &Option<Command<'_>>,
show_output: bool,
) -> io::Result<TimingResult> {
Expand Down
6 changes: 6 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ pub enum OptionsError<'a> {
TooManyCommandNames(usize),
UnexpectedCommandNameCount(usize, usize),
NumericParsingError(&'a str, ParseIntError),
EmptyShell,
ShellParseError(shell_words::ParseError),
}

impl<'a> fmt::Display for OptionsError<'a> {
Expand All @@ -85,6 +87,10 @@ impl<'a> fmt::Display for OptionsError<'a> {
cmd = cmd,
err = err
),
OptionsError::EmptyShell => write!(f, "Empty command at --shell option"),
OptionsError::ShellParseError(ref err) => {
write!(f, "Could not parse --shell value as command line: {}", err)
}
}
}
}
Expand Down
6 changes: 4 additions & 2 deletions src/export/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,14 +46,16 @@ pub struct ExportManager {
exporters: Vec<ExporterWithFilename>,
}

impl ExportManager {
impl Default for ExportManager {
/// Create a new ExportManager
pub fn new() -> ExportManager {
fn default() -> ExportManager {
ExportManager {
exporters: Vec::new(),
}
}
}

impl ExportManager {
/// Add an additional exporter to the ExportManager
pub fn add_exporter(&mut self, export_type: ExportType, filename: &str) -> Result<()> {
let _ = File::create(filename)?;
Expand Down
11 changes: 5 additions & 6 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ use benchmark_result::BenchmarkResult;
use command::Command;
use error::OptionsError;
use export::{ExportManager, ExportType};
use options::{CmdFailureAction, HyperfineOptions, OutputStyleOption};
use options::{CmdFailureAction, HyperfineOptions, OutputStyleOption, Shell};
use parameter_range::get_parameterized_commands;
use tokenize::tokenize;
use types::ParameterValue;
Expand Down Expand Up @@ -229,10 +229,9 @@ fn build_hyperfine_options<'a>(
OutputStyleOption::Disabled => {}
};

options.shell = matches
.value_of("shell")
.unwrap_or(&options.shell)
.to_string();
if let Some(shell) = matches.value_of("shell") {
options.shell = Shell::parse(shell)?;
}

if matches.is_present("ignore-failure") {
options.failure_action = CmdFailureAction::Ignore;
Expand All @@ -250,7 +249,7 @@ fn build_hyperfine_options<'a>(
/// Build the ExportManager that will export the results specified
/// in the given ArgMatches
fn build_export_manager(matches: &ArgMatches<'_>) -> io::Result<ExportManager> {
let mut export_manager = ExportManager::new();
let mut export_manager = ExportManager::default();
{
let mut add_exporter = |flag, exporttype| -> io::Result<()> {
if let Some(filename) = matches.value_of(flag) {
Expand Down
99 changes: 97 additions & 2 deletions src/options.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
use std::fmt;
use std::process::Command;

use crate::error::OptionsError;
use crate::units::{Second, Unit};

#[cfg(not(windows))]
Expand All @@ -6,6 +10,52 @@ pub const DEFAULT_SHELL: &str = "sh";
#[cfg(windows)]
pub const DEFAULT_SHELL: &str = "cmd.exe";

/// Shell to use for executing benchmarked commands
#[derive(Debug)]
pub enum Shell {
/// Default shell command
Default(&'static str),
/// Custom shell command specified via --shell
Custom(Vec<String>),
}

impl Default for Shell {
fn default() -> Self {
Shell::Default(DEFAULT_SHELL)
}
}

impl fmt::Display for Shell {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Shell::Default(cmd) => write!(f, "{}", cmd),
Shell::Custom(cmdline) => write!(f, "{}", shell_words::join(cmdline)),
}
}
}

impl Shell {
/// Parse given string as shell command line
pub fn parse<'a>(s: &str) -> Result<Self, OptionsError<'a>> {
let v = shell_words::split(s).map_err(OptionsError::ShellParseError)?;
if v.is_empty() || v[0].is_empty() {
return Err(OptionsError::EmptyShell);
}
Ok(Shell::Custom(v))
}

pub fn command(&self) -> Command {
match self {
Shell::Default(cmd) => Command::new(cmd),
Shell::Custom(cmdline) => {
let mut c = Command::new(&cmdline[0]);
c.args(&cmdline[1..]);
c
}
}
}
}

/// Action to take when an executed command fails.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CmdFailureAction {
Expand Down Expand Up @@ -74,7 +124,7 @@ pub struct HyperfineOptions {
pub output_style: OutputStyleOption,

/// The shell to use for executing commands.
pub shell: String,
pub shell: Shell,

/// Forward benchmark's stdout to hyperfine's stdout
pub show_output: bool,
Expand All @@ -99,9 +149,54 @@ impl Default for HyperfineOptions {
preparation_command: None,
cleanup_command: None,
output_style: OutputStyleOption::Full,
shell: DEFAULT_SHELL.to_string(),
shell: Shell::default(),
show_output: false,
time_unit: None,
}
}
}

#[test]
fn test_shell_default_command() {
let shell = Shell::default();

let s = format!("{}", shell);
assert_eq!(&s, DEFAULT_SHELL);

let cmd = shell.command();
// Command::get_program is not yet available in stable channel.
// https://doc.rust-lang.org/std/process/struct.Command.html#method.get_program
let s = format!("{:?}", cmd);
assert_eq!(s, format!("\"{}\"", DEFAULT_SHELL));
}

#[test]
fn test_shell_parse_command() {
let shell = Shell::parse("shell -x 'aaa bbb'").unwrap();

let s = format!("{}", shell);
assert_eq!(&s, "shell -x 'aaa bbb'");

let cmd = shell.command();
// Command::get_program and Command::args are not yet available in stable channel.
// https://doc.rust-lang.org/std/process/struct.Command.html#method.get_program
let s = format!("{:?}", cmd);
assert_eq!(&s, r#""shell" "-x" "aaa bbb""#);

// Error cases

match Shell::parse("shell 'foo").unwrap_err() {
OptionsError::ShellParseError(_) => { /* ok */ }
e => assert!(false, "Unexpected error: {}", e),
}

match Shell::parse("").unwrap_err() {
OptionsError::EmptyShell => { /* ok */ }
e => assert!(false, "Unexpected error: {}", e),
}

match Shell::parse("''").unwrap_err() {
OptionsError::EmptyShell => { /* ok */ }
e => assert!(false, "Unexpected error: {}", e),
}
}
17 changes: 10 additions & 7 deletions src/shell.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use std::io;
use std::process::{Command, ExitStatus, Stdio};
use std::process::{ExitStatus, Stdio};

use crate::options::Shell;
use crate::timer::get_cpu_timer;

/// Used to indicate the result of running a command
Expand All @@ -22,7 +23,7 @@ pub fn execute_and_time(
stdout: Stdio,
stderr: Stdio,
command: &str,
shell: &str,
shell: &Shell,
) -> io::Result<ExecuteResult> {
let mut child = run_shell_command(stdout, stderr, command, shell)?;
let cpu_timer = get_cpu_timer(&child);
Expand All @@ -42,7 +43,7 @@ pub fn execute_and_time(
stdout: Stdio,
stderr: Stdio,
command: &str,
shell: &str,
shell: &Shell,
) -> io::Result<ExecuteResult> {
let cpu_timer = get_cpu_timer();

Expand All @@ -63,9 +64,10 @@ fn run_shell_command(
stdout: Stdio,
stderr: Stdio,
command: &str,
shell: &str,
shell: &Shell,
) -> io::Result<std::process::ExitStatus> {
Command::new(shell)
shell
.command()
.arg("-c")
.arg(command)
.env(
Expand All @@ -84,9 +86,10 @@ fn run_shell_command(
stdout: Stdio,
stderr: Stdio,
command: &str,
shell: &str,
shell: &Shell,
) -> io::Result<std::process::Child> {
Command::new(shell)
shell
.command()
.arg("/C")
.arg(command)
.stdin(Stdio::null())
Expand Down