use std::borrow::Cow;
use std::error::Error;
use std::fmt::{self, Debug};
use std::iter;
static COMMAND_LIST_BEGIN: &str = "command_list_ok_begin\n";
static COMMAND_LIST_END: &str = "command_list_end\n";
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct Command {
base: Cow<'static, str>,
args: Vec<Cow<'static, str>>,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct CommandList {
first: Command,
tail: Vec<Command>,
}
pub trait Argument {
fn render(self) -> Cow<'static, str>;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CommandError {
Empty,
InvalidCharacter(usize, char),
UnncessaryWhitespace,
CommandList,
}
impl Command {
pub fn new(command: impl Into<Cow<'static, str>>) -> Self {
Self::build(command).expect("Invalid command")
}
pub fn build(command: impl Into<Cow<'static, str>>) -> Result<Self, CommandError> {
let base = command.into();
validate_command_part(&base)?;
Ok(Self {
base,
args: Vec::new(),
})
}
pub fn argument(mut self, argument: impl Argument) -> Self {
self.add_argument(argument).expect("Invalid argument");
self
}
pub fn add_argument(&mut self, argument: impl Argument) -> Result<(), CommandError> {
let argument = argument.render();
validate_argument(&argument)?;
self.args.push(argument);
Ok(())
}
pub(crate) fn render(self) -> String {
let mut out = self.base.into_owned();
for arg in self.args {
let quote = needs_quotes(&arg);
let arg = escape_argument(&arg);
out.reserve(1 + arg.len() + if quote { 2 } else { 0 } + 1);
out.push(' ');
if quote {
out.push('"');
}
out.push_str(&arg);
if quote {
out.push('"');
}
}
out.push('\n');
out
}
}
#[allow(clippy::len_without_is_empty)]
impl CommandList {
pub fn new(first: Command) -> Self {
Self {
first,
tail: Vec::new(),
}
}
pub fn command(mut self, command: Command) -> Self {
self.add(command);
self
}
pub fn add(&mut self, command: Command) {
self.tail.push(command);
}
pub fn len(&self) -> usize {
1 + self.tail.len()
}
pub(crate) fn render(self) -> String {
if self.tail.is_empty() {
return self.first.render();
}
let commands = iter::once(self.first)
.chain(self.tail)
.map(|c| c.render())
.collect::<Vec<_>>();
let mut out = String::with_capacity(
COMMAND_LIST_BEGIN.len()
+ commands.iter().map(|c| c.len()).sum::<usize>()
+ COMMAND_LIST_END.len(),
);
out.push_str(COMMAND_LIST_BEGIN);
out.extend(commands);
out.push_str(COMMAND_LIST_END);
out
}
}
impl Extend<Command> for CommandList {
fn extend<T: IntoIterator<Item = Command>>(&mut self, iter: T) {
self.tail.extend(iter);
}
}
impl Argument for String {
fn render(self) -> Cow<'static, str> {
Cow::Owned(self)
}
}
impl Argument for &'static str {
fn render(self) -> Cow<'static, str> {
Cow::Borrowed(self)
}
}
pub fn escape_argument(argument: &str) -> Cow<'_, str> {
let escape_count = argument.chars().filter(|c| should_escape(*c)).count();
if escape_count == 0 {
return Cow::Borrowed(argument);
}
let mut out = String::with_capacity(argument.len() + escape_count);
for c in argument.chars() {
if should_escape(c) {
out.push('\\');
}
out.push(c);
}
Cow::Owned(out)
}
fn needs_quotes(arg: &str) -> bool {
arg.chars().any(|c| c == ' ')
}
fn should_escape(c: char) -> bool {
c == '\\' || c == '"' || c == '\''
}
fn validate_no_extra_whitespace(command: &str) -> Result<(), CommandError> {
if command.chars().nth(0).unwrap().is_ascii_whitespace()
|| command.chars().last().unwrap().is_ascii_whitespace()
{
Err(CommandError::UnncessaryWhitespace)
} else {
Ok(())
}
}
fn validate_command_part(command: &str) -> Result<(), CommandError> {
if command.is_empty() {
return Err(CommandError::Empty);
}
validate_no_extra_whitespace(command)?;
if let Some((i, c)) = command
.char_indices()
.find(|(_, c)| !is_valid_command_char(*c))
{
Err(CommandError::InvalidCharacter(i, c))
} else if is_command_list_command(command) {
Err(CommandError::CommandList)
} else {
Ok(())
}
}
fn validate_argument(argument: &str) -> Result<&str, CommandError> {
validate_no_extra_whitespace(argument)?;
match argument.char_indices().find(|(_, c)| *c == '\n') {
None => Ok(argument),
Some((i, c)) => Err(CommandError::InvalidCharacter(i, c)),
}
}
fn is_valid_command_char(c: char) -> bool {
c.is_ascii_alphabetic() || c == '_'
}
fn is_command_list_command(command: &str) -> bool {
command.starts_with("command_list")
}
impl Error for CommandError {}
impl fmt::Display for CommandError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
CommandError::Empty => write!(f, "Command or command list was empty"),
CommandError::InvalidCharacter(i, c) => write!(
f,
"Command contained an invalid character: {:?} at position {}",
c, i
),
CommandError::UnncessaryWhitespace => {
write!(f, "Command contained leading or trailing whitespace")
}
CommandError::CommandList => {
write!(f, "Command attempted to open or close a command list")
}
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::filter::Filter;
#[test]
fn single_render() {
assert_eq!(Command::build("status").unwrap().render(), "status\n");
assert_eq!(Command::new("pause").argument("1").render(), "pause 1\n");
assert_eq!(
Command::new("hello").argument("foo bar").render(),
"hello \"foo bar\"\n"
);
assert_eq!(
Command::new("hello").argument("foo's bar\"").render(),
"hello \"foo\\'s bar\\\"\"\n"
);
assert_eq!(
Command::new("find")
.argument(Filter::equal("album", "hello world"))
.render(),
"find \"(album == \\\"hello world\\\")\"\n"
);
assert_eq!(
Command::build(" hello").unwrap_err(),
CommandError::UnncessaryWhitespace
);
assert_eq!(Command::build("").unwrap_err(), CommandError::Empty);
assert_eq!(
Command::build("hello world").unwrap_err(),
CommandError::InvalidCharacter(5, ' ')
);
assert_eq!(
Command::build("command_list_ok_begin").unwrap_err(),
CommandError::CommandList
);
}
#[test]
fn command_list_render() {
let starter = CommandList::new(Command::new("status"));
assert_eq!(starter.clone().render(), "status\n");
assert_eq!(
starter
.clone()
.command(Command::new("hello").argument("world"))
.render(),
"command_list_ok_begin\nstatus\nhello world\ncommand_list_end\n"
);
}
}