25

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

4 Answers 4

31
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.

Sign up to request clarification or add additional context in comments.

2 Comments

How would you do rn file{,.old}?
Like this? : $cmdargs = $(('','.old') | % { "test.txt$($_)".ToString() }) ; Rename-Item @cmdargs
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.

4 Comments

1..5 | %{ "test$_" } is even shorter still.
Correct you are - edited my code. I really need to break that concatenation habit.
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!
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.
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...

Comments

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

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.