dprint 0.54.0

Binary for dprint code formatter—a pluggable and configurable code formatting platform.
use thiserror::Error;

#[derive(Debug, Error)]
pub enum CommandParseError {
  #[error("Found zero arguments.")]
  Empty,
  #[error("Unclosed single quote.")]
  UnclosedSingleQuote,
  #[error("Unclosed double quote.")]
  UnclosedDoubleQuote,
}

pub fn parse_command_line(input: &str) -> Result<Vec<String>, CommandParseError> {
  use CommandParseError::*;

  let mut args = Vec::new();
  let mut current = String::new();
  let mut in_single = false;
  let mut in_double = false;
  let mut chars = input.chars();

  while let Some(c) = chars.next() {
    match c {
      c if c.is_whitespace() && !in_single && !in_double => {
        if !current.is_empty() {
          args.push(std::mem::take(&mut current));
        }
      }
      '\'' if !in_double => {
        in_single = !in_single;
      }
      '"' if !in_single => {
        in_double = !in_double;
      }
      '\\' if !in_single => {
        if let Some(next) = chars.next() {
          current.push(next);
        } else {
          current.push('\\');
        }
      }

      _ => current.push(c),
    }
  }

  if in_single {
    return Err(UnclosedSingleQuote);
  }
  if in_double {
    return Err(UnclosedDoubleQuote);
  }

  if !current.is_empty() {
    args.push(current);
  }

  if args.is_empty() { Err(Empty) } else { Ok(args) }
}

#[cfg(test)]
mod tests {
  use super::CommandParseError;
  use super::parse_command_line;

  #[test]
  fn parses_simple_command() {
    let args = parse_command_line("vim").unwrap();
    assert_eq!(args, vec!["vim"]);
  }

  #[test]
  fn parses_command_with_arg() {
    let args = parse_command_line("vim -f").unwrap();
    assert_eq!(args, vec!["vim", "-f"]);
  }

  #[test]
  fn collapses_multiple_spaces() {
    let args = parse_command_line("  vim   -f   file.txt  ").unwrap();
    assert_eq!(args, vec!["vim", "-f", "file.txt"]);
  }

  #[test]
  fn parses_double_quoted_args() {
    let args = parse_command_line(r#""code" "--wait""#).unwrap();
    assert_eq!(args, vec!["code", "--wait"]);
  }

  #[test]
  fn parses_single_quoted_args_with_spaces() {
    let args = parse_command_line("nvim --cmd 'set number'").unwrap();
    assert_eq!(args, vec!["nvim", "--cmd", "set number"]);
  }

  #[test]
  fn parses_mixed_quotes() {
    let args = parse_command_line(r#"emacsclient -c -a "emacs -nw""#).unwrap();
    assert_eq!(args, vec!["emacsclient", "-c", "-a", "emacs -nw"]);
  }

  #[test]
  fn parses_path_with_escaped_space() {
    let args = parse_command_line(r#"/Applications/Sublime\ Text.app/Contents/SharedSupport/bin/subl -w"#).unwrap();
    assert_eq!(args, vec!["/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl", "-w",]);
  }

  #[test]
  fn backslash_escapes_next_char_outside_single_quotes() {
    let args = parse_command_line(r#"vim \"weird\"-name"#).unwrap();
    assert_eq!(args, vec!["vim", "\"weird\"-name"]);
  }

  #[test]
  fn backslash_is_literal_inside_single_quotes() {
    let args = parse_command_line(r#"'a\b c' other"#).unwrap();
    assert_eq!(args, vec!["a\\b c", "other"]);
  }

  #[test]
  fn empty_string_is_error() {
    let err = parse_command_line("").unwrap_err();
    assert!(matches!(err, CommandParseError::Empty));
  }

  #[test]
  fn whitespace_only_is_error() {
    let err = parse_command_line("   \t  ").unwrap_err();
    assert!(matches!(err, CommandParseError::Empty));
  }

  #[test]
  fn unclosed_single_quote_is_error() {
    let err = parse_command_line("vim 'unclosed").unwrap_err();
    assert!(matches!(err, CommandParseError::UnclosedSingleQuote));
  }

  #[test]
  fn unclosed_double_quote_is_error() {
    let err = parse_command_line(r#"vim "unclosed"#).unwrap_err();
    assert!(matches!(err, CommandParseError::UnclosedDoubleQuote));
  }
}