shuck-formatter 0.0.42

Shell script formatter with configurable style options
Documentation
use super::arithmetic::*;
use super::command_substitution::*;
use super::parameter::*;
use super::raw_rewrites::*;
use super::*;

pub(crate) fn render_heredoc_body_to_buf(
    body: &HeredocBody,
    source: &str,
    options: &ResolvedShellFormatOptions,
    facts: &FormatterFacts<'_>,
    embedded_command_indent_levels: usize,
    rendered: &mut String,
) {
    for part in &body.parts {
        if render_heredoc_body_part(
            rendered,
            &part.kind,
            part.span,
            source,
            options,
            facts,
            embedded_command_indent_levels,
        )
        .is_err()
        {
            unreachable!("writing into a String should not fail");
        }
    }
}
pub(super) fn render_heredoc_body_part(
    rendered: &mut String,
    part: &HeredocBodyPart,
    span: shuck_ast::Span,
    source: &str,
    options: &ResolvedShellFormatOptions,
    facts: &FormatterFacts<'_>,
    embedded_command_indent_levels: usize,
) -> Result<(), std::fmt::Error> {
    let context = RenderContext::new(source, options, facts);
    match part {
        HeredocBodyPart::Literal(text) => {
            let raw = span.slice(source);
            let cooked = text.as_str(source, span);
            if raw != cooked && (raw.contains("\\$") || raw.contains("\\`") || raw.contains("\\\\"))
            {
                rendered.push_str(raw);
            } else {
                rendered.push_str(cooked);
            }
        }
        HeredocBodyPart::Variable(name) => {
            if let Some(raw) = escaped_heredoc_expansion_source(span, source) {
                rendered.push_str(raw);
            } else {
                std::write!(rendered, "${name}")?;
            }
        }
        HeredocBodyPart::CommandSubstitution { body, syntax } => {
            let raw = raw_source_slice(span, source);
            if let Some(raw) = escaped_heredoc_expansion_source(span, source) {
                rendered.push_str(raw);
            } else if render_heredoc_body_command_substitution(
                rendered,
                body,
                span.end.offset,
                source,
                options,
                facts,
                raw,
            )
            .is_none()
            {
                let layout = command_substitution_layout(
                    raw,
                    body,
                    context,
                    raw.is_none() && *syntax == CommandSubstitutionSyntax::DollarParen,
                    false,
                );

                if render_command_substitution(
                    rendered,
                    body,
                    span.end.offset,
                    context,
                    layout,
                    embedded_command_indent_levels,
                    raw,
                )
                .is_none()
                {
                    if let Some(raw) = raw {
                        rendered.push_str(raw);
                    } else {
                        std::write!(rendered, "$({body:?})")?;
                    }
                }
            }
        }
        HeredocBodyPart::ArithmeticExpansion {
            expression,
            expression_ast,
            syntax,
            ..
        } => {
            if let Some(raw) = escaped_heredoc_expansion_source(span, source) {
                rendered.push_str(raw);
            } else {
                push_arithmetic_expansion(
                    rendered,
                    expression,
                    expression_ast.as_ref(),
                    *syntax,
                    context,
                );
            }
        }
        HeredocBodyPart::Parameter(parameter) => {
            if let Some(raw) = escaped_heredoc_expansion_source(span, source) {
                rendered.push_str(raw);
            } else {
                push_parameter_word(rendered, parameter, context)?;
            }
        }
    }

    Ok(())
}

pub(super) fn escaped_heredoc_expansion_source(
    span: shuck_ast::Span,
    source: &str,
) -> Option<&str> {
    let raw = span.slice(source);
    if raw.starts_with(['\\', '\x00']) {
        return Some(raw);
    }

    let start = span.start.offset;
    if start > 0
        && source
            .as_bytes()
            .get(start - 1)
            .is_some_and(|byte| *byte == b'\\')
    {
        return source.get(start - 1..span.end.offset);
    }

    None
}

pub(super) fn render_heredoc_body_command_substitution(
    rendered: &mut String,
    body: &shuck_ast::StmtSeq,
    upper_bound: usize,
    source: &str,
    options: &ResolvedShellFormatOptions,
    facts: &FormatterFacts<'_>,
    raw: Option<&str>,
) -> Option<()> {
    let raw = raw?;
    if !command_substitution_source_starts_with_body_line(raw) {
        return None;
    }

    let context = RenderContext::new(source, options, facts);
    let mut nested = String::new();
    context
        .format_stmt_sequence_to_buf(body, Some(upper_bound), &mut nested)
        .ok()?;
    let trimmed = trim_trailing_line_endings(&nested);
    if trimmed.is_empty() {
        rendered.push_str("$()");
        return Some(());
    }

    let body_prefix = heredoc_command_substitution_body_prefix(raw, options);
    let close_prefix = heredoc_command_substitution_close_prefix(&body_prefix, options);

    rendered.push_str("$(\n");
    for (index, line) in trimmed.lines().enumerate() {
        if index > 0 {
            rendered.push('\n');
        }
        if !line.is_empty() {
            rendered.push_str(&body_prefix);
        }
        rendered.push_str(line);
    }
    rendered.push('\n');
    rendered.push_str(&close_prefix);
    rendered.push(')');
    Some(())
}

pub(super) fn heredoc_command_substitution_body_prefix(
    raw: &str,
    options: &ResolvedShellFormatOptions,
) -> String {
    let indent = raw
        .lines()
        .skip(1)
        .find_map(|line| {
            let trimmed = line.trim();
            (!trimmed.is_empty() && trimmed != ")").then(|| line_leading_shell_indent(line))
        })
        .unwrap_or("");

    match options.indent_style() {
        IndentStyle::Tab => indent.chars().map(|_| '\t').collect(),
        IndentStyle::Space => {
            let mut prefix = String::new();
            for ch in indent.chars() {
                if ch == '\t' {
                    prefix.push_str(&" ".repeat(usize::from(options.indent_width())));
                } else {
                    prefix.push(' ');
                }
            }
            prefix
        }
    }
}

pub(super) fn heredoc_command_substitution_close_prefix(
    body_prefix: &str,
    options: &ResolvedShellFormatOptions,
) -> String {
    let mut prefix = body_prefix.to_string();
    match options.indent_style() {
        IndentStyle::Tab => {
            prefix.pop();
        }
        IndentStyle::Space => {
            let width = usize::from(options.indent_width());
            for _ in 0..width {
                if !prefix.ends_with(' ') {
                    break;
                }
                prefix.pop();
            }
        }
    }
    prefix
}