Powershell equivalent of Bash Brace Expansion for generating lists/arrays
Asked Answered
G

4

23

When writing a Bash script you can use brace expansion to quickly generate lists:

Bash Brace Expansion

What is the simplest way to generate a similar list in Powershell? I can use the .. or , operators to generate an array, but how can I prefix the items with a static string literal?

PS C:\Users\gb> 1..5
1
2
3
4
5

PS C:\Users\gb> "test"+1..5
test1 2 3 4 5

PS C:\Users\gb> "test","dev","prod"
test
dev
prod

PS C:\Users\gb> "asdf"+"test","dev","prod"
asdftest dev prod
Groundless answered 25/4, 2013 at 18:50 Comment(0)
R
29
PS C:\> "test","dev","prod" | % { "server-$_" }
server-test
server-dev
server-prod
PS C:\> 1..5 | % { "server{0:D2}" -f $_ }
server01
server02
server03
server04
server05
PS C:\> 1..5 | % { "192.168.0.$_" }
192.168.0.1
192.168.0.2
192.168.0.3
192.168.0.4
192.168.0.5

Note that % is an alias for the ForEach-Object cmdlet.

Rehearse answered 25/4, 2013 at 21:4 Comment(2)
How would you do rn file{,.old}?Yung
Like this? : $cmdargs = $(('','.old') | % { "test.txt$($_)".ToString() }) ; Rename-Item @cmdargsYung
S
10

I'm hoping to be proven wrong here, but I don't believe there is a way to do it exactly like with bash, or with as few keystrokes.

You can iterate over the list by piping it through a foreach-object to achieve the same result though.

1..5 | foreach-object { "test" + $_ }

Or using the shorthand:

1..5 | %{"test$_"}

In both cases (% is an alias for foreach-object), the output is:

test1
test2
test3
test4
test5

Note: if you're building this into a script for publishing/distribution/reuse, use the more verbose foreach-object, not the shorthand % - for readability.

Servile answered 25/4, 2013 at 18:57 Comment(4)
1..5 | %{ "test$_" } is even shorter still.Sommer
Correct you are - edited my code. I really need to break that concatenation habit.Servile
Yeah I thought about just using for each, but it was a bit too verbose. 1..5 | %{"server{0:00}" -f $_} is short enough that I'll probably just use that unless someone can think of a better solution. Thanks!Groundless
foreach ($x in $y) {stuff} does get verbose, but for some things it's the way to go (and would be valid here). In this case, foreach-object with the pipeline is much less verbose.Servile
K
4

I have a way to do it using int's tostring method. The '000' at the end is a special format code. It always pads to the right number of zeros. You can also use wildcards with method names like t*g if you really want to be terse and mysterious.

1..10 | % tostring computer000

computer001
computer002
computer003
computer004
computer005
computer006
computer007
computer008
computer009
computer010


1..10 | % t*g 192\.168\.1\.0

192.168.1.1
192.168.1.2
192.168.1.3
192.168.1.4
192.168.1.5
192.168.1.6
192.168.1.7
192.168.1.8
192.168.1.9
192.168.1.10

'x' is also a format code for hex printing.

10..15 | % tostring x

a
b
c
d
e
f

There's always -replace, which also works on arrays. '^' is regex for 'beginning of line'. Use '$' instead for 'end of line'.

(echo test dev prod) -replace '^','server-'

server-test
server-dev
server-prod

Hah, I never tried this before.

(echo test dev prod) -replace (echo ^ server-)

server-test
server-dev
server-prod

Maybe they could do that brace expansion in powershell 8...

Kudos answered 16/6, 2020 at 15:7 Comment(0)
B
1

Major Edit: Updated to support multiple expansions in a single string. Should be a little safer not using Invoke-Expression for the number range.

I don't know how "Production-Ready" this is but it emulates the original syntax as much as possible.

This adds a special expand() method to the core string type which means I have committed a grave sin. If you want the function version please skip to the bottom.

Unfortunately, native Powershell cmdlets do not allow splatting unless the value is assigned to a variable first. So you can't do "do-thing "{foo,bar}".expand() but you could do "$foobar = "{foo,bar}".expand(); do-thing @foobar".

However, it does pair well with destructuring "$foo, $bar = "{foo,bar}".expand()" and pipelines "{foo,bar,baz}.txt".expand() | New-Item -Name { $_ }. That pipeline uses delayed binding to assigned the piped values to the correct parameter since the cmdlet can't do so itself.

$ExpandMethod = [scriptblock] {
    
    $Expandables = [Regex]::Matches($this, "{([^}]+)}")  # Match everything between curlies that isn't a right curly
    $Strings = @($this)  # Seed initial value for foreach loop

    foreach ($Expandable in $Expandables) {

        # Return array based on whether we're working with strings
        # or numbers.
        $Transforms = switch ($Expandable.Groups[1].Value) {
            { $_.Contains(',')  } { $_.Split(',') }
            { $_.Contains('..') } { [int]$Start, [int]$End = $_ -split '\.\.'; $Start..$End }
            default { throw [System.InvalidOperationException] "Could not determine how to expand string." }
        }

        $TempStrings = @()
        foreach ($Transform in $Transforms) {
            foreach ($String in $Strings) {
                $TempStrings += $String -Replace $Expandable.Value, $Transform
            }
        }

        # Overwrite to ensure that expandables in the next run only used
        # transformed strings.
        $Strings = $TempStrings
    }

    return $Strings
}

Update-TypeData -TypeName 'System.String' `
    -MemberType 'ScriptMethod' `
    -MemberName 'Expand' `
    -Value $ExpandMethod `
    -Force

"/{foo,bar}/{bim,bam}/test{1..3}.txt".expand()

# Result
#> /foo/bim/test1.txt
#> /bar/bim/test1.txt
#> /foo/bam/test1.txt
#> /bar/bam/test1.txt
#> /foo/bim/test2.txt
#> /bar/bim/test2.txt
#> /foo/bam/test2.txt
#> /bar/bam/test2.txt
#> /foo/bim/test3.txt
#> /bar/bim/test3.txt
#> /foo/bam/test3.txt
#> /bar/bam/test3.txt

Oh, did you think we were done? Perish the thought dear reader, I shall not go quietly into the night but screaming with this final blasphemous act.

Behold as we use the function version of expand() to take our command as a script block, expand the provided string, insert it into the script block and since it is a variable, it can now be splatted against our command, circumventing the restrictions mentioned above.

function Expand-StringBrace {
    [CmdletBinding()]
    [Alias('expand')]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [String] $Command,
        
        [Parameter(Mandatory,ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [String] $String,

        [Parameter(ValueFromRemainingArguments,DontShow)]
        [String] $UnboundArgs
    )
    
    $Expandables = [Regex]::Matches($String, "{([^}]+)}")  # Match everything between curlies that isn't a right curly
    $Strings = @($String)  # Seed initial value for foreach loop

    foreach ($Expandable in $Expandables) {

        # Return array based on whether we're working with strings
        # or numbers.
        $Transforms = switch ($Expandable.Groups[1].Value) {
            { $_.Contains(',')  } { $_.Split(',') }
            { $_.Contains('..') } { [int]$Start, [int]$End = $_ -split '\.\.'; $Start..$End }
            default { throw [System.InvalidOperationException] "Could not determine how to expand string." }
        }

        $TempStrings = @()
        foreach ($Transform in $Transforms) {
            foreach ($String in $Strings) {
                $TempStrings += $String -Replace $Expandable.Value, $Transform
            }
        }

        # Overwrite to ensure that expandables in the next run only used
        # transformed strings.
        $Strings = $TempStrings
    }

    # Create script that splats the given command with the parsed brace expansion
    # and passes the remaining args to the command.
    $ScriptBlock = [ScriptBlock]::Create("$Command @_ @UnboundArgs")

    $PSVars = @([psvariable]::new('_', $Strings), [psvariable]::new('UnboundArgs', $UnboundArgs))
    $ScriptBlock.InvokeWithContext($null, $PSVars)
}

new-item test.txt
expand Rename-Item "test{.txt,.log}" -Verbose
# or
#"test{.txt,.log}" | expand Rename-Item
Backbite answered 5/10, 2023 at 0:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.