Friday Fun: Expand Environmental Variables in PowerShell Strings

This week I was working on a project that involved using the %PATH% environmental variable. The challenge was that I have some entries that look like this: %SystemRoot%\system32\WindowsPowerShell\v1.0\. When I try to use that path in PowerShell, it complains because it doesn’t expand %SystemRoot%. What I needed was a way to replace it with the actual value, which I can find in the ENV: PSdrive, or reference as $env:systemroot. This seems reasonable enough. Take a string, use a regular expression to find the environmental variable, find the variable in ENV:, do a replacement, write the revised string back to the pipeline. So here I have Resolve-EnvVariable.

Function Resolve-EnvVariable {

[cmdletbinding()]
Param(
[Parameter(Position=0,ValueFromPipeline=$True,Mandatory=$True,
HelpMessage="Enter a string that contains an environmental variable like %WINDIR%")]
[ValidateNotNullOrEmpty()]
[string]$String
)

Begin {
Write-Verbose "Starting $($myinvocation.mycommand)"
} #Begin

Process {
#if string contains a % then process it
if ($string -match "%\S+%") {
Write-Verbose "Resolving environmental variables in $String"
#split string into an array of values
$values=$string.split("%") | Where {$_}
foreach ($text in $values) {
#find the corresponding value in ENV:
Write-Verbose "Looking for $text"
[string]$replace=(Get-Item env:$text -erroraction "SilentlyContinue").Value
if ($replace) {
#if found append it to the new string
Write-Verbose "Found $replace"
$newstring+=$replace
}
else {
#otherwise append the original text
$newstring+=$text
}

} #foreach value

Write-Verbose "Writing revised string to the pipeline"
#write the string back to the pipeline
Write-Output $NewString
} #if
else {
#skip the string and write it back to the pipeline
Write-Output $String
}
} #Process

End {
Write-Verbose "Ending $($myinvocation.mycommand)"
} #End
} #end Resolve-EnvVariable

The function takes a string as a parameter, or you can pipe into the function. The function looks to see if there is something that might be an environmental variable using a regular expression match.


if ($string -match "%\S+%") {

If there is an extra % character in the string, this won’t work so I’m assuming you have some control over what you provide as input. Now I need to get the match value. At first I tried using the Regex object. But when faced with a string like this “I am %username% and working on %computername%” it also tried to turn % and working on% as an environmental variable. I’m sure there’s a regex pattern that will work but I found it just as easy to split the string on the % character and trim off the extra space.


$values=$string.split("%") | Where {$_}

Now, I can go through each value and see if there is a corresponding environmental variable.


foreach ($text in $values) {
#find the corresponding value in ENV:
Write-Verbose "Looking for $text"
[string]$replace=(Get-Item env:$text -erroraction "SilentlyContinue").Value

I turned off the error pipeline to suppress errors about unfound entries. If something was found then I do a simple replace, otherwise, I re-use the original text.


if ($replace) {
#if found append it to the new string
Write-Verbose "Found $replace"
$newstring+=$replace
}
else {
#otherwise append the original text
$newstring+=$text
}

In essence I am building a new string adding the replacement values or original text. When finished I can write the new string, which has the variable replacements back to the pipeline.


Write-Verbose "Writing revised string to the pipeline"
#write the string back to the pipeline
Write-Output $NewString

Finally, I can pass strings that contain environmental variables to the function.


PS C:\> "I am %username% and working on %computername%" | resolve-envvariable
I am Jeff and working on SERENITY

This isn’t perfect. Look what happens if there is an undefined variable:


PS C:\> "I am %username% and working on %computername% with a %bogus% variable." | resolve-envvariable
I am Jeff and working on SERENITY with a bogus variable.

But as long as you are confident that variables are defined, then you can do things like this:


PS C:\> $env:path.split(";") | Resolve-EnvVariable | foreach { if (-Not (Test-Path $_)) {$_}}
c:\foo

Download Resolve-EnvVariable and let me know what you think. The download version includes comment based help.