use super::{
CommandBindings, IrGenError, QuoteContext,
command_substitution::{CommandSubstitution, CommandSubstitutionDelimiter},
find_substitution, invalid_command_error,
posix_lexical::{PosixCharacter, PosixLexicalState},
};
use crate::recipe_shell::RecipeShell;
#[derive(Clone, Copy)]
enum MarkerProtection {
Protected,
Unprotected,
}
impl MarkerProtection {
const fn from_protected_region(is_protected: bool) -> Self {
if is_protected {
Self::Protected
} else {
Self::Unprotected
}
}
}
pub(super) struct SubstitutionTraversal<'template, 'bindings> {
template: &'template str,
chars: &'template [char],
bindings: &'bindings CommandBindings,
output: String,
in_backticks: bool,
quote_context: QuoteContext,
command_substitutions: Vec<CommandSubstitution>,
posix_lexical: PosixLexicalState,
}
impl<'template, 'bindings> SubstitutionTraversal<'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()),
in_backticks: false,
quote_context: QuoteContext::Unquoted,
command_substitutions: Vec::new(),
posix_lexical: PosixLexicalState::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 self.bindings.shell == RecipeShell::PowerShell {
return self.append_power_shell_character(pos, ch);
}
self.append_posix_character(pos, ch)
}
fn append_posix_character(&mut self, pos: usize, ch: char) -> Result<usize, IrGenError> {
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 Ok(pos + 1);
}
if let Some(next) = self.append_escaped_character(pos, ch) {
return Ok(next);
}
if let Some(next) = self.append_unquoted_posix_lexical_character(pos, ch) {
return Ok(next);
}
if ch == '`' && !self.matches_single_quote_context() {
self.in_backticks ^= true;
self.output.push(ch);
return Ok(pos + 1);
}
let next = self.append_contextual_character(pos, ch, Self::posix_marker_protection)?;
if ch == '\n' {
self.posix_lexical.begin_pending_heredoc_after_newline(next);
}
Ok(next)
}
fn append_unquoted_posix_lexical_character(&mut self, pos: usize, ch: char) -> Option<usize> {
if !self.matches_unquoted_quote_context() {
return None;
}
let mut character = PosixCharacter {
chars: self.chars,
pos,
ch,
output: &mut self.output,
};
let declaration_end = self
.posix_lexical
.append_heredoc_declaration(&mut character, self.bindings);
if let Some(next) = declaration_end {
return Some(next);
}
if PosixLexicalState::starts_comment(self.chars, pos, ch) {
self.posix_lexical.begin_comment();
self.output.push(ch);
return Some(pos + 1);
}
None
}
fn append_power_shell_character(&mut self, pos: usize, ch: char) -> Result<usize, IrGenError> {
if let Some(next) = self.append_power_shell_escaped_character(pos, ch)? {
return Ok(next);
}
if self.append_power_shell_single_quote_escape(pos, ch) {
return Ok(pos + 2);
}
self.append_contextual_character(pos, ch, Self::power_shell_marker_protection)
}
fn append_contextual_character(
&mut self,
pos: usize,
ch: char,
marker_protection: fn(&Self) -> MarkerProtection,
) -> Result<usize, IrGenError> {
if let Some(next) = self.append_command_substitution_delimiter(pos, ch) {
return Ok(next);
}
self.update_quote_context(ch);
self.append_marker_for_context(pos, ch, marker_protection(self))
}
const fn posix_marker_protection(&self) -> MarkerProtection {
MarkerProtection::from_protected_region(
self.in_backticks || self.is_in_command_substitution(),
)
}
fn power_shell_marker_protection(&self) -> MarkerProtection {
MarkerProtection::from_protected_region(
!matches!(self.active_quote_context(), QuoteContext::Unquoted)
|| self.is_in_command_substitution(),
)
}
fn append_power_shell_escaped_character(
&mut self,
pos: usize,
ch: char,
) -> Result<Option<usize>, IrGenError> {
if ch != '`' || self.matches_single_quote_context() {
return Ok(None);
}
if find_substitution(self.chars, pos + 1).is_some() {
return Err(invalid_command_error(self.template.to_owned()));
}
let Some(next) = self.chars.get(pos + 1) else {
return Ok(None);
};
self.output.push(ch);
self.output.push(*next);
Ok(Some(pos + 2))
}
fn append_power_shell_single_quote_escape(&mut self, pos: usize, ch: char) -> bool {
if !self.matches_single_quote_context() || !self.has_doubled_apostrophe(pos, ch) {
return false;
}
self.output.push_str("''");
true
}
fn has_doubled_apostrophe(&self, pos: usize, ch: char) -> bool {
ch == '\'' && self.chars.get(pos + 1) == Some(&'\'')
}
fn append_escaped_character(&mut self, pos: usize, ch: char) -> Option<usize> {
if ch != '\\' || self.matches_single_quote_context() {
return None;
}
if find_substitution(self.chars, pos + 1).is_some() {
return None;
}
let next = *self.chars.get(pos + 1)?;
self.output.push(ch);
self.output.push(next);
Some(pos + 2)
}
fn append_command_substitution_delimiter(&mut self, pos: usize, ch: char) -> Option<usize> {
match self.classify_command_substitution_delimiter(pos, ch)? {
CommandSubstitutionDelimiter::Start => {
self.command_substitutions.push(CommandSubstitution::new());
self.output.push_str("$(");
Some(pos + 2)
}
CommandSubstitutionDelimiter::NestedOpen => {
self.increment_command_substitution_parenthesis_depth();
self.output.push(ch);
Some(pos + 1)
}
CommandSubstitutionDelimiter::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<CommandSubstitutionDelimiter> {
if !self.matches_single_quote_context() && self.starts_command_substitution(pos, ch) {
return Some(CommandSubstitutionDelimiter::Start);
}
if self.starts_nested_command_substitution_parenthesis(ch) {
return Some(CommandSubstitutionDelimiter::NestedOpen);
}
if !self.matches_single_quote_context() && self.ends_command_substitution(ch) {
return Some(CommandSubstitutionDelimiter::Close);
}
None
}
fn starts_command_substitution(&self, pos: usize, ch: char) -> bool {
ch == '$' && self.chars.get(pos + 1) == Some(&'(')
}
fn starts_nested_command_substitution_parenthesis(&self, ch: char) -> bool {
self.is_in_command_substitution() && self.matches_unquoted_quote_context() && ch == '('
}
fn ends_command_substitution(&self, ch: char) -> bool {
self.is_in_command_substitution() && self.matches_unquoted_quote_context() && ch == ')'
}
fn update_quote_context(&mut self, ch: char) {
let quote_context = self.active_quote_context_mut();
Self::update_quote_context_for_region(quote_context, ch);
}
fn matches_single_quote_context(&self) -> bool {
matches!(self.active_quote_context(), QuoteContext::Single)
}
fn matches_unquoted_quote_context(&self) -> bool {
matches!(self.active_quote_context(), QuoteContext::Unquoted)
}
const fn is_in_command_substitution(&self) -> bool {
!self.command_substitutions.is_empty()
}
fn active_quote_context(&self) -> QuoteContext {
self.command_substitutions
.last()
.map_or(self.quote_context, |substitution| {
substitution.quote_context
})
}
fn active_quote_context_mut(&mut self) -> &mut QuoteContext {
match self.command_substitutions.last_mut() {
Some(substitution) => &mut substitution.quote_context,
None => &mut self.quote_context,
}
}
const fn update_quote_context_for_region(quote_context: &mut QuoteContext, ch: char) {
match (*quote_context, ch) {
(QuoteContext::Unquoted, '\'') => *quote_context = QuoteContext::Single,
(QuoteContext::Unquoted, '"') => *quote_context = QuoteContext::Double,
(QuoteContext::Single, '\'') | (QuoteContext::Double, '"') => {
*quote_context = QuoteContext::Unquoted;
}
_ => {}
}
}
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) {
let Some(substitution) = self.command_substitutions.last_mut() else {
return;
};
substitution.parenthesis_depth -= 1;
if substitution.parenthesis_depth == 0 {
self.command_substitutions.pop();
}
}
fn append_marker_for_context(
&mut self,
pos: usize,
ch: char,
protection: MarkerProtection,
) -> Result<usize, IrGenError> {
let substitution = find_substitution(self.chars, pos);
if matches!(protection, MarkerProtection::Protected) {
return self.append_protected_character(pos, ch, substitution);
}
Ok(self.append_unprotected_character(pos, ch, substitution))
}
fn append_protected_character(
&mut self,
pos: usize,
ch: char,
substitution: Option<(super::Placeholder, usize)>,
) -> Result<usize, IrGenError> {
if substitution.is_some() {
return Err(invalid_command_error(self.template.to_owned()));
}
self.output.push(ch);
Ok(pos + 1)
}
fn append_unprotected_character(
&mut self,
pos: usize,
ch: char,
substitution: Option<(super::Placeholder, usize)>,
) -> usize {
let Some((placeholder, skip)) = substitution else {
self.output.push(ch);
return pos + 1;
};
self.output.push_str(
self.bindings
.substitution(placeholder, self.active_quote_context()),
);
pos + skip
}
pub(super) fn finish(self) -> String {
self.output
}
}