conch-parser 0.1.1

A library for parsing programs written in the shell programming language.
Documentation
extern crate conch_parser;

use conch_parser::ast::*;
use conch_parser::ast::ComplexWord::*;
use conch_parser::ast::PipeableCommand::*;
use conch_parser::ast::SimpleWord::*;
use conch_parser::parse::ParseError::*;
use conch_parser::token::Token;

mod parse_support;
use parse_support::*;

fn simple_command_with_redirect(cmd: &str, redirect: DefaultRedirect) -> DefaultPipeableCommand {
    Simple(Box::new(SimpleCommand {
        redirects_or_env_vars: vec!(),
        redirects_or_cmd_words: vec!(
            RedirectOrCmdWord::CmdWord(word(cmd)),
            RedirectOrCmdWord::Redirect(redirect),
        ),
    }))
}

#[test]
fn test_redirect_valid_close_without_whitespace() {
    let mut p = make_parser(">&-");
    assert_eq!(Some(Ok(Redirect::DupWrite(None, word("-")))), p.redirect().unwrap());
}

#[test]
fn test_redirect_valid_close_with_whitespace() {
    let mut p = make_parser("<&       -");
    assert_eq!(Some(Ok(Redirect::DupRead(None, word("-")))), p.redirect().unwrap());
}

#[test]
fn test_redirect_valid_start_with_dash_if_not_dup() {
    let path = word("-test");
    let cases = vec!(
        ("4<-test",  Redirect::Read(Some(4), path.clone())),
        ("4>-test",  Redirect::Write(Some(4), path.clone())),
        ("4<>-test", Redirect::ReadWrite(Some(4), path.clone())),
        ("4>>-test", Redirect::Append(Some(4), path.clone())),
        ("4>|-test", Redirect::Clobber(Some(4), path.clone())),
    );

    for (s, correct) in cases.into_iter() {
        match make_parser(s).redirect() {
            Ok(Some(Ok(ref r))) if *r == correct => {},
            Ok(r) => {
                panic!("Unexpectedly parsed the source \"{}\" as\n{:?} instead of\n{:?}", s, r, correct)
            },
            Err(err) => panic!("Failed to parse the source \"{}\": {}", s, err),
        }
    }
}

#[test]
fn test_redirect_valid_return_word_if_no_redirect() {
    let mut p = make_parser("hello");
    assert_eq!(Some(Err(word("hello"))), p.redirect().unwrap());
}

#[test]
fn test_redirect_valid_return_word_if_src_fd_is_definitely_non_numeric() {
    let mut p = make_parser("123$$'foo'>out");
    let correct = TopLevelWord(Concat(vec!(
        lit("123"),
        Word::Simple(Param(Parameter::Dollar)),
        Word::SingleQuoted(String::from("foo")),
    )));
    assert_eq!(Some(Err(correct)), p.redirect().unwrap());
}

#[test]
fn test_redirect_valid_return_word_if_src_fd_has_escaped_numerics() {
    let mut p = make_parser("\\2>");
    let correct = word_escaped("2");
    assert_eq!(Some(Err(correct)), p.redirect().unwrap());
}

#[test]
fn test_redirect_valid_dst_fd_can_have_escaped_numerics() {
    let mut p = make_parser(">\\2");
    let correct = Redirect::Write(None, word_escaped("2"));
    assert_eq!(Some(Ok(correct)), p.redirect().unwrap());
}

#[test]
fn test_redirect_invalid_dup_if_dst_fd_is_definitely_non_numeric() {
    assert_eq!(Err(BadFd(src(2, 1, 3), src(12, 1, 13))), make_parser(">&123$$'foo'").redirect());
}

#[test]
fn test_redirect_valid_dup_return_redirect_if_dst_fd_is_possibly_numeric() {
    let mut p = make_parser(">&123$$$(echo 2)`echo bar`");
    let correct = Redirect::DupWrite(
        None,
        TopLevelWord(Concat(vec!(
            lit("123"),
            Word::Simple(Param(Parameter::Dollar)),
            subst(ParameterSubstitution::Command(vec!(cmd_args("echo", &["2"])))),
            subst(ParameterSubstitution::Command(vec!(cmd_args("echo", &["bar"])))),
        ))),
    );
    assert_eq!(Some(Ok(correct)), p.redirect().unwrap());
}

#[test]
fn test_redirect_invalid_close_without_whitespace() {
    assert_eq!(Err(BadFd(src(2, 1, 3), src(7, 1, 8))), make_parser(">&-asdf").redirect());
}

#[test]
fn test_redirect_invalid_close_with_whitespace() {
    assert_eq!(Err(BadFd(src(9, 1, 10), src(14, 1, 15))), make_parser("<&       -asdf").redirect());
}

#[test]
fn test_redirect_fd_immediately_preceeding_redirection() {
    let mut p = make_parser("foo 1>>out");
    let cmd = p.simple_command().unwrap();
    assert_eq!(cmd, simple_command_with_redirect("foo", Redirect::Append(Some(1), word("out"))));
}

#[test]
fn test_redirect_fd_must_immediately_preceed_redirection() {
    let correct = Simple(Box::new(SimpleCommand {
        redirects_or_env_vars: vec!(),
        redirects_or_cmd_words: vec!(
            RedirectOrCmdWord::CmdWord(word("foo")),
            RedirectOrCmdWord::CmdWord(word("1")),
            RedirectOrCmdWord::Redirect(Redirect::ReadWrite(None, word("out"))),
        ),
    }));

    let mut p = make_parser("foo 1 <>out");
    assert_eq!(p.simple_command().unwrap(), correct);
}

#[test]
fn test_redirect_valid_dup_with_fd() {
    let mut p = make_parser("foo 1>&2");
    let cmd = p.simple_command().unwrap();
    assert_eq!(cmd, simple_command_with_redirect("foo", Redirect::DupWrite(Some(1), word("2"))));
}

#[test]
fn test_redirect_valid_dup_without_fd() {
    let correct = Simple(Box::new(SimpleCommand {
        redirects_or_env_vars: vec!(),
        redirects_or_cmd_words: vec!(
            RedirectOrCmdWord::CmdWord(word("foo")),
            RedirectOrCmdWord::CmdWord(word("1")),
            RedirectOrCmdWord::Redirect(Redirect::DupRead(None, word("2"))),
        ),
    }));

    let mut p = make_parser("foo 1 <&2");
    assert_eq!(p.simple_command().unwrap(), correct);
}

#[test]
fn test_redirect_valid_dup_with_whitespace() {
    let mut p = make_parser("foo 1<& 2");
    let cmd = p.simple_command().unwrap();
    assert_eq!(cmd, simple_command_with_redirect("foo", Redirect::DupRead(Some(1), word("2"))));
}

#[test]
fn test_redirect_valid_single_quoted_dup_fd() {
    let correct = Redirect::DupWrite(Some(1), single_quoted("2"));
    assert_eq!(Some(Ok(correct)), make_parser("1>&'2'").redirect().unwrap());
}

#[test]
fn test_redirect_valid_double_quoted_dup_fd() {
    let correct = Redirect::DupWrite(None, double_quoted("2"));
    assert_eq!(Some(Ok(correct)), make_parser(">&\"2\"").redirect().unwrap());
}

#[test]
fn test_redirect_src_fd_need_not_be_single_token() {
    let mut p = make_parser_from_tokens(vec!(
        Token::Literal(String::from("foo")),
        Token::Whitespace(String::from(" ")),
        Token::Literal(String::from("12")),
        Token::Literal(String::from("34")),
        Token::LessAnd,
        Token::Dash,
    ));

    let cmd = p.simple_command().unwrap();
    assert_eq!(cmd, simple_command_with_redirect("foo", Redirect::DupRead(Some(1234), word("-"))));
}

#[test]
fn test_redirect_dst_fd_need_not_be_single_token() {
    let mut p = make_parser_from_tokens(vec!(
        Token::GreatAnd,
        Token::Literal(String::from("12")),
        Token::Literal(String::from("34")),
    ));

    let correct = Redirect::DupWrite(None, word("1234"));
    assert_eq!(Some(Ok(correct)), p.redirect().unwrap());
}

#[test]
fn test_redirect_accept_literal_and_name_tokens() {
    let mut p = make_parser_from_tokens(vec!(
        Token::GreatAnd,
        Token::Literal(String::from("12")),
    ));
    assert_eq!(Some(Ok(Redirect::DupWrite(None, word("12")))), p.redirect().unwrap());

    let mut p = make_parser_from_tokens(vec!(
        Token::GreatAnd,
        Token::Name(String::from("12")),
    ));
    assert_eq!(Some(Ok(Redirect::DupWrite(None, word("12")))), p.redirect().unwrap());
}

#[test]
fn test_redirect_list_valid() {
    let mut p = make_parser("1>>out <& 2 2>&-");
    let io = p.redirect_list().unwrap();
    assert_eq!(io, vec!(
        Redirect::Append(Some(1), word("out")),
        Redirect::DupRead(None, word("2")),
        Redirect::DupWrite(Some(2), word("-")),
    ));
}

#[test]
fn test_redirect_list_rejects_non_fd_words() {
    assert_eq!(Err(BadFd(src(16, 1, 17), src(19, 1, 20))), make_parser("1>>out <in 2>&- abc").redirect_list());
    assert_eq!(Err(BadFd(src(7,  1, 8),  src(10,  1, 11))), make_parser("1>>out abc<in 2>&-").redirect_list());
    assert_eq!(Err(BadFd(src(7,  1, 8),  src(10,  1, 11))), make_parser("1>>out abc <in 2>&-").redirect_list());
}