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
rn file{,.old}
? – Yung