rustfmt.toml sets group_imports, imports_granularity and imports_layout, which are nightly-only; agents formatted with stable rustfmt, which ignored them. This is a formatting-only pass with `cargo +nightly fmt`. Test Plan: - cargo clippy --workspace --all-targets -- -D warnings - cargo test --workspace
275 lines
8.8 KiB
Rust
275 lines
8.8 KiB
Rust
//! A command to execute (port of `config/command.zig`).
|
|
|
|
use std::fmt;
|
|
|
|
use crate::{
|
|
cli_args::{ArgError, ConfigField},
|
|
formatter::EntryFormatter,
|
|
};
|
|
|
|
/// A command to execute (argv0 and args).
|
|
///
|
|
/// A command is specified as a simple string such as "nvim a b c".
|
|
/// By default, we expect the downstream to do some sort of shell expansion
|
|
/// on this string.
|
|
///
|
|
/// If a command is already expanded and the user does NOT want to do
|
|
/// shell expansion (because this usually requires a round trip into
|
|
/// /bin/sh or equivalent), specify a `direct:`-prefix. e.g.
|
|
/// `direct:nvim a b c`.
|
|
///
|
|
/// The whitespace before or around the prefix is ignored. For example,
|
|
/// ` direct:nvim a b c` and `direct: nvim a b c` are equivalent.
|
|
///
|
|
/// If the command is not absolute, it'll be looked up via the PATH.
|
|
/// For the shell-expansion case, we let the shell do this. For the
|
|
/// direct case, we do this directly.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum Command {
|
|
/// Execute a command directly, e.g. via `exec`. The format here
|
|
/// is already structured to be ready to passed directly to `exec`
|
|
/// with index zero being the command to execute.
|
|
///
|
|
/// Index zero is not guaranteed to be an absolute path, and may require
|
|
/// PATH lookup. It is up to the downstream to do this, usually via
|
|
/// delegation to something like `execvp`.
|
|
Direct(Vec<String>),
|
|
|
|
/// Execute a command via shell expansion. This provides the command
|
|
/// as a single string that is expected to be expanded in some way
|
|
/// (up to the downstream). Usually `/bin/sh -c`.
|
|
Shell(String),
|
|
}
|
|
|
|
impl Default for Command {
|
|
fn default() -> Self {
|
|
Command::Shell(String::new())
|
|
}
|
|
}
|
|
|
|
/// Iterates over each argument in the command.
|
|
#[derive(Debug, Clone)]
|
|
pub struct ArgIterator {
|
|
args: std::vec::IntoIter<String>,
|
|
}
|
|
|
|
impl Iterator for ArgIterator {
|
|
type Item = String;
|
|
|
|
fn next(&mut self) -> Option<String> {
|
|
self.args.next()
|
|
}
|
|
}
|
|
|
|
/// Best-effort shell string splitter matching Zig's
|
|
/// `std.process.ArgIteratorGeneral(.{})` (whitespace separated, double
|
|
/// quotes group, backslashes only special before a double quote).
|
|
fn split_general(s: &str) -> Vec<String> {
|
|
let b = s.as_bytes();
|
|
let mut out = vec![];
|
|
let mut i = 0;
|
|
loop {
|
|
// skip whitespace
|
|
while i < b.len() && (b[i] == b' ' || b[i] == b'\t') {
|
|
i += 1;
|
|
}
|
|
if i >= b.len() {
|
|
break;
|
|
}
|
|
|
|
let mut cur: Vec<u8> = vec![];
|
|
let mut quoted = false;
|
|
while i < b.len() {
|
|
match b[i] {
|
|
b' ' | b'\t' if !quoted => break,
|
|
b'\\' => {
|
|
let mut n = 0;
|
|
while i < b.len() && b[i] == b'\\' {
|
|
n += 1;
|
|
i += 1;
|
|
}
|
|
if i < b.len() && b[i] == b'"' {
|
|
cur.extend(std::iter::repeat_n(b'\\', n / 2));
|
|
if n % 2 == 1 {
|
|
cur.push(b'"');
|
|
i += 1;
|
|
}
|
|
} else {
|
|
cur.extend(std::iter::repeat_n(b'\\', n));
|
|
}
|
|
}
|
|
b'"' => {
|
|
quoted = !quoted;
|
|
i += 1;
|
|
}
|
|
c => {
|
|
cur.push(c);
|
|
i += 1;
|
|
}
|
|
}
|
|
}
|
|
out.push(String::from_utf8_lossy(&cur).into_owned());
|
|
}
|
|
out
|
|
}
|
|
|
|
impl Command {
|
|
/// Creates a command as a single string, joining arguments as
|
|
/// necessary with spaces. Its not guaranteed that this is a valid
|
|
/// command; it is only meant to be human readable.
|
|
#[must_use]
|
|
pub fn string(&self) -> String {
|
|
match self {
|
|
Command::Shell(v) => v.clone(),
|
|
Command::Direct(v) => v.join(" "),
|
|
}
|
|
}
|
|
|
|
/// Get an iterator over the arguments array.
|
|
///
|
|
/// For direct commands this just iterates over the array. For shell
|
|
/// commands this uses a best effort shell string parser (like Zig's
|
|
/// `ArgIteratorGeneral`). This is not guaranteed to be 100% accurate,
|
|
/// but it works for common cases.
|
|
#[must_use]
|
|
pub fn arg_iterator(&self) -> ArgIterator {
|
|
let args = match self {
|
|
Command::Direct(v) => v.clone(),
|
|
Command::Shell(v) => split_general(v),
|
|
};
|
|
ArgIterator {
|
|
args: args.into_iter(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ConfigField for Command {
|
|
fn parse_cli(&mut self, input: Option<&str>) -> Result<(), ArgError> {
|
|
// Input is required. Whitespace on the edges isn't needed.
|
|
// Commands must be non-empty.
|
|
let input = input.ok_or(ArgError::ValueRequired)?;
|
|
let trimmed = input.trim_matches(' ');
|
|
if trimmed.is_empty() {
|
|
return Err(ArgError::ValueRequired);
|
|
}
|
|
|
|
// If we have a `:` then we MIGHT have a prefix to specify what
|
|
// tag we should use.
|
|
let (direct, str) = match trimmed.find(':') {
|
|
Some(idx) if &trimmed[..idx] == "direct" => (true, &trimmed[idx + 1..]),
|
|
Some(idx) if &trimmed[..idx] == "shell" => (false, &trimmed[idx + 1..]),
|
|
_ => (false, trimmed),
|
|
};
|
|
|
|
let str = str.trim_matches(' ');
|
|
*self = if direct {
|
|
// We're not shell expanding, so the arguments are naively
|
|
// split on spaces.
|
|
Command::Direct(str.split(' ').map(str::to_string).collect())
|
|
} else {
|
|
Command::Shell(str.to_string())
|
|
};
|
|
Ok(())
|
|
}
|
|
|
|
fn format_entry(&self, f: &mut EntryFormatter<'_>) -> fmt::Result {
|
|
match self {
|
|
Command::Shell(v) => writeln!(f.writer, "{} = {v}", f.name),
|
|
Command::Direct(v) => writeln!(f.writer, "{} = direct:{}", f.name, v.join(" ")),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[allow(clippy::unwrap_used)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::formatter::format_entry;
|
|
|
|
#[test]
|
|
fn command_parse_cli_errors() {
|
|
let mut v = Command::default();
|
|
assert_eq!(v.parse_cli(None), Err(ArgError::ValueRequired));
|
|
assert_eq!(v.parse_cli(Some("")), Err(ArgError::ValueRequired));
|
|
assert_eq!(v.parse_cli(Some(" ")), Err(ArgError::ValueRequired));
|
|
}
|
|
|
|
#[test]
|
|
fn command_parse_cli_shell_expanded() {
|
|
let mut v = Command::default();
|
|
v.parse_cli(Some("echo hello")).unwrap();
|
|
assert_eq!(v, Command::Shell("echo hello".into()));
|
|
|
|
// Spaces are stripped
|
|
v.parse_cli(Some(" echo hello ")).unwrap();
|
|
assert_eq!(v, Command::Shell("echo hello".into()));
|
|
}
|
|
|
|
#[test]
|
|
fn command_parse_cli_direct() {
|
|
let mut v = Command::default();
|
|
v.parse_cli(Some("direct:echo hello")).unwrap();
|
|
assert_eq!(v, Command::Direct(vec!["echo".into(), "hello".into()]));
|
|
|
|
// Spaces around the prefix
|
|
v.parse_cli(Some(" direct: echo hello")).unwrap();
|
|
assert_eq!(v, Command::Direct(vec!["echo".into(), "hello".into()]));
|
|
}
|
|
|
|
#[test]
|
|
fn command_arg_iterator_shell() {
|
|
let v = Command::Shell("echo hello world".into());
|
|
let mut it = v.arg_iterator();
|
|
assert_eq!(it.next().unwrap(), "echo");
|
|
assert_eq!(it.next().unwrap(), "hello");
|
|
assert_eq!(it.next().unwrap(), "world");
|
|
assert!(it.next().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn command_arg_iterator_shell_quotes() {
|
|
let v = Command::Shell("echo \"hello world\" a\\\"b".into());
|
|
let args: Vec<String> = v.arg_iterator().collect();
|
|
assert_eq!(args, ["echo", "hello world", "a\"b"]);
|
|
}
|
|
|
|
#[test]
|
|
fn command_arg_iterator_direct() {
|
|
let v = Command::Direct(vec!["echo".into(), "hello world".into()]);
|
|
let mut it = v.arg_iterator();
|
|
assert_eq!(it.next().unwrap(), "echo");
|
|
assert_eq!(it.next().unwrap(), "hello world");
|
|
assert!(it.next().is_none());
|
|
}
|
|
|
|
#[test]
|
|
fn command_string_shell() {
|
|
let v = Command::Shell("echo hello world".into());
|
|
assert_eq!(v.string(), "echo hello world");
|
|
}
|
|
|
|
#[test]
|
|
fn command_string_direct() {
|
|
let v = Command::Direct(vec!["echo".into(), "hello world".into()]);
|
|
assert_eq!(v.string(), "echo hello world");
|
|
}
|
|
|
|
#[test]
|
|
fn command_format_config_shell() {
|
|
let mut v = Command::default();
|
|
v.parse_cli(Some("echo hello")).unwrap();
|
|
let mut s = String::new();
|
|
format_entry("a", &v, &mut s).unwrap();
|
|
assert_eq!(s, "a = echo hello\n");
|
|
}
|
|
|
|
#[test]
|
|
fn command_format_config_direct() {
|
|
let mut v = Command::default();
|
|
v.parse_cli(Some("direct: echo hello")).unwrap();
|
|
let mut s = String::new();
|
|
format_entry("a", &v, &mut s).unwrap();
|
|
assert_eq!(s, "a = direct:echo hello\n");
|
|
}
|
|
}
|