25fb843c40
The prior commit switched the recipe to `[source] path = "source"`, but .gitignore:77 still listed `local/recipes/shells/brush/source` (a leftover from when brush was a transient upstream git fetch), so the vendored tree was not tracked — a fresh clone would have no brush source and the build would fail. Drop that ignore line and commit the vendored working tree (reubeno/brush @ 897b373e, with the Redox port patches pre-applied). brush is now a durable local fork like the other path=source recipes. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2451 lines
75 KiB
Rust
2451 lines
75 KiB
Rust
//! Defines the Abstract Syntax Tree (ast) for shell programs. Includes types and utilities
|
|
//! for manipulating the AST.
|
|
|
|
use std::fmt::{Display, Write};
|
|
|
|
use crate::{SourceSpan, tokenizer};
|
|
|
|
const DISPLAY_INDENT: &str = " ";
|
|
|
|
/// Trait implemented by all AST nodes. Used to aggregate traits expected
|
|
/// to be implemented.
|
|
pub trait Node: Display + SourceLocation {}
|
|
|
|
/// Provides the source location for the syntax item
|
|
pub trait SourceLocation {
|
|
/// The location of the syntax item, when known
|
|
fn location(&self) -> Option<SourceSpan>;
|
|
}
|
|
|
|
pub(crate) fn maybe_location(
|
|
start: Option<&SourceSpan>,
|
|
end: Option<&SourceSpan>,
|
|
) -> Option<SourceSpan> {
|
|
if let (Some(s), Some(e)) = (start, end) {
|
|
Some(SourceSpan::within(s, e))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
/// Represents a complete shell program.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub struct Program {
|
|
/// A sequence of complete shell commands.
|
|
pub complete_commands: Vec<CompleteCommand>,
|
|
}
|
|
|
|
impl Node for Program {}
|
|
|
|
impl SourceLocation for Program {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
let start = self
|
|
.complete_commands
|
|
.first()
|
|
.and_then(SourceLocation::location);
|
|
let end = self
|
|
.complete_commands
|
|
.last()
|
|
.and_then(SourceLocation::location);
|
|
maybe_location(start.as_ref(), end.as_ref())
|
|
}
|
|
}
|
|
|
|
impl Display for Program {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
for complete_command in &self.complete_commands {
|
|
write!(f, "{complete_command}")?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Represents a complete shell command.
|
|
pub type CompleteCommand = CompoundList;
|
|
|
|
/// Represents a complete shell command item.
|
|
pub type CompleteCommandItem = CompoundListItem;
|
|
|
|
// TODO(tracing): decide if we want to trace this location or consider it a whitespace separator
|
|
/// Indicates whether the preceding command is executed synchronously or asynchronously.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub enum SeparatorOperator {
|
|
/// The preceding command is executed asynchronously.
|
|
Async,
|
|
/// The preceding command is executed synchronously.
|
|
Sequence,
|
|
}
|
|
|
|
impl Display for SeparatorOperator {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::Async => write!(f, "&"),
|
|
Self::Sequence => write!(f, ";"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Represents a sequence of command pipelines connected by boolean operators.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub struct AndOrList {
|
|
/// The first command pipeline.
|
|
pub first: Pipeline,
|
|
/// Any additional command pipelines, in sequence order.
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
serde(skip_serializing_if = "Vec::is_empty", default)
|
|
)]
|
|
pub additional: Vec<AndOr>,
|
|
}
|
|
|
|
impl Node for AndOrList {}
|
|
|
|
impl SourceLocation for AndOrList {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
let start = self.first.location();
|
|
let last = self.additional.last();
|
|
let end = last.and_then(SourceLocation::location);
|
|
|
|
match (start, end) {
|
|
(Some(s), Some(e)) => Some(SourceSpan::within(&s, &e)),
|
|
(start, _) => start,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for AndOrList {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{}", self.first)?;
|
|
for item in &self.additional {
|
|
write!(f, "{item}")?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Represents a boolean operator used to connect command pipelines in an [`AndOrList`]
|
|
#[derive(PartialEq, Eq)]
|
|
pub enum PipelineOperator {
|
|
/// The command pipelines are connected by a boolean AND operator.
|
|
And,
|
|
/// The command pipelines are connected by a boolean OR operator.
|
|
Or,
|
|
}
|
|
|
|
impl PartialEq<AndOr> for PipelineOperator {
|
|
fn eq(&self, other: &AndOr) -> bool {
|
|
matches!(
|
|
(self, other),
|
|
(Self::And, AndOr::And(_)) | (Self::Or, AndOr::Or(_))
|
|
)
|
|
}
|
|
}
|
|
|
|
// We cannot losslessly convert into `AndOr`, hence we can only do `Into`.
|
|
#[expect(clippy::from_over_into)]
|
|
impl Into<PipelineOperator> for AndOr {
|
|
fn into(self) -> PipelineOperator {
|
|
match self {
|
|
Self::And(_) => PipelineOperator::And,
|
|
Self::Or(_) => PipelineOperator::Or,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// An iterator over the pipelines in an [`AndOrList`].
|
|
pub struct AndOrListIter<'a> {
|
|
first: Option<&'a Pipeline>,
|
|
additional_iter: std::slice::Iter<'a, AndOr>,
|
|
}
|
|
|
|
impl<'a> Iterator for AndOrListIter<'a> {
|
|
type Item = (PipelineOperator, &'a Pipeline);
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
if let Some(first) = self.first.take() {
|
|
Some((PipelineOperator::And, first))
|
|
} else {
|
|
self.additional_iter.next().map(|and_or| match and_or {
|
|
AndOr::And(pipeline) => (PipelineOperator::And, pipeline),
|
|
AndOr::Or(pipeline) => (PipelineOperator::Or, pipeline),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> IntoIterator for &'a AndOrList {
|
|
type Item = (PipelineOperator, &'a Pipeline);
|
|
type IntoIter = AndOrListIter<'a>;
|
|
|
|
fn into_iter(self) -> Self::IntoIter {
|
|
AndOrListIter {
|
|
first: Some(&self.first),
|
|
additional_iter: self.additional.iter(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'a> From<(PipelineOperator, &'a Pipeline)> for AndOr {
|
|
fn from(value: (PipelineOperator, &'a Pipeline)) -> Self {
|
|
match value.0 {
|
|
PipelineOperator::Or => Self::Or(value.1.to_owned()),
|
|
PipelineOperator::And => Self::And(value.1.to_owned()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl AndOrList {
|
|
/// Returns an iterator over the pipelines in this `AndOrList`.
|
|
pub fn iter(&self) -> AndOrListIter<'_> {
|
|
self.into_iter()
|
|
}
|
|
}
|
|
|
|
/// Represents a boolean operator used to connect command pipelines, along with the
|
|
/// succeeding pipeline.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub enum AndOr {
|
|
/// Boolean AND operator; the embedded pipeline is only to be executed if the
|
|
/// preceding command has succeeded.
|
|
And(Pipeline),
|
|
/// Boolean OR operator; the embedded pipeline is only to be executed if the
|
|
/// preceding command has not succeeded.
|
|
Or(Pipeline),
|
|
}
|
|
|
|
impl Node for AndOr {}
|
|
|
|
// TODO(source-location): add a loc to account for the operator
|
|
impl SourceLocation for AndOr {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
match self {
|
|
Self::And(p) => p.location(),
|
|
Self::Or(p) => p.location(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for AndOr {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::And(pipeline) => write!(f, " && {pipeline}"),
|
|
Self::Or(pipeline) => write!(f, " || {pipeline}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The type of timing requested for a pipeline.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub enum PipelineTimed {
|
|
/// The pipeline should be timed with bash-like output.
|
|
Timed(SourceSpan),
|
|
/// The pipeline should be timed with POSIX-like output.
|
|
TimedWithPosixOutput(SourceSpan),
|
|
}
|
|
|
|
impl Node for PipelineTimed {}
|
|
|
|
impl SourceLocation for PipelineTimed {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
match self {
|
|
Self::Timed(t) => Some(t.to_owned()),
|
|
Self::TimedWithPosixOutput(t) => Some(t.to_owned()),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for PipelineTimed {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::Timed(_) => write!(f, "time"),
|
|
Self::TimedWithPosixOutput(_) => write!(f, "time -p"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PipelineTimed {
|
|
/// Returns true if the pipeline should be timed with POSIX-like output.
|
|
pub const fn is_posix_output(&self) -> bool {
|
|
matches!(self, Self::TimedWithPosixOutput(_))
|
|
}
|
|
}
|
|
|
|
/// A pipeline of commands, where each command's output is passed as standard input
|
|
/// to the command that follows it.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub struct Pipeline {
|
|
/// Indicates whether the pipeline's execution should be timed with reported
|
|
/// timings in output.
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
serde(skip_serializing_if = "Option::is_none", default)
|
|
)]
|
|
pub timed: Option<PipelineTimed>,
|
|
/// Indicates whether the result of the overall pipeline should be the logical
|
|
/// negation of the result of the pipeline.
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
serde(skip_serializing_if = "<&bool as std::ops::Not>::not", default)
|
|
)]
|
|
pub bang: bool,
|
|
/// The sequence of commands in the pipeline.
|
|
pub seq: Vec<Command>,
|
|
}
|
|
|
|
impl Node for Pipeline {}
|
|
|
|
// TODO(source-location): Handle the case where `self.timed` is `None` but there is a bang.
|
|
impl SourceLocation for Pipeline {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
let start = self
|
|
.timed
|
|
.as_ref()
|
|
.and_then(SourceLocation::location)
|
|
.or_else(|| self.seq.first().and_then(SourceLocation::location));
|
|
let end = self.seq.last().and_then(SourceLocation::location);
|
|
|
|
maybe_location(start.as_ref(), end.as_ref())
|
|
}
|
|
}
|
|
|
|
impl Display for Pipeline {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
if let Some(timed) = &self.timed {
|
|
write!(f, "{timed} ")?;
|
|
}
|
|
|
|
if self.bang {
|
|
write!(f, "! ")?;
|
|
}
|
|
for (i, command) in self.seq.iter().enumerate() {
|
|
if i > 0 {
|
|
write!(f, " |")?;
|
|
}
|
|
write!(f, "{command}")?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Represents a shell command.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub enum Command {
|
|
/// A simple command, directly invoking an external command, a built-in command,
|
|
/// a shell function, or similar.
|
|
Simple(SimpleCommand),
|
|
/// A compound command, composed of multiple commands.
|
|
Compound(CompoundCommand, Option<RedirectList>),
|
|
/// A command whose side effect is to define a shell function.
|
|
Function(FunctionDefinition),
|
|
/// A command that evaluates an extended test expression.
|
|
ExtendedTest(ExtendedTestExprCommand, Option<RedirectList>),
|
|
}
|
|
|
|
impl Node for Command {}
|
|
|
|
impl SourceLocation for Command {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
match self {
|
|
Self::Simple(s) => s.location(),
|
|
Self::Compound(c, r) => {
|
|
match (c.location(), r.as_ref().and_then(SourceLocation::location)) {
|
|
(Some(s), Some(e)) => Some(SourceSpan::within(&s, &e)),
|
|
(s, _) => s,
|
|
}
|
|
}
|
|
Self::Function(f) => f.location(),
|
|
Self::ExtendedTest(e, _) => e.location(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for Command {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::Simple(simple_command) => write!(f, "{simple_command}"),
|
|
Self::Compound(compound_command, redirect_list) => {
|
|
write!(f, "{compound_command}")?;
|
|
if let Some(redirect_list) = redirect_list {
|
|
write!(f, "{redirect_list}")?;
|
|
}
|
|
Ok(())
|
|
}
|
|
Self::Function(function_definition) => write!(f, "{function_definition}"),
|
|
Self::ExtendedTest(extended_test_expr, redirect_list) => {
|
|
write!(f, "[[ {extended_test_expr} ]]")?;
|
|
if let Some(redirect_list) = redirect_list {
|
|
write!(f, "{redirect_list}")?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Represents a compound command, potentially made up of multiple nested commands.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub enum CompoundCommand {
|
|
/// An arithmetic command, evaluating an arithmetic expression.
|
|
Arithmetic(ArithmeticCommand),
|
|
/// An arithmetic for clause, which loops until an arithmetic condition is reached.
|
|
ArithmeticForClause(ArithmeticForClauseCommand),
|
|
/// A brace group, which groups commands together.
|
|
BraceGroup(BraceGroupCommand),
|
|
/// A subshell, which executes commands in a subshell.
|
|
Subshell(SubshellCommand),
|
|
/// A for clause, which loops over a set of values.
|
|
ForClause(ForClauseCommand),
|
|
/// A case clause, which selects a command based on a value and a set of
|
|
/// pattern-based filters.
|
|
CaseClause(CaseClauseCommand),
|
|
/// An if clause, which conditionally executes a command.
|
|
IfClause(IfClauseCommand),
|
|
/// A while clause, which loops while a condition is met.
|
|
WhileClause(WhileOrUntilClauseCommand),
|
|
/// An until clause, which loops until a condition is met.
|
|
UntilClause(WhileOrUntilClauseCommand),
|
|
/// A coprocess, which runs a command asynchronously in a subshell.
|
|
Coprocess(CoprocessCommand),
|
|
}
|
|
|
|
impl Node for CompoundCommand {}
|
|
|
|
impl SourceLocation for CompoundCommand {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
match self {
|
|
Self::Arithmetic(a) => a.location(),
|
|
Self::ArithmeticForClause(a) => a.location(),
|
|
Self::BraceGroup(b) => b.location(),
|
|
Self::Subshell(s) => s.location(),
|
|
Self::ForClause(f) => f.location(),
|
|
Self::CaseClause(c) => c.location(),
|
|
Self::IfClause(i) => i.location(),
|
|
Self::WhileClause(w) => w.location(),
|
|
Self::UntilClause(u) => u.location(),
|
|
Self::Coprocess(c) => c.location(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for CompoundCommand {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::Arithmetic(arithmetic_command) => write!(f, "{arithmetic_command}"),
|
|
Self::ArithmeticForClause(arithmetic_for_clause_command) => {
|
|
write!(f, "{arithmetic_for_clause_command}")
|
|
}
|
|
Self::BraceGroup(brace_group_command) => {
|
|
write!(f, "{brace_group_command}")
|
|
}
|
|
Self::Subshell(subshell_command) => write!(f, "{subshell_command}"),
|
|
Self::ForClause(for_clause_command) => write!(f, "{for_clause_command}"),
|
|
Self::CaseClause(case_clause_command) => {
|
|
write!(f, "{case_clause_command}")
|
|
}
|
|
Self::IfClause(if_clause_command) => write!(f, "{if_clause_command}"),
|
|
Self::WhileClause(while_or_until_clause_command) => {
|
|
write!(f, "while {while_or_until_clause_command}")
|
|
}
|
|
Self::UntilClause(while_or_until_clause_command) => {
|
|
write!(f, "until {while_or_until_clause_command}")
|
|
}
|
|
Self::Coprocess(coproc_clause_command) => {
|
|
write!(f, "{coproc_clause_command}")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// An arithmetic command, evaluating an arithmetic expression.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub struct ArithmeticCommand {
|
|
/// The raw, unparsed and unexpanded arithmetic expression.
|
|
pub expr: UnexpandedArithmeticExpr,
|
|
/// Location of the command
|
|
pub loc: SourceSpan,
|
|
}
|
|
|
|
impl Node for ArithmeticCommand {}
|
|
|
|
impl SourceLocation for ArithmeticCommand {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
Some(self.loc.clone())
|
|
}
|
|
}
|
|
|
|
impl Display for ArithmeticCommand {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "(({}))", self.expr)
|
|
}
|
|
}
|
|
|
|
/// A subshell, which executes commands in a subshell.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub struct SubshellCommand {
|
|
/// Command list in the subshell
|
|
pub list: CompoundList,
|
|
/// Location of the subshell
|
|
pub loc: SourceSpan,
|
|
}
|
|
|
|
impl Node for SubshellCommand {}
|
|
|
|
impl SourceLocation for SubshellCommand {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
Some(self.loc.clone())
|
|
}
|
|
}
|
|
|
|
impl Display for SubshellCommand {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "( ")?;
|
|
write!(f, "{}", self.list)?;
|
|
write!(f, " )")
|
|
}
|
|
}
|
|
|
|
/// A for clause, which loops over a set of values.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub struct ForClauseCommand {
|
|
/// The name of the iterator variable.
|
|
pub variable_name: String,
|
|
/// The values being iterated over.
|
|
pub values: Option<Vec<Word>>,
|
|
/// The command to run for each iteration of the loop.
|
|
pub body: DoGroupCommand,
|
|
/// Location of the for command.
|
|
pub loc: SourceSpan,
|
|
}
|
|
|
|
impl Node for ForClauseCommand {}
|
|
|
|
impl SourceLocation for ForClauseCommand {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
Some(self.loc.clone())
|
|
}
|
|
}
|
|
|
|
impl Display for ForClauseCommand {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "for {} in ", self.variable_name)?;
|
|
|
|
if let Some(values) = &self.values {
|
|
for (i, value) in values.iter().enumerate() {
|
|
if i > 0 {
|
|
write!(f, " ")?;
|
|
}
|
|
|
|
write!(f, "{value}")?;
|
|
}
|
|
}
|
|
|
|
writeln!(f, ";")?;
|
|
|
|
write!(f, "{}", self.body)
|
|
}
|
|
}
|
|
|
|
/// An arithmetic for clause, which loops until an arithmetic condition is reached.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub struct ArithmeticForClauseCommand {
|
|
/// Optionally, the initializer expression evaluated before the first iteration of the loop.
|
|
pub initializer: Option<UnexpandedArithmeticExpr>,
|
|
/// Optionally, the expression evaluated as the exit condition of the loop.
|
|
pub condition: Option<UnexpandedArithmeticExpr>,
|
|
/// Optionally, the expression evaluated after each iteration of the loop.
|
|
pub updater: Option<UnexpandedArithmeticExpr>,
|
|
/// The command to run for each iteration of the loop.
|
|
pub body: DoGroupCommand,
|
|
/// Location of the clause
|
|
pub loc: SourceSpan,
|
|
}
|
|
|
|
impl Node for ArithmeticForClauseCommand {}
|
|
|
|
impl SourceLocation for ArithmeticForClauseCommand {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
Some(self.loc.clone())
|
|
}
|
|
}
|
|
|
|
impl Display for ArithmeticForClauseCommand {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "for ((")?;
|
|
|
|
if let Some(initializer) = &self.initializer {
|
|
write!(f, "{initializer}")?;
|
|
}
|
|
|
|
write!(f, "; ")?;
|
|
|
|
if let Some(condition) = &self.condition {
|
|
write!(f, "{condition}")?;
|
|
}
|
|
|
|
write!(f, "; ")?;
|
|
|
|
if let Some(updater) = &self.updater {
|
|
write!(f, "{updater}")?;
|
|
}
|
|
|
|
writeln!(f, "))")?;
|
|
|
|
write!(f, "{}", self.body)
|
|
}
|
|
}
|
|
|
|
/// A case clause, which selects a command based on a value and a set of
|
|
/// pattern-based filters.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub struct CaseClauseCommand {
|
|
/// The value being matched on.
|
|
pub value: Word,
|
|
/// The individual case branches.
|
|
pub cases: Vec<CaseItem>,
|
|
/// Location of the case command.
|
|
pub loc: SourceSpan,
|
|
}
|
|
|
|
impl Node for CaseClauseCommand {}
|
|
|
|
impl SourceLocation for CaseClauseCommand {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
Some(self.loc.clone())
|
|
}
|
|
}
|
|
|
|
impl Display for CaseClauseCommand {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "case {} in", self.value)?;
|
|
for case in &self.cases {
|
|
write!(indenter::indented(f).with_str(DISPLAY_INDENT), "{case}")?;
|
|
}
|
|
writeln!(f)?;
|
|
write!(f, "esac")
|
|
}
|
|
}
|
|
|
|
/// A sequence of commands.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub struct CompoundList(pub Vec<CompoundListItem>);
|
|
|
|
impl Node for CompoundList {}
|
|
|
|
// TODO(source-location): Handle the optional trailing separator.
|
|
impl SourceLocation for CompoundList {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
let start = self.0.first().and_then(SourceLocation::location);
|
|
let end = self.0.last().and_then(SourceLocation::location);
|
|
|
|
if let (Some(s), Some(e)) = (start, end) {
|
|
Some(SourceSpan::within(&s, &e))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for CompoundList {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
for (i, item) in self.0.iter().enumerate() {
|
|
if i > 0 {
|
|
writeln!(f)?;
|
|
}
|
|
|
|
// Write the and-or list.
|
|
write!(f, "{}", item.0)?;
|
|
|
|
// Write the separator... unless we're on the list item and it's a ';'.
|
|
if i == self.0.len() - 1 && matches!(item.1, SeparatorOperator::Sequence) {
|
|
// Skip
|
|
} else {
|
|
write!(f, "{}", item.1)?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// An element of a compound command list.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub struct CompoundListItem(pub AndOrList, pub SeparatorOperator);
|
|
|
|
impl Node for CompoundListItem {}
|
|
|
|
// TODO(source-location): Account for the location of the separator operator.
|
|
impl SourceLocation for CompoundListItem {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
self.0.location()
|
|
}
|
|
}
|
|
|
|
impl Display for CompoundListItem {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{}", self.0)?;
|
|
write!(f, "{}", self.1)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// An if clause, which conditionally executes a command.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub struct IfClauseCommand {
|
|
/// The command whose execution result is inspected.
|
|
pub condition: CompoundList,
|
|
/// The command to execute if the condition is true.
|
|
pub then: CompoundList,
|
|
/// Optionally, `else` clauses that will be evaluated if the condition is false.
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
serde(skip_serializing_if = "Option::is_none", default)
|
|
)]
|
|
pub elses: Option<Vec<ElseClause>>,
|
|
/// Location of the if clause
|
|
pub loc: SourceSpan,
|
|
}
|
|
|
|
impl Node for IfClauseCommand {}
|
|
|
|
impl SourceLocation for IfClauseCommand {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
Some(self.loc.clone())
|
|
}
|
|
}
|
|
|
|
impl Display for IfClauseCommand {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
writeln!(f, "if {}; then", self.condition)?;
|
|
write!(
|
|
indenter::indented(f).with_str(DISPLAY_INDENT),
|
|
"{}",
|
|
self.then
|
|
)?;
|
|
if let Some(elses) = &self.elses {
|
|
for else_clause in elses {
|
|
write!(f, "{else_clause}")?;
|
|
}
|
|
}
|
|
|
|
writeln!(f)?;
|
|
write!(f, "fi")?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Represents the `else` clause of a conditional command.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub struct ElseClause {
|
|
/// If present, the condition that must be met for this `else` clause to be executed.
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
serde(skip_serializing_if = "Option::is_none", default)
|
|
)]
|
|
pub condition: Option<CompoundList>,
|
|
/// The commands to execute if this `else` clause is selected.
|
|
pub body: CompoundList,
|
|
}
|
|
|
|
impl Display for ElseClause {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
writeln!(f)?;
|
|
if let Some(condition) = &self.condition {
|
|
writeln!(f, "elif {condition}; then")?;
|
|
} else {
|
|
writeln!(f, "else")?;
|
|
}
|
|
|
|
write!(
|
|
indenter::indented(f).with_str(DISPLAY_INDENT),
|
|
"{}",
|
|
self.body
|
|
)
|
|
}
|
|
}
|
|
|
|
/// A coprocess command, which runs a command asynchronously in a subshell.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub struct CoprocessCommand {
|
|
/// The optional name for the coprocess.
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
serde(skip_serializing_if = "Option::is_none", default)
|
|
)]
|
|
pub name: Option<Word>,
|
|
/// The command to run as a coprocess (can be simple or compound).
|
|
pub body: Box<Command>,
|
|
/// The location of this command in the source.
|
|
#[cfg_attr(any(test, feature = "serde"), serde(skip_serializing, default))]
|
|
pub loc: SourceSpan,
|
|
}
|
|
|
|
impl Node for CoprocessCommand {}
|
|
|
|
impl SourceLocation for CoprocessCommand {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
Some(self.loc.clone())
|
|
}
|
|
}
|
|
|
|
impl Display for CoprocessCommand {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "coproc")?;
|
|
if let Some(name) = &self.name {
|
|
write!(f, " {name}")?;
|
|
}
|
|
write!(f, " {}", self.body)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// An individual matching case item in a case clause.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub struct CaseItem {
|
|
/// The patterns that select this case branch.
|
|
pub patterns: Vec<Word>,
|
|
/// The commands to execute if this case branch is selected.
|
|
pub cmd: Option<CompoundList>,
|
|
/// When the case branch is selected, the action to take after the command is executed.
|
|
pub post_action: CaseItemPostAction,
|
|
/// Location of the item
|
|
pub loc: Option<SourceSpan>,
|
|
}
|
|
|
|
impl Node for CaseItem {}
|
|
|
|
impl SourceLocation for CaseItem {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
self.loc.clone()
|
|
}
|
|
}
|
|
|
|
impl Display for CaseItem {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
writeln!(f)?;
|
|
for (i, pattern) in self.patterns.iter().enumerate() {
|
|
if i > 0 {
|
|
write!(f, "|")?;
|
|
}
|
|
write!(f, "{pattern}")?;
|
|
}
|
|
writeln!(f, ")")?;
|
|
|
|
if let Some(cmd) = &self.cmd {
|
|
write!(indenter::indented(f).with_str(DISPLAY_INDENT), "{cmd}")?;
|
|
}
|
|
writeln!(f)?;
|
|
write!(f, "{}", self.post_action)
|
|
}
|
|
}
|
|
|
|
/// Describes the action to take after executing the body command of a case clause.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub enum CaseItemPostAction {
|
|
/// The containing case should be exited.
|
|
ExitCase,
|
|
/// If one is present, the command body of the succeeding case item should be
|
|
/// executed (without evaluating its pattern).
|
|
UnconditionallyExecuteNextCaseItem,
|
|
/// The case should continue evaluating the remaining case items, as if this
|
|
/// item had not been executed.
|
|
ContinueEvaluatingCases,
|
|
}
|
|
|
|
impl Display for CaseItemPostAction {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::ExitCase => write!(f, ";;"),
|
|
Self::UnconditionallyExecuteNextCaseItem => write!(f, ";&"),
|
|
Self::ContinueEvaluatingCases => write!(f, ";;&"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A while or until clause, whose looping is controlled by a condition.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub struct WhileOrUntilClauseCommand(pub CompoundList, pub DoGroupCommand, pub SourceSpan);
|
|
|
|
impl Node for WhileOrUntilClauseCommand {}
|
|
|
|
impl SourceLocation for WhileOrUntilClauseCommand {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
Some(self.2.clone())
|
|
}
|
|
}
|
|
|
|
impl Display for WhileOrUntilClauseCommand {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{}; {}", self.0, self.1)
|
|
}
|
|
}
|
|
|
|
/// Encapsulates the definition of a shell function.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub struct FunctionDefinition {
|
|
/// The name of the function.
|
|
pub fname: Word,
|
|
/// The body of the function.
|
|
pub body: FunctionBody,
|
|
}
|
|
|
|
impl Node for FunctionDefinition {}
|
|
|
|
// TODO(source-location): Account for the optional 'function' keyword that may
|
|
// precede the function name.
|
|
impl SourceLocation for FunctionDefinition {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
let start = self.fname.location();
|
|
let end = self.body.location();
|
|
|
|
if let (Some(s), Some(e)) = (start, end) {
|
|
Some(SourceSpan::within(&s, &e))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for FunctionDefinition {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
writeln!(f, "{} () ", self.fname.value)?;
|
|
write!(f, "{}", self.body)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Encapsulates the body of a function definition.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub struct FunctionBody(pub CompoundCommand, pub Option<RedirectList>);
|
|
|
|
impl Node for FunctionBody {}
|
|
|
|
impl SourceLocation for FunctionBody {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
let cmd_span = self.0.location();
|
|
let redirect_span = self.1.as_ref().and_then(SourceLocation::location);
|
|
|
|
match (cmd_span, redirect_span) {
|
|
// If there's a redirect, include it in the span.
|
|
(Some(cmd_span), Some(redirect_span)) => {
|
|
Some(SourceSpan::within(&cmd_span, &redirect_span))
|
|
}
|
|
// Otherwise, just return the command span.
|
|
(Some(cmd_span), None) => Some(cmd_span),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for FunctionBody {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{}", self.0)?;
|
|
if let Some(redirect_list) = &self.1 {
|
|
write!(f, "{redirect_list}")?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// A brace group, which groups commands together.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub struct BraceGroupCommand {
|
|
/// List of commands
|
|
pub list: CompoundList,
|
|
/// Location of the group
|
|
pub loc: SourceSpan,
|
|
}
|
|
|
|
impl Node for BraceGroupCommand {}
|
|
|
|
impl SourceLocation for BraceGroupCommand {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
Some(self.loc.clone())
|
|
}
|
|
}
|
|
|
|
impl Display for BraceGroupCommand {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
writeln!(f, "{{ ")?;
|
|
write!(
|
|
indenter::indented(f).with_str(DISPLAY_INDENT),
|
|
"{}",
|
|
self.list
|
|
)?;
|
|
writeln!(f)?;
|
|
write!(f, "}}")?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// A do group, which groups commands together.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub struct DoGroupCommand {
|
|
/// List of commands
|
|
pub list: CompoundList,
|
|
/// Location of the group
|
|
pub loc: SourceSpan,
|
|
}
|
|
|
|
impl Display for DoGroupCommand {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
writeln!(f, "do")?;
|
|
write!(
|
|
indenter::indented(f).with_str(DISPLAY_INDENT),
|
|
"{}",
|
|
self.list
|
|
)?;
|
|
writeln!(f)?;
|
|
write!(f, "done")
|
|
}
|
|
}
|
|
|
|
/// Represents the invocation of a simple command.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub struct SimpleCommand {
|
|
/// Optionally, a prefix to the command.
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
serde(skip_serializing_if = "Option::is_none", default)
|
|
)]
|
|
pub prefix: Option<CommandPrefix>,
|
|
/// The name of the command to execute.
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
serde(skip_serializing_if = "Option::is_none", default)
|
|
)]
|
|
pub word_or_name: Option<Word>,
|
|
/// Optionally, a suffix to the command.
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
serde(skip_serializing_if = "Option::is_none", default)
|
|
)]
|
|
pub suffix: Option<CommandSuffix>,
|
|
}
|
|
|
|
impl Node for SimpleCommand {}
|
|
|
|
impl SourceLocation for SimpleCommand {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
let mid = &self
|
|
.word_or_name
|
|
.as_ref()
|
|
.and_then(SourceLocation::location);
|
|
let start = self.prefix.as_ref().and_then(SourceLocation::location);
|
|
let end = self.suffix.as_ref().and_then(SourceLocation::location);
|
|
|
|
match (start, mid, end) {
|
|
(Some(start), _, Some(end)) => Some(SourceSpan::within(&start, &end)),
|
|
(Some(start), Some(mid), None) => Some(SourceSpan::within(&start, mid)),
|
|
(Some(start), None, None) => Some(start),
|
|
(None, Some(mid), Some(end)) => Some(SourceSpan::within(mid, &end)),
|
|
(None, Some(mid), None) => Some(mid.clone()),
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for SimpleCommand {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
let mut wrote_something = false;
|
|
|
|
if let Some(prefix) = &self.prefix {
|
|
if wrote_something {
|
|
write!(f, " ")?;
|
|
}
|
|
|
|
write!(f, "{prefix}")?;
|
|
wrote_something = true;
|
|
}
|
|
|
|
if let Some(word_or_name) = &self.word_or_name {
|
|
if wrote_something {
|
|
write!(f, " ")?;
|
|
}
|
|
|
|
write!(f, "{word_or_name}")?;
|
|
wrote_something = true;
|
|
}
|
|
|
|
if let Some(suffix) = &self.suffix {
|
|
if wrote_something {
|
|
write!(f, " ")?;
|
|
}
|
|
|
|
write!(f, "{suffix}")?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Represents a prefix to a simple command.
|
|
#[derive(Clone, Debug, Default)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub struct CommandPrefix(pub Vec<CommandPrefixOrSuffixItem>);
|
|
|
|
impl Node for CommandPrefix {}
|
|
|
|
impl SourceLocation for CommandPrefix {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
let start = self.0.first().and_then(SourceLocation::location);
|
|
let end = self.0.last().and_then(SourceLocation::location);
|
|
|
|
maybe_location(start.as_ref(), end.as_ref())
|
|
}
|
|
}
|
|
|
|
impl Display for CommandPrefix {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
for (i, item) in self.0.iter().enumerate() {
|
|
if i > 0 {
|
|
write!(f, " ")?;
|
|
}
|
|
|
|
write!(f, "{item}")?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Represents a suffix to a simple command; a word argument, declaration, or I/O redirection.
|
|
#[derive(Clone, Default, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub struct CommandSuffix(pub Vec<CommandPrefixOrSuffixItem>);
|
|
|
|
impl Node for CommandSuffix {}
|
|
|
|
impl SourceLocation for CommandSuffix {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
let start = self.0.first().and_then(SourceLocation::location);
|
|
let end = self.0.last().and_then(SourceLocation::location);
|
|
|
|
maybe_location(start.as_ref(), end.as_ref())
|
|
}
|
|
}
|
|
|
|
impl Display for CommandSuffix {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
for (i, item) in self.0.iter().enumerate() {
|
|
if i > 0 {
|
|
write!(f, " ")?;
|
|
}
|
|
|
|
write!(f, "{item}")?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Represents the I/O direction of a process substitution.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub enum ProcessSubstitutionKind {
|
|
/// The process is read from.
|
|
Read,
|
|
/// The process is written to.
|
|
Write,
|
|
}
|
|
|
|
impl Display for ProcessSubstitutionKind {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::Read => write!(f, "<"),
|
|
Self::Write => write!(f, ">"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A prefix or suffix for a simple command.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub enum CommandPrefixOrSuffixItem {
|
|
/// An I/O redirection.
|
|
IoRedirect(IoRedirect),
|
|
/// A word.
|
|
Word(Word),
|
|
/// An assignment/declaration word.
|
|
AssignmentWord(Assignment, Word),
|
|
/// A process substitution.
|
|
ProcessSubstitution(ProcessSubstitutionKind, SubshellCommand),
|
|
}
|
|
|
|
impl Node for CommandPrefixOrSuffixItem {}
|
|
|
|
impl SourceLocation for CommandPrefixOrSuffixItem {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
match self {
|
|
Self::Word(w) => w.location(),
|
|
Self::IoRedirect(io_redirect) => io_redirect.location(),
|
|
Self::AssignmentWord(assignment, _word) => assignment.location(),
|
|
// TODO(source-location): account for the kind token
|
|
Self::ProcessSubstitution(_kind, cmd) => cmd.location(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for CommandPrefixOrSuffixItem {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::IoRedirect(io_redirect) => write!(f, "{io_redirect}"),
|
|
Self::Word(word) => write!(f, "{word}"),
|
|
Self::AssignmentWord(_assignment, word) => write!(f, "{word}"),
|
|
Self::ProcessSubstitution(kind, subshell_command) => {
|
|
write!(f, "{kind}({subshell_command})")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Encapsulates an assignment declaration.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub struct Assignment {
|
|
/// Name being assigned to.
|
|
pub name: AssignmentName,
|
|
/// Value being assigned.
|
|
pub value: AssignmentValue,
|
|
/// Whether or not to append to the preexisting value associated with the named variable.
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
serde(skip_serializing_if = "<&bool as std::ops::Not>::not", default)
|
|
)]
|
|
pub append: bool,
|
|
/// Location of the assignment
|
|
pub loc: SourceSpan,
|
|
}
|
|
|
|
impl Node for Assignment {}
|
|
|
|
impl SourceLocation for Assignment {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
Some(self.loc.clone())
|
|
}
|
|
}
|
|
|
|
impl Display for Assignment {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{}", self.name)?;
|
|
if self.append {
|
|
write!(f, "+")?;
|
|
}
|
|
write!(f, "={}", self.value)
|
|
}
|
|
}
|
|
|
|
/// The target of an assignment.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub enum AssignmentName {
|
|
/// A named variable.
|
|
VariableName(String),
|
|
/// An element in a named array.
|
|
ArrayElementName(String, String),
|
|
}
|
|
|
|
impl Display for AssignmentName {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::VariableName(name) => write!(f, "{name}"),
|
|
Self::ArrayElementName(name, index) => {
|
|
write!(f, "{name}[{index}]")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A value being assigned to a variable.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub enum AssignmentValue {
|
|
/// A scalar (word) value.
|
|
Scalar(Word),
|
|
/// An array of elements.
|
|
Array(Vec<(Option<Word>, Word)>),
|
|
}
|
|
|
|
impl Node for AssignmentValue {}
|
|
|
|
impl SourceLocation for AssignmentValue {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
match self {
|
|
Self::Scalar(word) => word.location(),
|
|
Self::Array(words) => {
|
|
// TODO(source-location): account for the surrounding parentheses
|
|
let first = words.first().and_then(|(_key, value)| value.location());
|
|
let last = words.last().and_then(|(_key, value)| value.location());
|
|
maybe_location(first.as_ref(), last.as_ref())
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for AssignmentValue {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::Scalar(word) => write!(f, "{word}"),
|
|
Self::Array(words) => {
|
|
write!(f, "(")?;
|
|
for (i, value) in words.iter().enumerate() {
|
|
if i > 0 {
|
|
write!(f, " ")?;
|
|
}
|
|
match value {
|
|
(Some(key), value) => write!(f, "[{key}]={value}")?,
|
|
(None, value) => write!(f, "{value}")?,
|
|
}
|
|
}
|
|
write!(f, ")")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A list of I/O redirections to be applied to a command.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub struct RedirectList(pub Vec<IoRedirect>);
|
|
|
|
impl Node for RedirectList {}
|
|
|
|
impl SourceLocation for RedirectList {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
let first = self.0.first().and_then(SourceLocation::location);
|
|
let last = self.0.last().and_then(SourceLocation::location);
|
|
maybe_location(first.as_ref(), last.as_ref())
|
|
}
|
|
}
|
|
|
|
impl Display for RedirectList {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
for item in &self.0 {
|
|
write!(f, "{item}")?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// A file descriptor number.
|
|
pub type IoFd = i32;
|
|
|
|
/// An I/O redirection.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub enum IoRedirect {
|
|
/// Redirection to a file.
|
|
File(Option<IoFd>, IoFileRedirectKind, IoFileRedirectTarget),
|
|
/// Redirection from a here-document.
|
|
HereDocument(Option<IoFd>, IoHereDocument),
|
|
/// Redirection from a here-string.
|
|
HereString(Option<IoFd>, Word),
|
|
/// Redirection of both standard output and standard error (with optional append).
|
|
OutputAndError(Word, bool),
|
|
}
|
|
|
|
impl Node for IoRedirect {}
|
|
|
|
impl SourceLocation for IoRedirect {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
// TODO(source-location): complete
|
|
None
|
|
}
|
|
}
|
|
|
|
impl Display for IoRedirect {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::File(fd_num, kind, target) => {
|
|
if let Some(fd_num) = fd_num {
|
|
write!(f, "{fd_num}")?;
|
|
}
|
|
|
|
write!(f, "{kind} {target}")?;
|
|
}
|
|
Self::OutputAndError(target, append) => {
|
|
write!(f, "&>")?;
|
|
if *append {
|
|
write!(f, ">")?;
|
|
}
|
|
write!(f, " {target}")?;
|
|
}
|
|
Self::HereDocument(fd_num, here_doc) => {
|
|
if let Some(fd_num) = fd_num {
|
|
write!(f, "{fd_num}")?;
|
|
}
|
|
|
|
write!(f, "<<{here_doc}")?;
|
|
}
|
|
Self::HereString(fd_num, s) => {
|
|
if let Some(fd_num) = fd_num {
|
|
write!(f, "{fd_num}")?;
|
|
}
|
|
|
|
write!(f, "<<< {s}")?;
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// Kind of file I/O redirection.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub enum IoFileRedirectKind {
|
|
/// Read (`<`).
|
|
Read,
|
|
/// Write (`>`).
|
|
Write,
|
|
/// Append (`>>`).
|
|
Append,
|
|
/// Read and write (`<>`).
|
|
ReadAndWrite,
|
|
/// Clobber (`>|`).
|
|
Clobber,
|
|
/// Duplicate input (`<&`).
|
|
DuplicateInput,
|
|
/// Duplicate output (`>&`).
|
|
DuplicateOutput,
|
|
}
|
|
|
|
impl Display for IoFileRedirectKind {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::Read => write!(f, "<"),
|
|
Self::Write => write!(f, ">"),
|
|
Self::Append => write!(f, ">>"),
|
|
Self::ReadAndWrite => write!(f, "<>"),
|
|
Self::Clobber => write!(f, ">|"),
|
|
Self::DuplicateInput => write!(f, "<&"),
|
|
Self::DuplicateOutput => write!(f, ">&"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Target for an I/O file redirection.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub enum IoFileRedirectTarget {
|
|
/// Path to a file.
|
|
Filename(Word),
|
|
/// File descriptor number.
|
|
Fd(IoFd),
|
|
/// Process substitution: substitution with the results of executing the given
|
|
/// command in a subshell.
|
|
ProcessSubstitution(ProcessSubstitutionKind, SubshellCommand),
|
|
/// Item to duplicate in a word redirection. After expansion, this could be a
|
|
/// filename, a file descriptor, or a file descriptor and a "-" to indicate
|
|
/// requested closure.
|
|
Duplicate(Word),
|
|
}
|
|
|
|
impl Display for IoFileRedirectTarget {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::Filename(word) => write!(f, "{word}"),
|
|
Self::Fd(fd) => write!(f, "{fd}"),
|
|
Self::ProcessSubstitution(kind, subshell_command) => {
|
|
write!(f, "{kind}{subshell_command}")
|
|
}
|
|
Self::Duplicate(word) => write!(f, "{word}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Represents an I/O here document.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub struct IoHereDocument {
|
|
/// Whether to remove leading tabs from the here document.
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
serde(skip_serializing_if = "<&bool as std::ops::Not>::not", default)
|
|
)]
|
|
pub remove_tabs: bool,
|
|
/// Whether to basic-expand the contents of the here document.
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
serde(skip_serializing_if = "<&bool as std::ops::Not>::not", default)
|
|
)]
|
|
pub requires_expansion: bool,
|
|
/// The delimiter marking the end of the here document.
|
|
pub here_end: Word,
|
|
/// The contents of the here document.
|
|
pub doc: Word,
|
|
}
|
|
|
|
impl Node for IoHereDocument {}
|
|
|
|
impl SourceLocation for IoHereDocument {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
// TODO(source-location): complete
|
|
None
|
|
}
|
|
}
|
|
|
|
impl Display for IoHereDocument {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
if self.remove_tabs {
|
|
write!(f, "-")?;
|
|
}
|
|
|
|
writeln!(f, "{}", self.here_end)?;
|
|
write!(f, "{}", self.doc)?;
|
|
writeln!(f, "{}", self.here_end)?;
|
|
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
/// A (non-extended) test expression.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub enum TestExpr {
|
|
/// Always evaluates to false.
|
|
False,
|
|
/// A literal string.
|
|
Literal(String),
|
|
/// Logical AND operation on two nested expressions.
|
|
And(Box<Self>, Box<Self>),
|
|
/// Logical OR operation on two nested expressions.
|
|
Or(Box<Self>, Box<Self>),
|
|
/// Logical NOT operation on a nested expression.
|
|
Not(Box<Self>),
|
|
/// A parenthesized expression.
|
|
Parenthesized(Box<Self>),
|
|
/// A unary test operation.
|
|
UnaryTest(UnaryPredicate, String),
|
|
/// A binary test operation.
|
|
BinaryTest(BinaryPredicate, String, String),
|
|
}
|
|
|
|
impl Node for TestExpr {}
|
|
|
|
impl SourceLocation for TestExpr {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
// TODO(source-location): complete
|
|
None
|
|
}
|
|
}
|
|
|
|
impl Display for TestExpr {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::False => Ok(()),
|
|
Self::Literal(s) => write!(f, "{s}"),
|
|
Self::And(left, right) => write!(f, "{left} -a {right}"),
|
|
Self::Or(left, right) => write!(f, "{left} -o {right}"),
|
|
Self::Not(expr) => write!(f, "! {expr}"),
|
|
Self::Parenthesized(expr) => write!(f, "( {expr} )"),
|
|
Self::UnaryTest(pred, word) => write!(f, "{pred} {word}"),
|
|
Self::BinaryTest(left, op, right) => write!(f, "{left} {op} {right}"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// An extended test expression.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub enum ExtendedTestExpr {
|
|
/// Logical AND operation on two nested expressions.
|
|
And(Box<Self>, Box<Self>),
|
|
/// Logical OR operation on two nested expressions.
|
|
Or(Box<Self>, Box<Self>),
|
|
/// Logical NOT operation on a nested expression.
|
|
Not(Box<Self>),
|
|
/// A parenthesized expression.
|
|
Parenthesized(Box<Self>),
|
|
/// A unary test operation.
|
|
UnaryTest(UnaryPredicate, Word),
|
|
/// A binary test operation.
|
|
BinaryTest(BinaryPredicate, Word, Word),
|
|
}
|
|
|
|
impl Display for ExtendedTestExpr {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::And(left, right) => {
|
|
write!(f, "{left} && {right}")
|
|
}
|
|
Self::Or(left, right) => {
|
|
write!(f, "{left} || {right}")
|
|
}
|
|
Self::Not(expr) => {
|
|
write!(f, "! {expr}")
|
|
}
|
|
Self::Parenthesized(expr) => {
|
|
write!(f, "( {expr} )")
|
|
}
|
|
Self::UnaryTest(pred, word) => {
|
|
write!(f, "{pred} {word}")
|
|
}
|
|
Self::BinaryTest(pred, left, right) => {
|
|
write!(f, "{left} {pred} {right}")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// An extended test expression command.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub struct ExtendedTestExprCommand {
|
|
/// The extended test expression
|
|
pub expr: ExtendedTestExpr,
|
|
/// Location of the expression
|
|
pub loc: SourceSpan,
|
|
}
|
|
|
|
impl Node for ExtendedTestExprCommand {}
|
|
|
|
impl SourceLocation for ExtendedTestExprCommand {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
Some(self.loc.clone())
|
|
}
|
|
}
|
|
|
|
impl Display for ExtendedTestExprCommand {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
self.expr.fmt(f)
|
|
}
|
|
}
|
|
|
|
/// A unary predicate usable in an extended test expression.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub enum UnaryPredicate {
|
|
/// Computes if the operand is a path to an existing file.
|
|
FileExists,
|
|
/// Computes if the operand is a path to an existing block device file.
|
|
FileExistsAndIsBlockSpecialFile,
|
|
/// Computes if the operand is a path to an existing character device file.
|
|
FileExistsAndIsCharSpecialFile,
|
|
/// Computes if the operand is a path to an existing directory.
|
|
FileExistsAndIsDir,
|
|
/// Computes if the operand is a path to an existing regular file.
|
|
FileExistsAndIsRegularFile,
|
|
/// Computes if the operand is a path to an existing file with the setgid bit set.
|
|
FileExistsAndIsSetgid,
|
|
/// Computes if the operand is a path to an existing symbolic link.
|
|
FileExistsAndIsSymlink,
|
|
/// Computes if the operand is a path to an existing file with the sticky bit set.
|
|
FileExistsAndHasStickyBit,
|
|
/// Computes if the operand is a path to an existing FIFO file.
|
|
FileExistsAndIsFifo,
|
|
/// Computes if the operand is a path to an existing file that is readable.
|
|
FileExistsAndIsReadable,
|
|
/// Computes if the operand is a path to an existing file with a non-zero length.
|
|
FileExistsAndIsNotZeroLength,
|
|
/// Computes if the operand is a file descriptor that is an open terminal.
|
|
FdIsOpenTerminal,
|
|
/// Computes if the operand is a path to an existing file with the setuid bit set.
|
|
FileExistsAndIsSetuid,
|
|
/// Computes if the operand is a path to an existing file that is writable.
|
|
FileExistsAndIsWritable,
|
|
/// Computes if the operand is a path to an existing file that is executable.
|
|
FileExistsAndIsExecutable,
|
|
/// Computes if the operand is a path to an existing file owned by the current context's
|
|
/// effective group ID.
|
|
FileExistsAndOwnedByEffectiveGroupId,
|
|
/// Computes if the operand is a path to an existing file that has been modified since last
|
|
/// being read.
|
|
FileExistsAndModifiedSinceLastRead,
|
|
/// Computes if the operand is a path to an existing file owned by the current context's
|
|
/// effective user ID.
|
|
FileExistsAndOwnedByEffectiveUserId,
|
|
/// Computes if the operand is a path to an existing socket file.
|
|
FileExistsAndIsSocket,
|
|
/// Computes if the operand is a 'set -o' option that is enabled.
|
|
ShellOptionEnabled,
|
|
/// Computes if the operand names a shell variable that is set and assigned a value.
|
|
ShellVariableIsSetAndAssigned,
|
|
/// Computes if the operand names a shell variable that is set and of nameref type.
|
|
ShellVariableIsSetAndNameRef,
|
|
/// Computes if the operand is a string with zero length.
|
|
StringHasZeroLength,
|
|
/// Computes if the operand is a string with non-zero length.
|
|
StringHasNonZeroLength,
|
|
}
|
|
|
|
impl Display for UnaryPredicate {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::FileExists => write!(f, "-e"),
|
|
Self::FileExistsAndIsBlockSpecialFile => write!(f, "-b"),
|
|
Self::FileExistsAndIsCharSpecialFile => write!(f, "-c"),
|
|
Self::FileExistsAndIsDir => write!(f, "-d"),
|
|
Self::FileExistsAndIsRegularFile => write!(f, "-f"),
|
|
Self::FileExistsAndIsSetgid => write!(f, "-g"),
|
|
Self::FileExistsAndIsSymlink => write!(f, "-h"),
|
|
Self::FileExistsAndHasStickyBit => write!(f, "-k"),
|
|
Self::FileExistsAndIsFifo => write!(f, "-p"),
|
|
Self::FileExistsAndIsReadable => write!(f, "-r"),
|
|
Self::FileExistsAndIsNotZeroLength => write!(f, "-s"),
|
|
Self::FdIsOpenTerminal => write!(f, "-t"),
|
|
Self::FileExistsAndIsSetuid => write!(f, "-u"),
|
|
Self::FileExistsAndIsWritable => write!(f, "-w"),
|
|
Self::FileExistsAndIsExecutable => write!(f, "-x"),
|
|
Self::FileExistsAndOwnedByEffectiveGroupId => write!(f, "-G"),
|
|
Self::FileExistsAndModifiedSinceLastRead => write!(f, "-N"),
|
|
Self::FileExistsAndOwnedByEffectiveUserId => write!(f, "-O"),
|
|
Self::FileExistsAndIsSocket => write!(f, "-S"),
|
|
Self::ShellOptionEnabled => write!(f, "-o"),
|
|
Self::ShellVariableIsSetAndAssigned => write!(f, "-v"),
|
|
Self::ShellVariableIsSetAndNameRef => write!(f, "-R"),
|
|
Self::StringHasZeroLength => write!(f, "-z"),
|
|
Self::StringHasNonZeroLength => write!(f, "-n"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A binary predicate usable in an extended test expression.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub enum BinaryPredicate {
|
|
/// Computes if two files refer to the same device and inode numbers.
|
|
FilesReferToSameDeviceAndInodeNumbers,
|
|
/// Computes if the left file is newer than the right, or exists when the right does not.
|
|
LeftFileIsNewerOrExistsWhenRightDoesNot,
|
|
/// Computes if the left file is older than the right, or does not exist when the right does.
|
|
LeftFileIsOlderOrDoesNotExistWhenRightDoes,
|
|
/// Computes if a string exactly matches a pattern.
|
|
StringExactlyMatchesPattern,
|
|
/// Computes if a string does not exactly match a pattern.
|
|
StringDoesNotExactlyMatchPattern,
|
|
/// Computes if a string matches a regular expression.
|
|
StringMatchesRegex,
|
|
/// Computes if a string exactly matches another string.
|
|
StringExactlyMatchesString,
|
|
/// Computes if a string does not exactly match another string.
|
|
StringDoesNotExactlyMatchString,
|
|
/// Computes if a string contains a substring.
|
|
StringContainsSubstring,
|
|
/// Computes if the left value sorts before the right.
|
|
LeftSortsBeforeRight,
|
|
/// Computes if the left value sorts after the right.
|
|
LeftSortsAfterRight,
|
|
/// Computes if two values are equal via arithmetic comparison.
|
|
ArithmeticEqualTo,
|
|
/// Computes if two values are not equal via arithmetic comparison.
|
|
ArithmeticNotEqualTo,
|
|
/// Computes if the left value is less than the right via arithmetic comparison.
|
|
ArithmeticLessThan,
|
|
/// Computes if the left value is less than or equal to the right via arithmetic comparison.
|
|
ArithmeticLessThanOrEqualTo,
|
|
/// Computes if the left value is greater than the right via arithmetic comparison.
|
|
ArithmeticGreaterThan,
|
|
/// Computes if the left value is greater than or equal to the right via arithmetic comparison.
|
|
ArithmeticGreaterThanOrEqualTo,
|
|
}
|
|
|
|
impl Display for BinaryPredicate {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::FilesReferToSameDeviceAndInodeNumbers => write!(f, "-ef"),
|
|
Self::LeftFileIsNewerOrExistsWhenRightDoesNot => write!(f, "-nt"),
|
|
Self::LeftFileIsOlderOrDoesNotExistWhenRightDoes => write!(f, "-ot"),
|
|
Self::StringExactlyMatchesPattern => write!(f, "=="),
|
|
Self::StringDoesNotExactlyMatchPattern => write!(f, "!="),
|
|
Self::StringMatchesRegex => write!(f, "=~"),
|
|
Self::StringContainsSubstring => write!(f, "=~"),
|
|
Self::StringExactlyMatchesString => write!(f, "=="),
|
|
Self::StringDoesNotExactlyMatchString => write!(f, "!="),
|
|
Self::LeftSortsBeforeRight => write!(f, "<"),
|
|
Self::LeftSortsAfterRight => write!(f, ">"),
|
|
Self::ArithmeticEqualTo => write!(f, "-eq"),
|
|
Self::ArithmeticNotEqualTo => write!(f, "-ne"),
|
|
Self::ArithmeticLessThan => write!(f, "-lt"),
|
|
Self::ArithmeticLessThanOrEqualTo => write!(f, "-le"),
|
|
Self::ArithmeticGreaterThan => write!(f, "-gt"),
|
|
Self::ArithmeticGreaterThanOrEqualTo => write!(f, "-ge"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Represents a shell word.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub struct Word {
|
|
/// Raw text of the word.
|
|
pub value: String,
|
|
/// Location of the word
|
|
pub loc: Option<SourceSpan>,
|
|
}
|
|
|
|
impl Node for Word {}
|
|
|
|
impl SourceLocation for Word {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
self.loc.clone()
|
|
}
|
|
}
|
|
|
|
impl Display for Word {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{}", self.value)
|
|
}
|
|
}
|
|
|
|
impl From<&tokenizer::Token> for Word {
|
|
fn from(t: &tokenizer::Token) -> Self {
|
|
match t {
|
|
tokenizer::Token::Word(value, loc) => Self {
|
|
value: value.clone(),
|
|
loc: Some(loc.clone()),
|
|
},
|
|
tokenizer::Token::Operator(value, loc) => Self {
|
|
value: value.clone(),
|
|
loc: Some(loc.clone()),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<String> for Word {
|
|
fn from(s: String) -> Self {
|
|
Self {
|
|
value: s,
|
|
loc: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl AsRef<str> for Word {
|
|
fn as_ref(&self) -> &str {
|
|
&self.value
|
|
}
|
|
}
|
|
|
|
impl Word {
|
|
/// Constructs a new `Word` from a given string.
|
|
pub fn new(s: &str) -> Self {
|
|
Self {
|
|
value: s.to_owned(),
|
|
loc: None,
|
|
}
|
|
}
|
|
|
|
/// Constructs a new `Word` from a given string and location.
|
|
pub fn with_location(s: &str, loc: &SourceSpan) -> Self {
|
|
Self {
|
|
value: s.to_owned(),
|
|
loc: Some(loc.to_owned()),
|
|
}
|
|
}
|
|
|
|
/// Returns the raw text of the word, consuming the `Word`.
|
|
pub fn flatten(&self) -> String {
|
|
self.value.clone()
|
|
}
|
|
}
|
|
|
|
/// Encapsulates an unparsed arithmetic expression.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub struct UnexpandedArithmeticExpr {
|
|
/// The raw text of the expression.
|
|
pub value: String,
|
|
}
|
|
|
|
impl Display for UnexpandedArithmeticExpr {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
write!(f, "{}", self.value)
|
|
}
|
|
}
|
|
|
|
/// An arithmetic expression.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub enum ArithmeticExpr {
|
|
/// A literal integer value.
|
|
Literal(i64),
|
|
/// A dereference of a variable or array element.
|
|
Reference(ArithmeticTarget),
|
|
/// A unary operation on an the result of a given nested expression.
|
|
UnaryOp(UnaryOperator, Box<Self>),
|
|
/// A binary operation on two nested expressions.
|
|
BinaryOp(BinaryOperator, Box<Self>, Box<Self>),
|
|
/// A ternary conditional expression.
|
|
Conditional(Box<Self>, Box<Self>, Box<Self>),
|
|
/// An assignment operation.
|
|
Assignment(ArithmeticTarget, Box<Self>),
|
|
/// A binary assignment operation.
|
|
BinaryAssignment(BinaryOperator, ArithmeticTarget, Box<Self>),
|
|
/// A unary assignment operation.
|
|
UnaryAssignment(UnaryAssignmentOperator, ArithmeticTarget),
|
|
}
|
|
|
|
impl Node for ArithmeticExpr {}
|
|
|
|
impl SourceLocation for ArithmeticExpr {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
// TODO(source-location): complete and add loc for literal
|
|
None
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "arbitrary")]
|
|
impl<'a> arbitrary::Arbitrary<'a> for ArithmeticExpr {
|
|
fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
|
|
let variant = u.choose(&[
|
|
"Literal",
|
|
"Reference",
|
|
"UnaryOp",
|
|
"BinaryOp",
|
|
"Conditional",
|
|
"Assignment",
|
|
"BinaryAssignment",
|
|
"UnaryAssignment",
|
|
])?;
|
|
|
|
match *variant {
|
|
"Literal" => Ok(Self::Literal(i64::arbitrary(u)?)),
|
|
"Reference" => Ok(Self::Reference(ArithmeticTarget::arbitrary(u)?)),
|
|
"UnaryOp" => Ok(Self::UnaryOp(
|
|
UnaryOperator::arbitrary(u)?,
|
|
Box::new(Self::arbitrary(u)?),
|
|
)),
|
|
"BinaryOp" => Ok(Self::BinaryOp(
|
|
BinaryOperator::arbitrary(u)?,
|
|
Box::new(Self::arbitrary(u)?),
|
|
Box::new(Self::arbitrary(u)?),
|
|
)),
|
|
"Conditional" => Ok(Self::Conditional(
|
|
Box::new(Self::arbitrary(u)?),
|
|
Box::new(Self::arbitrary(u)?),
|
|
Box::new(Self::arbitrary(u)?),
|
|
)),
|
|
"Assignment" => Ok(Self::Assignment(
|
|
ArithmeticTarget::arbitrary(u)?,
|
|
Box::new(Self::arbitrary(u)?),
|
|
)),
|
|
"BinaryAssignment" => Ok(Self::BinaryAssignment(
|
|
BinaryOperator::arbitrary(u)?,
|
|
ArithmeticTarget::arbitrary(u)?,
|
|
Box::new(Self::arbitrary(u)?),
|
|
)),
|
|
"UnaryAssignment" => Ok(Self::UnaryAssignment(
|
|
UnaryAssignmentOperator::arbitrary(u)?,
|
|
ArithmeticTarget::arbitrary(u)?,
|
|
)),
|
|
_ => unreachable!(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Display for ArithmeticExpr {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::Literal(literal) => write!(f, "{literal}"),
|
|
Self::Reference(target) => write!(f, "{target}"),
|
|
Self::UnaryOp(op, operand) => write!(f, "{op}{operand}"),
|
|
Self::BinaryOp(op, left, right) => {
|
|
if matches!(op, BinaryOperator::Comma) {
|
|
write!(f, "{left}{op} {right}")
|
|
} else {
|
|
write!(f, "{left} {op} {right}")
|
|
}
|
|
}
|
|
Self::Conditional(condition, if_branch, else_branch) => {
|
|
write!(f, "{condition} ? {if_branch} : {else_branch}")
|
|
}
|
|
Self::Assignment(target, value) => write!(f, "{target} = {value}"),
|
|
Self::BinaryAssignment(op, target, operand) => {
|
|
write!(f, "{target} {op}= {operand}")
|
|
}
|
|
Self::UnaryAssignment(op, target) => match op {
|
|
UnaryAssignmentOperator::PrefixIncrement
|
|
| UnaryAssignmentOperator::PrefixDecrement => write!(f, "{op}{target}"),
|
|
UnaryAssignmentOperator::PostfixIncrement
|
|
| UnaryAssignmentOperator::PostfixDecrement => write!(f, "{target}{op}"),
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A binary arithmetic operator.
|
|
#[derive(Clone, Copy, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub enum BinaryOperator {
|
|
/// Exponentiation (e.g., `x ** y`).
|
|
Power,
|
|
/// Multiplication (e.g., `x * y`).
|
|
Multiply,
|
|
/// Division (e.g., `x / y`).
|
|
Divide,
|
|
/// Modulo (e.g., `x % y`).
|
|
Modulo,
|
|
/// Comma (e.g., `x, y`).
|
|
Comma,
|
|
/// Addition (e.g., `x + y`).
|
|
Add,
|
|
/// Subtraction (e.g., `x - y`).
|
|
Subtract,
|
|
/// Bitwise left shift (e.g., `x << y`).
|
|
ShiftLeft,
|
|
/// Bitwise right shift (e.g., `x >> y`).
|
|
ShiftRight,
|
|
/// Less than (e.g., `x < y`).
|
|
LessThan,
|
|
/// Less than or equal to (e.g., `x <= y`).
|
|
LessThanOrEqualTo,
|
|
/// Greater than (e.g., `x > y`).
|
|
GreaterThan,
|
|
/// Greater than or equal to (e.g., `x >= y`).
|
|
GreaterThanOrEqualTo,
|
|
/// Equals (e.g., `x == y`).
|
|
Equals,
|
|
/// Not equals (e.g., `x != y`).
|
|
NotEquals,
|
|
/// Bitwise AND (e.g., `x & y`).
|
|
BitwiseAnd,
|
|
/// Bitwise exclusive OR (xor) (e.g., `x ^ y`).
|
|
BitwiseXor,
|
|
/// Bitwise OR (e.g., `x | y`).
|
|
BitwiseOr,
|
|
/// Logical AND (e.g., `x && y`).
|
|
LogicalAnd,
|
|
/// Logical OR (e.g., `x || y`).
|
|
LogicalOr,
|
|
}
|
|
|
|
impl Display for BinaryOperator {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::Power => write!(f, "**"),
|
|
Self::Multiply => write!(f, "*"),
|
|
Self::Divide => write!(f, "/"),
|
|
Self::Modulo => write!(f, "%"),
|
|
Self::Comma => write!(f, ","),
|
|
Self::Add => write!(f, "+"),
|
|
Self::Subtract => write!(f, "-"),
|
|
Self::ShiftLeft => write!(f, "<<"),
|
|
Self::ShiftRight => write!(f, ">>"),
|
|
Self::LessThan => write!(f, "<"),
|
|
Self::LessThanOrEqualTo => write!(f, "<="),
|
|
Self::GreaterThan => write!(f, ">"),
|
|
Self::GreaterThanOrEqualTo => write!(f, ">="),
|
|
Self::Equals => write!(f, "=="),
|
|
Self::NotEquals => write!(f, "!="),
|
|
Self::BitwiseAnd => write!(f, "&"),
|
|
Self::BitwiseXor => write!(f, "^"),
|
|
Self::BitwiseOr => write!(f, "|"),
|
|
Self::LogicalAnd => write!(f, "&&"),
|
|
Self::LogicalOr => write!(f, "||"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A unary arithmetic operator.
|
|
#[derive(Clone, Copy, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub enum UnaryOperator {
|
|
/// Unary plus (e.g., `+x`).
|
|
UnaryPlus,
|
|
/// Unary minus (e.g., `-x`).
|
|
UnaryMinus,
|
|
/// Bitwise not (e.g., `~x`).
|
|
BitwiseNot,
|
|
/// Logical not (e.g., `!x`).
|
|
LogicalNot,
|
|
}
|
|
|
|
impl Display for UnaryOperator {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::UnaryPlus => write!(f, "+"),
|
|
Self::UnaryMinus => write!(f, "-"),
|
|
Self::BitwiseNot => write!(f, "~"),
|
|
Self::LogicalNot => write!(f, "!"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A unary arithmetic assignment operator.
|
|
#[derive(Clone, Copy, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub enum UnaryAssignmentOperator {
|
|
/// Prefix increment (e.g., `++x`).
|
|
PrefixIncrement,
|
|
/// Prefix increment (e.g., `--x`).
|
|
PrefixDecrement,
|
|
/// Postfix increment (e.g., `x++`).
|
|
PostfixIncrement,
|
|
/// Postfix decrement (e.g., `x--`).
|
|
PostfixDecrement,
|
|
}
|
|
|
|
impl Display for UnaryAssignmentOperator {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::PrefixIncrement => write!(f, "++"),
|
|
Self::PrefixDecrement => write!(f, "--"),
|
|
Self::PostfixIncrement => write!(f, "++"),
|
|
Self::PostfixDecrement => write!(f, "--"),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Identifies the target of an arithmetic assignment expression.
|
|
#[derive(Clone, Debug)]
|
|
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
|
|
#[cfg_attr(
|
|
any(test, feature = "serde"),
|
|
derive(PartialEq, Eq, serde::Serialize, serde::Deserialize)
|
|
)]
|
|
pub enum ArithmeticTarget {
|
|
/// A named variable.
|
|
Variable(String),
|
|
/// An element in an array.
|
|
ArrayElement(String, Box<ArithmeticExpr>),
|
|
}
|
|
|
|
impl Node for ArithmeticTarget {}
|
|
|
|
impl SourceLocation for ArithmeticTarget {
|
|
fn location(&self) -> Option<SourceSpan> {
|
|
// TODO(source-location): complete and add loc
|
|
None
|
|
}
|
|
}
|
|
|
|
impl Display for ArithmeticTarget {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
match self {
|
|
Self::Variable(name) => write!(f, "{name}"),
|
|
Self::ArrayElement(name, index) => write!(f, "{name}[{index}]"),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[allow(clippy::panic)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::{ParserOptions, SourcePosition};
|
|
use std::io::BufReader;
|
|
|
|
fn parse(input: &str) -> Program {
|
|
let reader = BufReader::new(input.as_bytes());
|
|
let mut parser = crate::Parser::new(reader, &ParserOptions::default());
|
|
parser.parse_program().unwrap()
|
|
}
|
|
|
|
#[test]
|
|
fn program_source_loc() {
|
|
const INPUT: &str = r"echo hi
|
|
echo there
|
|
";
|
|
|
|
let loc = parse(INPUT).location().unwrap();
|
|
|
|
assert_eq!(
|
|
*(loc.start),
|
|
SourcePosition {
|
|
line: 1,
|
|
column: 1,
|
|
index: 0
|
|
}
|
|
);
|
|
assert_eq!(
|
|
*(loc.end),
|
|
SourcePosition {
|
|
line: 2,
|
|
column: 11,
|
|
index: 18
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn function_def_loc() {
|
|
const INPUT: &str = r"my_func() {
|
|
echo hi
|
|
echo there
|
|
}
|
|
|
|
my_func
|
|
";
|
|
|
|
let program = parse(INPUT);
|
|
|
|
let Command::Function(func_def) = &program.complete_commands[0].0[0].0.first.seq[0] else {
|
|
panic!("expected function definition");
|
|
};
|
|
|
|
let loc = func_def.location().unwrap();
|
|
|
|
assert_eq!(
|
|
*(loc.start),
|
|
SourcePosition {
|
|
line: 1,
|
|
column: 1,
|
|
index: 0
|
|
}
|
|
);
|
|
assert_eq!(
|
|
*(loc.end),
|
|
SourcePosition {
|
|
line: 4,
|
|
column: 2,
|
|
index: 36
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn simple_cmd_loc() {
|
|
const INPUT: &str = r"var=value somecmd arg1 arg2
|
|
";
|
|
|
|
let program = parse(INPUT);
|
|
|
|
let Command::Simple(cmd) = &program.complete_commands[0].0[0].0.first.seq[0] else {
|
|
panic!("expected function definition");
|
|
};
|
|
|
|
let loc = cmd.location().unwrap();
|
|
|
|
assert_eq!(
|
|
*(loc.start),
|
|
SourcePosition {
|
|
line: 1,
|
|
column: 1,
|
|
index: 0
|
|
}
|
|
);
|
|
assert_eq!(
|
|
*(loc.end),
|
|
SourcePosition {
|
|
line: 1,
|
|
column: 28,
|
|
index: 27
|
|
}
|
|
);
|
|
}
|
|
}
|