shrub-core 0.0.3

Reusable core of a POSIX/bash shell (used by shrub-shell)
Documentation
use itertools::Itertools;
use shrub_parser::word;

pub(crate) fn generate_and_combine_brace_expansions(
    pieces: Vec<shrub_parser::word::BraceExpressionOrText>,
) -> impl IntoIterator<Item = String> {
    let expansions: Vec<Vec<String>> = pieces
        .into_iter()
        .map(|piece| expand_brace_expr_or_text(piece).collect())
        .collect();

    expansions
        .into_iter()
        .multi_cartesian_product()
        .map(|v| v.join(""))
}

fn expand_brace_expr_or_text(
    beot: word::BraceExpressionOrText,
) -> Box<dyn Iterator<Item = String>> {
    match beot {
        word::BraceExpressionOrText::Expr(members) => {
            // Chain all member iterators together
            Box::new(members.into_iter().flat_map(expand_brace_expr_member))
        }
        word::BraceExpressionOrText::Text(text) => Box::new(std::iter::once(text)),
    }
}

#[expect(clippy::cast_possible_truncation)]
fn expand_brace_expr_member(bem: word::BraceExpressionMember) -> Box<dyn Iterator<Item = String>> {
    match bem {
        word::BraceExpressionMember::NumberSequence {
            start,
            end,
            increment,
            zero_padded_width,
        } => {
            let mut increment = increment.unsigned_abs() as usize;
            if increment == 0 {
                increment = 1;
            }

            // A bound written with a leading zero asks for every member to be
            // padded out to the width of the longer bound, so
            // `{01..10}` counts `01 02 ...` and not `1 2 ...`. The
            // sign takes one of those columns.
            let format = move |n: i64| match zero_padded_width {
                Some(width) => std::format!("{n:0width$}"),
                None => n.to_string(),
            };

            if start <= end {
                Box::new((start..=end).step_by(increment).map(format))
            } else {
                // Iterate from start down to end by decrementing.
                let increment = increment as u64;
                Box::new(
                    std::iter::successors(Some(start), move |&n| {
                        let next = n.checked_sub_unsigned(increment)?;
                        (next >= end).then_some(next)
                    })
                    .map(format),
                )
            }
        }

        word::BraceExpressionMember::CharSequence {
            start,
            end,
            increment,
        } => {
            let mut increment = increment.unsigned_abs() as usize;
            if increment == 0 {
                increment = 1;
            }

            if start <= end {
                Box::new((start..=end).step_by(increment).map(|c| c.to_string()))
            } else {
                // Iterate from start down to end by decrementing.
                let increment = u32::try_from(increment).unwrap_or(u32::MAX);
                Box::new(
                    std::iter::successors(Some(start), move |&c| {
                        let next = char::from_u32((c as u32).checked_sub(increment)?)?;
                        (next >= end).then_some(next)
                    })
                    .map(|c| c.to_string()),
                )
            }
        }

        word::BraceExpressionMember::Child(elements) => {
            // Chain all element iterators together
            Box::new(generate_and_combine_brace_expansions(elements).into_iter())
        }
    }
}