Skip to content

Commit

Permalink
Merge branch 'main' into dependabot/cargo/cargo-a6b8b6006b
Browse files Browse the repository at this point in the history
  • Loading branch information
reubeno authored May 29, 2024
2 parents e0d540f + 9ff2060 commit 8e56582
Show file tree
Hide file tree
Showing 5 changed files with 85 additions and 75 deletions.
8 changes: 4 additions & 4 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ jobs:
uses: Swatinem/rust-cache@v2

- name: Install cargo-binstall
uses: cargo-bins/[email protected].7
uses: cargo-bins/[email protected].8

- name: Build
run: cargo build --release
Expand All @@ -61,7 +61,7 @@ jobs:
cache-all-crates: true

- name: Install cargo-binstall
uses: cargo-bins/[email protected].7
uses: cargo-bins/[email protected].8

- name: Install cargo-llvm-cov
run: cargo binstall --no-confirm --force cargo-llvm-cov
Expand Down Expand Up @@ -140,7 +140,7 @@ jobs:
cache-all-crates: true

- name: Install cargo-binstall
uses: cargo-bins/[email protected].7
uses: cargo-bins/[email protected].8

- name: Format check
run: cargo fmt --check --all
Expand Down Expand Up @@ -186,7 +186,7 @@ jobs:
./main
- name: Install cargo-binstall
uses: cargo-bins/[email protected].7
uses: cargo-bins/[email protected].8

- name: Performance analysis on PR
run: cargo bench --workspace -- --output-format bencher | tee benchmarks.txt
Expand Down
131 changes: 71 additions & 60 deletions parser/src/parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,14 +119,17 @@ pub fn parse_tokens(
impl<'a> peg::Parse for Tokens<'a> {
type PositionRepr = usize;

#[inline]
fn start(&self) -> usize {
0
}

#[inline]
fn is_eof(&self, p: usize) -> bool {
p >= self.tokens.len()
}

#[inline]
fn position_repr(&self, p: usize) -> Self::PositionRepr {
p
}
Expand All @@ -135,6 +138,7 @@ impl<'a> peg::Parse for Tokens<'a> {
impl<'a> peg::ParseElem<'a> for Tokens<'a> {
type Element = &'a Token;

#[inline]
fn parse_elem(&'a self, pos: usize) -> peg::RuleResult<Self::Element> {
match self.tokens.get(pos) {
Some(c) => peg::RuleResult::Matched(pos + 1, c),
Expand Down Expand Up @@ -211,9 +215,11 @@ peg::parser! {
first:pipeline() additional:_and_or_item()* { ast::AndOrList { first, additional } }

rule _and_or_item() -> ast::AndOr =
specific_operator("&&") linebreak() p:pipeline() { ast::AndOr::And(p) } /
specific_operator("||") linebreak() p:pipeline() { ast::AndOr::Or(p) }
op:_and_or_op() linebreak() p:pipeline() { op(p) }

rule _and_or_op() -> fn(ast::Pipeline) -> ast::AndOr =
specific_operator("&&") { ast::AndOr::And } /
specific_operator("||") { ast::AndOr::Or }

rule pipeline() -> ast::Pipeline =
bang:bang()? seq:pipe_sequence() { ast::Pipeline { bang: bang.is_some(), seq } }
Expand Down Expand Up @@ -252,7 +258,7 @@ peg::parser! {
}

rule arithmetic_expression() -> ast::UnexpandedArithmeticExpr =
raw_expr:$((!arithmetic_end() [_])*) { ast::UnexpandedArithmeticExpr { value: raw_expr.to_owned() } }
raw_expr:$((!arithmetic_end() [_])*) { ast::UnexpandedArithmeticExpr { value: raw_expr } }

// TODO: evaluate arithmetic end; the semicolon is used in arithmetic for loops.
rule arithmetic_end() -> () =
Expand Down Expand Up @@ -339,36 +345,39 @@ peg::parser! {
left:word() specific_operator("<") right:word() { ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::LeftSortsBeforeRight, ast::Word::from(left), ast::Word::from(right)) }
left:word() specific_operator(">") right:word() { ast::ExtendedTestExpr::BinaryTest(ast::BinaryPredicate::LeftSortsAfterRight, ast::Word::from(left), ast::Word::from(right)) }
--
specific_word("-a") f:word() { ast::ExtendedTestExpr::UnaryTest(ast::UnaryPredicate::FileExists, ast::Word::from(f)) }
specific_word("-b") f:word() { ast::ExtendedTestExpr::UnaryTest(ast::UnaryPredicate::FileExistsAndIsBlockSpecialFile, ast::Word::from(f)) }
specific_word("-c") f:word() { ast::ExtendedTestExpr::UnaryTest(ast::UnaryPredicate::FileExistsAndIsCharSpecialFile, ast::Word::from(f)) }
specific_word("-d") f:word() { ast::ExtendedTestExpr::UnaryTest(ast::UnaryPredicate::FileExistsAndIsDir, ast::Word::from(f)) }
specific_word("-e") f:word() { ast::ExtendedTestExpr::UnaryTest(ast::UnaryPredicate::FileExists, ast::Word::from(f)) }
specific_word("-f") f:word() { ast::ExtendedTestExpr::UnaryTest(ast::UnaryPredicate::FileExistsAndIsRegularFile, ast::Word::from(f)) }
specific_word("-g") f:word() { ast::ExtendedTestExpr::UnaryTest(ast::UnaryPredicate::FileExistsAndIsSetgid, ast::Word::from(f)) }
specific_word("-h") f:word() { ast::ExtendedTestExpr::UnaryTest(ast::UnaryPredicate::FileExistsAndIsSymlink, ast::Word::from(f)) }
specific_word("-k") f:word() { ast::ExtendedTestExpr::UnaryTest(ast::UnaryPredicate::FileExistsAndHasStickyBit, ast::Word::from(f)) }
specific_word("-n") f:word() { ast::ExtendedTestExpr::UnaryTest(ast::UnaryPredicate::StringHasNonZeroLength, ast::Word::from(f)) }
specific_word("-o") f:word() { ast::ExtendedTestExpr::UnaryTest(ast::UnaryPredicate::ShellOptionEnabled, ast::Word::from(f)) }
specific_word("-p") f:word() { ast::ExtendedTestExpr::UnaryTest(ast::UnaryPredicate::FileExistsAndIsFifo, ast::Word::from(f)) }
specific_word("-r") f:word() { ast::ExtendedTestExpr::UnaryTest(ast::UnaryPredicate::FileExistsAndIsReadable, ast::Word::from(f)) }
specific_word("-s") f:word() { ast::ExtendedTestExpr::UnaryTest(ast::UnaryPredicate::FileExistsAndIsNotZeroLength, ast::Word::from(f)) }
specific_word("-t") f:word() { ast::ExtendedTestExpr::UnaryTest(ast::UnaryPredicate::FdIsOpenTerminal, ast::Word::from(f)) }
specific_word("-u") f:word() { ast::ExtendedTestExpr::UnaryTest(ast::UnaryPredicate::FileExistsAndIsSetuid, ast::Word::from(f)) }
specific_word("-v") f:word() { ast::ExtendedTestExpr::UnaryTest(ast::UnaryPredicate::ShellVariableIsSetAndAssigned, ast::Word::from(f)) }
specific_word("-w") f:word() { ast::ExtendedTestExpr::UnaryTest(ast::UnaryPredicate::FileExistsAndIsWritable, ast::Word::from(f)) }
specific_word("-x") f:word() { ast::ExtendedTestExpr::UnaryTest(ast::UnaryPredicate::FileExistsAndIsExecutable, ast::Word::from(f)) }
specific_word("-z") f:word() { ast::ExtendedTestExpr::UnaryTest(ast::UnaryPredicate::StringHasZeroLength, ast::Word::from(f)) }
specific_word("-G") f:word() { ast::ExtendedTestExpr::UnaryTest(ast::UnaryPredicate::FileExistsAndOwnedByEffectiveGroupId, ast::Word::from(f)) }
specific_word("-L") f:word() { ast::ExtendedTestExpr::UnaryTest(ast::UnaryPredicate::FileExistsAndIsSymlink, ast::Word::from(f)) }
specific_word("-N") f:word() { ast::ExtendedTestExpr::UnaryTest(ast::UnaryPredicate::FileExistsAndModifiedSinceLastRead, ast::Word::from(f)) }
specific_word("-O") f:word() { ast::ExtendedTestExpr::UnaryTest(ast::UnaryPredicate::FileExistsAndOwnedByEffectiveUserId, ast::Word::from(f)) }
specific_word("-R") f:word() { ast::ExtendedTestExpr::UnaryTest(ast::UnaryPredicate::ShellVariableIsSetAndNameRef, ast::Word::from(f)) }
specific_word("-S") f:word() { ast::ExtendedTestExpr::UnaryTest(ast::UnaryPredicate::FileExistsAndIsSocket, ast::Word::from(f)) }
p:extended_unary_predicate() f:word() { ast::ExtendedTestExpr::UnaryTest(p, ast::Word::from(f)) }
--
w:word() { ast::ExtendedTestExpr::UnaryTest(ast::UnaryPredicate::StringHasNonZeroLength, ast::Word::from(w)) }
}

rule extended_unary_predicate() -> ast::UnaryPredicate =
specific_word("-a") { ast::UnaryPredicate::FileExists } /
specific_word("-b") { ast::UnaryPredicate::FileExistsAndIsBlockSpecialFile } /
specific_word("-c") { ast::UnaryPredicate::FileExistsAndIsCharSpecialFile } /
specific_word("-d") { ast::UnaryPredicate::FileExistsAndIsDir } /
specific_word("-e") { ast::UnaryPredicate::FileExists } /
specific_word("-f") { ast::UnaryPredicate::FileExistsAndIsRegularFile } /
specific_word("-g") { ast::UnaryPredicate::FileExistsAndIsSetgid } /
specific_word("-h") { ast::UnaryPredicate::FileExistsAndIsSymlink } /
specific_word("-k") { ast::UnaryPredicate::FileExistsAndHasStickyBit } /
specific_word("-n") { ast::UnaryPredicate::StringHasNonZeroLength } /
specific_word("-o") { ast::UnaryPredicate::ShellOptionEnabled } /
specific_word("-p") { ast::UnaryPredicate::FileExistsAndIsFifo } /
specific_word("-r") { ast::UnaryPredicate::FileExistsAndIsReadable } /
specific_word("-s") { ast::UnaryPredicate::FileExistsAndIsNotZeroLength } /
specific_word("-t") { ast::UnaryPredicate::FdIsOpenTerminal } /
specific_word("-u") { ast::UnaryPredicate::FileExistsAndIsSetuid } /
specific_word("-v") { ast::UnaryPredicate::ShellVariableIsSetAndAssigned } /
specific_word("-w") { ast::UnaryPredicate::FileExistsAndIsWritable } /
specific_word("-x") { ast::UnaryPredicate::FileExistsAndIsExecutable } /
specific_word("-z") { ast::UnaryPredicate::StringHasZeroLength } /
specific_word("-G") { ast::UnaryPredicate::FileExistsAndOwnedByEffectiveGroupId } /
specific_word("-L") { ast::UnaryPredicate::FileExistsAndIsSymlink } /
specific_word("-N") { ast::UnaryPredicate::FileExistsAndModifiedSinceLastRead } /
specific_word("-O") { ast::UnaryPredicate::FileExistsAndOwnedByEffectiveUserId } /
specific_word("-R") { ast::UnaryPredicate::ShellVariableIsSetAndNameRef } /
specific_word("-S") { ast::UnaryPredicate::FileExistsAndIsSocket }

// N.B. For some reason we seem to need to allow a select subset
// of unescaped operators in regex words.
rule regex_word() -> ast::Word =
Expand All @@ -393,10 +402,7 @@ peg::parser! {

// TODO: validate if this should call non_reserved_word() or word()
pub(crate) rule case_clause() -> ast::CaseClauseCommand =
specific_word("case") w:non_reserved_word() linebreak() _in() linebreak() c:case_list() specific_word("esac") {
ast::CaseClauseCommand { value: ast::Word::from(w), cases: c }
} /
specific_word("case") w:non_reserved_word() linebreak() _in() linebreak() c:case_list_ns() specific_word("esac") {
specific_word("case") w:non_reserved_word() linebreak() _in() linebreak() c:(case_list() / case_list_ns()) specific_word("esac") {
ast::CaseClauseCommand { value: ast::Word::from(w), cases: c }
} /
specific_word("case") w:non_reserved_word() linebreak() _in() linebreak() specific_word("esac") {
Expand Down Expand Up @@ -479,7 +485,6 @@ peg::parser! {
specific_word("until") c:compound_list() d:do_group() { ast::WhileOrUntilClauseCommand(c, d) }

// N.B. Non-sh extensions allows use of the 'function' word to indicate a function definition.
// TODO: Validate usage of this keyword.
rule function_definition() -> ast::FunctionDefinition =
specific_word("function")? fname:fname() specific_operator("(") specific_operator(")") linebreak() body:function_body() {
ast::FunctionDefinition { fname: fname.to_owned(), body, source: source_info.source.clone() }
Expand All @@ -502,9 +507,18 @@ peg::parser! {
specific_word("do") c:compound_list() specific_word("done") { ast::DoGroupCommand(c) }

rule simple_command() -> ast::SimpleCommand =
prefix:cmd_prefix() word_or_name:cmd_word() suffix:cmd_suffix()? { ast::SimpleCommand { prefix: Some(prefix), word_or_name: Some(ast::Word::from(word_or_name)), suffix } } /
prefix:cmd_prefix() { ast::SimpleCommand { prefix: Some(prefix), word_or_name: None, suffix: None } } /
word_or_name:cmd_name() suffix:cmd_suffix()? { ast::SimpleCommand { prefix: None, word_or_name: Some(ast::Word::from(word_or_name)), suffix } } /
prefix:cmd_prefix() word_and_suffix:(word_or_name:cmd_word() suffix:cmd_suffix()? { (word_or_name, suffix) })? {
match word_and_suffix {
Some((word_or_name, suffix)) => {
ast::SimpleCommand { prefix: Some(prefix), word_or_name: Some(ast::Word::from(word_or_name)), suffix }
}
None => {
ast::SimpleCommand { prefix: Some(prefix), word_or_name: None, suffix: None }
}
}
} /
word_or_name:cmd_name() suffix:cmd_suffix()? {
ast::SimpleCommand { prefix: None, word_or_name: Some(ast::Word::from(word_or_name)), suffix } } /
expected!("simple command")

rule cmd_name() -> &'input Token =
Expand All @@ -527,8 +541,6 @@ peg::parser! {
i:io_redirect() {
ast::CommandPrefixOrSuffixItem::IoRedirect(i)
} /
// TODO: this is a hack; we don't yet understand how other shells manage to parse command invocations
// like `local var=()`
assignment_and_word:assignment_word() {
let (assignment, word) = assignment_and_word;
ast::CommandPrefixOrSuffixItem::AssignmentWord(assignment, word)
Expand Down Expand Up @@ -617,26 +629,25 @@ peg::parser! {
rule word() -> &'input Token =
[Token::Word(_, _)]

rule reserved_word() -> &'input str =
t:reserved_word_token() { t.to_str() }

rule reserved_word_token() -> &'input Token =
specific_word("!") /
specific_word("{") /
specific_word("}") /
specific_word("case") /
specific_word("do") /
specific_word("done") /
specific_word("elif") /
specific_word("else") /
specific_word("esac") /
specific_word("fi") /
specific_word("for") /
specific_word("if") /
specific_word("in") /
specific_word("then") /
specific_word("until") /
specific_word("while") /
rule reserved_word() -> &'input Token =
[Token::Word(w, _) if matches!(w.as_str(),
"!" |
"{" |
"}" |
"case" |
"do" |
"done" |
"elif" |
"else" |
"esac" |
"fi" |
"for" |
"if" |
"in" |
"then" |
"until" |
"while"
)] /

// N.B. bash also treats the following as reserved.
non_posix_extensions_enabled() token:non_posix_reserved_word_token() { token }
Expand Down Expand Up @@ -756,7 +767,7 @@ fn parse_array_assignment(
elements: &[&String],
) -> Result<ast::Assignment, &'static str> {
let (assignment_name, append) =
assignments::name_and_equals(word).map_err(|_| "not assignment word")?;
assignments::name_and_equals(word).map_err(|_| "not array assignment word")?;

let elements = elements
.iter()
Expand Down
3 changes: 3 additions & 0 deletions parser/src/prompt.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::error;

#[derive(Clone)]
pub enum PromptPiece {
AsciiCharacter(u32),
Backslash,
Expand Down Expand Up @@ -30,11 +31,13 @@ pub enum PromptPiece {
Time(PromptTimeFormat),
}

#[derive(Clone)]
pub enum PromptDateFormat {
WeekdayMonthDate,
Custom(String),
}

#[derive(Clone)]
pub enum PromptTimeFormat {
TwelveHourAM,
TwelveHourHHMMSS,
Expand Down
11 changes: 1 addition & 10 deletions shell/src/expansion.rs
Original file line number Diff line number Diff line change
Expand Up @@ -492,16 +492,7 @@ impl<'a> WordExpander<'a> {
self.parser_options.enable_extended_globbing,
Some(&patterns::Pattern::accept_all_expand_filter),
)
.map_or_else(
|_err| vec![],
|expansions| {
if expansions.is_empty() {
vec![]
} else {
expansions
}
},
);
.unwrap_or_default();

if expansions.is_empty() {
vec![String::from(field)]
Expand Down
7 changes: 6 additions & 1 deletion shell/src/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const VERSION_PATCH: &str = env!("CARGO_PKG_VERSION_PATCH");

pub(crate) fn expand_prompt(shell: &Shell, spec: &str) -> Result<String, error::Error> {
// Now parse.
let prompt_pieces = parser::prompt::parse_prompt(spec)?;
let prompt_pieces = parse_prompt(spec.to_owned())?;

// Now render.
let formatted_prompt = prompt_pieces
Expand All @@ -19,6 +19,11 @@ pub(crate) fn expand_prompt(shell: &Shell, spec: &str) -> Result<String, error::
Ok(formatted_prompt)
}

#[cached::proc_macro::cached(size = 64, result = true)]
fn parse_prompt(spec: String) -> Result<Vec<parser::prompt::PromptPiece>, parser::WordParseError> {
parser::prompt::parse_prompt(spec.as_str())
}

pub(crate) fn format_prompt_piece(
shell: &Shell,
piece: &parser::prompt::PromptPiece,
Expand Down

0 comments on commit 8e56582

Please sign in to comment.