Delayed resolution of embedded PS variables within a parm

by scottbass at 2013-04-25 23:11:59


Given this test script:

[string] $pgm
[string] $log

$pgm = Get-ChildItem $pgm
if (! $log) {$log = $pgm.basename + "" + $(Get-Date -UFormat '%Y%m%d%H%M%S') + ".log"}
else {$log = Invoke-Expression "Write-Output $log"}


If a -log is not specified, the script builds a timestamped log based on the supplied pgm. Within the script, $pgm is converted to a System.IO.FileInfo object (the real script also accepts pipeline input).

I want the user to be able to override the default log value, include advanced approaches such as embedded Powershell parameters within single quotes.

For example, if the above script is named foo.ps1, I’ve tried:

.\foo.ps1 -pgm foo.ps1 -log ‘${pgm}.basename_bar.log’
.\foo.ps1 -pgm foo.ps1 -log '{${pgm}.basename}bar.log’
.\foo.ps1 -pgm foo.ps1 -log '${pgm}.basename + "bar.log"'
etc, etc.

What syntax can I use for -log such that it resolves to "foo_bar.log"?

by poshoholic at 2013-04-26 06:39:18
A few things:

First, if pgm is the path to a single file (it seems that is what you intend), then you should use Get-Item in your script, not Get-ChildItem.

Second, I recommend you don’t redefine variable types like that in your function. It is much easier to identify that you are changing a variable’s type when you assign the result to a new variable.

Last, you need to use a subexpression in order to access a property of an object within a string.

Here is an example how this could be done (one that defines the default value of $log in the parameter itself, but users can override it if desired):
[string] $pgm
[string] $log = '$($pgmItem.basename)
$(Get-Date -UFormat ''%Y%m%d
$pgmItem = Get-Item -LiteralPath $pgm
$logName = Invoke-Expression ""$log""

Then you could invoke it like this:
.\foo.ps1 -pgm foo.ps1 -log '$($pgmItem.basename)bar.log'
For the record though, I really don’t think this is a good design because it requires knowledge of the inner workings of the function in order for the caller to define the format used for the log file name. That means changing behaviour inside the function could break existing scripts that use that function very, very easily.

Another approach would be to use C-style formatting, so that you are not dependent on variable names at all in your script, with documentation highlighting what the format field indexes are used for. For example, consider this approach as an alternative:
[string] $pgm
[string] $log = '{0}
$pgmItem = Get-Item -LiteralPath $pgm
$logName = $log -f $pgmItem.basename,(Get-Date)

With this script, in order to get a log file that has the suffix _bar injected into it, you could invoke it like this:
.\foo.ps1 -pgm foo.ps1 -log '{0}_bar.log'
You could document that {0} represents the name of the pgm file without the extension, and {1} represents the current timestamp. This would be a more robust approach in my opinion because the log parameter does not have any dependency on internal variable names or types – instead it simply defines what strings {0} and {1} represent, and users can use those strings (or omit them) however they choose. Also, by removing the dependency on user-supplied subexpressions and Invoke-Expression, it isn’t prone to injection attacks either.