shuck-formatter 0.1.1

Shell script formatter with configurable style options
Documentation
use super::*;

pub(crate) fn format_arithmetic_command_source(raw: &str) -> String {
    raw.strip_prefix("((")
        .and_then(|body| body.strip_suffix("))"))
        .map(|body| {
            if body.contains('\n') {
                format_multiline_arithmetic_command_body(body)
            } else {
                format!("(({}))", format_arithmetic_for_init_source(body.trim()))
            }
        })
        .unwrap_or_else(|| raw.to_string())
}

pub(crate) fn format_arithmetic_for_init_source(raw: &str) -> String {
    let trimmed = raw.trim();
    if trimmed.is_empty() || trimmed.contains(',') {
        return raw.to_string();
    }
    let Some((lhs, op, rhs)) = split_simple_arithmetic_assignment(trimmed) else {
        return raw.to_string();
    };
    if lhs.is_empty() || rhs.is_empty() {
        return raw.to_string();
    }
    format!("{} {} {}", lhs.trim(), op, rhs.trim())
}

pub(crate) fn format_arithmetic_for_clause_source(
    raw: &str,
    ast: Option<&ArithmeticExprNode>,
    context: RenderContext<'_, '_>,
) -> String {
    if raw.trim().is_empty() {
        return String::new();
    }
    if let Some(ast) = ast {
        let mut rendered = String::new();
        render_arithmetic_expr_to_buf(&mut rendered, ast, context);
        rendered
    } else {
        format_arithmetic_for_init_source(raw)
    }
}

fn split_simple_arithmetic_assignment(raw: &str) -> Option<(&str, &str, &str)> {
    for op in [
        "<<=", ">>=", "+=", "-=", "*=", "/=", "%=", "&=", "|=", "^=", "=",
    ] {
        let Some(index) = raw.find(op) else {
            continue;
        };
        if byte_index_inside_braced_parameter(raw, index) {
            continue;
        }
        if op == "=" {
            let previous = raw[..index].chars().next_back();
            let next = raw[index + op.len()..].chars().next();
            if previous.is_some_and(|ch| matches!(ch, '!' | '<' | '>' | '=')) || next == Some('=') {
                continue;
            }
        }
        return Some((&raw[..index], op, &raw[index + op.len()..]));
    }
    None
}

fn byte_index_inside_braced_parameter(raw: &str, target: usize) -> bool {
    let mut depth = 0usize;
    let mut index = 0usize;
    while index < raw.len() {
        if index >= target {
            return depth > 0;
        }
        let rest = &raw[index..];
        if rest.starts_with("${") {
            depth += 1;
            index += 2;
            continue;
        }
        let Some(ch) = rest.chars().next() else {
            break;
        };
        if ch == '}' && depth > 0 {
            depth -= 1;
        }
        index += ch.len_utf8();
    }
    false
}

fn format_multiline_arithmetic_command_body(body: &str) -> String {
    let mut lines = body
        .lines()
        .map(str::trim)
        .filter(|line| !line.is_empty())
        .map(ToOwned::to_owned)
        .collect::<Vec<_>>();

    if lines.is_empty() {
        return "(( ))".to_string();
    }

    let mut index = 1;
    while index < lines.len() {
        if let Some(rest) = lines[index].strip_prefix('+') {
            let rest = rest.trim_start().to_string();
            if let Some(previous) = lines.get_mut(index - 1) {
                previous.push_str(" +");
            }
            lines[index] = rest;
        }
        index += 1;
    }

    let mut rendered = String::from("((\\\n");
    for (index, line) in lines.iter().enumerate() {
        if index > 0 {
            rendered.push('\n');
        }
        rendered.push_str(line);
        if index + 1 < lines.len() {
            rendered.push_str(" \\");
        } else {
            rendered.push_str("))\n");
        }
    }
    rendered
}