oursh 0.4.4

Modern, fast POSIX compatible shell
Documentation
//* vim: set ft=rust: */
use crate::program::posix::{ast, lex};

grammar<'input>(text: &'input str);

extern {
    type Location = usize;
    type Error = lex::Error;

    enum lex::Token<'input> {
        " "         => lex::Token::Space,
        "\t"        => lex::Token::Tab,
        "\n"        => lex::Token::Linefeed,
        ";"         => lex::Token::Semi,
        "&"         => lex::Token::Amper,
        "{"         => lex::Token::LBrace,
        "}"         => lex::Token::RBrace,
        "("         => lex::Token::LParen,
        ")"         => lex::Token::RParen,
        "`"         => lex::Token::Backtick,
        "!"         => lex::Token::Bang,
        "|"         => lex::Token::Pipe,
        "$"         => lex::Token::Dollar,
        "="         => lex::Token::Equals,
        "\\"        => lex::Token::Backslash,
        "\""        => lex::Token::DoubleQuote,
        "'"         => lex::Token::SingleQuote,
        ">"         => lex::Token::Great,
        ">>"        => lex::Token::DGreat,
        ">&"        => lex::Token::GreatAnd,
        ">|"        => lex::Token::Clobber,
        "<"         => lex::Token::Less,
        "<<"        => lex::Token::DLess,
        "<<-"       => lex::Token::DLessDash,
        "<&"        => lex::Token::LessAnd,
        "<>"        => lex::Token::LessGreat,
        "&&"        => lex::Token::And,
        "||"        => lex::Token::Or,
        "if"        => lex::Token::If,
        "then"      => lex::Token::Then,
        "else"      => lex::Token::Else,
        "elif"      => lex::Token::Elif,
        "fi"        => lex::Token::Fi,
        "export"    => lex::Token::Export,
        "WORD"      => lex::Token::Word(<&'input str>),
        "IO_NUMBER" => lex::Token::IoNumber(<usize>),
        "{#"        => lex::Token::HashLang(<&'input str>),
        "{#!"       => lex::Token::Shebang(<&'input str>),
        "TEXT"      => lex::Token::Text(<&'input str>),
    }
}

pub Program: ast::Program = {
    <p: Program> "\n" <l: Jobs> => p.append(&l),
    <p: Program> "\n" => p,
    <p: Program> ";" <g: Jobs> => p.append(&g),
    <p: Program> ";" => p,
    Jobs => <>,
}

Jobs: ast::Program = {
    <cs: Command> "&" <j: Jobs> => {
        j.insert(&ast::Command::Background(Box::new(cs)))
    },
    Job => ast::Program(vec![<>]),
}

Job: ast::Command = {
    <cs: Command> "&" => {
        ast::Command::Background(Box::new(cs))
    },
    Command => <>,
}

Compound: ast::Command = {
    <cs: Command> ";" <c: Compound> => {
        match c {
            c @ ast::Command::Compound(_) => c.insert(&cs),
            c => ast::Command::Compound(vec![cs, c]),
        }
    },
    <cs: Command> ";" => {
        ast::Command::Compound(vec![cs])
    },
}

pub Command: ast::Command = {
    // TODO #15: Hopefully in fixing #8 and #10 this can play nicely.
    // NOTE: This can be successfully complied, but will break a doc tests.
    <s: "{#!"> <t: "TEXT"> "}" => {
        let i = ast::Interpreter::Shebang(s.into());
        ast::Command::Lang(i, t.into())
    },
    <l: "{#"> <t: "TEXT"> "}" => {
        let i = if l.is_empty() {
            ast::Interpreter::Alternate
        } else {
            ast::Interpreter::HashLang(l.into())
        };
        ast::Command::Lang(i, t.into())
    },
    "$" "(" <p: Program> ")" => ast::Command::Subshell(Box::new(p)),
    "$" "(" ")"              => ast::Command::Subshell(Box::new(ast::Program(vec![]))),
    "{" <c: Compound> "}" => c,
    "if" <cond: Compound> "then" <then: Compound> <els: Else> "fi" => {
        let left = ast::Command::And(Box::new(cond), Box::new(then));
        ast::Command::Or(Box::new(left), Box::new(els))
    },
    "if" <cond: Compound> "then" <then: Compound> "fi" => {
        ast::Command::And(Box::new(cond), Box::new(then))
    },
    <cs: Command> "&&" <p: Pipeline> => {
        ast::Command::And(Box::new(cs), Box::new(p))
    },
    <cs: Command> "||" <p: Pipeline> => {
        ast::Command::Or(Box::new(cs), Box::new(p))
    },
    Pipeline => <>,
}

Else: ast::Command = {
    "elif" <elif: Compound> "then" <then: Compound> => {
        ast::Command::And(Box::new(elif), Box::new(then))
    },
    "elif" <elif: Compound> "then" <then: Compound> <els: Else> => {
        let left = ast::Command::And(Box::new(elif), Box::new(then));
        ast::Command::Or(Box::new(left), Box::new(els))
    },
    "else" <els: Compound> => els,
}

Pipeline: ast::Command = {
    "!" <ps: PipelineSeq> => {
        ast::Command::Not(Box::new(ps))
    },
    <ps: PipelineSeq> => ps,
}

PipelineSeq: ast::Command = {
    <ps: PipelineSeq> "|" "\n"* <c: Simple> => {
        ast::Command::Pipeline(Box::new(ps), Box::new(c))
    },
    <c: Simple> => c,
}



Simple: ast::Command = {
    <assignments: Assignment+> => {
        ast::Command::Simple(assignments, vec![], vec![])
    },
    <redirects: Redirect+> => {
        ast::Command::Simple(vec![], vec![], redirects)
    },
    <assignments: Assignment*>
    <mut prefix: Redirect*>
    <words: "WORD"+>
    <mut suffix: Redirect*> => {
        let redirects = { prefix.append(&mut suffix); prefix };
        ast::Command::Simple(assignments, words.iter().map(|w| {
            ast::Word(w.to_string())
        }).collect(), redirects)
    },

    // Export support.
    "export" <assignments: Assignment+> => {
        ast::Command::Simple(assignments, vec![], vec![])
    },
}

Redirect: ast::Redirect = {
    File => <>,
    // Here => <>,
    <n: "IO_NUMBER"> <mut r: File> => { *r.fd() = n as i32; r },
    // <n: "IO_NUMBER"> <mut r: Here> => { *r.fd() = n; r },
}

File: ast::Redirect = {
    "<"  <f: "WORD"> => ast::Redirect::Read {
        n: 0,
        duplicate: false,
        filename: f.into(),
    },
    "<&" <f: "WORD"> => ast::Redirect::Read {
        n: 0,
        duplicate: true,
        filename: f.into(),
    },
    ">"  <f: "WORD"> => ast::Redirect::Write {
        n: 1,
        duplicate: false,
        clobber: false,
        append: false,
        filename: f.into(),
    },
    ">&" <f: "WORD"> => ast::Redirect::Write {
        n: 1,
        duplicate: true,
        clobber: false,
        append: false,
        filename: f.into(),
    },
    ">>" <f: "WORD"> => ast::Redirect::Write {
        n: 1,
        duplicate: false,
        clobber: false,
        append: true,
        filename: f.into(),
    },
    ">|" <f: "WORD"> => ast::Redirect::Write {
        n: 1,
        duplicate: false,
        clobber: true,
        append: false,
        filename: f.into(),
    },
    "<>" <f: "WORD"> => ast::Redirect::RW {
        n: 0,
        filename: f.into(),
    },
}

// Here: ast::Redirect = {
//     "<<" < <words: "WORD"+>  => {
//         let s = "".into();
//         ast::Redirect::Here { n: 0, leading: true, string: s }
//     },
//     "<<-" <words: "WORD"+> => {
//         let s = "".into();
//         ast::Redirect::Here { n: 0, leading: false, string: s }
//     },
// }

// pub Word: ast::Word = {
//     <w: "WORD"> => ast::Word(w.into()),
//     "$" <v: "WORD"> => ast::Word(var(v).unwrap_or(format!("${}", v))),
// }

Assignment: ast::Assignment = {
    // TODO: Variable expansion.
    <k: "WORD"> "=" <v: "WORD"> => ast::Assignment(k.into(), v.into()),
}