globgroups 0.1.0-beta.2

Expands groups like foo{a,b}
Documentation
"""
Expands globs with grouping like foo{bar,baz}

This implementation is not maintained.
It has been superceeded by the rust implementation.
"""

import itertools
import os
import pprint
import sys
from dataclasses import dataclass
from typing import ForwardRef, Iterator, Optional, Sequence, TypeAlias

import funcparserlib.lexer as funclex
from funcparserlib.parser import NoParseError, finished, forward_decl, many, maybe, tok

__all__ = ("Glob",)

_TOKENIZER = funclex.make_tokenizer(
    [
        funclex.TokenSpec("op", r"[\{\},]"),
        funclex.TokenSpec("word", r"[^\{\},]+"),
    ]
)

GlobGroup = ForwardRef("GlobGroup")
GlobExpr: TypeAlias = str | GlobGroup


def _expand_expr(expr: GlobExpr) -> Iterator[str]:
    match expr:
        case str(word):
            yield word
        case GlobGroup(prefix, children, suffix):
            suffixes = list(_expand_expr(suffix))
            for child in children:
                for child_expansion in _expand_expr(child):
                    for suffix_expansion in suffixes:
                        yield prefix + child_expansion + suffix_expansion
        case other:
            raise TypeError(type(other))


class GlobParseError(ValueError):
    pass


@dataclass
class Glob:
    _expr: GlobExpr

    @staticmethod
    def parse(text: str) -> "Glob":
        if not text:
            return Glob("")
        try:
            return _whole_expr.parse(list(_TOKENIZER(text)))
        except (funclex.LexerError, NoParseError):
            raise ValueError(f"Failed to parse glob: {text!r}")

    def expand(self) -> list[str]:
        return list(_expand_expr(self._expr))

    def __str__(self):
        return str(self._expr)


@dataclass
class GlobGroup:
    _prefix: str
    _children: list[GlobExpr]
    _suffix: GlobExpr

    def expand(self) -> list[str]:
        return list(_expand_expr(self))

    @staticmethod
    def _process_parse(
        parts: tuple[
            Optional[str], tuple[GlobExpr, Sequence[GlobExpr]], Optional[GlobExpr]
        ]
    ) -> GlobGroup:
        prefix, (first_child, children), suffix = parts
        return GlobGroup(prefix or "", [first_child, *children], suffix or "")

    def __str__(self):
        parts = [self.prefix]
        if self.children:
            parts.append("{")
            parts.extend(",".join(map(str, self.children)))
            parts.append("}")
        parts.append(str(self.suffix))
        return "".join(parts)


_expr = forward_decl()
_word = tok("word")
_group = (
    maybe(_word)
    + (  # prefix
        -tok("op", "{")
        + (maybe(_expr) + many(-tok("op", ",") + maybe(_expr)))
        + -tok("op", "}")
    )
    + maybe(_expr)
) >> GlobGroup._process_parse
_expr.define(_group | _word)
_whole_expr = _expr + -finished

if __name__ == "__main__":
    if len(args := sys.argv) != 2:
        print("Expected only one argument", file=sys.stderr)
        exit(1)

    glob = sys.argv[1]
    expr = Glob.parse(glob)
    if os.getenv("DEBUG") == "globparse":
        pprint.print(expr)
        print()
        print()
    for expand in expr.expand():
        print(expand)