Brace expansion using ranges

From Rosetta Code
Brace expansion using ranges is a draft programming task. It is not yet considered ready to be promoted as a complete task, for reasons that should be found in its talk page.
This task has been clarified. Its programming examples are in need of review to ensure that they still fit the requirements of the task.


Task

Write and test a function which expands one or more Unix-style numeric and alphabetic range braces embedded in a larger string.

Specification

The brace strings used by Unix shells permit expansion of both:

  1. Recursive comma-separated lists (covered by the related task: Brace_expansion, and can be ignored here)
  2. ordered numeric and alphabetic ranges, which are the object of this task.


The general pattern of brace ranges is:

{<START>..<END>}

and, in more recent shells:

{<START>..<END>..<INCR>}

(See https://wiki.bash-hackers.org/syntax/expansion/brace)


Expandable ranges of this kind can be ascending or descending:

simpleNumber{1..3}.txt
simpleAlpha-{Z..X}.txt

and may have a third INCR element specifying ordinal intervals larger than one. The increment value can be preceded by a - minus sign, but not by a + sign.

The effect of the minus sign is to always to reverse the natural order suggested by the START and END values.

Any level of zero-padding used in either the START or END value of a numeric range is adopted in the expansions.

steppedDownAndPadded-{10..00..5}.txt
minusSignFlipsSequence {030..20..-5}.txt

A single string may contain more than one expansion range:

combined-{Q..P}{2..1}.txt

Alphabetic range values are limited to a single character for START and END but these characters are not confined to the ASCII alphabet.

emoji{🌵..🌶}{🌽..🌾}etc

Unmatched braces are simply ignored, as are empty braces, and braces which contain no range (or list).

li{teral
rangeless{}empty
rangeless{random}string
Tests

Generate and display here the expansion of (at least) each of the ten example lines shown below.

The JavaScript implementation below uses parser combinators, aiming to encode a more or less full and legible description of the

<PREAMBLE><AMBLE><POSTSCRIPT>

range brace grammar, but you should use any resource that suggests itself in your language, including parser libraries.

(The grammar of range expansion, unlike that of nested list expansion, is not recursive, so even regular expressions should prove serviceable here).

The output of the JS implementation, which aims to match the brace expansion behaviour of the default zsh shell on current versions of macOS:

simpleNumberRising{1..3}.txt -> 
    simpleNumberRising1.txt
    simpleNumberRising2.txt
    simpleNumberRising3.txt

simpleAlphaDescending-{Z..X}.txt -> 
    simpleAlphaDescending-Z.txt
    simpleAlphaDescending-Y.txt
    simpleAlphaDescending-X.txt

steppedDownAndPadded-{10..00..5}.txt -> 
    steppedDownAndPadded-10.txt
    steppedDownAndPadded-05.txt
    steppedDownAndPadded-00.txt

minusSignFlipsSequence {030..20..-5}.txt -> 
    minusSignFlipsSequence 020.txt
    minusSignFlipsSequence 025.txt
    minusSignFlipsSequence 030.txt

reverseSteppedNumberRising{1..6..-2}.txt -> 
    reverseSteppedNumberRising5.txt
    reverseSteppedNumberRising3.txt
    reverseSteppedNumberRising1.txt

combined-{Q..P}{2..1}.txt -> 
    combined-Q2.txt
    combined-Q1.txt
    combined-P2.txt
    combined-P1.txt

emoji{🌵..🌶}{🌽..🌾}etc -> 
    emoji🌵🌽etc
    emoji🌵🌾etc
    emoji🌶🌽etc
    emoji🌶🌾etc

li{teral -> 
    li{teral

rangeless{}empty -> 
    rangeless{}empty

rangeless{random}string -> 
    rangeless{random}string
Other tasks related to string operations:
Metrics
Counting
Remove/replace
Anagrams/Derangements/shuffling
Find/Search/Determine
Formatting
Song lyrics/poems/Mad Libs/phrases
Tokenize
Sequences



11l

Translation of: Nim
F intFromString(s) -> Int?
   X.try
      R Int(s)
   X.catch ValueError
      R N

F parseRange(r)
   I r.empty {R [‘{}’]}
   V sp = r.split(‘..’)
   I sp.len == 1 {R [‘{’r‘}’]}
   V first = sp[0]
   V last = sp[1]
   V incr = I sp.len == 2 {‘1’} E sp[2]

   Int? val1 = intFromString(first)
   Int? val2 = intFromString(last)
   Int? val3 = intFromString(incr)

   I val3 == N {R [‘{’r‘}’]}
   V n3 = val3
   V numeric = val1 != N & val2 != N

   Int n1
   Int n2
   I numeric
      n1 = val1
      n2 = val2
   E
      I (val1 != N & val2 == N) | (val1 == N & val2 != N)
         R [‘{’r‘}’]
      I first.len != 1 | last.len != 1
         R [‘{’r‘}’]
      n1 = first[0].code
      n2 = last[0].code

   V width = 1
   I numeric
      width = max(first.len, last.len)

   I n3 == 0
      R I numeric {[String(n1).zfill(width)]} E [first]

   V asc = n1 < n2
   I n3 < 0
      asc = !asc
      swap(&n1, &n2)
      n3 = -n3

   [String] result
   V i = n1
   I asc
      L i <= n2
         result.append(I numeric {String(i).zfill(width)} E Char(code' i))
         i += n3
   E
      L i >= n2
         result.append(I numeric {String(i).zfill(width)} E Char(code' i))
         i -= n3
   R result

F rangeExpand(s)
   V result = [‘’]
   V rng = ‘’
   V inRng = 0B

   L(c) s
      I c == ‘{’ & !inRng
         inRng = 1B
         rng = ‘’
      E I c == ‘}’ & inRng
         V rngRes = parseRange(rng)
         [String] res
         L(r) result
            L(rr) rngRes
               res.append(r‘’rr)
         result = move(res)
         inRng = 0B
      E I inRng
         rng ‘’= c
      E
         L(&s) result
            s ‘’= c

   I inRng
      L(&s) result
         s ‘’= ‘{’rng
   R result

-V examples = [‘simpleNumberRising{1..3}.txt’,
               ‘simpleAlphaDescending-{Z..X}.txt’,
               ‘steppedDownAndPadded-{10..00..5}.txt’,
               ‘minusSignFlipsSequence {030..20..-5}.txt’,
               ‘combined-{Q..P}{2..1}.txt’,
               ‘li{teral’,
               ‘rangeless{}empty’,
               ‘rangeless{random}string’,
               ‘mixedNumberAlpha{5..k}’,
               ‘steppedAlphaRising{P..Z..2}.txt’,
               ‘stops after endpoint-{02..10..3}.txt’]

L(s) examples
   print(s" ->\n    ", end' ‘’)
   V res = rangeExpand(s)
   print(res.join("\n    "))
   print()
Output:
simpleNumberRising{1..3}.txt ->
    simpleNumberRising1.txt
    simpleNumberRising2.txt
    simpleNumberRising3.txt

simpleAlphaDescending-{Z..X}.txt ->
    simpleAlphaDescending-Z.txt
    simpleAlphaDescending-Y.txt
    simpleAlphaDescending-X.txt

steppedDownAndPadded-{10..00..5}.txt ->
    steppedDownAndPadded-10.txt
    steppedDownAndPadded-05.txt
    steppedDownAndPadded-00.txt

minusSignFlipsSequence {030..20..-5}.txt ->
    minusSignFlipsSequence 020.txt
    minusSignFlipsSequence 025.txt
    minusSignFlipsSequence 030.txt

combined-{Q..P}{2..1}.txt ->
    combined-Q2.txt
    combined-Q1.txt
    combined-P2.txt
    combined-P1.txt

li{teral ->
    li{teral

rangeless{}empty ->
    rangeless{}empty

rangeless{random}string ->
    rangeless{random}string

mixedNumberAlpha{5..k} ->
    mixedNumberAlpha{5..k}

steppedAlphaRising{P..Z..2}.txt ->
    steppedAlphaRisingP.txt
    steppedAlphaRisingR.txt
    steppedAlphaRisingT.txt
    steppedAlphaRisingV.txt
    steppedAlphaRisingX.txt
    steppedAlphaRisingZ.txt

stops after endpoint-{02..10..3}.txt ->
    stops after endpoint-02.txt
    stops after endpoint-05.txt
    stops after endpoint-08.txt

AutoHotkey

Brace_expansion_using_ranges(line){
    needle := "^.*\K{(?P<Start>[^{}]+?)\..(?P<End>[^{}]+?)(?:\..(?P<Incr>[^{}]+?))?}"
    while true
    {
        while pos := RegExMatch(line, needle, m, A_Index=1?1:pos+StrLen(m))
        {
            char := false, step := "", output := ""
            reverse := InStr(mIncr, "-") ? true : false
            
            if mStart is number
                pad1 := pad(mStart), pad2 := pad(mEnd), pad := StrLen(pad1)>=StrLen(pad2) ? pad1 : pad2
            else
                mStart := Ord(mStart), mEnd := Ord(mEnd), char := true
            
            mIncr := (mIncr?Abs(mIncr):1) * (mStart>mEnd?-1:1)
            loop % Abs((mStart-mEnd)/mIncr) + 1
            {
                step := mStart + (A_Index-1) * mIncr
                step := pad <> "" ? SubStr(pad . step, 1-StrLen(pad)) : step
                step := char ? Chr(step) : step
                Rep := StrReplace(line, m, step)
                output := reverse ? rep "`n" output : output .= Rep "`n"
            }
            output := Trim(Output, "`n")
        }
        if RegExMatch(output, needle)
            line := output
        else 
            break
    }
    return output ? output : line
}
pad(num){
    if RegExMatch(num, "`am)^(0+)(?=[1-9]|0$)", m)
        loop % StrLen(num)
            pad .= "0"
    return pad
}

Examples:

data=
(
simpleNumberRising{1..3}.txt
simpleAlphaDescending-{Z..X}.txt
steppedDownAndPadded-{10..00..5}.txt
minusSignFlipsSequence {030..20..-5}.txt
reverseSteppedNumberRising{1..6..-2}.txt
combined-{Q..P}{2..1}.txt
emoji{🌵..🌶}{🌽..🌾}etc
li{teral
rangeless{}empty
rangeless{random}string
)
for i, line in StrSplit(data, "`n", "`r")
    result .= line " ->`n" RegExReplace(Brace_expansion_using_ranges(line), "`am)^", "`t") "`n`n"

MsgBox, 262144, , % result
return
Output:
simpleNumberRising{1..3}.txt ->
	simpleNumberRising1.txt
	simpleNumberRising2.txt
	simpleNumberRising3.txt

simpleAlphaDescending-{Z..X}.txt ->
	simpleAlphaDescending-Z.txt
	simpleAlphaDescending-Y.txt
	simpleAlphaDescending-X.txt

steppedDownAndPadded-{10..00..5}.txt ->
	steppedDownAndPadded-10.txt
	steppedDownAndPadded-05.txt
	steppedDownAndPadded-00.txt

minusSignFlipsSequence {030..20..-5}.txt ->
	minusSignFlipsSequence 020.txt
	minusSignFlipsSequence 025.txt
	minusSignFlipsSequence 030.txt

reverseSteppedNumberRising{1..6..-2}.txt ->
	reverseSteppedNumberRising5.txt
	reverseSteppedNumberRising3.txt
	reverseSteppedNumberRising1.txt

combined-{Q..P}{2..1}.txt ->
	combined-Q2.txt
	combined-Q1.txt
	combined-P2.txt
	combined-P1.txt

emoji{🌵..🌶}{🌽..🌾}etc ->
	emoji🌵🌽etc
	emoji🌵🌾etc
	emoji🌶🌽etc
	emoji🌶🌾etc

li{teral ->
	li{teral

rangeless{}empty ->
	rangeless{}empty

rangeless{random}string ->
	rangeless{random}string

F#

// Brace expansion using ranges. Nigel Galloway: October 6th., 2021
let fUC, fUR=System.Text.Rune.GetUnicodeCategory,(fun n->System.Text.Rune.GetRuneAt(n,0))
let fV(n,i,g,e,l,s)=let l=if l="" then 1 else int l in match l with 0->None |_->Some(n,i,g,e,int l,s)
let(|Valid|_|)(n:System.Text.RegularExpressions.Match)=let fN(g:string)=n.Groups.[g].Value in if n.Success then fV(fN "n",fN "i",fN "g",fN "e",fN "l",fN "s") else None
let fN(g:string)=let mutable g=g.EnumerateRunes() in if g.MoveNext() && not(g.MoveNext()) then true else false
let(|I|_|)(n,g)=if fN n && fN g then (let n,g=fUR n,fUR g in if fUC n=fUC g then Some(n,g) else None) else None
let(|G|_|)(n:string,g:string)=try let n,g=(int n,int g) in Some(n,g) with _->None
let(|E|_|)(n:string,g:string)=if n.[0]='0' || g.[0]='0' then match (n,g) with G(e,l)->Some(e,l,max n.Length g.Length) |_->None else None
let fL n=let fN i g e l=let n=[i..(if i>g then -l else l)..g] in if e="-" then List.rev n else n
         let fG n g=let n,buf=string n, System.Text.StringBuilder() in (for _ in 1..g-n.Length do buf.Append 0); buf.Append n; buf.ToString()
         match System.Text.RegularExpressions.Regex.Match(n,@"^(?<n>.*?){(?<i>.*?)\.\.(?<g>.*?)(\.\.(?<e>[-]+)?(?<l>[0-9]*?))?}(?<s>.*)$") with
          Valid(n,i,g,e,l,s)->match (i,g) with I(i,g)->Some(fN i.Value g.Value e l|>Seq.map(fun g->sprintf "%s%A%s" n (System.Text.Rune(g)) s))
                                              |E(i,g,z)->Some(fN i g e l|>Seq.map(fun g->sprintf "%s%s%s" n (fG g z) s))
                                              |G(i,g)->Some(fN i g e l|>Seq.map(fun g->sprintf "%s%A%s" n (string g) s)) |_->None
         |_->None
let rec expBraces n=seq{match fL n with Some n->yield!(n|>Seq.collect(expBraces)) |_->yield n}
let tests=["simpleNumberRising{1..3}.txt";"steppedNumberRising{1..6..2}.txt";"reverseSteppedNumberRising{1..6..-2}.txt";"steppedNumberDescending{20..9..2}.txt";"simpleAlphaDescending-{Z..X}.txt";"steppedDownAndPadded-{10..00..5}.txt";"minusSignFlipsSequence {030..20..-5}.txt";"combined-{Q..P}{2..1}.txt";"emoji{🌵..🌶}{🌽..🌾}etc";"li{teral";"rangeless{random}string";"rangeless{}empty";"steppedAlphaDescending-{Z..M..2}.txt";"reversedSteppedAlphaDescending-{Z..M..-2}.txt"]
tests|>List.iter(fun g->printfn $"%s{g}->"; for n in expBraces g do printfn $"    %s{n}")
Output:
simpleNumberRising{1..3}.txt->
    simpleNumberRising1.txt
    simpleNumberRising2.txt
    simpleNumberRising3.txt
steppedNumberRising{1..6..2}.txt->
    steppedNumberRising1.txt
    steppedNumberRising3.txt
    steppedNumberRising5.txt
reverseSteppedNumberRising{1..6..-2}.txt->
    reverseSteppedNumberRising5.txt
    reverseSteppedNumberRising3.txt
    reverseSteppedNumberRising1.txt
steppedNumberDescending{20..9..2}.txt->
    steppedNumberDescending"20".txt
    steppedNumberDescending"18".txt
    steppedNumberDescending"16".txt
    steppedNumberDescending"14".txt
    steppedNumberDescending"12".txt
    steppedNumberDescending"10".txt
simpleAlphaDescending-{Z..X}.txt->
    simpleAlphaDescending-Z.txt
    simpleAlphaDescending-Y.txt
    simpleAlphaDescending-X.txt
steppedDownAndPadded-{10..00..5}.txt->
    steppedDownAndPadded-10.txt
    steppedDownAndPadded-05.txt
    steppedDownAndPadded-00.txt
minusSignFlipsSequence {030..20..-5}.txt->
    minusSignFlipsSequence 020.txt
    minusSignFlipsSequence 025.txt
    minusSignFlipsSequence 030.txt
combined-{Q..P}{2..1}.txt->
    combined-Q2.txt
    combined-Q1.txt
    combined-P2.txt
    combined-P1.txt
emoji{🌵..🌶}{🌽..🌾}etc->
    emoji🌵🌽etc
    emoji🌵🌾etc
    emoji🌶🌽etc
    emoji🌶🌾etc
li{teral->
    li{teral
rangeless{random}string->
    rangeless{random}string
rangeless{}empty->
    rangeless{}empty
steppedAlphaDescending-{Z..M..2}.txt->
    steppedAlphaDescending-Z.txt
    steppedAlphaDescending-X.txt
    steppedAlphaDescending-V.txt
    steppedAlphaDescending-T.txt
    steppedAlphaDescending-R.txt
    steppedAlphaDescending-P.txt
    steppedAlphaDescending-N.txt
reversedSteppedAlphaDescending-{Z..M..-2}.txt->
    reversedSteppedAlphaDescending-N.txt
    reversedSteppedAlphaDescending-P.txt
    reversedSteppedAlphaDescending-R.txt
    reversedSteppedAlphaDescending-T.txt
    reversedSteppedAlphaDescending-V.txt
    reversedSteppedAlphaDescending-X.txt
    reversedSteppedAlphaDescending-Z.txt

Go

Translation of: Wren
package main

import (
    "fmt"
    "strconv"
    "strings"
    "unicode/utf8"
)

func sign(n int) int {
    switch {
    case n < 0:
        return -1
    case n > 0:
        return 1
    }
    return 0
}

func abs(n int) int {
    if n < 0 {
        return -n
    }
    return n
}

func parseRange(r string) []string {
    if r == "" {
        return []string{"{}"} // rangeless, empty
    }
    sp := strings.Split(r, "..")
    if len(sp) == 1 {
        return []string{"{" + r + "}"} // rangeless, random value
    }
    sta := sp[0]
    end := sp[1]
    inc := "1"
    if len(sp) > 2 {
        inc = sp[2]
    }
    n1, ok1 := strconv.Atoi(sta)
    n2, ok2 := strconv.Atoi(end)
    n3, ok3 := strconv.Atoi(inc)
    if ok3 != nil {
        return []string{"{" + r + "}"} // increment isn't a number
    }
    numeric := (ok1 == nil) && (ok2 == nil)
    if !numeric {
        if (ok1 == nil && ok2 != nil) || (ok1 != nil && ok2 == nil) {
            return []string{"{" + r + "}"} // mixed numeric/alpha not expanded
        }
        if utf8.RuneCountInString(sta) != 1 || utf8.RuneCountInString(end) != 1 {
            return []string{"{" + r + "}"} // start/end are not both single alpha
        }
        n1 = int(([]rune(sta))[0])
        n2 = int(([]rune(end))[0])
    }
    width := 1
    if numeric {
        if len(sta) < len(end) {
            width = len(end)
        } else {
            width = len(sta)
        }
    }
    if n3 == 0 { // zero increment
        if numeric {
            return []string{fmt.Sprintf("%0*d", width, n1)}
        } else {
            return []string{sta}
        }
    }
    var res []string
    asc := n1 < n2
    if n3 < 0 {
        asc = !asc
        t := n1
        d := abs(n1-n2) % (-n3)
        n1 = n2 - d*sign(n2-n1)
        n2 = t
        n3 = -n3
    }
    i := n1
    if asc {
        for ; i <= n2; i += n3 {
            if numeric {
                res = append(res, fmt.Sprintf("%0*d", width, i))
            } else {
                res = append(res, string(rune(i)))
            }
        }
    } else {
        for ; i >= n2; i -= n3 {
            if numeric {
                res = append(res, fmt.Sprintf("%0*d", width, i))
            } else {
                res = append(res, string(rune(i)))
            }
        }
    }
    return res
}

func rangeExpand(s string) []string {
    res := []string{""}
    rng := ""
    inRng := false
    for _, c := range s {
        if c == '{' && !inRng {
            inRng = true
            rng = ""
        } else if c == '}' && inRng {
            rngRes := parseRange(rng)
            rngLen := len(rngRes)
            var res2 []string
            for i := 0; i < len(res); i++ {
                for j := 0; j < rngLen; j++ {
                    res2 = append(res2, res[i]+rngRes[j])
                }
            }
            res = res2
            inRng = false
        } else if inRng {
            rng += string(c)
        } else {
            for i := 0; i < len(res); i++ {
                res[i] += string(c)
            }
        }
    }
    if inRng {
        for i := 0; i < len(res); i++ {
            res[i] += "{" + rng // unmatched braces
        }
    }
    return res
}

func main() {
    examples := []string{
        "simpleNumberRising{1..3}.txt",
        "simpleAlphaDescending-{Z..X}.txt",
        "steppedDownAndPadded-{10..00..5}.txt",
        "minusSignFlipsSequence {030..20..-5}.txt",
        "reverseSteppedNumberRising{1..6..-2}.txt",
        "combined-{Q..P}{2..1}.txt",
        "emoji{🌵..🌶}{🌽..🌾}etc",
        "li{teral",
        "rangeless{}empty",
        "rangeless{random}string",
        "mixedNumberAlpha{5..k}",
        "steppedAlphaRising{P..Z..2}.txt",
        "stops after endpoint-{02..10..3}.txt",
        "steppedNumberRising{1..6..2}.txt",
        "steppedNumberDescending{20..9..2}",
        "steppedAlphaDescending-{Z..M..2}.txt",
        "reversedSteppedAlphaDescending-{Z..M..-2}.txt",
    }
    for _, s := range examples {
        fmt.Print(s, "->\n    ")
        res := rangeExpand(s)
        fmt.Println(strings.Join(res, "\n    "))
        fmt.Println()
    }
}
Output:
Same as Wren entry.

JavaScript

(() => {
    "use strict";

    // -------------- BRACE-RANGE EXPANSION --------------

    // braceExpandWithRange :: String -> [String]
    const braceExpandWithRange = s => {
        // A list containing either the expansions
        // of s, if there are any, or s itself.
        const
            expansions = parse(some(
                braceRangeExpansion()
            ))(s);

        return 0 < expansions.length ? (() => {
            const [parsed, residue] = expansions[0];

            return suffixAdd(
                parsed.reduce(
                    uncurry(suffixMultiply),
                    [""]
                )
            )([residue.join("")]);
        })() : [s];
    };


    // ---------- BRACE-RANGE EXPANSION PARSER -----------

    // braceRangeExpansion :: [String]
    const braceRangeExpansion = () =>
        // List of strings expanded from a
        // a unix shell {<START>..<END>} or
        // {<START>..<END>..<INCR>} expression.
        // See https://wiki.bash-hackers.org/syntax/expansion/brace
        fmapP(([preamble, amble, postscript]) =>
            suffixAdd(
                suffixMultiply(preamble)(amble)
            )(postscript)
        )(sequenceP([
            affixLeaf(),
            fmapP(xs => [xs])(
                between(char("{"))(char("}"))(
                    altP(
                        numericSequence()
                    )(
                        characterSequence()
                    )
                )
            ),
            affixLeaf()
        ]));


    // ---------------------- TESTS ----------------------
    // main :: IO ()
    const main = () => {
        const tests = [
            "simpleNumberRising{1..3}.txt",
            "simpleAlphaDescending-{Z..X}.txt",
            "steppedDownAndPadded-{10..00..5}.txt",
            "minusSignFlipsSequence {030..20..-5}.txt",
            "reverseSteppedNumberRising{1..6..-2}.txt",
            "combined-{Q..P}{2..1}.txt",
            "emoji{🌵..🌶}{🌽..🌾}etc",
            "li{teral",
            "rangeless{}empty",
            "rangeless{random}string"
        ];

        return tests.map(s => {
                const
                    expanded = braceExpandWithRange(s)
                    .join("\n\t");

                return `${s} -> \n\t${expanded}`;
            })
            .join("\n\n");
    };


    // ---------- BRACE-RANGE COMPONENT PARSERS ----------

    // affixLeaf :: () -> Parser String
    const affixLeaf = () =>
        // A sequence of literal (non-syntactic)
        // characters before or after a pair of braces.
        fmapP(cs => [
            [cs.join("")]
        ])(
            many(choice([noneOf("{\\"), escape()]))
        );


    // characterSequence :: () -> Parser [Char]
    const characterSequence = () =>
        // A rising or descending alphabetic
        // sequence of characters.
        fmapP(ab => {
            const [from, to] = ab;

            return from !== to ? (
                enumFromThenToChar(from)(
                    (from < to ? succ : pred)(from)
                )(to)
            ) : [from];
        })(
            ordinalRange(satisfy(
                c => !"0123456789".includes(c)
            ))
        );


    // enumerationList :: ((Bool, String), String) ->
    // ((Bool, String), String) ->
    // ((Bool, String), String) -> [String]
    const enumerationList = triple => {
        // An ordered list of numeric strings either
        // rising or descending, in numeric order, and
        // possibly prefixed with zeros.
        const
            w = padWidth(triple[0][1])(triple[1][1]),
            [from, to, by] = triple.map(
                sn => (sn[0] ? negate : identity)(
                    parseInt(sn[1], 10)
                )
            );

        return map(
            compose(justifyRight(w)("0"), str)
        )(
            (
                0 > by ? (
                    reverse
                ) : identity
            )(
                enumFromThenTo(from)(
                    from + (
                        to < from ? (
                            -abs(by)
                        ) : abs(by)
                    )
                )(to)
            )
        );
    };


    // numericPart :: () -> Parser (Bool, String)
    const numericPart = () =>
        // The Bool is True if the string is
        // negated by a leading '-'
        // The String component contains the digits.
        bindP(
            option("")(char("-"))
        )(sign => bindP(
            some(digit())
        )(ds => pureP(
            Tuple(Boolean(sign))(concat(ds))
        )));


    // numericSequence :: () -> Parser [String]
    const numericSequence = () =>
        // An ascending or descending sequence
        // of numeric strings, possibly
        // left-padded with zeros.
        fmapP(enumerationList)(sequenceP([
            ordinalRange(numericPart()),
            numericStep()
        ]));


    // numericStep :: () -> Parser (Bool, Int)
    const numericStep = () =>
        //  The size of increment for a numeric
        //  series. Descending if the Bool is True.
        //  Defaults to (False, 1).
        option(Tuple(false)(1))(
            bindP(
                string("..")
            )(() => bindP(
                numericPart()
            )(pureP))
        );


    // ordinalRange :: Enum a =>
    // Parser a -> Parser (a, a)
    const ordinalRange = p =>
        // A pair of enumerable values of the same
        // type, representing the start and end of
        // a range.
        bindP(
            p
        )(from => bindP(
            string("..")
        )(() => bindP(
            p
        )(compose(pureP, append([from])))));


    // padWidth :: String -> String -> Int
    const padWidth = cs =>
        // The length of the first of cs and cs1 to
        // start with a zero. Otherwise (if neither
        // starts with a zero) then 0.
        cs1 => [cs, cs1].reduce(
            (a, x) => (0 < a) || (1 > x.length) ? (
                a
            ) : "0" !== x[0] ? a : x.length,
            0
        );


    // suffixAdd :: [String] -> [String] -> [String]
    const suffixAdd = xs =>
        ys => xs.flatMap(
            flip(append)(ys)
        );


    // suffixMultiply :: [String] -> [String] -> [String]
    const suffixMultiply = xs =>
        apList(xs.map(append));


    // ----------- GENERIC PARSER COMBINATORS ------------

    // Parser :: String -> [(a, String)] -> Parser a
    const Parser = f =>
        // A function lifted into a Parser object.
        ({
            type: "Parser",
            parser: f
        });


    // altP (<|>) :: Parser a -> Parser a -> Parser a
    const altP = p =>
        // p, or q if p doesn't match.
        q => Parser(s => {
            const xs = parse(p)(s);

            return 0 < xs.length ? (
                xs
            ) : parse(q)(s);
        });


    // apP <*> :: Parser (a -> b) -> Parser a -> Parser b
    const apP = pf =>
        // A new parser obtained by the application
        // of a Parser-wrapped function,
        // to a Parser-wrapped value.
        p => Parser(
            s => parse(pf)(s).flatMap(
                ([v, r]) => parse(
                    fmapP(v)(p)
                )(r)
            )
        );


    // between :: Parser open -> Parser close ->
    // Parser a -> Parser a
    const between = pOpen =>
        // A version of p which matches between
        // pOpen and pClose (both discarded).
        pClose => p => bindP(
            pOpen
        )(() => bindP(
            p
        )(x => bindP(
            pClose
        )(() => pureP(x))));


    // bindP (>>=) :: Parser a ->
    // (a -> Parser b) -> Parser b
    const bindP = p =>
        // A new parser obtained by the application of
        // a function to a Parser-wrapped value.
        // The function must enrich its output, lifting it
        // into a new Parser.
        // Allows for the nesting of parsers.
        f => Parser(
            s => parse(p)(s).flatMap(
                ([x, r]) => parse(f(x))(r)
            )
        );


    // char :: Char -> Parser Char
    const char = x =>
        // A particular single character.
        satisfy(c => x === c);


    // choice :: [Parser a] -> Parser a
    const choice = ps =>
        // A parser constructed from a
        // (left to right) list of alternatives.
        ps.reduce(uncurry(altP), emptyP());


    // digit :: Parser Char
    const digit = () =>
        // A single digit.
        satisfy(isDigit);


    // emptyP :: () -> Parser a
    const emptyP = () =>
        // The empty list.
        Parser(() => []);


    // escape :: Parser String
    const escape = () =>
        fmapP(xs => xs.join(""))(
            sequenceP([char("\\"), item()])
        );


    // fmapP :: (a -> b) -> Parser a -> Parser b
    const fmapP = f =>
        // A new parser derived by the structure-preserving
        // application of f to the value in p.
        p => Parser(
            s => parse(p)(s).flatMap(
                first(f)
            )
        );


    // item :: () -> Parser Char
    const item = () =>
        // A single character.
        Parser(s => {
            const [h, ...t] = s;

            return Boolean(h) ? [
                Tuple(h)(t)
            ] : [];
        });


    // liftA2P :: (a -> b -> c) ->
    // Parser a -> Parser b -> Parser c
    const liftA2P = op =>
        // The binary function op, lifted
        // to a function over two parsers.
        p => apP(fmapP(op)(p));


    // many :: Parser a -> Parser [a]
    const many = p => {
        // Zero or more instances of p.
        // Lifts a parser for a simple type of value
        // to a parser for a list of such values.
        const someP = q =>
            liftA2P(
                x => xs => [x].concat(xs)
            )(q)(many(q));

        return Parser(
            s => parse(
                0 < s.length ? (
                    altP(someP(p))(pureP([]))
                ) : pureP([])
            )(s)
        );
    };


    // noneOf :: String -> Parser Char
    const noneOf = s =>
        // Any character not found in the
        // exclusion string.
        satisfy(c => !s.includes(c));


    // option :: a -> Parser a -> Parser a
    const option = x =>
        // Either p or the default value x.
        p => altP(p)(pureP(x));


    // parse :: Parser a -> String -> [(a, String)]
    const parse = p =>
        // The result of parsing s with p.
        s => p.parser([...s]);


    // pureP :: a -> Parser a
    const pureP = x =>
        // The value x lifted, unchanged,
        // into the Parser monad.
        Parser(s => [Tuple(x)(s)]);


    // satisfy :: (Char -> Bool) -> Parser Char
    const satisfy = test =>
        // Any character for which the
        // given predicate returns true.
        Parser(s => {
            const [h, ...t] = s;

            return Boolean(h) ? (
                test(h) ? [
                    Tuple(h)(t)
                ] : []
            ) : [];
        });

    // sequenceP :: [Parser a] -> Parser [a]
    const sequenceP = ps =>
        // A single parser for a list of values, derived
        // from a list of parsers for single values.
        Parser(
            s => ps.reduce(
                (a, q) => a.flatMap(
                    ([v, r]) => parse(q)(r).flatMap(
                        first(xs => v.concat(xs))
                    )
                ),
                [Tuple([])(s)]
            )
        );


    // some :: Parser a -> Parser [a]
    const some = p => {
        // One or more instances of p.
        // Lifts a parser for a simple type of value
        // to a parser for a list of such values.
        const manyP = q =>
            altP(some(q))(pureP([]));

        return Parser(
            s => parse(
                liftA2P(
                    x => xs => [x].concat(xs)
                )(p)(manyP(p))
            )(s)
        );
    };


    // string :: String -> Parser String
    const string = s =>
        // A particular string.
        fmapP(cs => cs.join(""))(
            sequenceP([...s].map(char))
        );


    // ---------------- GENERAL FUNCTIONS ----------------

    // Tuple (,) :: a -> b -> (a, b)
    const Tuple = a =>
        b => ({
            type: "Tuple",
            "0": a,
            "1": b,
            length: 2,
            *[Symbol.iterator]() {
                for (const k in this) {
                    if (!isNaN(k)) {
                        yield this[k];
                    }
                }
            }
        });


    // abs :: Num -> Num
    const abs =
        // Absolute value of a given number
        // without the sign.
        x => 0 > x ? (
            -x
        ) : x;


    // apList (<*>) :: [(a -> b)] -> [a] -> [b]
    const apList = fs =>
        // The sequential application of each of a list
        // of functions to each of a list of values.
        // apList([x => 2 * x, x => 20 + x])([1, 2, 3])
        //     -> [2, 4, 6, 21, 22, 23]
        xs => fs.flatMap(f => xs.map(f));


    // append (++) :: [a] -> [a] -> [a]
    const append = xs =>
        // A list obtained by the
        // concatenation of two others.
        ys => xs.concat(ys);


    // compose (<<<) :: (b -> c) -> (a -> b) -> a -> c
    const compose = (...fs) =>
        // A function defined by the right-to-left
        // composition of all the functions in fs.
        fs.reduce(
            (f, g) => x => f(g(x)),
            x => x
        );


    // concat :: [[a]] -> [a]
    const concat = xs =>
        xs.flat(1);


    // enumFromThenTo :: Int -> Int -> Int -> [Int]
    const enumFromThenTo = m =>
        // Integer values enumerated from m to n
        // with a step defined by (nxt - m).
        nxt => n => {
            const d = nxt - m;

            return Array.from({
                length: (Math.floor(n - nxt) / d) + 2
            }, (_, i) => m + (d * i));
        };


    // enumFromThenToChar :: Char -> Char -> Char -> [Char]
    const enumFromThenToChar = x1 =>
        x2 => y => {
            const [i1, i2, iY] = Array.from([x1, x2, y])
                .map(x => x.codePointAt(0)),
                d = i2 - i1;

            return Array.from({
                length: (Math.floor(iY - i2) / d) + 2
            }, (_, i) => String.fromCodePoint(i1 + (d * i)));
        };


    // first :: (a -> b) -> ((a, c) -> (b, c))
    const first = f =>
        // A simple function lifted to one which applies
        // to a tuple, transforming only its first item.
        ([x, y]) => Tuple(f(x))(y);


    // flip :: (a -> b -> c) -> b -> a -> c
    const flip = op =>
        // The binary function op with
        // its arguments reversed.
        1 < op.length ? (
            (a, b) => op(b, a)
        ) : (x => y => op(y)(x));


    // fromEnum :: Enum a => a -> Int
    const fromEnum = x =>
        typeof x !== "string" ? (
            x.constructor === Object ? (
                x.value
            ) : parseInt(Number(x), 10)
        ) : x.codePointAt(0);


    // identity :: a -> a
    const identity = x =>
        // The identity function. (`id`, in Haskell)
        x;


    // isDigit :: Char -> Bool
    const isDigit = c => {
        const n = c.codePointAt(0);

        return 48 <= n && 57 >= n;
    };


    // justifyRight :: Int -> Char -> String -> String
    const justifyRight = n =>
        // The string s, preceded by enough padding (with
        // the character c) to reach the string length n.
        c => s => n > s.length ? (
            s.padStart(n, c)
        ) : s;


    // map :: (a -> b) -> [a] -> [b]
    const map = f =>
        // The list obtained by applying f
        // to each element of xs.
        // (The image of xs under f).
        xs => [...xs].map(f);


    // maxBound :: a -> a
    const maxBound = x => {
        const e = x.enum;

        return Boolean(e) ? (
            e[e[x.max]]
        ) : {
            "number": Number.MAX_SAFE_INTEGER,
            "string": String.fromCodePoint(0x10FFFF),
            "boolean": true
        } [typeof x];
    };

    // minBound :: a -> a
    const minBound = x => {
        const e = x.enum;

        return Boolean(e) ? (
            e[e[0]]
        ) : {
            "number": Number.MIN_SAFE_INTEGER,
            "string": String.fromCodePoint(0),
            "boolean": false
        } [typeof x];
    };


    // negate :: Num -> Num
    const negate = n =>
        -n;


    // pred :: Enum a => a -> a
    const pred = x => {
        const t = typeof x;

        return "number" !== t ? (() => {
            const [i, mn] = [x, minBound(x)].map(fromEnum);

            return i > mn ? (
                toEnum(x)(i - 1)
            ) : Error("succ :: enum out of range.");
        })() : x > Number.MIN_SAFE_INTEGER ? (
            x - 1
        ) : Error("succ :: Num out of range.");
    };


    // reverse :: [a] -> [a]
    const reverse = xs =>
        xs.slice(0).reverse();


    // str :: a -> String
    const str = x =>
        Array.isArray(x) && x.every(
            v => ("string" === typeof v) && (1 === v.length)
        ) ? (
            x.join("")
        ) : x.toString();


    // succ :: Enum a => a -> a
    const succ = x => {
        const t = typeof x;

        return "number" !== t ? (
            (() => {
                const [i, mx] = [x, maxBound(x)].map(
                    fromEnum
                );

                return i < mx ? (
                    toEnum(x)(1 + i)
                ) : Error("succ :: enum out of range.");
            })()
        ) : x < Number.MAX_SAFE_INTEGER ? (
            1 + x
        ) : Error("succ :: Num out of range.");
    };


    // toEnum :: a -> Int -> a
    const toEnum = e =>
        // The first argument is a sample of the type
        // allowing the function to make the right mapping
        x => ({
            "number": Number,
            "string": String.fromCodePoint,
            "boolean": Boolean,
            "object": v => e.min + v
        } [typeof e])(x);


    // uncurry :: (a -> b -> c) -> ((a, b) -> c)
    const uncurry = f =>
        // A function over a pair, derived
        // from a curried function.
        (...args) => {
            const
                xy = Boolean(args.length % 2) ? (
                    args[0]
                ) : args;

            return f(xy[0])(xy[1]);
        };

    // MAIN ---
    return main();
})();
Output:
simpleNumberRising{1..3}.txt -> 
    simpleNumberRising1.txt
    simpleNumberRising2.txt
    simpleNumberRising3.txt

simpleAlphaDescending-{Z..X}.txt -> 
    simpleAlphaDescending-Z.txt
    simpleAlphaDescending-Y.txt
    simpleAlphaDescending-X.txt

steppedDownAndPadded-{10..00..5}.txt -> 
    steppedDownAndPadded-10.txt
    steppedDownAndPadded-05.txt
    steppedDownAndPadded-00.txt

minusSignFlipsSequence {030..20..-5}.txt -> 
    minusSignFlipsSequence 020.txt
    minusSignFlipsSequence 025.txt
    minusSignFlipsSequence 030.txt

reverseSteppedNumberRising{1..6..-2}.txt -> 
    reverseSteppedNumberRising5.txt
    reverseSteppedNumberRising3.txt
    reverseSteppedNumberRising1.txt

combined-{Q..P}{2..1}.txt -> 
    combined-Q2.txt
    combined-Q1.txt
    combined-P2.txt
    combined-P1.txt

emoji{🌵..🌶}{🌽..🌾}etc -> 
    emoji🌵🌽etc
    emoji🌵🌾etc
    emoji🌶🌽etc
    emoji🌶🌾etc

li{teral -> 
    li{teral

rangeless{}empty -> 
    rangeless{}empty

rangeless{random}string -> 
    rangeless{random}string

jq

Works with jq, the C implementation of jq

Works with gojq, the Go implementation of jq

This implementation relies on "reluctant" regex parsing.

Range expressions of the form {x..y}, where x and y are single characters, are allowed, even if exactly one of them is a digit.

When expanding an expression with more than one range, the program as given below produces an ordering based on expansion of the left-most range first. A trivial change in two places is sufficient to produce the alternative ordering.

# Left-pad with 0s
def lpad($len): tostring | ($len - length) as $l | ("0" * $l) + .;

def expand:
  # The key to success here is reluctance (".*?")
  def cap:
    capture("(?<head>^.*?)[{](?<from>[0-9]+|.)[.][.](?<to>[0-9]+|.)"
    + "([.][.](?<sign>-)?(?<increment>[0-9]))?[}](?<tail>.*)$");

  def ton: if . == null then . else tonumber end;

  # Produce a stream of integers, handling implicit descent.
  # $i and $j should be integers.
  # If $i and $j are distinct, then expand($i;$j;null;null) will include both,
  # otherwise just $i.
  def expand($i; $j; $sign; $increment):
    (if $increment == null then 1 else $increment end) as $inc
    | if $sign == null
      then if $i <= $j 
            then range($i; $j + 1;  $inc)
            else range($i; $j - 1; - $inc)
            end
      else [expand($i; $j; null; $increment)] | reverse[]
      end ;

  # Produce a stream of single characters, handling implicit descent
  def explode($x; $y; $sign; $increment):
    ($x|explode[0]) as $x
    | ($y|explode[0]) as $y
    | expand($x; $y; $sign; $increment)
    | [.] | implode;

  # The number of leading 0s of the input string
  def leadingZeros: match("^0*") | .string | length;
  
  def padding($x; $y):
    ($x | leadingZeros) as $a
    |  ($y | leadingZeros) as $b
    | [if $a > 0 then ($x|length) else 0 end,
       if $b > 0 then ($y|length) else 0 end]
    | max;
      
  ( cap as $c
    | if ($c.from|test("[0-9]+")) and ($c.to|test("[0-9]+"))
      then  padding($c.from; $c.to) as $padding
      | $c.head
        + ( expand($c.from|tonumber;
                   $c.to|tonumber;
                   $c.sign;
                   $c.increment | ton) | lpad($padding))
        + ($c.tail | expand)
      elif ($c.from|length == 1) and ($c.to|length == 1)
      then $c.head + explode($c.from; $c.to; $c.sign; $c.increment|ton)
        + ($c.tail | expand)
      else ""
      end )
  // . ;

def examples:
    "simpleNumberRising{1..3}.txt",
    "simpleAlphaDescending-{Z..X}.txt",
    "steppedDownAndPadded-{10..00..5}.txt",
    "minusSignFlipsSequence {030..20..-5}.txt",
    "reverseSteppedNumberRising{1..6..-2}.txt",
    "combined-{Q..P}{2..1}.txt",
    "emoji{🌵..🌶}{🌽..🌾}etc",
    "li{teral",
    "rangeless{}empty",
    "rangeless{random}string",
    "mixedNumberAlpha{5..k}",
    "steppedAlphaRising{P..Z..2}.txt",
    "stops after endpoint-{02..10..3}.txt",
    "steppedNumberRising{1..6..2}.txt",
    "steppedNumberDescending{20..9..2}",
    "steppedAlphaDescending-{Z..M..2}.txt",
    "reversedSteppedAlphaDescending-{Z..M..-2}.txt"
;

examples
| "\(.) ->",
  "  \(expand)", ""
Output:
simpleNumberRising{1..3}.txt ->
  simpleNumberRising1.txt
  simpleNumberRising2.txt
  simpleNumberRising3.txt

simpleAlphaDescending-{Z..X}.txt ->
  simpleAlphaDescending-Z.txt
  simpleAlphaDescending-Y.txt
  simpleAlphaDescending-X.txt

steppedDownAndPadded-{10..00..5}.txt ->
  steppedDownAndPadded-10.txt
  steppedDownAndPadded-05.txt
  steppedDownAndPadded-00.txt

minusSignFlipsSequence {030..20..-5}.txt ->
  minusSignFlipsSequence 020.txt
  minusSignFlipsSequence 025.txt
  minusSignFlipsSequence 030.txt

reverseSteppedNumberRising{1..6..-2}.txt ->
  reverseSteppedNumberRising5.txt
  reverseSteppedNumberRising3.txt
  reverseSteppedNumberRising1.txt

combined-{Q..P}{2..1}.txt ->
  combined-Q2.txt
  combined-P2.txt
  combined-Q1.txt
  combined-P1.txt

emoji{🌵..🌶}{🌽..🌾}etc ->
  emoji🌵🌽etc
  emoji🌶🌽etc
  emoji🌵🌾etc
  emoji🌶🌾etc

li{teral ->
  li{teral

rangeless{}empty ->
  rangeless{}empty

rangeless{random}string ->
  rangeless{random}string

mixedNumberAlpha{5..k} ->
  mixedNumberAlpha5
  mixedNumberAlpha6
  mixedNumberAlpha7
  mixedNumberAlpha8
  mixedNumberAlpha9
  mixedNumberAlpha:
  mixedNumberAlpha;
  mixedNumberAlpha<
  mixedNumberAlpha=
  mixedNumberAlpha>
  mixedNumberAlpha?
  mixedNumberAlpha@
  mixedNumberAlphaA
  mixedNumberAlphaB
  mixedNumberAlphaC
  mixedNumberAlphaD
  mixedNumberAlphaE
  mixedNumberAlphaF
  mixedNumberAlphaG
  mixedNumberAlphaH
  mixedNumberAlphaI
  mixedNumberAlphaJ
  mixedNumberAlphaK
  mixedNumberAlphaL
  mixedNumberAlphaM
  mixedNumberAlphaN
  mixedNumberAlphaO
  mixedNumberAlphaP
  mixedNumberAlphaQ
  mixedNumberAlphaR
  mixedNumberAlphaS
  mixedNumberAlphaT
  mixedNumberAlphaU
  mixedNumberAlphaV
  mixedNumberAlphaW
  mixedNumberAlphaX
  mixedNumberAlphaY
  mixedNumberAlphaZ
  mixedNumberAlpha[
  mixedNumberAlpha\
  mixedNumberAlpha]
  mixedNumberAlpha^
  mixedNumberAlpha_
  mixedNumberAlpha`
  mixedNumberAlphaa
  mixedNumberAlphab
  mixedNumberAlphac
  mixedNumberAlphad
  mixedNumberAlphae
  mixedNumberAlphaf
  mixedNumberAlphag
  mixedNumberAlphah
  mixedNumberAlphai
  mixedNumberAlphaj
  mixedNumberAlphak

steppedAlphaRising{P..Z..2}.txt ->
  steppedAlphaRisingP.txt
  steppedAlphaRisingR.txt
  steppedAlphaRisingT.txt
  steppedAlphaRisingV.txt
  steppedAlphaRisingX.txt
  steppedAlphaRisingZ.txt

stops after endpoint-{02..10..3}.txt ->
  stops after endpoint-02.txt
  stops after endpoint-05.txt
  stops after endpoint-08.txt

steppedNumberRising{1..6..2}.txt ->
  steppedNumberRising1.txt
  steppedNumberRising3.txt
  steppedNumberRising5.txt

steppedNumberDescending{20..9..2} ->
  steppedNumberDescending20
  steppedNumberDescending18
  steppedNumberDescending16
  steppedNumberDescending14
  steppedNumberDescending12
  steppedNumberDescending10

steppedAlphaDescending-{Z..M..2}.txt ->
  steppedAlphaDescending-Z.txt
  steppedAlphaDescending-X.txt
  steppedAlphaDescending-V.txt
  steppedAlphaDescending-T.txt
  steppedAlphaDescending-R.txt
  steppedAlphaDescending-P.txt
  steppedAlphaDescending-N.txt

reversedSteppedAlphaDescending-{Z..M..-2}.txt ->
  reversedSteppedAlphaDescending-N.txt
  reversedSteppedAlphaDescending-P.txt
  reversedSteppedAlphaDescending-R.txt
  reversedSteppedAlphaDescending-T.txt
  reversedSteppedAlphaDescending-V.txt
  reversedSteppedAlphaDescending-X.txt
  reversedSteppedAlphaDescending-Z.txt

Julia

padzeros(str) = (len = length(str)) > 1 && str[1] == '0' ? len : 0

function ranged(str)
    rang = filter(!isempty, split(str, r"\{|\}|\.\."))
    delta = length(rang) > 2 ? parse(Int, rang[3]) : 1
    if delta < 0
        rang[1], rang[2], delta = rang[2], rang[1], -delta
    end
    if '0' <= rang[1][1] <= '9' || rang[1][1] == '-'
        try x, y = parse(Int, rang[1]), parse(Int, rang[2]) catch; return [str] end
        pad = max(padzeros(rang[1]), padzeros(rang[2]))
        return [string(x, pad=pad) for x in range(x, step=(x < y) ? delta : -delta, stop=y)]
    else
        x, y, z = rang[1][end], rang[2][end], rang[1][1:end-1]
        return [z * string(x) for x in range(x, step=(x < y) ? delta : -delta, stop=y)]
    end
end

function splatrange(s)
    m = match(r"([^\{]*)(\{[^}]+\.\.[^\}]+\})(.*)", s)
    m == nothing && return [s]
    c = m.captures
    return vec([a * b for b in splatrange(c[3]), a in [c[1] * x for x in ranged(c[2])]])
end

for test in [
    "simpleNumberRising{1..3}.txt",
    "simpleAlphaDescending-{Z..X}.txt",
    "steppedDownAndPadded-{10..00..5}.txt",
    "minusSignFlipsSequence {030..20..-5}.txt",
    "combined-{Q..P}{2..1}.txt",
    "emoji{🌵..🌶}{🌽..🌾}etc",
    "li{teral",
    "rangeless{}empty",
    "rangeless{random}string",
    "mixedNumberAlpha{5..k}",
    "steppedAlphaRising{P..Z..2}.txt",
    "stops after endpoint-{02..10..3}.txt",
    ]
    println(test, "->\n", ["    " * x * "\n" for x in splatrange(test)]...)
end
Output:
simpleNumberRising{1..3}.txt->
    simpleNumberRising1.txt
    simpleNumberRising2.txt
    simpleNumberRising3.txt

simpleAlphaDescending-{Z..X}.txt->
    simpleAlphaDescending-Z.txt
    simpleAlphaDescending-Y.txt
    simpleAlphaDescending-X.txt

steppedDownAndPadded-{10..00..5}.txt->
    steppedDownAndPadded-10.txt
    steppedDownAndPadded-05.txt
    steppedDownAndPadded-00.txt

minusSignFlipsSequence {030..20..-5}.txt->
    minusSignFlipsSequence 020.txt
    minusSignFlipsSequence 025.txt
    minusSignFlipsSequence 030.txt

combined-{Q..P}{2..1}.txt->
    combined-Q2.txt
    combined-Q1.txt
    combined-P2.txt
    combined-P1.txt

emoji{🌵..🌶}{🌽..🌾}etc->
    emoji🌵🌽etc
    emoji🌵🌾etc
    emoji🌶🌽etc
    emoji🌶🌾etc

li{teral->
    li{teral

rangeless{}empty->
    rangeless{}empty

rangeless{random}string->
    rangeless{random}string

mixedNumberAlpha{5..k}->
    mixedNumberAlpha{5..k}

steppedAlphaRising{P..Z..2}.txt->
    steppedAlphaRisingP.txt
    steppedAlphaRisingR.txt
    steppedAlphaRisingT.txt
    steppedAlphaRisingV.txt
    steppedAlphaRisingX.txt
    steppedAlphaRisingZ.txt

stops after endpoint-{02..10..3}.txt->
    stops after endpoint-02.txt
    stops after endpoint-05.txt
    stops after endpoint-08.txt

Nim

Translation of: Wren
import options, strutils, unicode


func intFromString(s: string): Option[int] =
  ## Try to parse an int. Return some(int) if parsing
  ## was successful, return none(int) if it failed.
  try:
    let n = s.parseInt()
    result = some(n)
  except ValueError:
    result = none(int)


func parseRange(r: string): seq[string] =

  if r.len == 0: return @["{}"]   # rangeless, empty.
  let sp = r.split("..")
  if sp.len == 1: return @['{' & r & '}']
  let first = sp[0]
  let last = sp[1]
  let incr = if sp.len == 2: "1" else: sp[2]

  let val1 = intFromString(first)
  let val2 = intFromString(last)
  let val3 = intFromString(incr)

  if val3.isNone(): return @['{' & r & '}']   # increment isn't a number.
  var n3 = val3.get()
  let numeric = val1.isSome and val2.isSome

  var n1, n2: int
  if numeric:
    n1 = val1.get()
    n2 = val2.get()
  else:
    if val1.isSome and val2.isNone or val1.isNone and val2.isSome:
      return @['{' & r & '}']   # mixed numeric/alpha not expanded.
    if first.runeLen != 1 or last.runeLen != 1:
      return @['{' & r & '}']   # start/end are not both single alpha.
    n1 = first.toRunes[0].int
    n2 = last.toRunes[0].int

  var width = 1
  if numeric:
    width = if first.len < last.len: last.len else: first.len

  if n3 == 0:
    # Zero increment.
    return if numeric: @[n1.intToStr(width)] else: @[first]

  var asc = n1 < n2
  if n3 < 0:
    asc = not asc
    swap n1, n2
    n3 = -n3

  var i = n1
  if asc:
    while i <= n2:
      result.add if numeric: i.intToStr(width) else: $Rune(i)
      inc i, n3
  else:
    while i >= n2:
      result.add if numeric: i.intToStr(width) else: $Rune(i)
      dec i, n3


func rangeExpand(s: string): seq[string] =
  
  result = @[""]
  var rng = ""
  var inRng = false

  for c in s:
    if c == '{' and not inRng:
      inRng = true
      rng = ""
    elif c == '}' and inRng:
      let rngRes = rng.parseRange()
      var res: seq[string]
      for i in 0..result.high:
        for j in 0..rngRes.high:
          res.add result[i] & rngRes[j]
      result = move(res)
      inRng = false
    elif inRng:
      rng.add c
    else:
      for s in result.mitems: s.add c

  if inRng:
    for s in result.mitems: s.add '{' & rng   # unmatched braces.


when isMainModule:

  const Examples = ["simpleNumberRising{1..3}.txt",
                    "simpleAlphaDescending-{Z..X}.txt",
                    "steppedDownAndPadded-{10..00..5}.txt",
                    "minusSignFlipsSequence {030..20..-5}.txt",
                    "combined-{Q..P}{2..1}.txt",
                    "emoji{🌵..🌶}{🌽..🌾}etc",
                    "li{teral",
                    "rangeless{}empty",
                    "rangeless{random}string",
                    "mixedNumberAlpha{5..k}",
                    "steppedAlphaRising{P..Z..2}.txt",
                    "stops after endpoint-{02..10..3}.txt"]

  for s in Examples:
    stdout.write s, " →\n    "
    let res = rangeExpand(s)
    stdout.write res.join("\n    ")
    echo '\n'
Output:
simpleNumberRising{1..3}.txt →
    simpleNumberRising1.txt
    simpleNumberRising2.txt
    simpleNumberRising3.txt

simpleAlphaDescending-{Z..X}.txt →
    simpleAlphaDescending-Z.txt
    simpleAlphaDescending-Y.txt
    simpleAlphaDescending-X.txt

steppedDownAndPadded-{10..00..5}.txt →
    steppedDownAndPadded-10.txt
    steppedDownAndPadded-05.txt
    steppedDownAndPadded-00.txt

minusSignFlipsSequence {030..20..-5}.txt →
    minusSignFlipsSequence 020.txt
    minusSignFlipsSequence 025.txt
    minusSignFlipsSequence 030.txt

combined-{Q..P}{2..1}.txt →
    combined-Q2.txt
    combined-Q1.txt
    combined-P2.txt
    combined-P1.txt

emoji{🌵..🌶}{🌽..🌾}etc →
    emoji🌵🌽etc
    emoji🌵🌾etc
    emoji🌶🌽etc
    emoji🌶🌾etc

li{teral →
    li{teral

rangeless{}empty →
    rangeless{}empty

rangeless{random}string →
    rangeless{random}string

mixedNumberAlpha{5..k} →
    mixedNumberAlpha{5..k}

steppedAlphaRising{P..Z..2}.txt →
    steppedAlphaRisingP.txt
    steppedAlphaRisingR.txt
    steppedAlphaRisingT.txt
    steppedAlphaRisingV.txt
    steppedAlphaRisingX.txt
    steppedAlphaRisingZ.txt

stops after endpoint-{02..10..3}.txt →
    stops after endpoint-02.txt
    stops after endpoint-05.txt
    stops after endpoint-08.txt

Phix

Translation of: Wren
requires("0.8.2") -- (is_integer() is new, plus "==sign(inc)" found me a long-buried compiler bug)
 
function parse_range(string r)
    sequence sp = split(r,"..")&{"1"},
             res = {}
    if length(sp)>=3 then 
        string {strange,ending,step} = sp
        integer inc = to_integer(step)
        if inc!=0 then
            bool ns = is_integer(strange),
                 ne = is_integer(ending)
            if ns=ne then
                if ns then
                    integer s = to_integer(strange),
                            e = to_integer(ending),
                            w = max(length(strange),length(ending))
                    if inc<0 then {s,e,inc} = {e,s,-inc} end if
                    if s>e then inc *= -1 end if
                    integer zfill = (length(strange)>1 and strange[1]='0') or
                                    (length(ending)>1 and ending[1]='0')
                    string fmt = iff(zfill?sprintf("%%0%dd",{w}):"%d")
                    for k=s to e by inc do
                        res = append(res,sprintf(fmt,k))
                    end for
                    return res
                elsif length(strange)=length(ending) then
                    bool ok = (length(strange)=1)
                    if not ok then
                        object s32 = utf8_to_utf32(strange,-1),
                               e32 = utf8_to_utf32(ending,-1)
                        if sequence(s32) and length(s32)=1
                        and sequence(e32) and length(e32)=1 then
                            ok = true
                        end if
                    end if
                    if ok then
                        if strange>ending then inc *= -1 end if
                        while true do
                            res = append(res,strange)
                            integer sdx = length(strange)
                            while true do
                                integer ch = strange[sdx]+inc
                                if ch<=#FF and ch>=#00 then
                                    strange[sdx] = ch
                                    exit
                                end if
                                strange[sdx] = iff(inc<0?#FF:#00)
                                sdx -= 1
                            end while
                            if compare(strange,ending)==sign(inc) then exit end if
                            if length(res)>10 then ?9/0 end if -- (sanity check)
                        end while
                        return res
                    end if -- ([utf8] strings not single char)
                end if -- (neither numeric nor same-length alpha)
            end if -- (mixed numeric/alpha)
        end if -- (non-numeric increment)
    end if -- (rangeless)
    return {"{"&r&"}"}
end function
 
function range_expand(string s)
    sequence res = {""}
    string range = ""
    bool in_range = false
    for k=1 to length(s) do
        integer c = s[k]
        if c == '{' and not in_range then
            in_range = true
            range = ""
        elsif c == '}' and in_range then
            sequence range_res = parse_range(range),
                     prev_res = res
            res = {}
            for i=1 to length(prev_res) do
                for j=1 to length(range_res) do
                    res = append(res, prev_res[i] & range_res[j])
                end for
            end for
            in_range = false
        elsif in_range then
            range &= c
        else
            for i=1 to length(res) do
                res[i] &= c
            end for
        end if
    end for
    if in_range then
        for i=1 to length(res) do
            res[i] &= "{" & range // unmatched braces
        end for
    end if
    return res
end function
 
constant examples = {
    "simpleNumberRising{1..3}.txt",
    "simpleAlphaDescending-{Z..X}.txt",
    "steppedDownAndPadded-{10..00..5}.txt",
    "minusSignFlipsSequence {030..20..-5}.txt",
    "combined-{Q..P}{2..1}.txt",
    "emoji{🌵..🌶}{🌽..🌾}etc",
    "multi char emoji ranges fail {🌵🌵..🌵🌶}",
    "li{teral",
    "rangeless{}empty",
    "rangeless{random}string",
    "mixedNumberAlpha{5..k}",
    "steppedAlphaRising{P..Z..2}.txt",
    "stops after endpoint-{02..10..3}.txt"
}
 
for i=1 to length(examples) do
    string s = examples[i]
    printf(1,"%s ->\n    %s\n",{s,join(range_expand(s),"\n    ")})
end for
Output:

Note that, as usual, unicode output does not look good on a windows console for tests 6 & 7 (linux output shown)

simpleNumberRising{1..3}.txt ->
    simpleNumberRising1.txt
    simpleNumberRising2.txt
    simpleNumberRising3.txt
simpleAlphaDescending-{Z..X}.txt ->
    simpleAlphaDescending-Z.txt
    simpleAlphaDescending-Y.txt
    simpleAlphaDescending-X.txt
steppedDownAndPadded-{10..00..5}.txt ->
    steppedDownAndPadded-10.txt
    steppedDownAndPadded-05.txt
    steppedDownAndPadded-00.txt
minusSignFlipsSequence {030..20..-5}.txt ->
    minusSignFlipsSequence 020.txt
    minusSignFlipsSequence 025.txt
    minusSignFlipsSequence 030.txt
combined-{Q..P}{2..1}.txt ->
    combined-Q2.txt
    combined-Q1.txt
    combined-P2.txt
    combined-P1.txt
emoji{🌵..🌶}{🌽..🌾}etc ->
    emoji🌵🌽etc
    emoji🌵🌾etc
    emoji🌶🌽etc
    emoji🌶🌾etc
multi char emoji ranges fail {🌵🌵..🌵🌶} ->
    multi char emoji ranges fail {🌵🌵..🌵🌶}
li{teral ->
    li{teral
rangeless{}empty ->
    rangeless{}empty
rangeless{random}string ->
    rangeless{random}string
mixedNumberAlpha{5..k} ->
    mixedNumberAlpha{5..k}
steppedAlphaRising{P..Z..2}.txt ->
    steppedAlphaRisingP.txt
    steppedAlphaRisingR.txt
    steppedAlphaRisingT.txt
    steppedAlphaRisingV.txt
    steppedAlphaRisingX.txt
    steppedAlphaRisingZ.txt
stops after endpoint-{02..10..3}.txt ->
    stops after endpoint-02.txt
    stops after endpoint-05.txt
    stops after endpoint-08.txt

Python

"""Brace expansion using ranges. Requires Python >= 3.6.

Here we use regular expressions for parsing and take an object orientated approach
to expansion of range expressions.

This implementation supports stepped ordinal range expressions.
"""

from __future__ import annotations

import itertools
import re

from abc import ABC
from abc import abstractmethod

from typing import Iterable
from typing import Optional


RE_SPEC = [
    (
        "INT_RANGE",
        r"\{(?P<int_start>[0-9]+)..(?P<int_stop>[0-9]+)(?:(?:..)?(?P<int_step>-?[0-9]+))?}",
    ),
    (
        "ORD_RANGE",
        r"\{(?P<ord_start>[^0-9])..(?P<ord_stop>[^0-9])(?:(?:..)?(?P<ord_step>-?[0-9]+))?}",
    ),
    (
        "LITERAL",
        r".+?(?=\{|$)",
    ),
]


RE_EXPRESSION = re.compile(
    "|".join(rf"(?P<{name}>{pattern})" for name, pattern in RE_SPEC)
)


class Expression(ABC):
    """Brace expression abstract base class."""

    @abstractmethod
    def expand(self, prefix: str) -> Iterable[str]:
        pass


class Literal(Expression):
    """An expression literal."""

    def __init__(self, value: str):
        self.value = value

    def expand(self, prefix: str) -> Iterable[str]:
        return [f"{prefix}{self.value}"]


class IntRange(Expression):
    """An integer range expression."""

    def __init__(
        self, start: int, stop: int, step: Optional[int] = None, zfill: int = 0
    ):
        self.start, self.stop, self.step = fix_range(start, stop, step)
        self.zfill = zfill

    def expand(self, prefix: str) -> Iterable[str]:
        return (
            f"{prefix}{str(i).zfill(self.zfill)}"
            for i in range(self.start, self.stop, self.step)
        )


class OrdRange(Expression):
    """An ordinal range expression."""

    def __init__(self, start: str, stop: str, step: Optional[int] = None):
        self.start, self.stop, self.step = fix_range(ord(start), ord(stop), step)

    def expand(self, prefix: str) -> Iterable[str]:
        return (f"{prefix}{chr(i)}" for i in range(self.start, self.stop, self.step))


def expand(expressions: Iterable[Expression]) -> Iterable[str]:
    """Expand a sequence of ``Expression``s. Each expression builds on the results
    of the expressions that come before it in the sequence."""
    expanded = [""]

    for expression in expressions:
        expanded = itertools.chain.from_iterable(
            [expression.expand(prefix) for prefix in expanded]
        )

    return expanded


def zero_fill(start, stop) -> int:
    """Return the target zero padding width."""

    def _zfill(s):
        if len(s) <= 1 or not s.startswith("0"):
            return 0
        return len(s)

    return max(_zfill(start), _zfill(stop))


def fix_range(start, stop, step):
    """Transform start, stop and step so that we can pass them to Python's
    built-in ``range`` function."""
    if not step:
        # Zero or None. Explicit zero gets changed to default.
        if start <= stop:
            # Default step for ascending ranges.
            step = 1
        else:
            # Default step for descending ranges.
            step = -1

    elif step < 0:
        # A negative step means we reverse the range.
        start, stop = stop, start

        if start < stop:
            step = abs(step)
        else:
            start -= 1
            stop -= 1

    elif start > stop:
        # A descending range with explicit step.
        step = -step

    # Don't overshoot or fall short.
    if (start - stop) % step == 0:
        stop += step

    return start, stop, step


def parse(expression: str) -> Iterable[Expression]:
    """Generate a sequence of ``Expression``s from the given range expression."""
    for match in RE_EXPRESSION.finditer(expression):
        kind = match.lastgroup

        if kind == "INT_RANGE":
            start = match.group("int_start")
            stop = match.group("int_stop")
            step = match.group("int_step")
            zfill = zero_fill(start, stop)

            if step is not None:
                step = int(step)

            yield IntRange(int(start), int(stop), step, zfill=zfill)

        elif kind == "ORD_RANGE":
            start = match.group("ord_start")
            stop = match.group("ord_stop")
            step = match.group("ord_step")

            if step is not None:
                step = int(step)

            yield OrdRange(start, stop, step)

        elif kind == "LITERAL":
            yield Literal(match.group())


def examples():
    cases = [
        r"simpleNumberRising{1..3}.txt",
        r"simpleAlphaDescending-{Z..X}.txt",
        r"steppedDownAndPadded-{10..00..5}.txt",
        r"minusSignFlipsSequence {030..20..-5}.txt",
        r"reverseSteppedNumberRising{1..6..-2}.txt",
        r"combined-{Q..P}{2..1}.txt",
        r"emoji{🌵..🌶}{🌽..🌾}etc",
        r"li{teral",
        r"rangeless{}empty",
        r"rangeless{random}string",
        # Extra examples, not from the task description.
        r"steppedNumberRising{1..6..2}.txt",
        r"steppedNumberDescending{20..9..2}.txt",
        r"steppedAlphaDescending-{Z..M..2}.txt",
        r"reverseSteppedAlphaRising{A..F..-2}.txt",
        r"reversedSteppedAlphaDescending-{Z..M..-2}.txt",
    ]

    for case in cases:
        print(f"{case} ->")
        expressions = parse(case)

        for itm in expand(expressions):
            print(f"{' '*4}{itm}")

        print("")  # Blank line between cases


if __name__ == "__main__":
    examples()
Output:
simpleNumberRising{1..3}.txt ->
    simpleNumberRising1.txt
    simpleNumberRising2.txt
    simpleNumberRising3.txt

simpleAlphaDescending-{Z..X}.txt ->
    simpleAlphaDescending-Z.txt
    simpleAlphaDescending-Y.txt
    simpleAlphaDescending-X.txt

steppedDownAndPadded-{10..00..5}.txt ->
    steppedDownAndPadded-10.txt
    steppedDownAndPadded-05.txt
    steppedDownAndPadded-00.txt

minusSignFlipsSequence {030..20..-5}.txt ->
    minusSignFlipsSequence 020.txt
    minusSignFlipsSequence 025.txt
    minusSignFlipsSequence 030.txt

reverseSteppedNumberRising{1..6..-2}.txt ->
    reverseSteppedNumberRising5.txt
    reverseSteppedNumberRising3.txt
    reverseSteppedNumberRising1.txt

combined-{Q..P}{2..1}.txt ->
    combined-Q2.txt
    combined-Q1.txt
    combined-P2.txt
    combined-P1.txt

emoji{🌵..🌶}{🌽..🌾}etc ->
    emoji🌵🌽etc
    emoji🌵🌾etc
    emoji🌶🌽etc
    emoji🌶🌾etc

li{teral ->
    li{teral

rangeless{}empty ->
    rangeless{}empty

rangeless{random}string ->
    rangeless{random}string

steppedNumberRising{1..6..2}.txt ->
    steppedNumberRising1.txt
    steppedNumberRising3.txt
    steppedNumberRising5.txt

steppedNumberDescending{20..9..2}.txt ->
    steppedNumberDescending20.txt
    steppedNumberDescending18.txt
    steppedNumberDescending16.txt
    steppedNumberDescending14.txt
    steppedNumberDescending12.txt
    steppedNumberDescending10.txt

steppedAlphaDescending-{Z..M..2}.txt ->
    steppedAlphaDescending-Z.txt
    steppedAlphaDescending-X.txt
    steppedAlphaDescending-V.txt
    steppedAlphaDescending-T.txt
    steppedAlphaDescending-R.txt
    steppedAlphaDescending-P.txt
    steppedAlphaDescending-N.txt

reverseSteppedAlphaRising{A..F..-2}.txt ->
    reverseSteppedAlphaRisingE.txt
    reverseSteppedAlphaRisingC.txt
    reverseSteppedAlphaRisingA.txt

reversedSteppedAlphaDescending-{Z..M..-2}.txt ->
    reversedSteppedAlphaDescending-M.txt
    reversedSteppedAlphaDescending-O.txt
    reversedSteppedAlphaDescending-Q.txt
    reversedSteppedAlphaDescending-S.txt
    reversedSteppedAlphaDescending-U.txt
    reversedSteppedAlphaDescending-W.txt
    reversedSteppedAlphaDescending-Y.txt

Raku

Works with: Rakudo version 2020.08.1

Also implements some of the string list functions described on the bash-hackers page.

my $range = rx/ '{' $<start> = <-[.]>+? '..' $<end> = <-[.]>+? ['..' $<incr> = ['-'?\d+] ]? '}' /;
my $list  = rx/ ^ $<prefix> = .*? '{' (<-[,}]>+) +%% ',' '}' $<postfix> = .* $/;

sub expand (Str $string) {
    my @return = $string;
    if $string ~~ $range {
        quietly my ($start, $end, $incr) = $/<start end incr>».Str;
        $incr ||= 1;
        ($end, $start) = $start, $end if $incr < 0;
        $incr.=abs;

        if try all( +$start, +$end ) ~~ Numeric {
            $incr = - $incr if $start > $end;

            my ($sl, $el) = 0, 0;
            $sl = $start.chars if $start.starts-with('0');
            $el = $end.chars   if   $end.starts-with('0');

            my @this = $start < $end ?? (+$start, * + $incr …^ * > +$end) !! (+$start, * + $incr …^ * < +$end);
            @return  = @this.map: { $string.subst($range, sprintf("%{'0' ~ max $sl, $el}d", $_) ) }
        }
        elsif try +$start ~~ Numeric or +$end ~~ Numeric {
            return $string #fail
        }
        else {
            my @this;
            if $start.chars + $end.chars > 2 {
                return $string if $start.succ eq $start or $end.succ eq $end; # fail
                @this = $start lt $end ?? ($start, (*.succ xx $incr).tail …^ * gt $end) !! ($start, (*.pred xx $incr).tail …^ * lt $end);
            }
            else {
                $incr = -$incr if $start gt $end;
                @this = $start lt $end ?? ($start, (*.ord + $incr).chr …^ * gt $end) !! ($start, (*.ord + $incr).chr …^ * lt $end);
            }
            @return = @this.map: { $string.subst($range, sprintf("%s", $_) ) }
        }
    }
    if $string ~~ $list {
        my $these = $/[0]».Str;
        my ($prefix, $postfix) = $/<prefix postfix>».Str;
        if ($prefix ~ $postfix).chars {
            @return = $these.map: { $string.subst($list, $prefix ~ $_ ~ $postfix) } if $these.elems > 1
        }
        else {
            @return = $these.join: ' '
        }
    }
    my $cnt = 1;
    while $cnt != +@return {
        $cnt = +@return;
        @return.=map: { |.&expand }
    }
    @return
}

for qww<
    # Required tests

    simpleNumberRising{1..3}.txt
    simpleAlphaDescending-{Z..X}.txt
    steppedDownAndPadded-{10..00..5}.txt
    minusSignFlipsSequence{030..20..-5}.txt
    combined-{Q..P}{2..1}.txt
    emoji{🌵..🌶}{🌽..🌾}etc
    li{teral
    rangeless{}empty
    rangeless{random}string

    # Test some other features

    'stop point not in sequence-{02..10..3}.txt'
    steppedAlphaRising{P..Z..2}.txt
    'simple {just,give,me,money} list'
    {thatʼs,what,I,want}
    'emoji {☃,☄}{★,🇺🇸,☆} lists'
    'alphanumeric mix{ab7..ac1}.txt'
    'alphanumeric mix{0A..0C}.txt'

    # fail by design

    'mixed terms fail {7..C}.txt'
    'multi char emoji ranges fail {🌵🌵..🌵🌶}'
  > -> $test {
     say "$test ->";
     say ('    ' xx * Z~ expand $test).join: "\n";
     say '';
}
Output:
simpleNumberRising{1..3}.txt ->
    simpleNumberRising1.txt
    simpleNumberRising2.txt
    simpleNumberRising3.txt

simpleAlphaDescending-{Z..X}.txt ->
    simpleAlphaDescending-Z.txt
    simpleAlphaDescending-Y.txt
    simpleAlphaDescending-X.txt

steppedDownAndPadded-{10..00..5}.txt ->
    steppedDownAndPadded-10.txt
    steppedDownAndPadded-05.txt
    steppedDownAndPadded-00.txt

minusSignFlipsSequence{030..20..-5}.txt ->
    minusSignFlipsSequence020.txt
    minusSignFlipsSequence025.txt
    minusSignFlipsSequence030.txt

combined-{Q..P}{2..1}.txt ->
    combined-Q2.txt
    combined-Q1.txt
    combined-P2.txt
    combined-P1.txt

emoji{🌵..🌶}{🌽..🌾}etc ->
    emoji🌵🌽etc
    emoji🌵🌾etc
    emoji🌶🌽etc
    emoji🌶🌾etc

li{teral ->
    li{teral

rangeless{}empty ->
    rangeless{}empty

rangeless{random}string ->
    rangeless{random}string

stop point not in sequence-{02..10..3}.txt ->
    stop point not in sequence-02.txt
    stop point not in sequence-05.txt
    stop point not in sequence-08.txt

steppedAlphaRising{P..Z..2}.txt ->
    steppedAlphaRisingP.txt
    steppedAlphaRisingR.txt
    steppedAlphaRisingT.txt
    steppedAlphaRisingV.txt
    steppedAlphaRisingX.txt
    steppedAlphaRisingZ.txt

simple {just,give,me,money} list ->
    simple just list
    simple give list
    simple me list
    simple money list

{thatʼs,what,I,want} ->
    thatʼs what I want

emoji {☃,☄}{★,🇺🇸,☆} lists ->
    emoji ☃★ lists
    emoji ☃🇺🇸 lists
    emoji ☃☆ lists
    emoji ☄★ lists
    emoji ☄🇺🇸 lists
    emoji ☄☆ lists

alphanumeric mix{ab7..ac1}.txt ->
    alphanumeric mixab7.txt
    alphanumeric mixab8.txt
    alphanumeric mixab9.txt
    alphanumeric mixac0.txt
    alphanumeric mixac1.txt

alphanumeric mix{0A..0C}.txt ->
    alphanumeric mix0A.txt
    alphanumeric mix0B.txt
    alphanumeric mix0C.txt

mixed terms fail {7..C}.txt ->
    mixed terms fail {7..C}.txt

multi char emoji ranges fail {🌵🌵..🌵🌶} ->
    multi char emoji ranges fail {🌵🌵..🌵🌶}

Wren

Library: Wren-fmt

Added a few more examples to the minimum number needed for the task.

import "./fmt" for Fmt

var parseRange = Fn.new { |r|
    if (r == "") return ["{}"] // rangeless, empty
    var sp = r.split("..")
    if (sp.count == 1) return ["{%(r)}"] // rangeless, random value
    var sta = sp[0]
    var end = sp[1]
    var inc = (sp.count == 2) ? "1" : sp[2]
    var n1 = Num.fromString(sta)
    var n2 = Num.fromString(end)
    var n3 = Num.fromString(inc)
    if (!n3) return ["{%(r)}"] // increment isn't a number
    var numeric = n1 && n2
    if (!numeric) {
        if ((n1 && !n2) || (!n1 && n2)) return ["{%(r)}"] // mixed numeric/alpha not expanded
        if (sta.count != 1 || end.count != 1) return ["{%(r)}"] // start/end are not both single alpha
        n1 = sta.codePoints[0]
        n2 = end.codePoints[0]
    }
    var width = 1
    if (numeric) width = (sta.count < end.count) ? end.count : sta.count
    if (n3 == 0) return (numeric) ? [Fmt.dz(width, n1)] : [sta] // zero increment
    var res = []
    var asc = n1 < n2
    if (n3 < 0) {
        asc = !asc
        var t = n1
        var d = (n1 - n2).abs % (-n3)
        n1 = n2 - d * (n2 - n1).sign
        n2 = t
        n3 = -n3
    }
    var i = n1
    if (asc) {
        while (i <= n2) {
            res.add( (numeric) ? Fmt.dz(width, i) : String.fromCodePoint(i) )
            i = i + n3
        }
    } else {
        while (i >= n2) {
            res.add(( numeric) ? Fmt.dz(width, i) : String.fromCodePoint(i) )
            i = i - n3
        }
    }
    return res
}

var rangeExpand = Fn.new { |s|
    var res = [""]
    var rng = ""
    var inRng = false
    for (c in s) {
        if (c == "{" && !inRng) {
            inRng = true
            rng = ""
        } else if (c == "}" && inRng) {
            var rngRes = parseRange.call(rng)
            var rngCount = rngRes.count
            var res2 = []
            for (i in 0...res.count) {
                for (j in 0...rngCount) res2.add(res[i] + rngRes[j])
            }
            res = res2
            inRng = false
        } else if (inRng) {
            rng = rng + c
        } else {
            for (i in 0...res.count) res[i] = res[i] + c
        }
    }
    if (inRng) for (i in 0...res.count) res[i] = res[i] + "{" + rng // unmatched braces
    return res
}

var examples = [
    "simpleNumberRising{1..3}.txt",
    "simpleAlphaDescending-{Z..X}.txt",
    "steppedDownAndPadded-{10..00..5}.txt",
    "minusSignFlipsSequence {030..20..-5}.txt",
    "reverseSteppedNumberRising{1..6..-2}.txt",
    "combined-{Q..P}{2..1}.txt",
    "emoji{🌵..🌶}{🌽..🌾}etc",
    "li{teral",
    "rangeless{}empty",
    "rangeless{random}string",
    "mixedNumberAlpha{5..k}",
    "steppedAlphaRising{P..Z..2}.txt",
    "stops after endpoint-{02..10..3}.txt",
    "steppedNumberRising{1..6..2}.txt",
    "steppedNumberDescending{20..9..2}",
    "steppedAlphaDescending-{Z..M..2}.txt",
    "reversedSteppedAlphaDescending-{Z..M..-2}.txt"
]

for (s in examples) {
    System.write("%(s) ->\n    ")
    var res = rangeExpand.call(s)
    System.print(res.join("\n    "))
    System.print()
}
Output:
simpleNumberRising{1..3}.txt ->
    simpleNumberRising1.txt
    simpleNumberRising2.txt
    simpleNumberRising3.txt

simpleAlphaDescending-{Z..X}.txt ->
    simpleAlphaDescending-Z.txt
    simpleAlphaDescending-Y.txt
    simpleAlphaDescending-X.txt

steppedDownAndPadded-{10..00..5}.txt ->
    steppedDownAndPadded-10.txt
    steppedDownAndPadded-05.txt
    steppedDownAndPadded-00.txt

minusSignFlipsSequence {030..20..-5}.txt ->
    minusSignFlipsSequence 020.txt
    minusSignFlipsSequence 025.txt
    minusSignFlipsSequence 030.txt

reverseSteppedNumberRising{1..6..-2}.txt ->
    reverseSteppedNumberRising5.txt
    reverseSteppedNumberRising3.txt
    reverseSteppedNumberRising1.txt

combined-{Q..P}{2..1}.txt ->
    combined-Q2.txt
    combined-Q1.txt
    combined-P2.txt
    combined-P1.txt

emoji{🌵..🌶}{🌽..🌾}etc ->
    emoji🌵🌽etc
    emoji🌵🌾etc
    emoji🌶🌽etc
    emoji🌶🌾etc

li{teral ->
    li{teral

rangeless{}empty ->
    rangeless{}empty

rangeless{random}string ->
    rangeless{random}string

mixedNumberAlpha{5..k} ->
    mixedNumberAlpha{5..k}

steppedAlphaRising{P..Z..2}.txt ->
    steppedAlphaRisingP.txt
    steppedAlphaRisingR.txt
    steppedAlphaRisingT.txt
    steppedAlphaRisingV.txt
    steppedAlphaRisingX.txt
    steppedAlphaRisingZ.txt

stops after endpoint-{02..10..3}.txt ->
    stops after endpoint-02.txt
    stops after endpoint-05.txt
    stops after endpoint-08.txt

steppedNumberRising{1..6..2}.txt ->
    steppedNumberRising1.txt
    steppedNumberRising3.txt
    steppedNumberRising5.txt

steppedNumberDescending{20..9..2} ->
    steppedNumberDescending20
    steppedNumberDescending18
    steppedNumberDescending16
    steppedNumberDescending14
    steppedNumberDescending12
    steppedNumberDescending10

steppedAlphaDescending-{Z..M..2}.txt ->
    steppedAlphaDescending-Z.txt
    steppedAlphaDescending-X.txt
    steppedAlphaDescending-V.txt
    steppedAlphaDescending-T.txt
    steppedAlphaDescending-R.txt
    steppedAlphaDescending-P.txt
    steppedAlphaDescending-N.txt

reversedSteppedAlphaDescending-{Z..M..-2}.txt ->
    reversedSteppedAlphaDescending-N.txt
    reversedSteppedAlphaDescending-P.txt
    reversedSteppedAlphaDescending-R.txt
    reversedSteppedAlphaDescending-T.txt
    reversedSteppedAlphaDescending-V.txt
    reversedSteppedAlphaDescending-X.txt
    reversedSteppedAlphaDescending-Z.txt