1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
// Copyright © ArkBig
//! This file provides cli options and args.
pub fn parse() -> CliArgs {
CliArgs::parse()
}
use clap::Parser as _;
/// Command Line Arguments
#[derive(Debug, clap::Parser)]
#[clap(author, version, about, long_about = None, trailing_var_arg = true)]
pub struct CliArgs {
/// Perform NUM runs for each command.
#[clap(short, long, value_parser, value_name = "NUM", default_value_t = 10)]
pub runs: u16,
/// Loop NUM times with one measurement run for each command.
///
/// That is, each command is executed "runs" × "loops" times.
///
/// This is used when a single run is very fast and does not meet the resolution of the time command.
/// (e.g., less than 10 ms).
///
/// Statistics are calculated in a NUM loop, but are displayed as a value per one run.
/// Specifically, time-related items, such as "User time", is divided by NUM and displayed,
/// and memory-related items, such as "Maximum resident set size", are displayed as they are.
/// Divided values are indicated by "/NUM" in the item name.
///
/// The loop uses a "for" statement in "sh", so the extra processing is measured.
/// But if loops is 1, it is executed directly without "for" statement.
#[clap(long, value_parser, value_name = "NUM", default_value_t = 1)]
pub loops: u16,
/// Set the shell to use for executing benchmarked commands.
///
/// This is executed as `sh -c time command1`.
/// If execution confirmation is not obtained, also try `/usr/bin/env bash`.
///
/// e.g.) sh, /opt/homebrew/bin/zsh
#[clap(short = 'S', long, value_name = "COMMAND", default_value = "sh")]
pub shell: String,
/// Set the shell args to use for executing benchmarked commands.
///
/// This would be specified when executing in a POSIX incompatible shell.
#[clap(long, value_name = "ARG", default_value = "-c")]
pub shell_arg: String,
/// Use shell built-in time only.
#[clap(long)]
pub use_builtin_only: bool,
/// Change shell built-in time command.
#[clap(long, value_name = "COMMAND", default_value = "time")]
pub builtin: String,
/// No BSD time.
///
/// The default is to try to run BSD and GNU alternately.
/// If neither of those is available, use built-in.
/// When this is specified, bsd time is not used.
#[clap(long)]
pub no_bsd: bool,
/// Change BSD time command.
#[clap(long, value_name = "COMMAND", default_value = "/usr/bin/env time -l")]
pub bsd: String,
/// No GNU time.
///
/// The default is to try to run BSD and GNU alternately.
/// If neither of those is available, use built-in.
/// When this is specified, gnu time is not used.
#[clap(long)]
pub no_gnu: bool,
/// Change GNU time command.
///
/// If execution confirmation is not obtained, also try `/usr/bin/env time -v`.
#[clap(long, value_name = "COMMAND", default_value = "gtime -v")]
pub gnu: String,
/// The commands to benchmark.
///
/// If multiple commands are specified, each is executed and compared.
/// One command is specified with "--" delimiters (recommended) or quotation.
/// However, in the case of command-only quotation marks,
/// the subsequent ones are considered to be the arguments of the command.
///
/// e.g.) mntime command1 --flag arg -- command2 -- 'command3 -f -- args' command4 -o "output files"
#[clap(value_parser, required = true)]
commands: Vec<String>,
}
impl CliArgs {
pub fn normalized_commands(&self) -> Vec<String> {
let mut commands = Vec::new();
let delimiters = "--";
let mut one_command_and_args = Vec::new();
for s in &self.commands {
let one = s.clone();
if one_command_and_args.is_empty() {
if one == delimiters {
// nop
} else if one.contains(' ') {
// These are one quoted command.
commands.push(one);
} else {
// This is a command.
one_command_and_args.push(one);
}
} else if one == delimiters {
// One command determined.
if !one_command_and_args.is_empty() {
commands.push(one_command_and_args.join(" "));
one_command_and_args.clear();
}
} else {
one_command_and_args.push(to_quoted(one));
}
}
if !one_command_and_args.is_empty() {
commands.push(one_command_and_args.join(" "));
}
commands
}
}
fn is_quoted(str: &str) -> bool {
str.starts_with('"') && str.ends_with('"') || str.starts_with('\'') && str.ends_with('\'')
}
fn to_quoted(str: String) -> String {
if is_quoted(&str) || str.starts_with('-') {
return str;
}
format!("'{}'", str.replace('\'', "\\'"))
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn cli_args_normalized_commands() {
// only command
let cli_args = CliArgs::parse_from(vec!["mntime", "cmd1"]);
let commands = cli_args.normalized_commands();
assert_eq!(commands, vec!["cmd1"]);
// one command and arg pair
let cli_args = CliArgs::parse_from(vec!["mntime", "cmd1", "arg1"]);
let commands = cli_args.normalized_commands();
assert_eq!(commands, vec!["cmd1 'arg1'"]);
// two commands
let cli_args =
CliArgs::parse_from(vec!["mntime", "cmd1", "arg1", "--", "cmd2", "arg1", "arg2"]);
let commands = cli_args.normalized_commands();
assert_eq!(commands, vec!["cmd1 'arg1'", "cmd2 'arg1' 'arg2'"]);
// quoted separator
let cli_args = CliArgs::parse_from(vec!["mntime", "cmd1 arg1", "cmd2 arg1 arg2"]);
let commands = cli_args.normalized_commands();
assert_eq!(commands, vec!["cmd1 arg1", "cmd2 arg1 arg2"]);
// quoted args
let cli_args = CliArgs::parse_from(vec![
"mntime",
"cmd1",
"arg1",
"--",
"cmd2",
"\"arg1 arg2\"",
]);
let commands = cli_args.normalized_commands();
assert_eq!(commands, vec!["cmd1 'arg1'", "cmd2 \"arg1 arg2\""]);
// combination
let cli_args = CliArgs::parse_from(vec![
"mntime",
"command1",
"--flag",
"arg",
"--",
"command2",
"--",
"command3 -f -- args",
"command4",
"-o",
"output files",
]);
let commands = cli_args.normalized_commands();
assert_eq!(
commands,
vec![
"command1 --flag 'arg'",
"command2",
"command3 -f -- args",
"command4 -o 'output files'"
]
);
}
}