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
//! `comma` parses command-line-style strings. See [`Command::from_str`] for syntax details,
//! and [`Command`] for structure details.

use std::str::FromStr;
use std::iter::FromIterator;
use characters::ParserData;
use syntax_blocks::*;

#[macro_use]
mod error_types;
mod syntax_blocks;
mod characters;

err_type!(pub, EmptyCommandError, "command string has no command name or arguments");

/// Contains the result of a parsed command. See [`Command::from_str`] documentation for details on
/// available command syntax.
#[derive(Debug, Clone)]
pub struct Command {
    /// The name of the command being run (i.e. the first argument)
    pub name: String,
    /// All arguments being passed
    pub arguments: Vec<String>
}

impl FromStr for Command {
    type Err = EmptyCommandError;

    /// Parse a command from the commandline. Commands consist of separate 'tokens' separated by
    /// whitespace.
    ///
    /// Multiple whitespace characters are permitted between tokens, including at the beginning and
    /// end of the command string. All extra whitespace is stripped unless explicitly escaped using
    /// quotation marks or backslash escaping.
    ///
    /// Preceding a character with a backslash (`\`) will cause any special meaning for the
    /// character to be ignored. To convey a *real* backslash in a command it must be prefixed with
    /// another backslash, such as: (`\\`).
    ///
    /// Quotation marks surrounding a portion of text will also cause the text to be included
    /// verbatim, including whitespace. However, backslashes retain their special meaning, to allow
    /// for escaped quotes (`\"`) inside a quoted string.
    ///
    /// `from_str` will only fail if zero tokens are provided (i.e. there is no command name). In
    /// this case it will provide an [`EmptyCommandError`].
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        // We prepend whitespace to force the whitespace syntax block to add in a token.
        let mut input = String::from(" ");
        input.push_str(s);

        // Parse all data using syntax blocks
        let mut data = ParserData::new(&input);
        while data.not_empty() {
            handle_or_push(&mut data, &vec![ &EscapeBlock{}, &QuoteBlock{}, &WhitespaceBlock{} ]);
        }
        let mut tokens = data.get_result().clone();

        // Prevents whitespace at the end of the command from creating an empty garbage argument.
        if tokens.last().unwrap().is_empty() {
            tokens.pop();
        }

        if tokens.is_empty() {
            // Fail if no command was provided
            Err(EmptyCommandError)
        } else {
            // Turn the first token into the command name and others into arguments
            Ok(Command {
                name: tokens[0].clone(),
                arguments: Vec::from_iter(tokens[1..].iter().cloned())
            })
        }
    }
}

#[cfg(test)]
mod tests {
    use crate::Command;
    use std::str::FromStr;

    #[test]
    fn no_arguments_works() {
        let result = Command::from_str("hello").unwrap();
        if !result.name.eq(&String::from("hello")) {
            panic!("Argument-free command doesn't handle command name correctly");
        }
        if !result.arguments.is_empty() {
            panic!("Argument-free command doesn't have empty argument list");
        }
    }

    #[test]
    fn arguments_works() {
        let result =
            Command::from_str("hello world \\\"this is\\\" a \"quoted \\\"string\\\"\"")
                .unwrap();
        if !result.arguments.len() == 5 {
            panic!("Wrong number of arguments parsed");
        }
        if !result.arguments[1].eq(&String::from("\"this")) {
            panic!("Escaped quotes not handled correctly");
        }
        if !result.arguments[4].eq(&String::from("quoted \"string\"")) {
            panic!("Quoted string not handled correctly");
        }
    }

    #[test]
    #[should_panic]
    fn empty_fails() {
        Command::from_str("    ").unwrap();
    }
}