#![allow(dead_code)]
use crate::{
Name,
span::{Position, Span, TextRange},
};
use std::{
borrow::Cow,
fmt,
ops::{Deref, DerefMut},
};
fn assert_string_write(result: fmt::Result) {
if result.is_err() {
unreachable!("writing into a String should not fail");
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SourceText {
span: Span,
cooked: Option<Box<str>>,
}
impl SourceText {
pub fn source(span: Span) -> Self {
Self { span, cooked: None }
}
pub fn cooked(span: Span, text: impl Into<Box<str>>) -> Self {
Self {
span,
cooked: Some(text.into()),
}
}
pub fn span(&self) -> Span {
self.span
}
pub fn slice<'a>(&'a self, source: &'a str) -> &'a str {
self.cooked
.as_deref()
.unwrap_or_else(|| self.span.slice(source))
}
pub fn is_source_backed(&self) -> bool {
self.cooked.is_none()
}
pub fn rebased(&mut self, base: Position) {
self.span = self.span.rebased(base);
}
}
impl From<Span> for SourceText {
fn from(span: Span) -> Self {
Self::source(span)
}
}
impl From<&str> for SourceText {
fn from(value: &str) -> Self {
Self::cooked(Span::new(), value)
}
}
impl From<String> for SourceText {
fn from(value: String) -> Self {
Self::cooked(Span::new(), value)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LiteralText {
Source,
Owned(Box<str>),
CookedSource(Box<str>),
}
impl LiteralText {
pub fn source() -> Self {
Self::Source
}
pub fn owned(text: impl Into<Box<str>>) -> Self {
Self::Owned(text.into())
}
pub fn cooked_source(text: impl Into<Box<str>>) -> Self {
Self::CookedSource(text.into())
}
pub fn as_str<'a>(&'a self, source: &'a str, span: Span) -> &'a str {
match self {
Self::Source => span.slice(source),
Self::Owned(text) | Self::CookedSource(text) => text.as_ref(),
}
}
pub fn syntax_str<'a>(&'a self, source: &'a str, span: Span) -> &'a str {
match self {
Self::Source | Self::CookedSource(_) => span.slice(source),
Self::Owned(text) => text.as_ref(),
}
}
pub fn eq_str(&self, source: &str, span: Span, other: &str) -> bool {
self.as_str(source, span) == other
}
pub fn is_source_backed(&self) -> bool {
matches!(self, Self::Source | Self::CookedSource(_))
}
pub fn is_empty(&self) -> bool {
matches!(self, Self::Owned(text) | Self::CookedSource(text) if text.is_empty())
}
}
impl From<&str> for LiteralText {
fn from(value: &str) -> Self {
Self::owned(value)
}
}
impl From<String> for LiteralText {
fn from(value: String) -> Self {
Self::owned(value)
}
}
impl PartialEq<str> for LiteralText {
fn eq(&self, other: &str) -> bool {
matches!(self, Self::Owned(text) | Self::CookedSource(text) if text.as_ref() == other)
}
}
impl PartialEq<&str> for LiteralText {
fn eq(&self, other: &&str) -> bool {
self == *other
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Comment {
pub range: TextRange,
}
#[derive(Debug, Clone)]
pub struct File {
pub body: StmtSeq,
pub span: Span,
}
#[derive(Debug, Clone)]
pub struct StmtSeq {
pub leading_comments: Vec<Comment>,
pub stmts: Vec<Stmt>,
pub trailing_comments: Vec<Comment>,
pub span: Span,
}
impl StmtSeq {
pub fn len(&self) -> usize {
self.stmts.len()
}
pub fn is_empty(&self) -> bool {
self.stmts.is_empty()
}
pub fn as_slice(&self) -> &[Stmt] {
&self.stmts
}
pub fn as_mut_slice(&mut self) -> &mut [Stmt] {
&mut self.stmts
}
pub fn iter(&self) -> std::slice::Iter<'_, Stmt> {
self.stmts.iter()
}
pub fn iter_mut(&mut self) -> std::slice::IterMut<'_, Stmt> {
self.stmts.iter_mut()
}
pub fn first(&self) -> Option<&Stmt> {
self.stmts.first()
}
pub fn last(&self) -> Option<&Stmt> {
self.stmts.last()
}
}
impl std::ops::Index<usize> for StmtSeq {
type Output = Stmt;
fn index(&self, index: usize) -> &Self::Output {
&self.stmts[index]
}
}
impl std::ops::IndexMut<usize> for StmtSeq {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
&mut self.stmts[index]
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StmtTerminator {
Semicolon,
Background(BackgroundOperator),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackgroundOperator {
Plain,
Pipe,
Bang,
}
#[derive(Debug, Clone)]
pub struct Stmt {
pub leading_comments: Vec<Comment>,
pub command: Command,
pub negated: bool,
pub redirects: Box<[Redirect]>,
pub terminator: Option<StmtTerminator>,
pub terminator_span: Option<Span>,
pub inline_comment: Option<Comment>,
pub span: Span,
}
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum Command {
Simple(SimpleCommand),
Builtin(BuiltinCommand),
Decl(DeclClause),
Binary(BinaryCommand),
Compound(CompoundCommand),
Function(FunctionDef),
AnonymousFunction(AnonymousFunctionCommand),
}
#[derive(Debug, Clone)]
pub struct SimpleCommand {
pub name: Word,
pub args: Vec<Word>,
pub assignments: Box<[Assignment]>,
pub span: Span,
}
#[derive(Debug, Clone)]
pub struct DeclClause {
pub variant: Name,
pub variant_span: Span,
pub operands: Vec<DeclOperand>,
pub assignments: Box<[Assignment]>,
pub span: Span,
}
#[derive(Debug, Clone)]
pub enum DeclOperand {
Flag(Word),
Name(VarRef),
Assignment(Assignment),
Dynamic(Word),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SubscriptInterpretation {
Indexed,
Associative,
Contextual,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SubscriptKind {
Ordinary,
Selector(SubscriptSelector),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SubscriptSelector {
At,
Star,
}
impl SubscriptSelector {
pub const fn as_char(self) -> char {
match self {
Self::At => '@',
Self::Star => '*',
}
}
}
#[derive(Debug, Clone)]
pub struct Subscript {
pub text: SourceText,
pub raw: Option<SourceText>,
pub kind: SubscriptKind,
pub interpretation: SubscriptInterpretation,
pub word_ast: Option<Word>,
pub arithmetic_ast: Option<ArithmeticExprNode>,
}
impl Subscript {
pub fn span(&self) -> Span {
self.text.span()
}
pub fn syntax_source_text(&self) -> &SourceText {
self.raw.as_ref().unwrap_or(&self.text)
}
pub fn syntax_text<'a>(&'a self, source: &'a str) -> &'a str {
self.syntax_source_text().slice(source)
}
pub fn is_array_selector(&self) -> bool {
matches!(self.kind, SubscriptKind::Selector(_))
}
pub fn selector(&self) -> Option<SubscriptSelector> {
match self.kind {
SubscriptKind::Ordinary => None,
SubscriptKind::Selector(selector) => Some(selector),
}
}
pub fn is_source_backed(&self) -> bool {
self.syntax_source_text().is_source_backed()
}
pub fn word_ast(&self) -> Option<&Word> {
self.word_ast.as_ref()
}
}
#[derive(Debug, Clone)]
pub struct VarRef {
pub name: Name,
pub name_span: Span,
pub subscript: Option<Subscript>,
pub span: Span,
}
impl VarRef {
pub fn has_array_selector(&self) -> bool {
self.subscript
.as_ref()
.is_some_and(Subscript::is_array_selector)
}
pub fn is_source_backed(&self) -> bool {
self.subscript
.as_ref()
.is_none_or(Subscript::is_source_backed)
}
}
#[derive(Debug, Clone)]
pub enum BuiltinCommand {
Break(BreakCommand),
Continue(ContinueCommand),
Return(ReturnCommand),
Exit(ExitCommand),
}
#[derive(Debug, Clone)]
pub struct BreakCommand {
pub depth: Option<Word>,
pub extra_args: Vec<Word>,
pub assignments: Box<[Assignment]>,
pub span: Span,
}
#[derive(Debug, Clone)]
pub struct ContinueCommand {
pub depth: Option<Word>,
pub extra_args: Vec<Word>,
pub assignments: Box<[Assignment]>,
pub span: Span,
}
#[derive(Debug, Clone)]
pub struct ReturnCommand {
pub code: Option<Word>,
pub extra_args: Vec<Word>,
pub assignments: Box<[Assignment]>,
pub span: Span,
}
#[derive(Debug, Clone)]
pub struct ExitCommand {
pub code: Option<Word>,
pub extra_args: Vec<Word>,
pub assignments: Box<[Assignment]>,
pub span: Span,
}
#[derive(Debug, Clone)]
pub struct BinaryCommand {
pub left: Box<Stmt>,
pub op: BinaryOp,
pub op_span: Span,
pub right: Box<Stmt>,
pub span: Span,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BinaryOp {
And,
Or,
Pipe,
PipeAll,
}
#[derive(Debug, Clone)]
pub enum CompoundCommand {
If(IfCommand),
For(ForCommand),
Repeat(RepeatCommand),
Foreach(ForeachCommand),
ArithmeticFor(Box<ArithmeticForCommand>),
While(WhileCommand),
Until(UntilCommand),
Case(CaseCommand),
Select(SelectCommand),
Subshell(StmtSeq),
BraceGroup(StmtSeq),
Arithmetic(ArithmeticCommand),
Time(TimeCommand),
Conditional(ConditionalCommand),
Coproc(CoprocCommand),
Always(AlwaysCommand),
}
#[derive(Debug, Clone)]
pub struct CoprocCommand {
pub name: Name,
pub name_span: Option<Span>,
pub body: Box<Stmt>,
pub span: Span,
}
#[derive(Debug, Clone)]
pub struct AlwaysCommand {
pub body: StmtSeq,
pub always_body: StmtSeq,
pub span: Span,
}
#[derive(Debug, Clone)]
pub struct TimeCommand {
pub posix_format: bool,
pub command: Option<Box<Stmt>>,
pub span: Span,
}
#[derive(Debug, Clone)]
pub struct ConditionalCommand {
pub expression: ConditionalExpr,
pub span: Span,
pub left_bracket_span: Span,
pub right_bracket_span: Span,
}
#[derive(Debug, Clone)]
pub enum ConditionalExpr {
Binary(ConditionalBinaryExpr),
Unary(ConditionalUnaryExpr),
Parenthesized(ConditionalParenExpr),
Word(Word),
Pattern(Pattern),
Regex(Word),
VarRef(Box<VarRef>),
}
impl ConditionalExpr {
pub fn span(&self) -> Span {
match self {
Self::Binary(expr) => expr.span(),
Self::Unary(expr) => expr.span(),
Self::Parenthesized(expr) => expr.span(),
Self::Word(word) | Self::Regex(word) => word.span,
Self::Pattern(pattern) => pattern.span,
Self::VarRef(var_ref) => var_ref.span,
}
}
}
#[derive(Debug, Clone)]
pub struct ConditionalBinaryExpr {
pub left: Box<ConditionalExpr>,
pub op: ConditionalBinaryOp,
pub op_span: Span,
pub right: Box<ConditionalExpr>,
}
impl ConditionalBinaryExpr {
pub fn span(&self) -> Span {
self.left.span().merge(self.right.span())
}
}
#[derive(Debug, Clone)]
pub struct ConditionalUnaryExpr {
pub op: ConditionalUnaryOp,
pub op_span: Span,
pub expr: Box<ConditionalExpr>,
}
impl ConditionalUnaryExpr {
pub fn span(&self) -> Span {
self.op_span.merge(self.expr.span())
}
}
#[derive(Debug, Clone)]
pub struct ConditionalParenExpr {
pub left_paren_span: Span,
pub expr: Box<ConditionalExpr>,
pub right_paren_span: Span,
}
impl ConditionalParenExpr {
pub fn span(&self) -> Span {
self.left_paren_span.merge(self.right_paren_span)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConditionalBinaryOp {
RegexMatch,
NewerThan,
OlderThan,
SameFile,
ArithmeticEq,
ArithmeticNe,
ArithmeticLe,
ArithmeticGe,
ArithmeticLt,
ArithmeticGt,
And,
Or,
PatternEqShort,
PatternEq,
PatternNe,
LexicalBefore,
LexicalAfter,
}
impl ConditionalBinaryOp {
pub fn as_str(self) -> &'static str {
match self {
Self::RegexMatch => "=~",
Self::NewerThan => "-nt",
Self::OlderThan => "-ot",
Self::SameFile => "-ef",
Self::ArithmeticEq => "-eq",
Self::ArithmeticNe => "-ne",
Self::ArithmeticLe => "-le",
Self::ArithmeticGe => "-ge",
Self::ArithmeticLt => "-lt",
Self::ArithmeticGt => "-gt",
Self::And => "&&",
Self::Or => "||",
Self::PatternEqShort => "=",
Self::PatternEq => "==",
Self::PatternNe => "!=",
Self::LexicalBefore => "<",
Self::LexicalAfter => ">",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ConditionalUnaryOp {
Exists,
RegularFile,
Directory,
CharacterSpecial,
BlockSpecial,
NamedPipe,
Socket,
Symlink,
Sticky,
SetGroupId,
SetUserId,
GroupOwned,
UserOwned,
Modified,
Readable,
Writable,
Executable,
NonEmptyFile,
FdTerminal,
EmptyString,
NonEmptyString,
OptionSet,
VariableSet,
ReferenceVariable,
Not,
}
impl ConditionalUnaryOp {
pub fn as_str(self) -> &'static str {
match self {
Self::Exists => "-e",
Self::RegularFile => "-f",
Self::Directory => "-d",
Self::CharacterSpecial => "-c",
Self::BlockSpecial => "-b",
Self::NamedPipe => "-p",
Self::Socket => "-S",
Self::Symlink => "-L",
Self::Sticky => "-k",
Self::SetGroupId => "-g",
Self::SetUserId => "-u",
Self::GroupOwned => "-G",
Self::UserOwned => "-O",
Self::Modified => "-N",
Self::Readable => "-r",
Self::Writable => "-w",
Self::Executable => "-x",
Self::NonEmptyFile => "-s",
Self::FdTerminal => "-t",
Self::EmptyString => "-z",
Self::NonEmptyString => "-n",
Self::OptionSet => "-o",
Self::VariableSet => "-v",
Self::ReferenceVariable => "-R",
Self::Not => "!",
}
}
}
#[derive(Debug, Clone)]
pub struct IfCommand {
pub condition: StmtSeq,
pub then_branch: StmtSeq,
pub elif_branches: Vec<(StmtSeq, StmtSeq)>,
pub else_branch: Option<StmtSeq>,
pub syntax: IfSyntax,
pub span: Span,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IfSyntax {
ThenFi {
then_span: Span,
fi_span: Span,
},
Brace {
left_brace_span: Span,
right_brace_span: Span,
},
}
#[derive(Debug, Clone)]
pub struct ForCommand {
pub targets: Vec<ForTarget>,
pub words: Option<Vec<Word>>,
pub body: StmtSeq,
pub syntax: ForSyntax,
pub span: Span,
}
#[derive(Debug, Clone)]
pub struct ForTarget {
pub word: Word,
pub name: Option<Name>,
pub span: Span,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ForSyntax {
InDoDone {
in_span: Option<Span>,
do_span: Span,
done_span: Span,
},
InDirect {
in_span: Option<Span>,
},
InBrace {
in_span: Option<Span>,
left_brace_span: Span,
right_brace_span: Span,
},
ParenDoDone {
left_paren_span: Span,
right_paren_span: Span,
do_span: Span,
done_span: Span,
},
ParenDirect {
left_paren_span: Span,
right_paren_span: Span,
},
ParenBrace {
left_paren_span: Span,
right_paren_span: Span,
left_brace_span: Span,
right_brace_span: Span,
},
}
#[derive(Debug, Clone)]
pub struct RepeatCommand {
pub count: Word,
pub body: StmtSeq,
pub syntax: RepeatSyntax,
pub span: Span,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RepeatSyntax {
DoDone {
do_span: Span,
done_span: Span,
},
Direct,
Brace {
left_brace_span: Span,
right_brace_span: Span,
},
}
#[derive(Debug, Clone)]
pub struct ForeachCommand {
pub variable: Name,
pub variable_span: Span,
pub words: Vec<Word>,
pub body: StmtSeq,
pub syntax: ForeachSyntax,
pub span: Span,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ForeachSyntax {
ParenBrace {
left_paren_span: Span,
right_paren_span: Span,
left_brace_span: Span,
right_brace_span: Span,
},
InDoDone {
in_span: Span,
do_span: Span,
done_span: Span,
},
}
#[derive(Debug, Clone)]
pub struct SelectCommand {
pub variable: Name,
pub variable_span: Span,
pub words: Vec<Word>,
pub body: StmtSeq,
pub span: Span,
}
#[derive(Debug, Clone)]
pub struct ArithmeticCommand {
pub span: Span,
pub left_paren_span: Span,
pub expr_span: Option<Span>,
pub expr_ast: Option<ArithmeticExprNode>,
pub right_paren_span: Span,
}
#[derive(Debug, Clone)]
pub struct ArithmeticForCommand {
pub left_paren_span: Span,
pub init_span: Option<Span>,
pub init_ast: Option<ArithmeticExprNode>,
pub first_semicolon_span: Span,
pub condition_span: Option<Span>,
pub condition_ast: Option<ArithmeticExprNode>,
pub second_semicolon_span: Span,
pub step_span: Option<Span>,
pub step_ast: Option<ArithmeticExprNode>,
pub right_paren_span: Span,
pub body: StmtSeq,
pub span: Span,
}
#[derive(Debug, Clone)]
pub struct ArithmeticExprNode {
pub kind: ArithmeticExpr,
pub span: Span,
}
impl ArithmeticExprNode {
pub fn new(kind: ArithmeticExpr, span: Span) -> Self {
Self { kind, span }
}
}
#[derive(Debug, Clone)]
pub enum ArithmeticExpr {
Number(SourceText),
Variable(Name),
Indexed {
name: Name,
index: Box<ArithmeticExprNode>,
},
ShellWord(Word),
Parenthesized {
expression: Box<ArithmeticExprNode>,
},
Unary {
op: ArithmeticUnaryOp,
expr: Box<ArithmeticExprNode>,
},
Postfix {
expr: Box<ArithmeticExprNode>,
op: ArithmeticPostfixOp,
},
Binary {
left: Box<ArithmeticExprNode>,
op: ArithmeticBinaryOp,
right: Box<ArithmeticExprNode>,
},
Conditional {
condition: Box<ArithmeticExprNode>,
then_expr: Box<ArithmeticExprNode>,
else_expr: Box<ArithmeticExprNode>,
},
Assignment {
target: ArithmeticLvalue,
op: ArithmeticAssignOp,
value: Box<ArithmeticExprNode>,
},
}
#[derive(Debug, Clone)]
pub enum ArithmeticLvalue {
Variable(Name),
Indexed {
name: Name,
index: Box<ArithmeticExprNode>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArithmeticUnaryOp {
PreIncrement,
PreDecrement,
Plus,
Minus,
LogicalNot,
BitwiseNot,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArithmeticPostfixOp {
Increment,
Decrement,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArithmeticBinaryOp {
Comma,
Power,
Multiply,
Divide,
Modulo,
Add,
Subtract,
ShiftLeft,
ShiftRight,
LessThan,
LessThanOrEqual,
GreaterThan,
GreaterThanOrEqual,
Equal,
NotEqual,
BitwiseAnd,
BitwiseXor,
BitwiseOr,
LogicalAnd,
LogicalOr,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArithmeticAssignOp {
Assign,
AddAssign,
SubAssign,
MulAssign,
DivAssign,
ModAssign,
ShiftLeftAssign,
ShiftRightAssign,
AndAssign,
XorAssign,
OrAssign,
}
#[derive(Debug, Clone)]
pub struct WhileCommand {
pub condition: StmtSeq,
pub body: StmtSeq,
pub span: Span,
}
#[derive(Debug, Clone)]
pub struct UntilCommand {
pub condition: StmtSeq,
pub body: StmtSeq,
pub span: Span,
}
#[derive(Debug, Clone)]
pub struct CaseCommand {
pub word: Word,
pub cases: Vec<CaseItem>,
pub span: Span,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CaseTerminator {
Break,
FallThrough,
Continue,
ContinueMatching,
}
#[derive(Debug, Clone)]
pub struct CaseItem {
pub patterns: Vec<Pattern>,
pub body: StmtSeq,
pub terminator: CaseTerminator,
pub terminator_span: Option<Span>,
}
#[derive(Debug, Clone)]
pub struct FunctionHeaderEntry {
pub word: Word,
pub static_name: Option<Name>,
}
impl FunctionHeaderEntry {
pub fn static_name_span(&self) -> Option<Span> {
self.static_name.as_ref().map(|_| self.word.span)
}
}
#[derive(Debug, Clone, Default)]
pub struct FunctionHeader {
pub function_keyword_span: Option<Span>,
pub entries: Vec<FunctionHeaderEntry>,
pub trailing_parens_span: Option<Span>,
}
impl FunctionHeader {
pub fn uses_function_keyword(&self) -> bool {
self.function_keyword_span.is_some()
}
pub fn has_trailing_parens(&self) -> bool {
self.trailing_parens_span.is_some()
}
pub fn has_name_parens(&self) -> bool {
self.has_trailing_parens()
}
pub fn static_names(&self) -> impl Iterator<Item = &Name> + '_ {
self.entries
.iter()
.filter_map(|entry| entry.static_name.as_ref())
}
pub fn static_name_entries(&self) -> impl Iterator<Item = (&Name, Span)> + '_ {
self.entries.iter().filter_map(|entry| {
entry
.static_name
.as_ref()
.map(|name| (name, entry.word.span))
})
}
pub fn span(&self) -> Span {
let mut span = self.function_keyword_span.unwrap_or_default();
for entry in &self.entries {
span = merge_non_empty_span(span, entry.word.span);
}
if let Some(parens_span) = self.trailing_parens_span {
span = merge_non_empty_span(span, parens_span);
}
span
}
}
#[derive(Debug, Clone)]
pub struct FunctionDef {
pub header: FunctionHeader,
pub body: Box<Stmt>,
pub span: Span,
}
impl FunctionDef {
pub fn uses_function_keyword(&self) -> bool {
self.header.uses_function_keyword()
}
pub fn has_trailing_parens(&self) -> bool {
self.header.has_trailing_parens()
}
pub fn has_name_parens(&self) -> bool {
self.has_trailing_parens()
}
pub fn static_names(&self) -> impl Iterator<Item = &Name> + '_ {
self.header.static_names()
}
pub fn static_name_entries(&self) -> impl Iterator<Item = (&Name, Span)> + '_ {
self.header.static_name_entries()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AnonymousFunctionSurface {
FunctionKeyword { function_keyword_span: Span },
Parens { parens_span: Span },
}
impl AnonymousFunctionSurface {
pub fn uses_function_keyword(&self) -> bool {
matches!(self, Self::FunctionKeyword { .. })
}
pub fn parens_span(self) -> Option<Span> {
match self {
Self::FunctionKeyword { .. } => None,
Self::Parens { parens_span } => Some(parens_span),
}
}
}
#[derive(Debug, Clone)]
pub struct AnonymousFunctionCommand {
pub surface: AnonymousFunctionSurface,
pub body: Box<Stmt>,
pub args: Vec<Word>,
pub span: Span,
}
impl AnonymousFunctionCommand {
pub fn uses_function_keyword(&self) -> bool {
self.surface.uses_function_keyword()
}
}
fn merge_non_empty_span(current: Span, next: Span) -> Span {
match (current == Span::new(), next == Span::new()) {
(true, _) => next,
(_, true) => current,
(false, false) => current.merge(next),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommandSubstitutionSyntax {
DollarParen,
Backtick,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArithmeticExpansionSyntax {
DollarParenParen,
LegacyBracket,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PrefixMatchKind {
At,
Star,
}
impl PrefixMatchKind {
pub const fn as_char(self) -> char {
match self {
Self::At => '@',
Self::Star => '*',
}
}
}
#[derive(Debug, Clone)]
pub struct ParameterExpansion {
pub syntax: ParameterExpansionSyntax,
pub span: Span,
pub raw_body: SourceText,
}
impl ParameterExpansion {
pub fn is_zsh(&self) -> bool {
matches!(self.syntax, ParameterExpansionSyntax::Zsh(_))
}
pub fn bourne(&self) -> Option<&BourneParameterExpansion> {
match &self.syntax {
ParameterExpansionSyntax::Bourne(syntax) => Some(syntax),
ParameterExpansionSyntax::Zsh(_) => None,
}
}
pub fn zsh(&self) -> Option<&ZshParameterExpansion> {
match &self.syntax {
ParameterExpansionSyntax::Bourne(_) => None,
ParameterExpansionSyntax::Zsh(syntax) => Some(syntax),
}
}
}
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum ParameterExpansionSyntax {
Bourne(BourneParameterExpansion),
Zsh(ZshParameterExpansion),
}
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum BourneParameterExpansion {
Access {
reference: VarRef,
},
Length {
reference: VarRef,
},
Indices {
reference: VarRef,
},
Indirect {
reference: VarRef,
operator: Option<ParameterOp>,
operand: Option<SourceText>,
operand_word_ast: Option<Word>,
colon_variant: bool,
},
PrefixMatch {
prefix: Name,
kind: PrefixMatchKind,
},
Slice {
reference: VarRef,
offset: SourceText,
offset_ast: Option<ArithmeticExprNode>,
offset_word_ast: Word,
length: Option<SourceText>,
length_ast: Option<ArithmeticExprNode>,
length_word_ast: Option<Word>,
},
Operation {
reference: VarRef,
operator: ParameterOp,
operand: Option<SourceText>,
operand_word_ast: Option<Word>,
colon_variant: bool,
},
Transformation {
reference: VarRef,
operator: char,
},
}
impl BourneParameterExpansion {
pub fn operand_word_ast(&self) -> Option<&Word> {
match self {
Self::Indirect {
operand_word_ast, ..
}
| Self::Operation {
operand_word_ast, ..
} => operand_word_ast.as_ref(),
Self::Access { .. }
| Self::Length { .. }
| Self::Indices { .. }
| Self::PrefixMatch { .. }
| Self::Slice { .. }
| Self::Transformation { .. } => None,
}
}
pub fn offset_word_ast(&self) -> Option<&Word> {
match self {
Self::Slice {
offset_word_ast, ..
} => Some(offset_word_ast),
Self::Access { .. }
| Self::Length { .. }
| Self::Indices { .. }
| Self::Indirect { .. }
| Self::PrefixMatch { .. }
| Self::Operation { .. }
| Self::Transformation { .. } => None,
}
}
pub fn length_word_ast(&self) -> Option<&Word> {
match self {
Self::Slice {
length_word_ast, ..
} => length_word_ast.as_ref(),
Self::Access { .. }
| Self::Length { .. }
| Self::Indices { .. }
| Self::Indirect { .. }
| Self::PrefixMatch { .. }
| Self::Operation { .. }
| Self::Transformation { .. } => None,
}
}
}
#[derive(Debug, Clone)]
pub struct ZshParameterExpansion {
pub target: ZshExpansionTarget,
pub modifiers: Vec<ZshModifier>,
pub length_prefix: Option<Span>,
pub operation: Option<ZshExpansionOperation>,
}
#[derive(Debug, Clone)]
#[allow(clippy::large_enum_variant)]
pub enum ZshExpansionTarget {
Reference(VarRef),
Nested(Box<ParameterExpansion>),
Word(Word),
Empty,
}
#[derive(Debug, Clone)]
pub struct ZshModifier {
pub name: char,
pub argument: Option<SourceText>,
pub argument_word_ast: Option<Word>,
pub argument_delimiter: Option<char>,
pub span: Span,
}
impl ZshModifier {
pub fn argument_word_ast(&self) -> Option<&Word> {
self.argument_word_ast.as_ref()
}
}
#[derive(Debug, Clone)]
pub enum ZshExpansionOperation {
PatternOperation {
kind: ZshPatternOp,
operand: SourceText,
operand_word_ast: Word,
},
Defaulting {
kind: ZshDefaultingOp,
operand: SourceText,
operand_word_ast: Word,
colon_variant: bool,
},
TrimOperation {
kind: ZshTrimOp,
operand: SourceText,
operand_word_ast: Word,
},
ReplacementOperation {
kind: ZshReplacementOp,
pattern: SourceText,
pattern_word_ast: Word,
replacement: Option<SourceText>,
replacement_word_ast: Option<Word>,
},
Slice {
offset: SourceText,
offset_word_ast: Word,
length: Option<SourceText>,
length_word_ast: Option<Word>,
},
Unknown {
text: SourceText,
word_ast: Word,
},
}
impl ZshExpansionOperation {
pub fn operand_word_ast(&self) -> Option<&Word> {
match self {
Self::PatternOperation {
operand_word_ast, ..
}
| Self::Defaulting {
operand_word_ast, ..
}
| Self::TrimOperation {
operand_word_ast, ..
} => Some(operand_word_ast),
Self::ReplacementOperation { .. } | Self::Slice { .. } => None,
Self::Unknown { word_ast, .. } => Some(word_ast),
}
}
pub fn pattern_word_ast(&self) -> Option<&Word> {
match self {
Self::ReplacementOperation {
pattern_word_ast, ..
} => Some(pattern_word_ast),
Self::PatternOperation { .. }
| Self::Defaulting { .. }
| Self::TrimOperation { .. }
| Self::Slice { .. }
| Self::Unknown { .. } => None,
}
}
pub fn replacement_word_ast(&self) -> Option<&Word> {
match self {
Self::ReplacementOperation {
replacement_word_ast,
..
} => replacement_word_ast.as_ref(),
Self::PatternOperation { .. }
| Self::Defaulting { .. }
| Self::TrimOperation { .. }
| Self::Slice { .. }
| Self::Unknown { .. } => None,
}
}
pub fn offset_word_ast(&self) -> Option<&Word> {
match self {
Self::Slice {
offset_word_ast, ..
} => Some(offset_word_ast),
Self::PatternOperation { .. }
| Self::Defaulting { .. }
| Self::TrimOperation { .. }
| Self::ReplacementOperation { .. }
| Self::Unknown { .. } => None,
}
}
pub fn length_word_ast(&self) -> Option<&Word> {
match self {
Self::Slice {
length_word_ast, ..
} => length_word_ast.as_ref(),
Self::PatternOperation { .. }
| Self::Defaulting { .. }
| Self::TrimOperation { .. }
| Self::ReplacementOperation { .. }
| Self::Unknown { .. } => None,
}
}
pub fn source_text(&self) -> Option<&SourceText> {
match self {
Self::PatternOperation { operand, .. }
| Self::Defaulting { operand, .. }
| Self::TrimOperation { operand, .. } => Some(operand),
Self::ReplacementOperation { .. } | Self::Slice { .. } => None,
Self::Unknown { text, .. } => Some(text),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ZshPatternOp {
Filter,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ZshDefaultingOp {
UseDefault,
AssignDefault,
UseReplacement,
Error,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ZshTrimOp {
RemovePrefixShort,
RemovePrefixLong,
RemoveSuffixShort,
RemoveSuffixLong,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ZshReplacementOp {
ReplaceFirst,
ReplaceAll,
ReplacePrefix,
ReplaceSuffix,
}
#[derive(Debug, Clone)]
pub struct ZshQualifiedGlob {
pub span: Span,
pub segments: Vec<ZshGlobSegment>,
pub qualifiers: Option<ZshGlobQualifierGroup>,
}
#[derive(Debug, Clone)]
pub enum ZshGlobSegment {
Pattern(Pattern),
InlineControl(ZshInlineGlobControl),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ZshInlineGlobControl {
CaseInsensitive { span: Span },
Backreferences { span: Span },
StartAnchor { span: Span },
EndAnchor { span: Span },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ZshGlobQualifierKind {
Classic,
HashQ,
}
#[derive(Debug, Clone)]
pub struct ZshGlobQualifierGroup {
pub span: Span,
pub kind: ZshGlobQualifierKind,
pub fragments: Vec<ZshGlobQualifier>,
}
#[derive(Debug, Clone)]
pub enum ZshGlobQualifier {
Negation {
span: Span,
},
Flag {
name: char,
span: Span,
},
LetterSequence {
text: SourceText,
span: Span,
},
NumericArgument {
span: Span,
start: SourceText,
end: Option<SourceText>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BraceExpansionKind {
CommaList,
Sequence,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BraceQuoteContext {
Unquoted,
DoubleQuoted,
SingleQuoted,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BraceSyntaxKind {
Expansion(BraceExpansionKind),
Literal,
TemplatePlaceholder,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BraceSyntax {
pub kind: BraceSyntaxKind,
pub span: Span,
pub quote_context: BraceQuoteContext,
}
impl BraceSyntax {
pub const fn expansion_kind(self) -> Option<BraceExpansionKind> {
match self.kind {
BraceSyntaxKind::Expansion(kind) => Some(kind),
BraceSyntaxKind::Literal | BraceSyntaxKind::TemplatePlaceholder => None,
}
}
pub const fn is_recognized_expansion(self) -> bool {
matches!(self.kind, BraceSyntaxKind::Expansion(_))
}
pub const fn expands(self) -> bool {
self.is_recognized_expansion() && matches!(self.quote_context, BraceQuoteContext::Unquoted)
}
pub const fn treated_literally(self) -> bool {
!self.expands()
}
}
#[derive(Debug, Clone)]
pub struct WordPartNode {
pub kind: WordPart,
pub span: Span,
}
impl WordPartNode {
pub fn new(kind: WordPart, span: Span) -> Self {
Self { kind, span }
}
}
#[derive(Debug, Clone)]
pub struct Word {
pub parts: Vec<WordPartNode>,
pub span: Span,
pub brace_syntax: Vec<BraceSyntax>,
}
impl Word {
pub fn literal(s: impl Into<String>) -> Self {
Self::literal_with_span(s, Span::new())
}
pub fn literal_with_span(s: impl Into<String>, span: Span) -> Self {
Self {
parts: vec![WordPartNode::new(
WordPart::Literal(LiteralText::owned(s.into())),
span,
)],
span,
brace_syntax: Vec::new(),
}
}
pub fn quoted_literal(s: impl Into<String>) -> Self {
Self::quoted_literal_with_span(s, Span::new())
}
pub fn quoted_literal_with_span(s: impl Into<String>, span: Span) -> Self {
Self {
parts: vec![WordPartNode::new(
WordPart::SingleQuoted {
value: SourceText::cooked(span, s.into()),
dollar: false,
},
span,
)],
span,
brace_syntax: Vec::new(),
}
}
pub fn source_literal_with_spans(span: Span, part_span: Span) -> Self {
Self {
parts: vec![WordPartNode::new(
WordPart::Literal(LiteralText::source()),
part_span,
)],
span,
brace_syntax: Vec::new(),
}
}
pub fn quoted_source_literal_with_spans(span: Span, part_span: Span) -> Self {
Self {
parts: vec![WordPartNode::new(
WordPart::SingleQuoted {
value: SourceText::source(part_span),
dollar: false,
},
span,
)],
span,
brace_syntax: Vec::new(),
}
}
pub fn with_span(mut self, span: Span) -> Self {
let previous_span = self.span;
self.span = span;
if let [part] = self.parts.as_mut_slice()
&& part.span == previous_span
{
part.span = span;
}
self
}
pub fn part_span(&self, index: usize) -> Option<Span> {
self.parts.get(index).map(|part| part.span)
}
pub fn part(&self, index: usize) -> Option<&WordPart> {
self.parts.get(index).map(|part| &part.kind)
}
pub fn parts_with_spans(&self) -> impl Iterator<Item = (&WordPart, Span)> + '_ {
self.parts.iter().map(|part| (&part.kind, part.span))
}
pub fn brace_syntax(&self) -> &[BraceSyntax] {
&self.brace_syntax
}
pub fn has_active_brace_expansion(&self) -> bool {
self.brace_syntax.iter().copied().any(BraceSyntax::expands)
}
pub fn is_fully_quoted(&self) -> bool {
matches!(self.parts.as_slice(), [part] if part.kind.is_quoted())
}
pub fn quoted_content_span_in_source(&self, source: &str) -> Option<Span> {
if !self.is_fully_quoted() {
return None;
}
let raw = self.span.slice(source);
let quote = raw.chars().next()?;
if !matches!(quote, '"' | '\'') {
return None;
}
let body = raw.strip_prefix(quote)?.strip_suffix(quote)?;
if body.is_empty() {
return None;
}
let start = self.span.start.advanced_by(&raw[..quote.len_utf8()]);
let end = start.advanced_by(body);
Some(Span::from_positions(start, end))
}
pub fn is_fully_double_quoted(&self) -> bool {
matches!(
self.parts.as_slice(),
[WordPartNode {
kind: WordPart::DoubleQuoted { .. },
..
}]
)
}
pub fn has_quoted_parts(&self) -> bool {
self.parts.iter().any(|part| part.kind.is_quoted())
}
pub fn try_static_text<'a>(&'a self, source: &'a str) -> Option<Cow<'a, str>> {
static_word_text(self, source)
}
pub fn render(&self, source: &str) -> String {
let mut rendered = String::new();
self.render_to_buf(source, &mut rendered);
rendered
}
pub fn render_syntax(&self, source: &str) -> String {
let mut rendered = String::new();
self.render_syntax_to_buf(source, &mut rendered);
rendered
}
pub fn render_to_buf(&self, source: &str, rendered: &mut String) {
assert_string_write(self.fmt_with_source_mode(rendered, Some(source), RenderMode::Decoded));
}
pub fn render_syntax_to_buf(&self, source: &str, rendered: &mut String) {
assert_string_write(self.fmt_with_source_mode(rendered, Some(source), RenderMode::Syntax));
}
fn fmt_with_source_mode(
&self,
f: &mut impl fmt::Write,
source: Option<&str>,
mode: RenderMode,
) -> fmt::Result {
if matches!(mode, RenderMode::Syntax)
&& let Some(source) = source
&& word_prefers_whole_source_slice_in_syntax(self)
&& let Some(slice) = syntax_source_slice(self.span, source)
{
if slice.contains('\n') {
f.write_str(slice)?;
} else {
f.write_str(trim_unescaped_trailing_whitespace(slice))?;
}
return Ok(());
}
for (part, span) in self.parts_with_spans() {
fmt_word_part_with_source_mode(f, part, span, source, mode)?;
}
Ok(())
}
}
impl fmt::Display for Word {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.fmt_with_source_mode(f, None, RenderMode::Decoded)
}
}
pub fn static_word_text<'a>(word: &'a Word, source: &'a str) -> Option<Cow<'a, str>> {
try_static_word_parts_text(&word.parts, source)
}
pub fn static_command_name_text<'a>(word: &'a Word, source: &'a str) -> Option<Cow<'a, str>> {
try_static_command_name_parts_text(&word.parts, source, StaticCommandNameContext::Unquoted)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StaticCommandWrapperTarget {
NotWrapper,
Wrapper {
target_index: Option<usize>,
},
}
pub fn static_command_wrapper_target_index<'a>(
word_count: usize,
current_index: usize,
current_name: &str,
mut static_text_at: impl FnMut(usize) -> Option<Cow<'a, str>>,
) -> StaticCommandWrapperTarget {
let target_index = match current_name {
"noglob" => next_word_index(word_count, current_index),
"command" => command_wrapper_target_index(word_count, current_index, &mut static_text_at),
"builtin" => builtin_wrapper_target_index(word_count, current_index, &mut static_text_at),
"exec" => exec_wrapper_target_index(word_count, current_index, &mut static_text_at),
_ => return StaticCommandWrapperTarget::NotWrapper,
};
StaticCommandWrapperTarget::Wrapper { target_index }
}
pub fn try_static_word_parts_text<'a>(
parts: &'a [WordPartNode],
source: &'a str,
) -> Option<Cow<'a, str>> {
if let [part] = parts {
return try_static_word_part_text(part, source);
}
let mut result = String::new();
collect_static_word_parts_text(parts, source, &mut result).then_some(Cow::Owned(result))
}
fn try_static_word_part_text<'a>(part: &'a WordPartNode, source: &'a str) -> Option<Cow<'a, str>> {
match &part.kind {
WordPart::Literal(text) => Some(Cow::Borrowed(text.as_str(source, part.span))),
WordPart::SingleQuoted { value, .. } => Some(Cow::Borrowed(value.slice(source))),
WordPart::DoubleQuoted { parts, .. } => try_static_word_parts_text(parts, source),
_ => None,
}
}
fn collect_static_word_parts_text(parts: &[WordPartNode], source: &str, out: &mut String) -> bool {
for part in parts {
match &part.kind {
WordPart::Literal(text) => out.push_str(text.as_str(source, part.span)),
WordPart::SingleQuoted { value, .. } => out.push_str(value.slice(source)),
WordPart::DoubleQuoted { parts, .. } => {
if !collect_static_word_parts_text(parts, source, out) {
return false;
}
}
_ => return false,
}
}
true
}
#[derive(Clone, Copy)]
enum StaticCommandNameContext {
Unquoted,
DoubleQuoted,
}
fn try_static_command_name_parts_text<'a>(
parts: &'a [WordPartNode],
source: &'a str,
context: StaticCommandNameContext,
) -> Option<Cow<'a, str>> {
if let [part] = parts {
return try_static_command_name_part_text(part, source, context);
}
let mut result = String::new();
collect_static_command_name_parts_text(parts, source, context, &mut result)
.then_some(Cow::Owned(result))
}
fn try_static_command_name_part_text<'a>(
part: &'a WordPartNode,
source: &'a str,
context: StaticCommandNameContext,
) -> Option<Cow<'a, str>> {
match &part.kind {
WordPart::Literal(text) => Some(decode_static_command_literal(
text.as_str(source, part.span),
context,
)),
WordPart::SingleQuoted { value, .. } => Some(Cow::Borrowed(value.slice(source))),
WordPart::DoubleQuoted { parts, .. } => try_static_command_name_parts_text(
parts,
source,
StaticCommandNameContext::DoubleQuoted,
),
_ => None,
}
}
fn collect_static_command_name_parts_text(
parts: &[WordPartNode],
source: &str,
context: StaticCommandNameContext,
out: &mut String,
) -> bool {
for part in parts {
match &part.kind {
WordPart::Literal(text) => {
append_static_command_literal(text.as_str(source, part.span), context, out);
}
WordPart::SingleQuoted { value, .. } => out.push_str(value.slice(source)),
WordPart::DoubleQuoted { parts, .. } => {
if !collect_static_command_name_parts_text(
parts,
source,
StaticCommandNameContext::DoubleQuoted,
out,
) {
return false;
}
}
_ => return false,
}
}
true
}
fn decode_static_command_literal(text: &str, context: StaticCommandNameContext) -> Cow<'_, str> {
let Some(first_escape) = first_static_command_literal_escape(text.as_bytes()) else {
return Cow::Borrowed(text);
};
let mut result = String::with_capacity(text.len());
append_decoded_static_command_literal(text, first_escape, context, &mut result);
Cow::Owned(result)
}
fn append_static_command_literal(text: &str, context: StaticCommandNameContext, out: &mut String) {
let Some(first_escape) = first_static_command_literal_escape(text.as_bytes()) else {
out.push_str(text);
return;
};
append_decoded_static_command_literal(text, first_escape, context, out);
}
fn append_decoded_static_command_literal(
text: &str,
first_escape: usize,
context: StaticCommandNameContext,
out: &mut String,
) {
let bytes = text.as_bytes();
let mut copy_start = 0usize;
let mut index = first_escape;
while index < bytes.len() {
if bytes[index] != b'\\' {
index += 1;
continue;
}
out.push_str(&text[copy_start..index]);
index += 1;
let Some(&next) = bytes.get(index) else {
out.push('\\');
return;
};
match context {
StaticCommandNameContext::Unquoted => {
copy_start = if next == b'\n' { index + 1 } else { index };
}
StaticCommandNameContext::DoubleQuoted => match next {
b'$' | b'`' | b'"' | b'\\' => copy_start = index,
b'\n' => copy_start = index + 1,
_ => {
out.push('\\');
copy_start = index;
}
},
}
index += 1;
}
out.push_str(&text[copy_start..]);
}
fn first_static_command_literal_escape(bytes: &[u8]) -> Option<usize> {
bytes.iter().position(|&byte| byte == b'\\')
}
fn next_word_index(word_count: usize, current_index: usize) -> Option<usize> {
let index = current_index + 1;
(index < word_count).then_some(index)
}
fn command_wrapper_target_index<'a>(
word_count: usize,
current_index: usize,
static_text_at: &mut impl FnMut(usize) -> Option<Cow<'a, str>>,
) -> Option<usize> {
let mut index = current_index + 1;
while index < word_count {
let Some(arg) = static_text_at(index) else {
return Some(index);
};
if arg == "--" {
return next_word_index(word_count, index);
}
if let Some(options) = arg.strip_prefix('-') {
if options.is_empty() {
return Some(index);
}
let mut lookup_mode = false;
for option in options.chars() {
match option {
'p' => {}
'v' | 'V' => lookup_mode = true,
_ => return None,
}
}
if lookup_mode {
return None;
}
index += 1;
continue;
}
return Some(index);
}
None
}
fn builtin_wrapper_target_index<'a>(
word_count: usize,
current_index: usize,
static_text_at: &mut impl FnMut(usize) -> Option<Cow<'a, str>>,
) -> Option<usize> {
let index = current_index + 1;
if index >= word_count {
return None;
}
let Some(arg) = static_text_at(index) else {
return Some(index);
};
if arg == "--" {
return next_word_index(word_count, index);
}
if arg.starts_with('-') && arg != "-" {
return None;
}
Some(index)
}
fn exec_wrapper_target_index<'a>(
word_count: usize,
current_index: usize,
static_text_at: &mut impl FnMut(usize) -> Option<Cow<'a, str>>,
) -> Option<usize> {
let mut index = current_index + 1;
while index < word_count {
let Some(arg) = static_text_at(index) else {
return Some(index);
};
if arg == "--" {
return next_word_index(word_count, index);
}
if let Some(options) = arg.strip_prefix('-') {
if options.is_empty() {
return Some(index);
}
let mut consumed_words = 1;
for (offset, option) in options.char_indices() {
match option {
'c' | 'l' => {}
'a' => {
let has_attached_name = offset + option.len_utf8() < options.len();
if !has_attached_name {
if index + consumed_words >= word_count {
return None;
}
consumed_words += 1;
}
break;
}
_ => return None,
}
}
index += consumed_words;
continue;
}
return Some(index);
}
None
}
pub fn is_shell_variable_name(name: &str) -> bool {
let mut chars = name.chars();
match chars.next() {
Some(first) if first == '_' || first.is_ascii_alphabetic() => {
chars.all(|ch| ch == '_' || ch.is_ascii_alphanumeric())
}
_ => false,
}
}
pub fn word_is_standalone_variable_like(word: &Word) -> bool {
match word.parts.as_slice() {
[part] => matches!(
part.kind,
WordPart::Variable(_)
| WordPart::Parameter(_)
| WordPart::ParameterExpansion { .. }
| WordPart::Length(_)
| WordPart::ArrayAccess(_)
| WordPart::ArrayLength(_)
| WordPart::ArrayIndices(_)
| WordPart::Substring { .. }
| WordPart::ArraySlice { .. }
| WordPart::IndirectExpansion { .. }
| WordPart::PrefixMatch { .. }
| WordPart::Transformation { .. }
),
_ => false,
}
}
pub fn word_is_standalone_status_capture(word: &Word) -> bool {
matches!(word.parts.as_slice(), [part] if is_standalone_status_capture_part(&part.kind))
}
fn is_standalone_status_capture_part(part: &WordPart) -> bool {
match part {
WordPart::Variable(name) => name.as_str() == "?",
WordPart::DoubleQuoted { parts, .. } => {
matches!(parts.as_slice(), [part] if is_standalone_status_capture_part(&part.kind))
}
WordPart::Parameter(parameter) => matches!(
parameter.bourne(),
Some(BourneParameterExpansion::Access { reference })
if reference.name.as_str() == "?" && reference.subscript.is_none()
),
_ => false,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HeredocBodyMode {
Literal,
Expanding,
}
impl HeredocBodyMode {
pub const fn expands(self) -> bool {
matches!(self, Self::Expanding)
}
}
#[derive(Debug, Clone)]
pub struct HeredocBodyPartNode {
pub kind: HeredocBodyPart,
pub span: Span,
}
impl HeredocBodyPartNode {
pub fn new(kind: HeredocBodyPart, span: Span) -> Self {
Self { kind, span }
}
}
#[derive(Debug, Clone)]
pub enum HeredocBodyPart {
Literal(LiteralText),
Variable(Name),
CommandSubstitution {
body: StmtSeq,
syntax: CommandSubstitutionSyntax,
},
ArithmeticExpansion {
expression: SourceText,
expression_ast: Option<ArithmeticExprNode>,
expression_word_ast: Word,
syntax: ArithmeticExpansionSyntax,
},
Parameter(Box<ParameterExpansion>),
}
#[derive(Debug, Clone)]
pub struct HeredocBody {
pub mode: HeredocBodyMode,
pub source_backed: bool,
pub parts: Vec<HeredocBodyPartNode>,
pub span: Span,
}
impl HeredocBody {
pub fn literal_with_span(s: impl Into<String>, span: Span) -> Self {
Self {
mode: HeredocBodyMode::Literal,
source_backed: true,
parts: vec![HeredocBodyPartNode::new(
HeredocBodyPart::Literal(LiteralText::owned(s.into())),
span,
)],
span,
}
}
pub fn source_literal_with_spans(span: Span, part_span: Span) -> Self {
Self {
mode: HeredocBodyMode::Literal,
source_backed: true,
parts: vec![HeredocBodyPartNode::new(
HeredocBodyPart::Literal(LiteralText::source()),
part_span,
)],
span,
}
}
pub fn with_mode(mut self, mode: HeredocBodyMode) -> Self {
self.mode = mode;
self
}
pub fn with_source_backed(mut self, source_backed: bool) -> Self {
self.source_backed = source_backed;
self
}
pub fn part_span(&self, index: usize) -> Option<Span> {
self.parts.get(index).map(|part| part.span)
}
pub fn part(&self, index: usize) -> Option<&HeredocBodyPart> {
self.parts.get(index).map(|part| &part.kind)
}
pub fn parts_with_spans(&self) -> impl Iterator<Item = (&HeredocBodyPart, Span)> + '_ {
self.parts.iter().map(|part| (&part.kind, part.span))
}
pub fn render(&self, source: &str) -> String {
let mut rendered = String::new();
self.render_to_buf(source, &mut rendered);
rendered
}
pub fn render_syntax(&self, source: &str) -> String {
let mut rendered = String::new();
self.render_syntax_to_buf(source, &mut rendered);
rendered
}
pub fn render_to_buf(&self, source: &str, rendered: &mut String) {
assert_string_write(self.fmt_with_source(rendered, Some(source)));
}
pub fn render_syntax_to_buf(&self, source: &str, rendered: &mut String) {
assert_string_write(self.fmt_with_source(rendered, Some(source)));
}
fn fmt_with_source(&self, f: &mut impl fmt::Write, source: Option<&str>) -> fmt::Result {
let source = source.filter(|_| self.source_backed);
if let Some(source) = source
&& heredoc_body_prefers_whole_source_slice(self)
&& let Some(slice) = syntax_source_slice(self.span, source)
{
f.write_str(slice)?;
return Ok(());
}
for (part, span) in self.parts_with_spans() {
fmt_heredoc_body_part_with_source(f, part, span, source)?;
}
Ok(())
}
}
impl fmt::Display for HeredocBody {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.fmt_with_source(f, None)
}
}
#[derive(Debug, Clone)]
pub struct Pattern {
pub parts: Vec<PatternPartNode>,
pub span: Span,
}
impl Pattern {
pub fn part_span(&self, index: usize) -> Option<Span> {
self.parts.get(index).map(|part| part.span)
}
pub fn is_source_backed(&self) -> bool {
self.parts
.iter()
.all(|part| pattern_part_is_source_backed(&part.kind))
}
pub fn parts_with_spans(&self) -> impl Iterator<Item = (&PatternPart, Span)> + '_ {
self.parts.iter().map(|part| (&part.kind, part.span))
}
pub fn render(&self, source: &str) -> String {
let mut rendered = String::new();
self.render_to_buf(source, &mut rendered);
rendered
}
pub fn render_syntax(&self, source: &str) -> String {
let mut rendered = String::new();
self.render_syntax_to_buf(source, &mut rendered);
rendered
}
pub fn render_to_buf(&self, source: &str, rendered: &mut String) {
assert_string_write(self.fmt_with_source_mode(rendered, Some(source), RenderMode::Decoded));
}
pub fn render_syntax_to_buf(&self, source: &str, rendered: &mut String) {
assert_string_write(self.fmt_with_source_mode(rendered, Some(source), RenderMode::Syntax));
}
fn fmt_with_source_mode(
&self,
f: &mut impl fmt::Write,
source: Option<&str>,
mode: RenderMode,
) -> fmt::Result {
if matches!(mode, RenderMode::Syntax)
&& let Some(source) = source
&& pattern_prefers_whole_source_slice_in_syntax(self)
&& let Some(slice) = syntax_source_slice(self.span, source)
{
if slice.contains('\n') {
f.write_str(slice)?;
} else {
f.write_str(trim_unescaped_trailing_whitespace(slice))?;
}
return Ok(());
}
for (part, span) in self.parts_with_spans() {
fmt_pattern_part_with_source_mode(f, part, span, source, mode)?;
}
Ok(())
}
}
impl fmt::Display for Pattern {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.fmt_with_source_mode(f, None, RenderMode::Decoded)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PatternGroupKind {
ZeroOrOne,
ZeroOrMore,
OneOrMore,
ExactlyOne,
NoneOf,
}
impl PatternGroupKind {
pub fn prefix(self) -> char {
match self {
Self::ZeroOrOne => '?',
Self::ZeroOrMore => '*',
Self::OneOrMore => '+',
Self::ExactlyOne => '@',
Self::NoneOf => '!',
}
}
}
#[derive(Debug, Clone)]
pub struct PatternPartNode {
pub kind: PatternPart,
pub span: Span,
}
impl PatternPartNode {
pub fn new(kind: PatternPart, span: Span) -> Self {
Self { kind, span }
}
}
#[derive(Debug, Clone)]
pub enum PatternPart {
Literal(LiteralText),
AnyString,
AnyChar,
CharClass(SourceText),
Group {
kind: PatternGroupKind,
patterns: Vec<Pattern>,
},
Word(Word),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RenderMode {
Decoded,
Syntax,
}
fn syntax_source_slice(span: Span, source: &str) -> Option<&str> {
(span.start.offset < span.end.offset && span.end.offset <= source.len())
.then(|| span.slice(source))
}
fn word_prefers_whole_source_slice_in_syntax(word: &Word) -> bool {
matches!(
word.parts.as_slice(),
[part] if part.span == word.span && top_level_word_part_prefers_source_slice_in_syntax(&part.kind)
)
}
fn top_level_word_part_prefers_source_slice_in_syntax(part: &WordPart) -> bool {
match part {
WordPart::Literal(text) => text.is_source_backed(),
WordPart::SingleQuoted { value, .. } => value.is_source_backed(),
WordPart::DoubleQuoted { parts, .. } => parts.iter().all(|part| match &part.kind {
WordPart::Literal(_) => true,
other => part_prefers_source_slice_in_syntax(other) && part_is_source_backed(other),
}),
_ => part_prefers_source_slice_in_syntax(part) && part_is_source_backed(part),
}
}
fn pattern_prefers_whole_source_slice_in_syntax(pattern: &Pattern) -> bool {
!pattern.parts.is_empty()
&& pattern
.parts
.iter()
.all(|part| top_level_pattern_part_prefers_source_slice_in_syntax(&part.kind))
}
fn heredoc_body_prefers_whole_source_slice(body: &HeredocBody) -> bool {
!body.parts.is_empty()
&& body
.parts
.iter()
.all(|part| heredoc_body_part_is_source_backed(&part.kind))
}
fn top_level_pattern_part_prefers_source_slice_in_syntax(part: &PatternPart) -> bool {
match part {
PatternPart::Literal(_) | PatternPart::AnyString | PatternPart::AnyChar => true,
PatternPart::CharClass(text) => text.is_source_backed(),
PatternPart::Group { patterns, .. } => patterns
.iter()
.all(pattern_prefers_whole_source_slice_in_syntax),
PatternPart::Word(word) => word_prefers_whole_source_slice_in_syntax(word),
}
}
fn display_source_text<'a>(text: Option<&'a SourceText>, source: Option<&'a str>) -> &'a str {
match (text, source) {
(Some(text), Some(source)) => text.slice(source),
(
Some(SourceText {
cooked: Some(text), ..
}),
None,
) => text.as_ref(),
(Some(_), None) => "...",
(None, _) => "",
}
}
fn display_subscript_text<'a>(subscript: &'a Subscript, source: Option<&'a str>) -> Cow<'a, str> {
match (source, subscript.selector()) {
(Some(source), _) => Cow::Borrowed(subscript.syntax_text(source)),
(None, Some(selector)) => Cow::Owned(selector.as_char().to_string()),
(None, None) => Cow::Borrowed(display_source_text(
Some(subscript.syntax_source_text()),
source,
)),
}
}
fn fmt_var_ref_with_source(
f: &mut impl fmt::Write,
reference: &VarRef,
source: Option<&str>,
) -> fmt::Result {
write!(f, "{}", reference.name)?;
if let Some(subscript) = &reference.subscript {
write!(f, "[{}]", display_subscript_text(subscript, source))?;
}
Ok(())
}
#[derive(Debug, Clone)]
pub enum WordPart {
Literal(LiteralText),
ZshQualifiedGlob(ZshQualifiedGlob),
SingleQuoted { value: SourceText, dollar: bool },
DoubleQuoted {
parts: Vec<WordPartNode>,
dollar: bool,
},
Variable(Name),
CommandSubstitution {
body: StmtSeq,
syntax: CommandSubstitutionSyntax,
},
ArithmeticExpansion {
expression: SourceText,
expression_ast: Option<ArithmeticExprNode>,
expression_word_ast: Word,
syntax: ArithmeticExpansionSyntax,
},
Parameter(ParameterExpansion),
ParameterExpansion {
reference: VarRef,
operator: ParameterOp,
operand: Option<SourceText>,
operand_word_ast: Option<Word>,
colon_variant: bool,
},
Length(VarRef),
ArrayAccess(VarRef),
ArrayLength(VarRef),
ArrayIndices(VarRef),
Substring {
reference: VarRef,
offset: SourceText,
offset_ast: Option<ArithmeticExprNode>,
offset_word_ast: Word,
length: Option<SourceText>,
length_ast: Option<ArithmeticExprNode>,
length_word_ast: Option<Word>,
},
ArraySlice {
reference: VarRef,
offset: SourceText,
offset_ast: Option<ArithmeticExprNode>,
offset_word_ast: Word,
length: Option<SourceText>,
length_ast: Option<ArithmeticExprNode>,
length_word_ast: Option<Word>,
},
IndirectExpansion {
reference: VarRef,
operator: Option<ParameterOp>,
operand: Option<SourceText>,
operand_word_ast: Option<Word>,
colon_variant: bool,
},
PrefixMatch { prefix: Name, kind: PrefixMatchKind },
ProcessSubstitution {
body: StmtSeq,
is_input: bool,
},
Transformation { reference: VarRef, operator: char },
}
impl WordPart {
pub fn is_quoted(&self) -> bool {
matches!(self, Self::SingleQuoted { .. } | Self::DoubleQuoted { .. })
}
}
#[derive(Debug, Clone)]
pub struct ArrayExpr {
pub kind: ArrayKind,
pub elements: Vec<ArrayElem>,
pub span: Span,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ArrayKind {
Indexed,
Associative,
Contextual,
}
#[derive(Debug, Clone)]
pub struct ArrayValueWord {
pub word: Word,
pub has_top_level_unquoted_comma: bool,
}
impl ArrayValueWord {
pub fn new(word: Word, has_top_level_unquoted_comma: bool) -> Self {
Self {
word,
has_top_level_unquoted_comma,
}
}
pub fn has_top_level_unquoted_comma(&self) -> bool {
self.has_top_level_unquoted_comma
}
pub fn span(&self) -> Span {
self.word.span
}
}
impl From<Word> for ArrayValueWord {
fn from(word: Word) -> Self {
Self::new(word, false)
}
}
impl Deref for ArrayValueWord {
type Target = Word;
fn deref(&self) -> &Self::Target {
&self.word
}
}
impl DerefMut for ArrayValueWord {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.word
}
}
#[derive(Debug, Clone)]
pub enum ArrayElem {
Sequential(ArrayValueWord),
Keyed {
key: Subscript,
value: ArrayValueWord,
},
KeyedAppend {
key: Subscript,
value: ArrayValueWord,
},
}
impl ArrayElem {
pub fn span(&self) -> Span {
match self {
Self::Sequential(word) => word.span(),
Self::Keyed { key, value } | Self::KeyedAppend { key, value } => {
key.span().merge(value.span())
}
}
}
pub fn value(&self) -> &ArrayValueWord {
match self {
Self::Sequential(word) => word,
Self::Keyed { value, .. } | Self::KeyedAppend { value, .. } => value,
}
}
pub fn value_mut(&mut self) -> &mut ArrayValueWord {
match self {
Self::Sequential(word) => word,
Self::Keyed { value, .. } | Self::KeyedAppend { value, .. } => value,
}
}
}
fn fmt_literal_text(
f: &mut impl fmt::Write,
text: &LiteralText,
span: Span,
source: Option<&str>,
) -> fmt::Result {
match source {
Some(source) => f.write_str(text.as_str(source, span)),
None => match text {
LiteralText::Source => f.write_str("<source>"),
LiteralText::Owned(text) | LiteralText::CookedSource(text) => f.write_str(text),
},
}
}
fn fmt_double_quoted_literal_text(
f: &mut impl fmt::Write,
text: &LiteralText,
span: Span,
source: Option<&str>,
) -> fmt::Result {
let rendered = match source {
Some(source) => text.as_str(source, span),
None => match text {
LiteralText::Source => "<source>",
LiteralText::Owned(text) | LiteralText::CookedSource(text) => text,
},
};
for ch in rendered.chars() {
match ch {
'"' | '\\' | '$' | '`' => {
f.write_char('\\')?;
f.write_char(ch)?;
}
_ => f.write_char(ch)?,
}
}
Ok(())
}
fn fmt_pattern_part_with_source_mode(
f: &mut impl fmt::Write,
part: &PatternPart,
span: Span,
source: Option<&str>,
mode: RenderMode,
) -> fmt::Result {
match part {
PatternPart::Literal(text) => match (mode, source) {
(RenderMode::Syntax, Some(source))
if text.is_source_backed() && span.end.offset <= source.len() =>
{
f.write_str(text.syntax_str(source, span))?
}
_ => fmt_literal_text(f, text, span, source)?,
},
PatternPart::AnyString => f.write_str("*")?,
PatternPart::AnyChar => f.write_str("?")?,
PatternPart::CharClass(text) => match source {
Some(source) if span.end.offset <= source.len() => f.write_str(span.slice(source))?,
_ => f.write_str(display_source_text(Some(text), source))?,
},
PatternPart::Group { kind, patterns } => {
write!(f, "{}(", kind.prefix())?;
let mut patterns = patterns.iter();
if let Some(pattern) = patterns.next() {
pattern.fmt_with_source_mode(f, source, mode)?;
for pattern in patterns {
f.write_str("|")?;
pattern.fmt_with_source_mode(f, source, mode)?;
}
}
f.write_str(")")?;
}
PatternPart::Word(word) => word.fmt_with_source_mode(f, source, mode)?,
}
Ok(())
}
fn fmt_word_part_with_source_mode(
f: &mut impl fmt::Write,
part: &WordPart,
span: Span,
source: Option<&str>,
mode: RenderMode,
) -> fmt::Result {
if matches!(mode, RenderMode::Syntax)
&& let Some(source) = source
&& part_prefers_source_slice_in_syntax(part)
&& part_is_source_backed(part)
&& span.end.offset <= source.len()
{
f.write_str(span.slice(source))?;
return Ok(());
}
match part {
WordPart::Literal(text) => match (mode, source) {
(RenderMode::Syntax, Some(source))
if text.is_source_backed() && span.end.offset <= source.len() =>
{
f.write_str(trim_unescaped_trailing_whitespace(span.slice(source)))?;
}
_ => fmt_literal_text(f, text, span, source)?,
},
WordPart::ZshQualifiedGlob(glob) => {
if let Some(source) = source
&& zsh_qualified_glob_is_source_backed(glob)
&& glob.span.end.offset <= source.len()
{
f.write_str(trim_unescaped_trailing_whitespace(glob.span.slice(source)))?;
} else {
for segment in &glob.segments {
fmt_zsh_glob_segment_with_source(f, segment, source)?;
}
if let Some(qualifiers) = &glob.qualifiers {
fmt_zsh_glob_qualifier_group_with_source(f, qualifiers, source)?;
}
}
}
WordPart::SingleQuoted { value, dollar } => match mode {
RenderMode::Decoded => f.write_str(display_source_text(Some(value), source))?,
RenderMode::Syntax => match source {
Some(source)
if value.is_source_backed()
&& part_is_source_backed(part)
&& span.end.offset <= source.len() =>
{
f.write_str(span.slice(source))?;
}
_ => {
if *dollar {
f.write_str("$")?;
}
f.write_str("'")?;
f.write_str(display_source_text(Some(value), source))?;
f.write_str("'")?;
}
},
},
WordPart::DoubleQuoted { parts, dollar } => match mode {
RenderMode::Decoded => {
for part in parts {
fmt_word_part_with_source_mode(f, &part.kind, part.span, source, mode)?;
}
}
RenderMode::Syntax => match source {
Some(source) if part_is_source_backed(part) && span.end.offset <= source.len() => {
f.write_str(span.slice(source))?;
}
_ => {
if *dollar {
f.write_str("$")?;
}
f.write_str("\"")?;
for part in parts {
match &part.kind {
WordPart::Literal(text) => {
fmt_double_quoted_literal_text(f, text, part.span, source)?;
}
_ => {
fmt_word_part_with_source_mode(
f, &part.kind, part.span, source, mode,
)?;
}
}
}
f.write_str("\"")?;
}
},
},
WordPart::Variable(name) => write!(f, "${}", name)?,
WordPart::CommandSubstitution { body, syntax } => match source {
Some(source) if span.end.offset <= source.len() => f.write_str(span.slice(source))?,
_ => match syntax {
CommandSubstitutionSyntax::DollarParen => write!(f, "$({:?})", body)?,
CommandSubstitutionSyntax::Backtick => write!(f, "`{:?}`", body)?,
},
},
WordPart::ArithmeticExpansion {
expression, syntax, ..
} => match source {
Some(source) if expression.is_source_backed() && span.end.offset <= source.len() => {
f.write_str(span.slice(source))?
}
_ => match syntax {
ArithmeticExpansionSyntax::DollarParenParen => {
write!(f, "$(({}))", display_source_text(Some(expression), source))?
}
ArithmeticExpansionSyntax::LegacyBracket => {
write!(f, "$[{}]", display_source_text(Some(expression), source))?
}
},
},
WordPart::Parameter(parameter) => {
write!(
f,
"${{{}}}",
display_source_text(Some(¶meter.raw_body), source)
)?;
}
WordPart::ParameterExpansion {
reference,
operator,
operand,
colon_variant,
..
} => match operator {
ParameterOp::UseDefault => {
let c = if *colon_variant { ":" } else { "" };
write!(f, "${{")?;
fmt_var_ref_with_source(f, reference, source)?;
write!(
f,
"{}-{}}}",
c,
display_source_text(operand.as_ref(), source)
)?
}
ParameterOp::AssignDefault => {
let c = if *colon_variant { ":" } else { "" };
write!(f, "${{")?;
fmt_var_ref_with_source(f, reference, source)?;
write!(
f,
"{}={}}}",
c,
display_source_text(operand.as_ref(), source)
)?
}
ParameterOp::UseReplacement => {
let c = if *colon_variant { ":" } else { "" };
write!(f, "${{")?;
fmt_var_ref_with_source(f, reference, source)?;
write!(
f,
"{}+{}}}",
c,
display_source_text(operand.as_ref(), source)
)?
}
ParameterOp::Error => {
let c = if *colon_variant { ":" } else { "" };
write!(f, "${{")?;
fmt_var_ref_with_source(f, reference, source)?;
write!(
f,
"{}?{}}}",
c,
display_source_text(operand.as_ref(), source)
)?
}
ParameterOp::RemovePrefixShort { pattern } => {
write!(f, "${{")?;
fmt_var_ref_with_source(f, reference, source)?;
f.write_str("#")?;
pattern.fmt_with_source_mode(f, source, mode)?;
f.write_str("}")?;
}
ParameterOp::RemovePrefixLong { pattern } => {
write!(f, "${{")?;
fmt_var_ref_with_source(f, reference, source)?;
f.write_str("##")?;
pattern.fmt_with_source_mode(f, source, mode)?;
f.write_str("}")?;
}
ParameterOp::RemoveSuffixShort { pattern } => {
write!(f, "${{")?;
fmt_var_ref_with_source(f, reference, source)?;
f.write_str("%")?;
pattern.fmt_with_source_mode(f, source, mode)?;
f.write_str("}")?;
}
ParameterOp::RemoveSuffixLong { pattern } => {
write!(f, "${{")?;
fmt_var_ref_with_source(f, reference, source)?;
f.write_str("%%")?;
pattern.fmt_with_source_mode(f, source, mode)?;
f.write_str("}")?;
}
ParameterOp::ReplaceFirst {
pattern,
replacement,
..
} => {
write!(f, "${{")?;
fmt_var_ref_with_source(f, reference, source)?;
f.write_str("/")?;
pattern.fmt_with_source_mode(f, source, mode)?;
write!(f, "/{}}}", display_source_text(Some(replacement), source))?;
}
ParameterOp::ReplaceAll {
pattern,
replacement,
..
} => {
write!(f, "${{")?;
fmt_var_ref_with_source(f, reference, source)?;
f.write_str("//")?;
pattern.fmt_with_source_mode(f, source, mode)?;
write!(f, "/{}}}", display_source_text(Some(replacement), source))?;
}
ParameterOp::UpperFirst => {
write!(f, "${{")?;
fmt_var_ref_with_source(f, reference, source)?;
f.write_str("^}")?;
}
ParameterOp::UpperAll => {
write!(f, "${{")?;
fmt_var_ref_with_source(f, reference, source)?;
f.write_str("^^}")?;
}
ParameterOp::LowerFirst => {
write!(f, "${{")?;
fmt_var_ref_with_source(f, reference, source)?;
f.write_str(",}")?;
}
ParameterOp::LowerAll => {
write!(f, "${{")?;
fmt_var_ref_with_source(f, reference, source)?;
f.write_str(",,}")?;
}
},
WordPart::Length(reference) => {
write!(f, "${{#")?;
fmt_var_ref_with_source(f, reference, source)?;
f.write_str("}")?;
}
WordPart::ArrayAccess(reference) => {
write!(f, "${{")?;
fmt_var_ref_with_source(f, reference, source)?;
f.write_str("}")?;
}
WordPart::ArrayLength(reference) => {
write!(f, "${{#")?;
fmt_var_ref_with_source(f, reference, source)?;
f.write_str("}")?;
}
WordPart::ArrayIndices(reference) => {
write!(f, "${{!")?;
fmt_var_ref_with_source(f, reference, source)?;
f.write_str("}")?;
}
WordPart::Substring {
reference,
offset,
length,
..
} => {
if let Some(length) = length {
write!(f, "${{")?;
fmt_var_ref_with_source(f, reference, source)?;
write!(
f,
":{}:{}}}",
display_source_text(Some(offset), source),
display_source_text(Some(length), source)
)?
} else {
write!(f, "${{")?;
fmt_var_ref_with_source(f, reference, source)?;
write!(f, ":{}}}", display_source_text(Some(offset), source))?
}
}
WordPart::ArraySlice {
reference,
offset,
length,
..
} => {
if let Some(length) = length {
write!(f, "${{")?;
fmt_var_ref_with_source(f, reference, source)?;
write!(
f,
":{}:{}}}",
display_source_text(Some(offset), source),
display_source_text(Some(length), source)
)?
} else {
write!(f, "${{")?;
fmt_var_ref_with_source(f, reference, source)?;
write!(f, ":{}}}", display_source_text(Some(offset), source))?
}
}
WordPart::IndirectExpansion {
reference,
operator,
operand,
colon_variant,
..
} => {
let mut reference_syntax = String::new();
fmt_var_ref_with_source(&mut reference_syntax, reference, source)?;
if let Some(op) = operator {
let c = if *colon_variant { ":" } else { "" };
let op_char = match op {
ParameterOp::UseDefault => "-",
ParameterOp::AssignDefault => "=",
ParameterOp::UseReplacement => "+",
ParameterOp::Error => "?",
_ => "",
};
write!(
f,
"${{!{}{}{}{}}}",
reference_syntax,
c,
op_char,
display_source_text(operand.as_ref(), source)
)?
} else {
write!(f, "${{!{}}}", reference_syntax)?
}
}
WordPart::PrefixMatch { prefix, kind } => write!(f, "${{!{}{}}}", prefix, kind.as_char())?,
WordPart::ProcessSubstitution { body, is_input } => match source {
Some(source) if span.end.offset <= source.len() => f.write_str(span.slice(source))?,
_ => {
let prefix = if *is_input { "<" } else { ">" };
write!(f, "{}({:?})", prefix, body)?
}
},
WordPart::Transformation {
reference,
operator,
} => {
write!(f, "${{")?;
fmt_var_ref_with_source(f, reference, source)?;
write!(f, "@{}}}", operator)?;
}
}
Ok(())
}
fn fmt_heredoc_body_part_with_source(
f: &mut impl fmt::Write,
part: &HeredocBodyPart,
span: Span,
source: Option<&str>,
) -> fmt::Result {
if let Some(source) = source
&& heredoc_body_part_is_source_backed(part)
&& span.end.offset <= source.len()
{
f.write_str(span.slice(source))?;
return Ok(());
}
match part {
HeredocBodyPart::Literal(text) => fmt_literal_text(f, text, span, source)?,
HeredocBodyPart::Variable(name) => write!(f, "${}", name)?,
HeredocBodyPart::CommandSubstitution { body, syntax } => match syntax {
CommandSubstitutionSyntax::DollarParen => write!(f, "$({:?})", body)?,
CommandSubstitutionSyntax::Backtick => write!(f, "`{:?}`", body)?,
},
HeredocBodyPart::ArithmeticExpansion {
expression, syntax, ..
} => match syntax {
ArithmeticExpansionSyntax::DollarParenParen => {
write!(f, "$(({}))", display_source_text(Some(expression), source))?
}
ArithmeticExpansionSyntax::LegacyBracket => {
write!(f, "$[{}]", display_source_text(Some(expression), source))?
}
},
HeredocBodyPart::Parameter(parameter) => {
write!(
f,
"${{{}}}",
display_source_text(Some(¶meter.raw_body), source)
)?;
}
}
Ok(())
}
fn part_prefers_source_slice_in_syntax(part: &WordPart) -> bool {
matches!(
part,
WordPart::Variable(_)
| WordPart::ZshQualifiedGlob(_)
| WordPart::CommandSubstitution { .. }
| WordPart::ArithmeticExpansion { .. }
| WordPart::Parameter(_)
| WordPart::ParameterExpansion { .. }
| WordPart::Length(_)
| WordPart::ArrayAccess(_)
| WordPart::ArrayLength(_)
| WordPart::ArrayIndices(_)
| WordPart::Substring { .. }
| WordPart::ArraySlice { .. }
| WordPart::IndirectExpansion { .. }
| WordPart::PrefixMatch { .. }
| WordPart::ProcessSubstitution { .. }
| WordPart::Transformation { .. }
)
}
fn trim_unescaped_trailing_whitespace(text: &str) -> &str {
let mut end = text.len();
while end > 0 {
let Some((whitespace_start, ch)) = text[..end].char_indices().next_back() else {
break;
};
if !ch.is_whitespace() {
break;
}
let backslash_count = text.as_bytes()[..whitespace_start]
.iter()
.rev()
.take_while(|byte| **byte == b'\\')
.count();
if backslash_count % 2 == 1 {
break;
}
end = whitespace_start;
}
&text[..end]
}
fn part_is_source_backed(part: &WordPart) -> bool {
match part {
WordPart::Literal(text) => text.is_source_backed(),
WordPart::ZshQualifiedGlob(glob) => zsh_qualified_glob_is_source_backed(glob),
WordPart::SingleQuoted { value, .. } => value.is_source_backed(),
WordPart::DoubleQuoted { parts, .. } => {
parts.iter().all(|part| part_is_source_backed(&part.kind))
}
WordPart::Parameter(parameter) => parameter.raw_body.is_source_backed(),
WordPart::ArithmeticExpansion { expression, .. } => expression.is_source_backed(),
WordPart::ParameterExpansion {
reference,
operand,
operator,
..
} => {
reference.is_source_backed()
&& operator_is_source_backed(operator)
&& operand.as_ref().is_none_or(SourceText::is_source_backed)
}
WordPart::Length(reference)
| WordPart::ArrayAccess(reference)
| WordPart::ArrayLength(reference)
| WordPart::ArrayIndices(reference)
| WordPart::Transformation { reference, .. } => reference.is_source_backed(),
WordPart::Substring {
reference,
offset: index,
..
}
| WordPart::ArraySlice {
reference,
offset: index,
..
} => reference.is_source_backed() && index.is_source_backed(),
WordPart::IndirectExpansion {
reference,
operand,
operator,
..
} => {
reference.is_source_backed()
&& operator.is_none()
&& operand.as_ref().is_none_or(SourceText::is_source_backed)
}
WordPart::CommandSubstitution { .. }
| WordPart::Variable(_)
| WordPart::PrefixMatch { .. }
| WordPart::ProcessSubstitution { .. } => true,
}
}
fn heredoc_body_part_is_source_backed(part: &HeredocBodyPart) -> bool {
match part {
HeredocBodyPart::Literal(text) => text.is_source_backed(),
HeredocBodyPart::Variable(_) | HeredocBodyPart::CommandSubstitution { .. } => true,
HeredocBodyPart::ArithmeticExpansion { expression, .. } => expression.is_source_backed(),
HeredocBodyPart::Parameter(parameter) => parameter.raw_body.is_source_backed(),
}
}
fn pattern_part_is_source_backed(part: &PatternPart) -> bool {
match part {
PatternPart::Literal(text) => text.is_source_backed(),
PatternPart::AnyString | PatternPart::AnyChar => true,
PatternPart::CharClass(text) => text.is_source_backed(),
PatternPart::Group { patterns, .. } => patterns.iter().all(Pattern::is_source_backed),
PatternPart::Word(word) => word
.parts
.iter()
.all(|part| part_is_source_backed(&part.kind)),
}
}
fn zsh_qualified_glob_is_source_backed(glob: &ZshQualifiedGlob) -> bool {
glob.segments.iter().all(zsh_glob_segment_is_source_backed)
&& glob
.qualifiers
.as_ref()
.is_none_or(zsh_glob_qualifier_group_is_source_backed)
}
fn zsh_glob_segment_is_source_backed(segment: &ZshGlobSegment) -> bool {
match segment {
ZshGlobSegment::Pattern(pattern) => pattern.is_source_backed(),
ZshGlobSegment::InlineControl(control) => zsh_inline_glob_control_is_source_backed(control),
}
}
fn zsh_inline_glob_control_is_source_backed(_control: &ZshInlineGlobControl) -> bool {
true
}
fn fmt_zsh_glob_segment_with_source(
f: &mut impl fmt::Write,
segment: &ZshGlobSegment,
source: Option<&str>,
) -> fmt::Result {
match segment {
ZshGlobSegment::Pattern(pattern) => {
pattern.fmt_with_source_mode(f, source, RenderMode::Syntax)
}
ZshGlobSegment::InlineControl(control) => {
fmt_zsh_inline_glob_control_with_source(f, control, source)
}
}
}
fn fmt_zsh_inline_glob_control_with_source(
f: &mut impl fmt::Write,
control: &ZshInlineGlobControl,
_source: Option<&str>,
) -> fmt::Result {
match control {
ZshInlineGlobControl::CaseInsensitive { .. } => f.write_str("(#i)"),
ZshInlineGlobControl::Backreferences { .. } => f.write_str("(#b)"),
ZshInlineGlobControl::StartAnchor { .. } => f.write_str("(#s)"),
ZshInlineGlobControl::EndAnchor { .. } => f.write_str("(#e)"),
}
}
fn zsh_glob_qualifier_group_is_source_backed(group: &ZshGlobQualifierGroup) -> bool {
group
.fragments
.iter()
.all(zsh_glob_qualifier_is_source_backed)
}
fn zsh_glob_qualifier_is_source_backed(fragment: &ZshGlobQualifier) -> bool {
match fragment {
ZshGlobQualifier::Negation { .. } | ZshGlobQualifier::Flag { .. } => true,
ZshGlobQualifier::LetterSequence { text, .. } => text.is_source_backed(),
ZshGlobQualifier::NumericArgument { start, end, .. } => {
start.is_source_backed() && end.as_ref().is_none_or(SourceText::is_source_backed)
}
}
}
fn fmt_zsh_glob_qualifier_group_with_source(
f: &mut impl fmt::Write,
group: &ZshGlobQualifierGroup,
source: Option<&str>,
) -> fmt::Result {
match group.kind {
ZshGlobQualifierKind::Classic => f.write_str("(")?,
ZshGlobQualifierKind::HashQ => f.write_str("(#q")?,
}
for fragment in &group.fragments {
match fragment {
ZshGlobQualifier::Negation { .. } => f.write_str("^")?,
ZshGlobQualifier::Flag { name, .. } => write!(f, "{name}")?,
ZshGlobQualifier::LetterSequence { text, .. } => {
f.write_str(display_source_text(Some(text), source))?;
}
ZshGlobQualifier::NumericArgument { start, end, .. } => {
f.write_str("[")?;
f.write_str(display_source_text(Some(start), source))?;
if let Some(end) = end {
f.write_str(",")?;
f.write_str(display_source_text(Some(end), source))?;
}
f.write_str("]")?;
}
}
}
f.write_str(")")
}
fn operator_is_source_backed(operator: &ParameterOp) -> bool {
match operator {
ParameterOp::RemovePrefixShort { pattern }
| ParameterOp::RemovePrefixLong { pattern }
| ParameterOp::RemoveSuffixShort { pattern }
| ParameterOp::RemoveSuffixLong { pattern } => pattern.is_source_backed(),
ParameterOp::ReplaceFirst {
pattern,
replacement,
..
}
| ParameterOp::ReplaceAll {
pattern,
replacement,
..
} => pattern.is_source_backed() && replacement.is_source_backed(),
_ => true,
}
}
#[derive(Debug, Clone)]
pub enum ParameterOp {
UseDefault,
AssignDefault,
UseReplacement,
Error,
RemovePrefixShort { pattern: Pattern },
RemovePrefixLong { pattern: Pattern },
RemoveSuffixShort { pattern: Pattern },
RemoveSuffixLong { pattern: Pattern },
ReplaceFirst {
pattern: Pattern,
replacement: SourceText,
replacement_word_ast: Word,
},
ReplaceAll {
pattern: Pattern,
replacement: SourceText,
replacement_word_ast: Word,
},
UpperFirst,
UpperAll,
LowerFirst,
LowerAll,
}
impl ParameterOp {
pub fn replacement_word_ast(&self) -> Option<&Word> {
match self {
Self::ReplaceFirst {
replacement_word_ast,
..
}
| Self::ReplaceAll {
replacement_word_ast,
..
} => Some(replacement_word_ast),
Self::UseDefault
| Self::AssignDefault
| Self::UseReplacement
| Self::Error
| Self::RemovePrefixShort { .. }
| Self::RemovePrefixLong { .. }
| Self::RemoveSuffixShort { .. }
| Self::RemoveSuffixLong { .. }
| Self::UpperFirst
| Self::UpperAll
| Self::LowerFirst
| Self::LowerAll => None,
}
}
}
#[derive(Debug, Clone)]
pub struct Redirect {
pub fd: Option<i32>,
pub fd_var: Option<Name>,
pub fd_var_span: Option<Span>,
pub kind: RedirectKind,
pub span: Span,
pub target: RedirectTarget,
}
impl Redirect {
pub fn word_target(&self) -> Option<&Word> {
match &self.target {
RedirectTarget::Word(word) => Some(word),
RedirectTarget::Heredoc(_) => None,
}
}
pub fn word_target_mut(&mut self) -> Option<&mut Word> {
match &mut self.target {
RedirectTarget::Word(word) => Some(word),
RedirectTarget::Heredoc(_) => None,
}
}
pub fn heredoc(&self) -> Option<&Heredoc> {
match &self.target {
RedirectTarget::Word(_) => None,
RedirectTarget::Heredoc(heredoc) => Some(heredoc),
}
}
pub fn heredoc_mut(&mut self) -> Option<&mut Heredoc> {
match &mut self.target {
RedirectTarget::Word(_) => None,
RedirectTarget::Heredoc(heredoc) => Some(heredoc),
}
}
}
#[derive(Debug, Clone)]
pub enum RedirectTarget {
Word(Word),
Heredoc(Heredoc),
}
#[derive(Debug, Clone)]
pub struct Heredoc {
pub delimiter: HeredocDelimiter,
pub body: HeredocBody,
}
#[derive(Debug, Clone)]
pub struct HeredocDelimiter {
pub raw: Word,
pub cooked: String,
pub span: Span,
pub quoted: bool,
pub expands_body: bool,
pub strip_tabs: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RedirectKind {
Output,
Clobber,
Append,
Input,
ReadWrite,
HereDoc,
HereDocStrip,
HereString,
DupOutput,
DupInput,
OutputBoth,
}
#[derive(Debug, Clone)]
pub struct Assignment {
pub target: VarRef,
pub value: AssignmentValue,
pub append: bool,
pub span: Span,
}
#[derive(Debug, Clone)]
pub enum AssignmentValue {
Scalar(Word),
Compound(ArrayExpr),
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use super::*;
fn word(parts: Vec<WordPart>) -> Word {
let span = Span::new();
Word {
parts: parts
.into_iter()
.map(|part| WordPartNode::new(part, span))
.collect(),
span,
brace_syntax: Vec::new(),
}
}
fn pattern(parts: Vec<PatternPart>) -> Pattern {
let span = Span::new();
Pattern {
parts: parts
.into_iter()
.map(|part| PatternPartNode::new(part, span))
.collect(),
span,
}
}
fn plain_ref(name: &str) -> VarRef {
let span = Span::new();
VarRef {
name: name.into(),
name_span: span,
subscript: None,
span,
}
}
fn indexed_ref(name: &str, index: &str) -> VarRef {
let span = Span::new();
VarRef {
name: name.into(),
name_span: span,
subscript: Some(Subscript {
text: index.into(),
raw: None,
kind: SubscriptKind::Ordinary,
interpretation: SubscriptInterpretation::Contextual,
word_ast: None,
arithmetic_ast: None,
}),
span,
}
}
fn selector_ref(name: &str, selector: SubscriptSelector) -> VarRef {
let span = Span::new();
VarRef {
name: name.into(),
name_span: span,
subscript: Some(Subscript {
text: selector.as_char().to_string().into(),
raw: None,
kind: SubscriptKind::Selector(selector),
interpretation: SubscriptInterpretation::Contextual,
word_ast: None,
arithmetic_ast: None,
}),
span,
}
}
fn assignment(target: VarRef, value: AssignmentValue) -> Assignment {
Assignment {
target,
value,
append: false,
span: Span::new(),
}
}
fn stmt(command: Command) -> Stmt {
Stmt {
leading_comments: vec![],
command,
negated: false,
redirects: Box::default(),
terminator: None,
terminator_span: None,
inline_comment: None,
span: Span::new(),
}
}
fn stmt_with_redirects(command: Command, redirects: Vec<Redirect>) -> Stmt {
Stmt {
redirects: redirects.into_boxed_slice(),
..stmt(command)
}
}
fn stmt_seq(stmts: Vec<Stmt>) -> StmtSeq {
StmtSeq {
leading_comments: vec![],
stmts,
trailing_comments: vec![],
span: Span::new(),
}
}
fn simple_command(name: &str, args: Vec<Word>) -> SimpleCommand {
SimpleCommand {
name: Word::literal(name),
args,
assignments: Box::default(),
span: Span::new(),
}
}
fn simple_stmt(name: &str, args: Vec<Word>) -> Stmt {
stmt(Command::Simple(simple_command(name, args)))
}
#[test]
fn word_try_static_text_borrows_simple_static_words() {
assert!(matches!(
Word::literal("plain").try_static_text(""),
Some(Cow::Borrowed("plain"))
));
let single_quoted = word(vec![WordPart::SingleQuoted {
value: "single".into(),
dollar: false,
}]);
assert!(matches!(
single_quoted.try_static_text(""),
Some(Cow::Borrowed("single"))
));
}
#[test]
fn word_try_static_text_concatenates_nested_static_parts() {
let span = Span::new();
let word = Word {
parts: vec![
WordPartNode::new(WordPart::Literal(LiteralText::owned("foo")), span),
WordPartNode::new(
WordPart::DoubleQuoted {
parts: vec![WordPartNode::new(
WordPart::Literal(LiteralText::owned("bar")),
span,
)],
dollar: false,
},
span,
),
],
span,
brace_syntax: Vec::new(),
};
assert!(matches!(
word.try_static_text(""),
Some(Cow::Owned(ref value)) if value == "foobar"
));
}
#[test]
fn word_try_static_text_rejects_runtime_expansions() {
let variable = word(vec![WordPart::Variable("name".into())]);
assert!(variable.try_static_text("").is_none());
}
#[test]
fn command_name_text_decodes_unquoted_backslashes() {
assert_eq!(
decode_static_command_literal("\\foo\\ bar\\\nqux", StaticCommandNameContext::Unquoted)
.as_ref(),
"foo barqux"
);
}
#[test]
fn command_name_text_decodes_double_quoted_backslashes_selectively() {
assert_eq!(
decode_static_command_literal("\\$foo\\q\\\\", StaticCommandNameContext::DoubleQuoted)
.as_ref(),
"$foo\\q\\"
);
}
#[test]
fn command_name_text_concatenates_nested_static_parts() {
let span = Span::new();
let word = Word {
parts: vec![
WordPartNode::new(WordPart::Literal(LiteralText::owned("\\foo")), span),
WordPartNode::new(
WordPart::DoubleQuoted {
parts: vec![WordPartNode::new(
WordPart::Literal(LiteralText::owned("\\$bar")),
span,
)],
dollar: false,
},
span,
),
],
span,
brace_syntax: Vec::new(),
};
assert_eq!(
static_command_name_text(&word, "").as_deref(),
Some("foo$bar")
);
}
#[test]
fn shell_variable_name_helper_matches_identifier_rules() {
assert!(is_shell_variable_name("name"));
assert!(is_shell_variable_name("_name123"));
assert!(!is_shell_variable_name("1name"));
assert!(!is_shell_variable_name("name-value"));
}
#[test]
fn word_is_standalone_variable_like_matches_single_expansion_words() {
assert!(word_is_standalone_variable_like(&word(vec![
WordPart::Variable("name".into())
])));
assert!(word_is_standalone_variable_like(&word(vec![
WordPart::Length(plain_ref("name"))
])));
assert!(!word_is_standalone_variable_like(&word(vec![
WordPart::Literal(LiteralText::owned("prefix")),
WordPart::Variable("name".into()),
])));
assert!(!word_is_standalone_variable_like(&word(vec![
WordPart::DoubleQuoted {
parts: vec![WordPartNode::new(
WordPart::Variable("name".into()),
Span::new(),
)],
dollar: false,
}
])));
}
#[test]
fn word_is_standalone_status_capture_handles_plain_quoted_and_parameter_forms() {
assert!(word_is_standalone_status_capture(&word(vec![
WordPart::Variable("?".into())
])));
assert!(word_is_standalone_status_capture(&word(vec![
WordPart::DoubleQuoted {
parts: vec![WordPartNode::new(
WordPart::Variable("?".into()),
Span::new(),
)],
dollar: false,
}
])));
assert!(word_is_standalone_status_capture(&word(vec![
WordPart::Parameter(ParameterExpansion {
syntax: ParameterExpansionSyntax::Bourne(BourneParameterExpansion::Access {
reference: plain_ref("?"),
}),
span: Span::new(),
raw_body: "?".into(),
})
])));
assert!(!word_is_standalone_status_capture(&word(vec![
WordPart::Variable("name".into())
])));
assert!(!word_is_standalone_status_capture(&word(vec![
WordPart::Literal(LiteralText::owned("status=")),
WordPart::Variable("?".into()),
])));
}
fn span_for_source(source: &str) -> Span {
Span::from_positions(
Position {
line: 1,
column: 1,
offset: 0,
},
Position {
line: 1,
column: source.chars().count() + 1,
offset: source.len(),
},
)
}
#[test]
fn word_literal_creates_unquoted_word() {
let w = Word::literal("hello");
assert_eq!(w.parts.len(), 1);
assert!(matches!(w.part(0), Some(WordPart::Literal(s)) if s == "hello"));
}
#[test]
fn word_literal_empty_string() {
let w = Word::literal("");
assert!(matches!(w.part(0), Some(WordPart::Literal(s)) if s.is_empty()));
}
#[test]
fn literal_text_owned_compares_equal_to_str() {
let text = LiteralText::owned("hello");
assert!(text == "hello");
assert!(text != "world");
}
#[test]
fn literal_text_source_does_not_compare_equal_to_str_without_source() {
let text = LiteralText::source();
assert!(!text.is_empty());
assert!(text != "hello");
}
#[test]
fn literal_text_eq_str_uses_source_for_source_backed_literals() {
let source = "hello";
let span = span_for_source(source);
let text = LiteralText::source();
assert!(text.eq_str(source, span, "hello"));
assert!(!text.eq_str(source, span, "world"));
}
#[test]
fn word_quoted_literal_creates_single_quoted_part() {
let w = Word::quoted_literal("world");
assert_eq!(w.parts.len(), 1);
assert!(matches!(
w.part(0),
Some(WordPart::SingleQuoted { dollar: false, .. })
));
assert_eq!(format!("{w}"), "world");
}
#[test]
fn word_display_literal() {
let w = Word::literal("echo");
assert_eq!(format!("{w}"), "echo");
}
#[test]
fn word_render_syntax_preserves_cooked_double_quoted_literal() {
let w = word(vec![WordPart::DoubleQuoted {
parts: vec![WordPartNode::new(
WordPart::Literal(LiteralText::owned("hello".to_string())),
Span::new(),
)],
dollar: false,
}]);
assert_eq!(w.render_syntax(""), "\"hello\"");
}
#[test]
fn word_render_syntax_reescapes_cooked_double_quoted_literal_text() {
let w = word(vec![WordPart::DoubleQuoted {
parts: vec![WordPartNode::new(
WordPart::Literal(LiteralText::owned(
"quoted \"value\" uses $HOME and `pwd` with \\".to_string(),
)),
Span::new(),
)],
dollar: false,
}]);
assert_eq!(
w.render_syntax(""),
"\"quoted \\\"value\\\" uses \\$HOME and \\`pwd\\` with \\\\\""
);
}
#[test]
fn word_render_syntax_preserves_nested_parameter_expansion_inside_double_quotes() {
let w = word(vec![WordPart::DoubleQuoted {
parts: vec![
WordPartNode::new(
WordPart::Literal(LiteralText::owned("N/A: version \"".to_string())),
Span::new(),
),
WordPartNode::new(
WordPart::ParameterExpansion {
reference: plain_ref("PREFIXED_VERSION"),
operator: ParameterOp::UseDefault,
operand: Some("$PROVIDED_VERSION".into()),
operand_word_ast: Some(word(vec![WordPart::Variable(
"PROVIDED_VERSION".into(),
)])),
colon_variant: true,
},
Span::new(),
),
WordPartNode::new(
WordPart::Literal(LiteralText::owned("\" is not yet installed.".to_string())),
Span::new(),
),
],
dollar: false,
}]);
assert_eq!(
w.render_syntax(""),
"\"N/A: version \\\"${PREFIXED_VERSION:-$PROVIDED_VERSION}\\\" is not yet installed.\""
);
}
#[test]
fn word_render_syntax_preserves_source_backed_braced_variable() {
let span = Span::from_positions(
Position {
line: 1,
column: 1,
offset: 0,
},
Position {
line: 1,
column: 5,
offset: 4,
},
);
let w = Word {
parts: vec![WordPartNode::new(WordPart::Variable("1".into()), span)],
span,
brace_syntax: Vec::new(),
};
assert_eq!(w.render_syntax("${1}"), "${1}");
}
#[test]
fn word_render_syntax_trims_source_backed_literal_delimiters() {
let span = Span::from_positions(
Position {
line: 1,
column: 1,
offset: 0,
},
Position {
line: 1,
column: 5,
offset: 4,
},
);
let w = Word {
parts: vec![WordPartNode::new(
WordPart::Literal(LiteralText::source()),
span,
)],
span,
brace_syntax: Vec::new(),
};
assert_eq!(w.render_syntax("foo "), "foo");
}
#[test]
fn word_render_syntax_prefers_whole_word_source_slice() {
let source = "\"source \\\"$fzf_base/shell/completion.${shell}\\\"\"";
let span = span_for_source(source);
let w = Word {
parts: vec![WordPartNode::new(
WordPart::DoubleQuoted {
parts: vec![WordPartNode::new(
WordPart::Literal(LiteralText::owned(
"source \"$fzf_base/shell/completion.${shell}\"".to_string(),
)),
span,
)],
dollar: false,
},
span,
)],
span,
brace_syntax: Vec::new(),
};
assert_eq!(w.render_syntax(source), source);
}
#[test]
fn word_render_to_buf_appends_to_existing_contents() {
let word = word(vec![
WordPart::Literal("hello ".into()),
WordPart::Variable("USER".into()),
]);
let mut rendered = String::from("prefix:");
word.render_to_buf("hello $USER", &mut rendered);
assert_eq!(rendered, "prefix:hello $USER");
assert_eq!(rendered["prefix:".len()..], word.render("hello $USER"));
}
#[test]
fn word_render_syntax_to_buf_matches_render_syntax() {
let source = "\"hello\"";
let span = span_for_source(source);
let word = Word {
parts: vec![WordPartNode::new(
WordPart::DoubleQuoted {
parts: vec![WordPartNode::new(
WordPart::Literal(LiteralText::owned("hello".to_string())),
span,
)],
dollar: false,
},
span,
)],
span,
brace_syntax: Vec::new(),
};
let mut rendered = String::from("prefix:");
word.render_syntax_to_buf(source, &mut rendered);
assert_eq!(rendered, format!("prefix:{}", word.render_syntax(source)));
}
#[test]
fn word_display_variable() {
let w = word(vec![WordPart::Variable("HOME".into())]);
assert_eq!(format!("{w}"), "$HOME");
}
#[test]
fn word_display_arithmetic_expansion() {
let w = word(vec![WordPart::ArithmeticExpansion {
expression: "1+2".into(),
expression_ast: None,
expression_word_ast: Word::literal("1+2"),
syntax: ArithmeticExpansionSyntax::DollarParenParen,
}]);
assert_eq!(format!("{w}"), "$((1+2))");
}
#[test]
fn word_display_length() {
let w = word(vec![WordPart::Length(plain_ref("var"))]);
assert_eq!(format!("{w}"), "${#var}");
}
#[test]
fn word_display_array_access() {
let w = word(vec![WordPart::ArrayAccess(indexed_ref("arr", "0"))]);
assert_eq!(format!("{w}"), "${arr[0]}");
}
#[test]
fn word_display_array_length() {
let w = word(vec![WordPart::ArrayLength(selector_ref(
"arr",
SubscriptSelector::At,
))]);
assert_eq!(format!("{w}"), "${#arr[@]}");
}
#[test]
fn word_display_array_indices() {
let w = word(vec![WordPart::ArrayIndices(selector_ref(
"arr",
SubscriptSelector::At,
))]);
assert_eq!(format!("{w}"), "${!arr[@]}");
}
#[test]
fn word_display_substring_with_length() {
let w = word(vec![WordPart::Substring {
reference: plain_ref("var"),
offset: "2".into(),
offset_ast: None,
offset_word_ast: Word::literal("2"),
length: Some("3".into()),
length_ast: None,
length_word_ast: Some(Word::literal("3")),
}]);
assert_eq!(format!("{w}"), "${var:2:3}");
}
#[test]
fn word_display_substring_without_length() {
let w = word(vec![WordPart::Substring {
reference: plain_ref("var"),
offset: "2".into(),
offset_ast: None,
offset_word_ast: Word::literal("2"),
length: None,
length_ast: None,
length_word_ast: None,
}]);
assert_eq!(format!("{w}"), "${var:2}");
}
#[test]
fn word_display_array_slice_with_length() {
let w = word(vec![WordPart::ArraySlice {
reference: selector_ref("arr", SubscriptSelector::At),
offset: "1".into(),
offset_ast: None,
offset_word_ast: Word::literal("1"),
length: Some("2".into()),
length_ast: None,
length_word_ast: Some(Word::literal("2")),
}]);
assert_eq!(format!("{w}"), "${arr[@]:1:2}");
}
#[test]
fn word_display_array_slice_without_length() {
let w = word(vec![WordPart::ArraySlice {
reference: selector_ref("arr", SubscriptSelector::At),
offset: "1".into(),
offset_ast: None,
offset_word_ast: Word::literal("1"),
length: None,
length_ast: None,
length_word_ast: None,
}]);
assert_eq!(format!("{w}"), "${arr[@]:1}");
}
#[test]
fn word_display_indirect_expansion() {
let w = word(vec![WordPart::IndirectExpansion {
reference: plain_ref("ref"),
operator: None,
operand: None,
operand_word_ast: None,
colon_variant: false,
}]);
assert_eq!(format!("{w}"), "${!ref}");
}
#[test]
fn word_display_prefix_match() {
let w = word(vec![WordPart::PrefixMatch {
prefix: "MY_".into(),
kind: PrefixMatchKind::Star,
}]);
assert_eq!(format!("{w}"), "${!MY_*}");
}
#[test]
fn word_display_prefix_match_at() {
let w = word(vec![WordPart::PrefixMatch {
prefix: "MY_".into(),
kind: PrefixMatchKind::At,
}]);
assert_eq!(format!("{w}"), "${!MY_@}");
}
#[test]
fn word_render_syntax_preserves_raw_quoted_subscript() {
let w = word(vec![WordPart::ArrayAccess(VarRef {
name: "assoc".into(),
name_span: Span::new(),
subscript: Some(Subscript {
text: "key".into(),
raw: Some("\"key\"".into()),
kind: SubscriptKind::Ordinary,
interpretation: SubscriptInterpretation::Associative,
word_ast: None,
arithmetic_ast: None,
}),
span: Span::new(),
})]);
assert_eq!(format!("{w}"), "${assoc[\"key\"]}");
assert_eq!(w.render_syntax(""), "${assoc[\"key\"]}");
}
#[test]
fn word_display_transformation() {
let w = word(vec![WordPart::Transformation {
reference: plain_ref("var"),
operator: 'Q',
}]);
assert_eq!(format!("{w}"), "${var@Q}");
}
#[test]
fn word_display_multiple_parts() {
let w = word(vec![
WordPart::Literal("hello ".into()),
WordPart::Variable("USER".into()),
]);
assert_eq!(format!("{w}"), "hello $USER");
}
#[test]
fn pattern_display_multiple_parts() {
let p = pattern(vec![
PatternPart::Literal("file".into()),
PatternPart::AnyString,
PatternPart::CharClass("[[:digit:]]".into()),
]);
assert_eq!(format!("{p}"), "file*[[:digit:]]");
}
#[test]
fn pattern_render_syntax_prefers_whole_pattern_source_slice() {
let source = "Darwin\\ arm64*";
let span = span_for_source(source);
let p = Pattern {
parts: vec![PatternPartNode::new(
PatternPart::Literal(LiteralText::owned("Darwin arm64*".to_string())),
span,
)],
span,
};
assert_eq!(p.render_syntax(source), source);
}
#[test]
fn pattern_render_to_buf_appends_to_existing_contents() {
let pattern = pattern(vec![
PatternPart::Literal("file".into()),
PatternPart::AnyString,
PatternPart::CharClass("[[:digit:]]".into()),
]);
let source = "file*[[:digit:]]";
let mut rendered = String::from("prefix:");
pattern.render_to_buf(source, &mut rendered);
assert_eq!(rendered, format!("prefix:{}", pattern.render(source)));
}
#[test]
fn pattern_render_syntax_to_buf_matches_render_syntax() {
let source = "Darwin\\ arm64*";
let span = span_for_source(source);
let pattern = Pattern {
parts: vec![PatternPartNode::new(
PatternPart::Literal(LiteralText::owned("Darwin arm64*".to_string())),
span,
)],
span,
};
let mut rendered = String::from("prefix:");
pattern.render_syntax_to_buf(source, &mut rendered);
assert_eq!(
rendered,
format!("prefix:{}", pattern.render_syntax(source))
);
}
#[test]
fn pattern_display_extglob_group() {
let p = pattern(vec![PatternPart::Group {
kind: PatternGroupKind::ExactlyOne,
patterns: vec![
pattern(vec![PatternPart::Literal("foo".into())]),
pattern(vec![PatternPart::Literal("bar".into())]),
],
}]);
assert_eq!(format!("{p}"), "@(foo|bar)");
}
#[test]
fn word_display_parameter_expansion_use_default_colon() {
let w = word(vec![WordPart::ParameterExpansion {
reference: plain_ref("var"),
operator: ParameterOp::UseDefault,
operand: Some("fallback".into()),
operand_word_ast: Some(Word::literal("fallback")),
colon_variant: true,
}]);
assert_eq!(format!("{w}"), "${var:-fallback}");
}
#[test]
fn word_display_parameter_expansion_use_default_no_colon() {
let w = word(vec![WordPart::ParameterExpansion {
reference: plain_ref("var"),
operator: ParameterOp::UseDefault,
operand: Some("fallback".into()),
operand_word_ast: Some(Word::literal("fallback")),
colon_variant: false,
}]);
assert_eq!(format!("{w}"), "${var-fallback}");
}
#[test]
fn word_display_parameter_expansion_assign_default() {
let w = word(vec![WordPart::ParameterExpansion {
reference: plain_ref("var"),
operator: ParameterOp::AssignDefault,
operand: Some("val".into()),
operand_word_ast: Some(Word::literal("val")),
colon_variant: true,
}]);
assert_eq!(format!("{w}"), "${var:=val}");
}
#[test]
fn word_display_parameter_expansion_use_replacement() {
let w = word(vec![WordPart::ParameterExpansion {
reference: plain_ref("var"),
operator: ParameterOp::UseReplacement,
operand: Some("alt".into()),
operand_word_ast: Some(Word::literal("alt")),
colon_variant: true,
}]);
assert_eq!(format!("{w}"), "${var:+alt}");
}
#[test]
fn word_display_parameter_expansion_error() {
let w = word(vec![WordPart::ParameterExpansion {
reference: plain_ref("var"),
operator: ParameterOp::Error,
operand: Some("msg".into()),
operand_word_ast: Some(Word::literal("msg")),
colon_variant: true,
}]);
assert_eq!(format!("{w}"), "${var:?msg}");
}
#[test]
fn word_display_parameter_expansion_prefix_suffix() {
let w = word(vec![WordPart::ParameterExpansion {
reference: plain_ref("var"),
operator: ParameterOp::RemovePrefixShort {
pattern: pattern(vec![PatternPart::Literal("pat".into())]),
},
operand: None,
operand_word_ast: None,
colon_variant: false,
}]);
assert_eq!(format!("{w}"), "${var#pat}");
let w = word(vec![WordPart::ParameterExpansion {
reference: plain_ref("var"),
operator: ParameterOp::RemovePrefixLong {
pattern: pattern(vec![PatternPart::Literal("pat".into())]),
},
operand: None,
operand_word_ast: None,
colon_variant: false,
}]);
assert_eq!(format!("{w}"), "${var##pat}");
let w = word(vec![WordPart::ParameterExpansion {
reference: plain_ref("var"),
operator: ParameterOp::RemoveSuffixShort {
pattern: pattern(vec![PatternPart::Literal("pat".into())]),
},
operand: None,
operand_word_ast: None,
colon_variant: false,
}]);
assert_eq!(format!("{w}"), "${var%pat}");
let w = word(vec![WordPart::ParameterExpansion {
reference: plain_ref("var"),
operator: ParameterOp::RemoveSuffixLong {
pattern: pattern(vec![PatternPart::Literal("pat".into())]),
},
operand: None,
operand_word_ast: None,
colon_variant: false,
}]);
assert_eq!(format!("{w}"), "${var%%pat}");
}
#[test]
fn word_display_parameter_expansion_replace() {
let w = word(vec![WordPart::ParameterExpansion {
reference: plain_ref("var"),
operator: ParameterOp::ReplaceFirst {
pattern: pattern(vec![PatternPart::Literal("old".into())]),
replacement: "new".into(),
replacement_word_ast: Word::literal("new"),
},
operand: None,
operand_word_ast: None,
colon_variant: false,
}]);
assert_eq!(format!("{w}"), "${var/old/new}");
let w = word(vec![WordPart::ParameterExpansion {
reference: plain_ref("var"),
operator: ParameterOp::ReplaceAll {
pattern: pattern(vec![PatternPart::Literal("old".into())]),
replacement: "new".into(),
replacement_word_ast: Word::literal("new"),
},
operand: None,
operand_word_ast: None,
colon_variant: false,
}]);
assert_eq!(format!("{w}"), "${var//old/new}");
}
#[test]
fn word_display_parameter_expansion_case() {
let check = |op: ParameterOp, expected: &str| {
let w = word(vec![WordPart::ParameterExpansion {
reference: plain_ref("var"),
operator: op,
operand: None,
operand_word_ast: None,
colon_variant: false,
}]);
assert_eq!(format!("{w}"), expected);
};
check(ParameterOp::UpperFirst, "${var^}");
check(ParameterOp::UpperAll, "${var^^}");
check(ParameterOp::LowerAll, "${var,,}");
}
#[test]
fn simple_command_construction() {
let cmd = simple_command("ls", vec![Word::literal("-la")]);
assert_eq!(format!("{}", cmd.name), "ls");
assert_eq!(cmd.args.len(), 1);
assert_eq!(format!("{}", cmd.args[0]), "-la");
}
#[test]
fn statement_redirects_are_stored_on_stmt() {
let cmd = stmt_with_redirects(
Command::Simple(simple_command("echo", vec![Word::literal("hi")])),
vec![Redirect {
fd: Some(1),
fd_var: None,
fd_var_span: None,
kind: RedirectKind::Output,
span: Span::new(),
target: RedirectTarget::Word(Word::literal("out.txt")),
}],
);
assert_eq!(cmd.redirects.len(), 1);
assert_eq!(cmd.redirects[0].fd, Some(1));
assert_eq!(cmd.redirects[0].kind, RedirectKind::Output);
}
#[test]
fn simple_command_with_assignments() {
let cmd = SimpleCommand {
assignments: vec![assignment(
plain_ref("FOO"),
AssignmentValue::Scalar(Word::literal("bar")),
)]
.into_boxed_slice(),
..simple_command("env", vec![])
};
assert_eq!(cmd.assignments.len(), 1);
assert_eq!(cmd.assignments[0].target.name, "FOO");
assert!(!cmd.assignments[0].append);
}
#[test]
fn builtin_break_command_construction() {
let cmd = BuiltinCommand::Break(BreakCommand {
depth: Some(Word::literal("2")),
extra_args: vec![Word::literal("extra")],
assignments: Box::default(),
span: Span::new(),
});
if let BuiltinCommand::Break(command) = &cmd {
assert_eq!(command.depth.as_ref().unwrap().to_string(), "2");
assert_eq!(command.extra_args.len(), 1);
assert_eq!(command.extra_args[0].to_string(), "extra");
} else {
panic!("expected Break builtin");
}
}
#[test]
fn builtin_return_command_with_redirects_and_assignments() {
let cmd = stmt_with_redirects(
Command::Builtin(BuiltinCommand::Return(ReturnCommand {
code: Some(Word::literal("42")),
extra_args: vec![],
assignments: vec![assignment(
plain_ref("FOO"),
AssignmentValue::Scalar(Word::literal("bar")),
)]
.into_boxed_slice(),
span: Span::new(),
})),
vec![Redirect {
fd: None,
fd_var: None,
fd_var_span: None,
kind: RedirectKind::Output,
span: Span::new(),
target: RedirectTarget::Word(Word::literal("out.txt")),
}],
);
if let Command::Builtin(BuiltinCommand::Return(command)) = &cmd.command {
assert_eq!(command.code.as_ref().unwrap().to_string(), "42");
assert_eq!(command.assignments.len(), 1);
assert_eq!(cmd.redirects.len(), 1);
} else {
panic!("expected Return builtin");
}
}
#[test]
fn binary_command_construction() {
let pipe = BinaryCommand {
left: Box::new(simple_stmt("ls", vec![])),
op: BinaryOp::Pipe,
op_span: Span::new(),
right: Box::new(simple_stmt("grep", vec![Word::literal("foo")])),
span: Span::new(),
};
assert_eq!(pipe.op, BinaryOp::Pipe);
assert!(matches!(pipe.left.command, Command::Simple(_)));
assert!(matches!(pipe.right.command, Command::Simple(_)));
}
#[test]
fn stmt_negated() {
let mut command = simple_stmt("echo", vec![Word::literal("hi")]);
command.negated = true;
assert!(command.negated);
}
#[test]
fn stmt_seq_with_multiple_statements() {
let list = stmt_seq(vec![
simple_stmt("true", vec![]),
simple_stmt("echo", vec![Word::literal("ok")]),
]);
assert_eq!(list.len(), 2);
assert!(matches!(list[0].command, Command::Simple(_)));
}
#[test]
fn statement_operators_equality() {
assert_eq!(BinaryOp::And, BinaryOp::And);
assert_eq!(BinaryOp::Or, BinaryOp::Or);
assert_eq!(BinaryOp::Pipe, BinaryOp::Pipe);
assert_eq!(BinaryOp::PipeAll, BinaryOp::PipeAll);
assert_ne!(BinaryOp::And, BinaryOp::Or);
assert_eq!(StmtTerminator::Semicolon, StmtTerminator::Semicolon);
assert_eq!(
StmtTerminator::Background(BackgroundOperator::Plain),
StmtTerminator::Background(BackgroundOperator::Plain)
);
}
#[test]
fn redirect_kind_equality() {
assert_eq!(RedirectKind::Output, RedirectKind::Output);
assert_eq!(RedirectKind::Append, RedirectKind::Append);
assert_eq!(RedirectKind::Input, RedirectKind::Input);
assert_eq!(RedirectKind::ReadWrite, RedirectKind::ReadWrite);
assert_eq!(RedirectKind::HereDoc, RedirectKind::HereDoc);
assert_eq!(RedirectKind::HereDocStrip, RedirectKind::HereDocStrip);
assert_eq!(RedirectKind::HereString, RedirectKind::HereString);
assert_eq!(RedirectKind::DupOutput, RedirectKind::DupOutput);
assert_eq!(RedirectKind::DupInput, RedirectKind::DupInput);
assert_eq!(RedirectKind::OutputBoth, RedirectKind::OutputBoth);
assert_ne!(RedirectKind::Output, RedirectKind::Append);
}
#[test]
fn redirect_default_fd_none() {
let r = Redirect {
fd: None,
fd_var: None,
fd_var_span: None,
kind: RedirectKind::Input,
span: Span::new(),
target: RedirectTarget::Word(Word::literal("input.txt")),
};
assert!(r.fd.is_none());
assert_eq!(r.kind, RedirectKind::Input);
}
#[test]
fn redirect_exposes_word_target() {
let redirect = Redirect {
fd: None,
fd_var: None,
fd_var_span: None,
kind: RedirectKind::Output,
span: Span::new(),
target: RedirectTarget::Word(Word::literal("out.txt")),
};
assert_eq!(redirect.word_target().unwrap().to_string(), "out.txt");
assert!(redirect.heredoc().is_none());
}
#[test]
fn redirect_exposes_heredoc_payload() {
let delimiter = HeredocDelimiter {
raw: Word::quoted_literal("EOF"),
cooked: "EOF".to_owned(),
span: Span::new(),
quoted: true,
expands_body: false,
strip_tabs: false,
};
let redirect = Redirect {
fd: None,
fd_var: None,
fd_var_span: None,
kind: RedirectKind::HereDoc,
span: Span::new(),
target: RedirectTarget::Heredoc(Heredoc {
delimiter,
body: HeredocBody::literal_with_span("body", Span::new())
.with_mode(HeredocBodyMode::Literal),
}),
};
let heredoc = redirect.heredoc().expect("expected heredoc payload");
assert_eq!(heredoc.delimiter.cooked, "EOF");
assert!(heredoc.delimiter.quoted);
assert!(redirect.word_target().is_none());
}
#[test]
fn assignment_scalar() {
let a = assignment(plain_ref("X"), AssignmentValue::Scalar(Word::literal("1")));
assert_eq!(a.target.name, "X");
assert!(a.target.subscript.is_none());
assert!(!a.append);
}
#[test]
fn assignment_array() {
let a = assignment(
plain_ref("ARR"),
AssignmentValue::Compound(ArrayExpr {
kind: ArrayKind::Indexed,
elements: vec![
ArrayElem::Sequential(Word::literal("a").into()),
ArrayElem::Sequential(Word::literal("b").into()),
ArrayElem::Sequential(Word::literal("c").into()),
],
span: Span::new(),
}),
);
if let AssignmentValue::Compound(array) = &a.value {
assert_eq!(array.elements.len(), 3);
} else {
panic!("expected Compound");
}
}
#[test]
fn assignment_append() {
let mut a = assignment(
plain_ref("PATH"),
AssignmentValue::Scalar(Word::literal("/usr/bin")),
);
a.append = true;
assert!(a.append);
}
#[test]
fn assignment_indexed() {
let a = assignment(
indexed_ref("arr", "0"),
AssignmentValue::Scalar(Word::literal("val")),
);
assert_eq!(
a.target
.subscript
.as_ref()
.map(|subscript| subscript.syntax_text("")),
Some("0")
);
}
#[test]
fn case_terminator_equality() {
assert_eq!(CaseTerminator::Break, CaseTerminator::Break);
assert_eq!(CaseTerminator::FallThrough, CaseTerminator::FallThrough);
assert_eq!(CaseTerminator::Continue, CaseTerminator::Continue);
assert_eq!(
CaseTerminator::ContinueMatching,
CaseTerminator::ContinueMatching
);
assert_ne!(CaseTerminator::Break, CaseTerminator::FallThrough);
}
#[test]
fn if_command_construction() {
let if_cmd = IfCommand {
condition: stmt_seq(vec![]),
then_branch: stmt_seq(vec![]),
elif_branches: vec![],
else_branch: None,
syntax: IfSyntax::ThenFi {
then_span: Span::new(),
fi_span: Span::new(),
},
span: Span::new(),
};
assert!(if_cmd.else_branch.is_none());
assert!(if_cmd.elif_branches.is_empty());
}
#[test]
fn for_command_without_words() {
let for_cmd = ForCommand {
targets: vec![ForTarget {
word: Word::literal("i"),
name: Some("i".into()),
span: Span::new(),
}],
words: None,
body: stmt_seq(vec![]),
syntax: ForSyntax::InDoDone {
in_span: None,
do_span: Span::new(),
done_span: Span::new(),
},
span: Span::new(),
};
assert!(for_cmd.words.is_none());
assert_eq!(for_cmd.targets[0].word.render(""), "i");
assert_eq!(for_cmd.targets[0].name.as_deref(), Some("i"));
}
#[test]
fn for_command_with_words() {
let for_cmd = ForCommand {
targets: vec![ForTarget {
word: Word::literal("x"),
name: Some("x".into()),
span: Span::new(),
}],
words: Some(vec![Word::literal("1"), Word::literal("2")]),
body: stmt_seq(vec![]),
syntax: ForSyntax::InDoDone {
in_span: Some(Span::new()),
do_span: Span::new(),
done_span: Span::new(),
},
span: Span::new(),
};
assert_eq!(for_cmd.words.as_ref().unwrap().len(), 2);
}
#[test]
fn arithmetic_for_command() {
let cmd = ArithmeticForCommand {
left_paren_span: Span::new(),
init_span: Some(Span::new()),
init_ast: None,
first_semicolon_span: Span::new(),
condition_span: Some(Span::new()),
condition_ast: None,
second_semicolon_span: Span::new(),
step_span: Some(Span::new()),
step_ast: None,
right_paren_span: Span::new(),
body: stmt_seq(vec![]),
span: Span::new(),
};
assert!(cmd.init_span.is_some());
assert!(cmd.condition_span.is_some());
assert!(cmd.step_span.is_some());
}
#[test]
fn function_def_construction() {
let func = FunctionDef {
header: FunctionHeader {
function_keyword_span: None,
entries: vec![FunctionHeaderEntry {
word: Word::literal("my_func"),
static_name: Some("my_func".into()),
}],
trailing_parens_span: Some(Span::new()),
},
body: Box::new(simple_stmt("echo", vec![Word::literal("hello")])),
span: Span::new(),
};
assert_eq!(func.static_names().next().unwrap(), "my_func");
}
#[test]
fn file_empty() {
let file = File {
body: stmt_seq(vec![]),
span: Span::new(),
};
assert!(file.body.is_empty());
}
#[test]
fn command_variants_constructible() {
let simple = Command::Simple(simple_command("echo", vec![]));
assert!(matches!(simple, Command::Simple(_)));
let pipe = Command::Binary(BinaryCommand {
left: Box::new(simple_stmt("echo", vec![])),
op: BinaryOp::Pipe,
op_span: Span::new(),
right: Box::new(simple_stmt("cat", vec![])),
span: Span::new(),
});
assert!(matches!(pipe, Command::Binary(_)));
let builtin = Command::Builtin(BuiltinCommand::Exit(ExitCommand {
code: Some(Word::literal("1")),
extra_args: vec![],
assignments: Box::default(),
span: Span::new(),
}));
assert!(matches!(builtin, Command::Builtin(_)));
let compound = Command::Compound(CompoundCommand::BraceGroup(stmt_seq(vec![])));
assert!(matches!(compound, Command::Compound(_)));
let func = Command::Function(FunctionDef {
header: FunctionHeader {
function_keyword_span: None,
entries: vec![FunctionHeaderEntry {
word: Word::literal("f"),
static_name: Some("f".into()),
}],
trailing_parens_span: Some(Span::new()),
},
body: Box::new(simple_stmt("true", vec![])),
span: Span::new(),
});
assert!(matches!(func, Command::Function(_)));
let anonymous = Command::AnonymousFunction(AnonymousFunctionCommand {
surface: AnonymousFunctionSurface::Parens {
parens_span: Span::new(),
},
body: Box::new(simple_stmt("true", vec![])),
args: vec![Word::literal("x")],
span: Span::new(),
});
assert!(matches!(anonymous, Command::AnonymousFunction(_)));
}
#[test]
fn compound_command_subshell() {
let cmd = CompoundCommand::Subshell(stmt_seq(vec![]));
assert!(matches!(cmd, CompoundCommand::Subshell(_)));
}
#[test]
fn compound_command_arithmetic() {
let cmd = CompoundCommand::Arithmetic(ArithmeticCommand {
span: Span::new(),
left_paren_span: Span::new(),
expr_span: Some(Span::new()),
expr_ast: None,
right_paren_span: Span::new(),
});
assert!(matches!(cmd, CompoundCommand::Arithmetic(_)));
}
#[test]
fn compound_command_conditional() {
let cmd = CompoundCommand::Conditional(ConditionalCommand {
expression: ConditionalExpr::Unary(ConditionalUnaryExpr {
op: ConditionalUnaryOp::RegularFile,
op_span: Span::new(),
expr: Box::new(ConditionalExpr::Word(Word::literal("file"))),
}),
span: Span::new(),
left_bracket_span: Span::new(),
right_bracket_span: Span::new(),
});
if let CompoundCommand::Conditional(command) = &cmd {
let ConditionalExpr::Unary(expr) = &command.expression else {
panic!("expected unary conditional");
};
assert_eq!(expr.op, ConditionalUnaryOp::RegularFile);
} else {
panic!("expected Conditional");
}
}
#[test]
fn time_command_construction() {
let cmd = TimeCommand {
posix_format: true,
command: None,
span: Span::new(),
};
assert!(cmd.posix_format);
assert!(cmd.command.is_none());
}
}