4

Imagine the following code:

# Script Start
$WelcomeMessage = "Hello $UserName, today is $($Date.DayOfWeek)"

..
..
# 100 lines of other functions and what not...
..

function Greet-User
{
    $Username = Get-UserNameFromSomewhereFancy
    $Date = Get-DateFromSomewhereFancy

    $WelcomeMessage
}

This is a very basic example, but what it tries to show is a script where there is a $WelcomeMessage that the person running the script can set at the top of the script and controls how/what the message displayed is.

First thing's first: why do something like this? Well, if you're passing your script around to multiple people, they might want different messages. Maybe they don't like $($Date.DayOfWeek) and want to get the full date. Maybe they don't want to show the username, whatever.

Second, why put it at the top of the script? Simplicity. If you have 1000 lines in your script and messages like these spread all over the script, it makes it a nightmare for people to find and change these messages. We already do that for static messages, in the form of localized strings and stuff, so this is nothing new, except for the variable parts in it.

So, now to the issue. If you run that code and invoke Greet-User (assuming the functions/cmdlets for retrieving username and date actually exist and return something proper...) Greet-User will always return Hello , today is.

This is because the string is expanded when you declare it, at the top of the script, when neither $UserName nor $Date objects have a value.

A potential workaround would be to create the strings with single quotes, and use Invoke-Expression to expand them. But because of the spaces, that gets a bit messy. I.e.:

$WelcomeMessage = 'Hello $env:USERNAME'
Invoke-Expression $WelcomeMessage

This throws an error because of the space, to get it to work properly it would have to be declared as such:

$WelcomeMessage = 'Hello $env:USERNAME'
$InvokeExpression = "`"$WelcomeMessage`""

Messy...

Also, there's another problem in the form of code injection. Since we're allowing the user to write their own welcome message with no bounds specified, what's to prevent them from putting in something like...

$WelcomeMessage 'Hello $([void] (Remove-Item C:\Windows -Force -Recurse))'

(Yes, I know this will not delete everything but it is an example)

Granted this is a script and if they can modify that string they can also modify everything else on the script, but whereas the example I gave was someone maliciously taking advantage of the nature of the script, it can also happen that someone accidentally puts something in the string that ends up having unwanted consequences.

So... there's got to be a better way without the use of Invoke-Expression, I just can't quite thing of one so help would be appreciated :)

13
  • 1
    Do you need complete expression-like freedom for the contents of $WelcomeMessage? Or do you just need to let them control formatting/order/etc. around the known fields? Could you use "Hello {0}, today is {1:ddd}" or something as the default and then use $WelcomeMessage -f $Username,$Date in the function? Commented Jul 14, 2015 at 22:10
  • 2
    The outer quotes on the single-quote version appear to be unnecessary That is Invoke-Expression `"$WelcomeMessage`" appears to work here too (not that that's much better). Commented Jul 14, 2015 at 22:26
  • Why not make that a parameter of the script, with "Hello $UserName, today is $($Date.DayOfWeek)" as the default value? Then they can change it to whatever they want at invocation without messing with the script at all. Commented Jul 14, 2015 at 22:38
  • @EtanReisner Do need complete freedom unfortunately. As I said, some users might not want the DayOfWeek property but instead the full date, or maybe no date at all (as an example) Commented Jul 14, 2015 at 22:39
  • 1
    All great answers here. I don't know if you are going to find what you are looking for given your specifications. If the users have access to the script they can do whatever they want anyway as they will already have the permissions to do so. You users would need to have some coding experience in order to be playing around anyway. If not then I would suggest you use script parameters like thors hammer suggests Commented Jul 15, 2015 at 0:39

3 Answers 3

4

Embedding variables into strings is not the only way to create dynamic text, the way I would do it is like this:

$WelcomeMessage = 'Hello {0}, today is {1}'

# 100 lines of other functions and what not...

function Greet-User
{
    $Username = Get-UserNameFromSomewhereFancy
    $Date = Get-DateFromSomewhereFancy

    $WelcomeMessage -f $Username, $Date
}
Sign up to request clarification or add additional context in comments.

4 Comments

Nice, but that is less readable by the user (e.g. they have to know that {0} is username) and removes the ability to select properties from the objects on the welcome message (e.g. .DayOfWeek); I assume that's why @JohnUbuntu's taking this approach instead...
They have to know that $user is supposed to be the username also (it could be anything as variable names are entirely unreliable in general) they also have to know that it will be available in the (random) context where the message is going to be used. A documented set of format arguments to the string is, arguably, more user-friendly since it explicitly defines the contract.
If going down this road I'd much rather use something like: "Hello [Name.ToUpper()], today is [Date.DayOfWeek]", that way there are no actual variables, just a fixed set of names that I then bind to objects. Easy enough to document, still provides full control over the properties/methods of said objects, prevents code injection (if they tried "Hello [Remove-Item C:\Windows -Force -Recurse]" the string inside [] is not recognized as a valid variable so nothing happens. Could use regex for replacing for example, making it rather effective.
@EtanReisner I like your solution and how you defended it, it's not that different from my comment above, except that using names instead of integers might be easier for users (but more work to code). However, your solution doesn't answer the bit about users being able to use properties/methods of those objects. So for example .DayOfWeek, which would have to be defined on the $WelcomeMessage itself
3

The canonical way to delay evaluation of expressions/variables in strings is to define them as single-quoted strings and use $ExecutionContext.InvokeCommand.ExpandString() later on.

Demonstration:

PS C:\> $s = '$env:COMPUTERNAME'
PS C:\> $s
$env:COMPUTERNAME
PS C:\> $ExecutionContext.InvokeCommand.ExpandString($s)
FOO

Applied to your sample code:

$WelcomeMessage = 'Hello $UserName, today is $($Date.DayOfWeek)'

...
...
...

function Greet-User {
  $Username = Get-UserNameFromSomewhereFancy
  $Date = Get-DateFromSomewhereFancy

  $ExecutionContext.InvokeCommand.ExpandString($WelcomeMessage)
}

7 Comments

It looks better than using the Invoke-Expression cmdlet, I'll give you that, but it suffers from the same problems in regards to code injection, be it malicious or accidental.
Umm... yes. How do you expect to avoid this problem if you want to allow users to define strings with arbitrary code/variables?
Look at my comment on the answer above as an example of a possible solution.
That basically means to not allow arbitrary code/variables, which contraticts your requirement of "complete freedom".
Accessing a method could easily be used for code injection e.g. $host.Runspace.CreateNestedPipeline('Get-Date', $false).Invoke(). $host always exists.
|
2

Have you considered using a lambda expression; i.e. instead of defining the variable as a string value define it as a function, then invoke that function passing the relevant parameters at runtime.

$WelcomeMessage = {param($UserName,$Date);"Hello $UserName, today is $($Date.DayOfWeek) $([void](remove-item c:\test\test.txt))"}

#...
# 100 lines of other functions and what not...
#...

"testfile" >> c:\test\test.txt #ensure we have a test file to be deleted

function Get-UserNameFromSomewhereFancy(){return "myUsername";}
function Get-DateFromSomewhereFancy(){return (get-date);}

function Greet-User
{
    $Username = Get-UserNameFromSomewhereFancy
    $Date = Get-DateFromSomewhereFancy

    $WelcomeMessage.invoke($username,$date)
}

cls
Greet-User

Update

If you only wish to allow variable replacement the below code would do the trick; but this fails to do more advanced functions (e.g. .DayOfWeek)

$WelcomeMessage = 'Hello $Username, today is $($Date.DayOfWeek) $([void](remove-item c:\test\test.txt))'
#...
# 100 lines of other functions and what not...
#...

"testfile" >> c:\test\test.txt #ensure we have a test file to be deleted

function Get-UserNameFromSomewhereFancy(){return "myUsername";}
function Get-DateFromSomewhereFancy(){return (get-date);}
function Resolve-WelcomeMessage(){
    write-output {param($UserName,$Date);"$WelcomeMessage";}
}
function Greet-User
{
    $Username = Get-UserNameFromSomewhereFancy
    $Date = Get-DateFromSomewhereFancy
    $temp = $WelcomeMessage 
    get-variable | ?{@('$','?','^') -notcontains $_.Name} | sort name -Descending | %{
        $temp  = $temp -replace ("\`${0}" -f $_.name),$_.value
    }
    $temp 
}

cls
Greet-User

Update

To avoid code injection this makes use of -whatif; that will only help where the injected code supports the whatif functionality, but hopefully better than nothing...

Also the code now doesn't require parameters to be declared; but just takes those variables which are available at the time of execution.

$WelcomeMessage = {"Hello $Username, today is $($Date.DayOfWeek) $([void](remove-item c:\test\test.txt))"}

#...
# 100 lines of other functions and what not...
#...

function Get-UserNameFromSomewhereFancy(){return "myUsername";}
function Get-DateFromSomewhereFancy(){return (get-date);}
function Resolve-WelcomeMessage(){
    write-output {param($UserName,$Date);"$WelcomeMessage";}
}

"testfile" >> c:\test\test.txt #ensure we have a test file to be deleted

function Greet-User {
    [cmdletbinding(SupportsShouldProcess=$True)]
    param()
    begin {$original = $WhatIfPreference; $WhatIfPreference = $true;}
    process {
        $Username = Get-UserNameFromSomewhereFancy
        $Date = Get-DateFromSomewhereFancy
        & $WelcomeMessage 
    }
    end {$WhatIfPreference = $original;}
}

cls
Greet-User

5 Comments

Couple of problems: too complicated for 'normal' users that just want to modify a string and not have to think about creating a function and worrying about whether that function works properly or not... and it means that you're assuming that both $username and $date are always being displayed in the string (or at least passed to the function. There is always the possibility that the user wants the message to just be "Hello $Username" and not use $Date at all, but you'd still need to declare $Date as a parameter in your function... That, and user can still accidentally (or not) inject code
Agreed on complexity issue.
With regards to code injection I think there's a contradition in requirements; i.e. you want to be able to run any code (so as to get the output), but also not run any code (to avoid side effects); so how do you determine which code should be allowed to be executed?
With regards to variable availability, do you mean that if $date isn't supplied you'd want to see Hello myUsername or Hello myUsername, today is ; as the former would require more sophisticated logic to know to remove some of the fixed string when the date variable isn't available as well as not providing the variable itself.
ps. sadly it looks like -whatif outputs to the host stream; so there's no easy way to block those messages...

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.