Reputation: 15737
When writing a Bash script you can use brace expansion to quickly generate lists:
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
Upvotes: 23
Views: 6307
Reputation: 872
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
Upvotes: 1
Reputation: 27516
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...
Upvotes: 4
Reputation: 24585
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.
Upvotes: 29
Reputation: 28194
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.
Upvotes: 10