use super::{
CommandBindings, IrGenError, Placeholder, QuoteContext, find_script_substitution,
find_substitution, invalid_command_error,
posix_lexical::{PosixCharacter, PosixLexicalState},
};
pub(super) struct ScriptSubstitutionTraversal<'template, 'bindings> {
template: &'template str,
chars: &'template [char],
bindings: &'bindings CommandBindings,
output: String,
quote_context: ShellQuoteContext,
backtick_outer_context: Option<ShellQuoteContext>,
posix_lexical: PosixLexicalState,
is_escaped: bool,
command_substitutions: Vec<ScriptCommandSubstitution>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum ShellQuoteContext {
Unquoted,
SingleQuoted,
DoubleQuoted,
}
struct ScriptCommandSubstitution {
quote_context: ShellQuoteContext,
parenthesis_depth: usize,
}
enum ScriptCommandSubstitutionDelimiter {
Start,
NestedOpen,
Close,
}
impl ScriptCommandSubstitution {
const fn new() -> Self {
Self {
quote_context: ShellQuoteContext::Unquoted,
parenthesis_depth: 1,
}
}
}
impl<'template, 'bindings> ScriptSubstitutionTraversal<'template, 'bindings> {
pub(super) fn new(
template: &'template str,
chars: &'template [char],
bindings: &'bindings CommandBindings,
) -> Self {
Self {
template,
chars,
bindings,
output: String::with_capacity(template.len()),
quote_context: ShellQuoteContext::Unquoted,
backtick_outer_context: None,
posix_lexical: PosixLexicalState::new(),
is_escaped: false,
command_substitutions: Vec::new(),
}
}
pub(super) fn append_substitution_at_position(
&mut self,
pos: usize,
) -> Result<usize, IrGenError> {
let ch = *self
.chars
.get(pos)
.ok_or_else(|| invalid_command_error(self.template.to_owned()))?;
if let Some(next) = self.append_non_substitution_character(pos, ch) {
return Ok(next);
}
let substitution = find_script_substitution(self.chars, pos);
let next = self.append_substitution_or_character(pos, ch, substitution)?;
if ch == '\n' {
self.posix_lexical.begin_pending_heredoc_after_newline(next);
}
Ok(next)
}
fn append_non_substitution_character(&mut self, pos: usize, ch: char) -> Option<usize> {
let copied_inert_character = {
let mut character = PosixCharacter {
chars: self.chars,
pos,
ch,
output: &mut self.output,
};
self.posix_lexical.append_inert_character(&mut character)
};
if copied_inert_character {
return Some(pos + 1);
}
if self.is_escaped {
self.output.push(ch);
self.is_escaped = false;
return Some(pos + 1);
}
if self.should_escape_next(pos, ch) {
self.output.push(ch);
self.is_escaped = true;
return Some(pos + 1);
}
if let Some(next) = self.append_unquoted_posix_lexical_character(pos, ch) {
return Some(next);
}
if self.starts_comment(pos, ch) {
self.output.push(ch);
self.posix_lexical.begin_comment();
return Some(pos + 1);
}
if let Some(next) = self.append_command_substitution_delimiter(pos, ch) {
return Some(next);
}
if self.update_quote_context(ch) {
self.output.push(ch);
return Some(pos + 1);
}
None
}
fn append_unquoted_posix_lexical_character(&mut self, pos: usize, ch: char) -> Option<usize> {
if self.active_quote_context() != ShellQuoteContext::Unquoted
|| self.backtick_outer_context.is_some()
{
return None;
}
let mut character = PosixCharacter {
chars: self.chars,
pos,
ch,
output: &mut self.output,
};
self.posix_lexical
.append_heredoc_declaration(&mut character, self.bindings)
}
fn starts_comment(&self, pos: usize, ch: char) -> bool {
self.active_quote_context() == ShellQuoteContext::Unquoted
&& self.backtick_outer_context.is_none()
&& PosixLexicalState::starts_comment(self.chars, pos, ch)
}
fn should_escape_next(&self, pos: usize, ch: char) -> bool {
ch == '\\'
&& self.active_quote_context() != ShellQuoteContext::SingleQuoted
&& find_substitution(self.chars, pos + 1).is_none()
}
fn update_quote_context(&mut self, ch: char) -> bool {
if let Some(outer_context) = self.backtick_outer_context {
if ch == '`' {
self.quote_context = outer_context;
self.backtick_outer_context = None;
return true;
}
return false;
}
let quote_context = self.active_quote_context_mut();
match (*quote_context, ch) {
(ShellQuoteContext::Unquoted, '\'') => {
*quote_context = ShellQuoteContext::SingleQuoted;
true
}
(ShellQuoteContext::Unquoted, '"') => {
*quote_context = ShellQuoteContext::DoubleQuoted;
true
}
(ShellQuoteContext::SingleQuoted, '\'') | (ShellQuoteContext::DoubleQuoted, '"') => {
*quote_context = ShellQuoteContext::Unquoted;
true
}
(ShellQuoteContext::Unquoted | ShellQuoteContext::DoubleQuoted, '`') => {
self.backtick_outer_context = Some(*quote_context);
true
}
_ => false,
}
}
fn append_substitution_or_character(
&mut self,
pos: usize,
ch: char,
substitution: Option<(Placeholder, usize)>,
) -> Result<usize, IrGenError> {
let Some((placeholder, skip)) = substitution else {
self.output.push(ch);
return Ok(pos + 1);
};
if self.backtick_outer_context.is_some() || self.is_in_command_substitution() {
return Err(invalid_command_error(self.template.to_owned()));
}
let context = match self.active_quote_context() {
ShellQuoteContext::Unquoted => QuoteContext::Unquoted,
ShellQuoteContext::SingleQuoted => QuoteContext::Single,
ShellQuoteContext::DoubleQuoted => QuoteContext::Double,
};
self.output
.push_str(self.bindings.substitution(placeholder, context));
Ok(pos + skip)
}
fn append_command_substitution_delimiter(&mut self, pos: usize, ch: char) -> Option<usize> {
match self.classify_command_substitution_delimiter(pos, ch)? {
ScriptCommandSubstitutionDelimiter::Start => {
self.command_substitutions
.push(ScriptCommandSubstitution::new());
self.output.push_str("$(");
Some(pos + 2)
}
ScriptCommandSubstitutionDelimiter::NestedOpen => {
self.increment_command_substitution_parenthesis_depth();
self.output.push(ch);
Some(pos + 1)
}
ScriptCommandSubstitutionDelimiter::Close => {
self.decrement_command_substitution_parenthesis_depth()?;
self.output.push(ch);
Some(pos + 1)
}
}
}
fn classify_command_substitution_delimiter(
&self,
pos: usize,
ch: char,
) -> Option<ScriptCommandSubstitutionDelimiter> {
if self.starts_command_substitution(pos, ch) {
return Some(ScriptCommandSubstitutionDelimiter::Start);
}
if self.is_unquoted_command_substitution() && ch == '(' {
return Some(ScriptCommandSubstitutionDelimiter::NestedOpen);
}
if self.is_unquoted_command_substitution() && ch == ')' {
return Some(ScriptCommandSubstitutionDelimiter::Close);
}
None
}
fn starts_command_substitution(&self, pos: usize, ch: char) -> bool {
matches!(
(ch, self.chars.get(pos + 1), self.active_quote_context()),
('$', Some('('), context) if context != ShellQuoteContext::SingleQuoted
)
}
fn is_unquoted_command_substitution(&self) -> bool {
self.is_in_command_substitution()
&& self.active_quote_context() == ShellQuoteContext::Unquoted
}
fn active_quote_context(&self) -> ShellQuoteContext {
self.command_substitutions
.last()
.map_or(self.quote_context, |substitution| {
substitution.quote_context
})
}
fn active_quote_context_mut(&mut self) -> &mut ShellQuoteContext {
match self.command_substitutions.last_mut() {
Some(substitution) => &mut substitution.quote_context,
None => &mut self.quote_context,
}
}
const fn is_in_command_substitution(&self) -> bool {
!self.command_substitutions.is_empty()
}
fn increment_command_substitution_parenthesis_depth(&mut self) {
if let Some(substitution) = self.command_substitutions.last_mut() {
substitution.parenthesis_depth += 1;
}
}
fn decrement_command_substitution_parenthesis_depth(&mut self) -> Option<()> {
let substitution = self.command_substitutions.last_mut()?;
substitution.parenthesis_depth -= 1;
if substitution.parenthesis_depth == 0 {
self.command_substitutions.pop();
}
Some(())
}
pub(super) fn finish(self) -> String {
self.output
}
}