use super::arithmetic::*;
use super::command_substitution::*;
use super::core::*;
use super::raw_rewrites::*;
use super::*;
pub(super) fn push_parameter_word(
rendered: &mut String,
parameter: &shuck_ast::ParameterExpansion,
context: RenderContext<'_, '_>,
) -> Result<(), std::fmt::Error> {
let source = context.source;
let Some(syntax) = parameter.bourne() else {
let raw = parameter.raw_body.slice(source);
rendered.push_str("${");
rendered.push_str(&compact_raw_parameter_subscript(raw));
rendered.push('}');
return Ok(());
};
match syntax {
BourneParameterExpansion::Access { reference } => {
push_braced_var_ref(rendered, "", reference, context);
}
BourneParameterExpansion::Length { reference } => {
push_braced_var_ref(rendered, "#", reference, context);
}
BourneParameterExpansion::Indices { reference } => {
push_braced_var_ref(rendered, "!", reference, context);
}
BourneParameterExpansion::Indirect {
reference,
operator,
operand,
colon_variant,
..
} => {
rendered.push_str("${!");
push_var_ref(rendered, reference, context);
if let Some(operator) = operator {
if *colon_variant {
rendered.push(':');
}
rendered.push_str(parameter_defaulting_operator(operator.as_ref()));
if let Some(operand) = operand {
rendered.push_str(operand.slice(source));
}
}
rendered.push('}');
}
BourneParameterExpansion::PrefixMatch { prefix, kind } => {
rendered.push_str("${!");
rendered.push_str(prefix);
rendered.push(kind.as_char());
rendered.push('}');
}
BourneParameterExpansion::Slice {
reference,
offset,
offset_ast,
length,
length_ast,
..
} => {
rendered.push_str("${");
push_var_ref(rendered, reference, context);
rendered.push(':');
push_parameter_slice_offset(rendered, offset, offset_ast.as_deref(), context);
if let Some(length) = length {
rendered.push(':');
push_arithmetic_source_text(rendered, length, length_ast.as_deref(), context);
}
rendered.push('}');
}
BourneParameterExpansion::Operation {
reference,
operator,
operand,
colon_variant,
..
} => {
render_parameter_expansion(
rendered,
reference,
operator.as_ref(),
operand.as_ref(),
*colon_variant,
Some(parameter.span),
context,
)?;
}
BourneParameterExpansion::Transformation {
reference,
operator,
} => {
rendered.push_str("${");
push_var_ref(rendered, reference, context);
rendered.push('@');
std::write!(rendered, "{operator}")?;
rendered.push('}');
}
}
Ok(())
}
pub(super) fn push_parameter_operand(
rendered: &mut String,
operand: &shuck_ast::SourceText,
context: RenderContext<'_, '_>,
) {
let operand = compact_parameter_operand_subscripts(operand.slice(context.source));
if operand.contains("$(") || operand.contains('`') {
let mut normalized = String::new();
push_raw_shell_text_with_normalized_redirect_spacing(&mut normalized, &operand);
if let Some(command_normalized) =
normalize_inline_command_substitutions_in_parameter_operand(
&normalized,
context.options,
)
{
rendered.push_str(&command_normalized);
} else {
rendered.push_str(&normalized);
}
} else {
rendered.push_str(&operand);
}
}
pub(super) fn normalize_inline_command_substitutions_in_parameter_operand(
raw: &str,
options: &ResolvedShellFormatOptions,
) -> Option<String> {
let mut rendered = String::with_capacity(raw.len());
let mut cursor = 0usize;
let mut index = 0usize;
let mut changed = false;
while let Some((open_offset, close_offset)) = next_raw_command_substitution(raw, index) {
let body = &raw[open_offset + 2..close_offset];
if !body.contains('\n')
&& let Some(normalized_body) =
normalize_inline_parameter_command_substitution_body(body, options)
&& normalized_body != body
{
rendered.push_str(&raw[cursor..open_offset]);
rendered.push_str("$(");
rendered.push_str(&normalized_body);
rendered.push(')');
cursor = close_offset + 1;
changed = true;
}
index = close_offset + 1;
}
finish_raw_rewrite(rendered, raw, cursor, changed)
}
pub(super) fn next_raw_command_substitution(raw: &str, index: usize) -> Option<(usize, usize)> {
RawShellText::new(raw).next_command_substitution(index)
}
pub(super) fn finish_raw_rewrite(
mut rendered: String,
raw: &str,
cursor: usize,
changed: bool,
) -> Option<String> {
changed.then(|| {
rendered.push_str(&raw[cursor..]);
rendered
})
}
pub(super) fn normalize_inline_parameter_command_substitution_body(
body: &str,
options: &ResolvedShellFormatOptions,
) -> Option<String> {
let trimmed = body.trim_matches([' ', '\t', '\r']);
if trimmed.is_empty() {
return None;
}
let fragment = FragmentFormatter::parse(trimmed, options)?;
let nested = fragment.format_body(None).ok()?;
let formatted = trim_trailing_line_endings(&nested);
(!formatted.is_empty() && !formatted.contains('\n')).then(|| formatted.to_string())
}
pub(super) fn compact_raw_parameter_subscript(raw: &str) -> String {
let Some(open) = raw.find('[') else {
return raw.to_string();
};
let Some(close) = raw.rfind(']') else {
return raw.to_string();
};
if close <= open {
return raw.to_string();
}
let mut rendered = String::with_capacity(raw.len());
rendered.push_str(&raw[..=open]);
rendered.push_str(&compact_dynamic_arithmetic_subscript(&raw[open + 1..close]));
rendered.push_str(&raw[close..]);
rendered
}
pub(super) fn compact_parameter_operand_subscripts(text: &str) -> String {
let Some(body) = text
.strip_prefix("${")
.and_then(|body| body.strip_suffix('}'))
else {
return text.to_string();
};
let compacted = compact_raw_parameter_subscript(body);
if compacted == body {
return text.to_string();
}
let mut rendered = String::with_capacity(text.len());
rendered.push_str("${");
rendered.push_str(&compacted);
rendered.push('}');
rendered
}
pub(super) fn render_parameter_expansion(
rendered: &mut String,
reference: &VarRef,
operator: &ParameterOp,
operand: Option<&shuck_ast::SourceText>,
colon_variant: bool,
raw_parameter_span: Option<shuck_ast::Span>,
env: RenderContext<'_, '_>,
) -> Result<(), std::fmt::Error> {
let (source, options) = (env.source, env.options);
rendered.push_str("${");
push_var_ref(rendered, reference, env);
match operator {
ParameterOp::UseDefault
| ParameterOp::AssignDefault
| ParameterOp::UseReplacement
| ParameterOp::Error => {
if colon_variant {
rendered.push(':');
}
rendered.push_str(parameter_defaulting_operator(operator));
if let Some(operand) = operand {
push_parameter_operand(rendered, operand, env);
}
}
ParameterOp::RemovePrefixShort { pattern }
| ParameterOp::RemovePrefixLong { pattern }
| ParameterOp::RemoveSuffixShort { pattern }
| ParameterOp::RemoveSuffixLong { pattern } => {
let removal_operator = match operator {
ParameterOp::RemovePrefixShort { .. } => "#",
ParameterOp::RemovePrefixLong { .. } => "##",
ParameterOp::RemoveSuffixShort { .. } => "%",
ParameterOp::RemoveSuffixLong { .. } => "%%",
_ => unreachable!(),
};
rendered.push_str(removal_operator);
render_pattern_syntax_to_buf(pattern, env, rendered);
}
ParameterOp::ReplaceFirst {
pattern,
replacement,
..
}
| ParameterOp::ReplaceAll {
pattern,
replacement,
..
} => {
let replace_all = matches!(operator, ParameterOp::ReplaceAll { .. });
rendered.push('/');
if replace_all {
rendered.push('/');
}
if let Some((raw_pattern, raw_replacement)) = raw_parameter_replacement_parts(
raw_parameter_span,
reference,
replace_all,
source,
options,
) {
rendered.push_str(raw_pattern);
rendered.push('/');
rendered.push_str(raw_replacement);
} else {
render_parameter_replacement_pattern(rendered, pattern, env);
rendered.push('/');
push_parameter_replacement_text(rendered, replacement, source);
}
}
ParameterOp::UpperFirst => rendered.push('^'),
ParameterOp::UpperAll => rendered.push_str("^^"),
ParameterOp::LowerFirst => rendered.push(','),
ParameterOp::LowerAll => rendered.push_str(",,"),
}
rendered.push('}');
Ok(())
}
pub(super) fn raw_parameter_replacement_parts<'a>(
raw_parameter_span: Option<shuck_ast::Span>,
reference: &VarRef,
replace_all: bool,
source: &'a str,
options: &ResolvedShellFormatOptions,
) -> Option<(&'a str, &'a str)> {
if options.simplify() || options.minify() {
return None;
}
let span = raw_parameter_span?;
let raw_parameter = source.get(span.start.offset()..span.end.offset())?;
let raw = raw_parameter.strip_prefix("${")?.strip_suffix('}')?;
let raw_body_start = span.start.offset().checked_add("${".len())?;
let reference_end = reference
.name_span
.end
.offset()
.checked_sub(raw_body_start)?;
let operator = if replace_all { "//" } else { "/" };
let after_operator = raw.get(reference_end..)?.strip_prefix(operator)?;
Some(split_raw_parameter_replacement(after_operator))
}
pub(super) fn split_raw_parameter_replacement(raw: &str) -> (&str, &str) {
let mut escaped = false;
let mut parameter_depth = 0usize;
let mut chars = raw.char_indices().peekable();
while let Some((index, ch)) = chars.next() {
if escaped {
escaped = false;
continue;
}
match ch {
'\\' => escaped = true,
'$' if chars.peek().is_some_and(|(_, next)| *next == '{') => {
chars.next();
parameter_depth += 1;
}
'}' if parameter_depth > 0 => parameter_depth -= 1,
'/' if parameter_depth == 0 => {
return (&raw[..index], &raw[index + '/'.len_utf8()..]);
}
_ => {}
}
}
(raw, "")
}
pub(super) fn render_parameter_replacement_pattern(
rendered: &mut String,
pattern: &Pattern,
context: RenderContext<'_, '_>,
) {
let source = context.source;
let options = context.options;
if !options.simplify()
&& !options.minify()
&& let Some(raw) = raw_pattern_source_slice(pattern, source)
{
rendered.push_str(raw);
return;
}
render_pattern_syntax_to_buf(pattern, context, rendered);
}
pub(super) fn push_parameter_replacement_text(
rendered: &mut String,
replacement: &shuck_ast::SourceText,
source: &str,
) {
if let Some(raw) = raw_source_slice(replacement.span(), source) {
rendered.push_str(raw);
} else {
rendered.push_str(replacement.slice(source));
}
}
pub(crate) fn parameter_defaulting_operator(operator: &ParameterOp) -> &'static str {
match operator {
ParameterOp::UseDefault => "-",
ParameterOp::AssignDefault => "=",
ParameterOp::UseReplacement => "+",
ParameterOp::Error => "?",
_ => "",
}
}
pub(crate) fn render_pattern_syntax_to_buf(
pattern: &Pattern,
context: RenderContext<'_, '_>,
rendered: &mut String,
) {
let source = context.source;
let options = context.options;
if pattern_needs_formatter_rendering(pattern) {
render_pattern_parts_syntax_to_buf(pattern, context, rendered);
return;
}
if !options.simplify()
&& !options.minify()
&& let Some(slice) = raw_pattern_source_slice(pattern, source)
&& could_need_preserve_raw_syntax(slice)
{
let start = rendered.len();
pattern.render_syntax_to_buf(source, rendered);
if should_preserve_raw_syntax(slice, &rendered[start..]) {
rendered.truncate(start);
rendered.push_str(slice);
}
return;
}
pattern.render_syntax_to_buf(source, rendered);
}
pub(super) fn pattern_needs_formatter_rendering(pattern: &Pattern) -> bool {
pattern.parts.iter().any(|part| match &part.kind {
PatternPart::Word(word) => word_needs_special_rendering(word),
PatternPart::Group { patterns, .. } => {
patterns.iter().any(pattern_needs_formatter_rendering)
}
_ => false,
})
}
pub(super) fn render_pattern_parts_syntax_to_buf(
pattern: &Pattern,
context: RenderContext<'_, '_>,
rendered: &mut String,
) {
for part in &pattern.parts {
match &part.kind {
PatternPart::Word(word) => {
render_word_syntax_to_buf(word, context, rendered);
}
PatternPart::Group { kind, patterns } => {
let _ = std::write!(rendered, "{}(", kind.prefix());
for (index, pattern) in patterns.iter().enumerate() {
if index > 0 {
rendered.push('|');
}
render_pattern_syntax_to_buf(pattern, context, rendered);
}
rendered.push(')');
}
_ => {
let single = Pattern {
parts: vec![part.clone()],
span: part.span,
};
single.render_syntax_to_buf(context.source, rendered);
}
}
}
}