Submitting parameters to Invoke-Expression - powershell

I've written a sample Powershell Script with name C:\Script\Scrip1.ps1
Below is the code
Function Testfunction(){
Param(
$Node1,
$Node2
)
$SQLNodes = #($Node1, $Node2)
foreach ($node in $SQLNodes)
{
#Some code below is dummy code
"$node" | Out-File C:\File1.txt -Append
}
}
When i try to call this function using invoke-Expression it doesn't work
Used below method with no luck
$string = 'C:\Script\Script1.ps1 Testfunction -Node1 "test" -Node2 "test2"'
Invoke-Expression $string
I have opened a PS Window and ran below command without luck
.\Script1.ps1 -Node1 Hello -Node2 Aquib
or
.\Script1.ps1 Testfunction -Node1 Hello -Node2 Aquib
I do not see any file1 under C:\File1
when when I open the script file and then run the function, it does work and generate the file.

You don't need to use Invoke-Expression in your scenario.
If you want to make Testfunction visible in the current scope, you will need to "dot-source" your script:
PS C:\> . C:\Scripts\Script1.ps1
This executes Script.ps1 in the current scope, which will define Testfunction in the current scope, and then you can run the function:
PS C:\> Testfunction -Node1 "Test1" -Node2 "Test2"
Another alternative is to skip defining Testfunction as a function in a script, and just use it as a script itself:
# Script file
param(
$Node1,
$Node2
)
$SQLNodes = #($Node1, $Node2)
foreach ($node in $SQLNodes) {
#Some code below is dummy code
"$node" | Out-File C:\File1.txt -Append
}
If you name the script Testfunction.ps1, you can run it by typing the script's name:
PS C:\> C:\Scripts\Testfunction.ps1 -Node1 "Test1" -Node2 "Test2"

Related

How to execute powershell code which is in variable

suppose I have the following powershell code stored in a file:
## file1.ps1
$myvar = "i am here"
if ($myvar -ne $null) {
"($myvar) variable is Full"
} else {
"($myvar) variable is Empty"
}
And this code is then stored in a variable:
$code_in_var = cat file1.ps1
How do I execute the code in the variable through piping?
I have tried the following:
PS C:\Mrm> $code_in_var = cat file1.ps1
PS C:\Mrm>
PS C:\Mrm> cat file1.ps1 | powershell -
PS C:\Mrm>
PS C:\Mrm> cat file1.ps1 | Invoke-expression
Invoke-expression ; At line:1 char:23
+ if ($myvar -ne $null) {
+ ~
Missing closing '}' in statement bllock or type definition
At line:1 char:17
PS C:\Mrm>
PS C:\Mrm> $code_in_var | powershell - ***(this does not return anything)***
PS C:\Mrm>
PS C:\Mrm>
PS C:\Mrm> $code_in_var | Invoke-expression
**same error**
However, If I run this script directly:
PS C:\Mrm> .\file1.ps1
(i am here) variable is Full
It works as it is expected to.
My question is, how do I run a full powershell code that is stored in a variable, as though it were in a file?
You can store a ScriptBlock in a variable and use the & call operator like this:
$scriptBlock = {
$myvar = "i am here"
if ($myvar -ne $null) {
"($myvar) variable is Full"
} else {
"($myvar) variable is Empty"
}
}
& $scriptBlock
Output:
(i am here) variable is Full
A similar thing is fully documented in the MS Docs PowerShell help file.
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/invoke-expression?view=powershell-7
You will note that at no point is the formatted multi-line code used. It's all one-liner execution code.
Note that in the call of your code, it's just this.
#($myvar = "i am here"
if ($myvar -ne $null) {
"($myvar) variable is Full"
} else {
"($myvar) variable is Empty"
}) |
Out-File -FilePath 'D:\Temp\MyCode.ps1'
($Command = Get-Content -Path 'D:\Temp\MyCode.ps1')
# Results the code just runs with no Invoke-* needed
<#
(i am here) variable is Full
#>
Update as per my comments
You define a var, with whatever string you want, and just type the var name.
But you must do it all in the PowerShell process.
Why define PowerShell code outside the PowerShell process, just to send it to the PowerShell process?
# In a PowerShell Script development session
$MyCode = #($myvar = "i am here"
if ($myvar -ne $null) {
"($myvar) variable is Full"
} else {
"($myvar) variable is Empty"
})
$MyCode
# Results
<#
(i am here) variable is Full
#>
$myvar = "i am here"
if ($myvar -ne $null) {
"($myvar) variable is Full"
} else {
"($myvar) variable is Empty"
}
$myvar
# Results
<#
i am here
#>
# In a PowerShell terminal interactive session.
$myvar = "i am here";if ($myvar -ne $null) {"($myvar) variable is Full"} else {"($myvar) variable is Empty"}
$myvar
# Results
<#
i am here
#>
$myvar = $null;if ($myvar -ne $null) {"$myvar variable is Full"} else {Write-Warning -Message "$myvar variable is Empty"}
# Results
<#
WARNING: variable is Empty
#>
You cannot define a var outside of PowerShell then start PowerShell and try to use that var content. It's not in scope. All code, vars, functions, etc., have a scope and the scope must be defined in the PowerShell process
If you are saying you are wanting to run PowerShell code started from another terminal, say cmd.exe, then PowerShell has start parameters, and proper quoting, when needed, is a thing.
# There are several PowerShell startup parameters
<#
The syntax of Powershell.exe Command is:
PowerShell[.exe]
[-EncodedCommand ]
[-ExecutionPolicy ]
[-InputFormat {Text | XML}]
[-Mta]
[-NoExit]
[-NoLogo]
[-NonInteractive]
[-NoProfile]
[-OutputFormat {Text | XML}]
[-PSConsoleFile ]
[ -Version <Windows PowerShell version> ]
[-Sta]
[-WindowStyle ]<style>
[-File <FilePath> [<Args>]]
[-Command { - | <script-block> [-args <arg-array> ] | <string> [<CommandParameters>] } ]
#>
powershell -Command "Whatever command you want to run"
# Example, from a cmd.exe prompt
powershell -NoProfile -Command Get-Date
# Results
<#
Sunday, 14 June, 2020 04:31:38
#>
powershell -NoProfile -Command '{"Hello World"}'
# Results
<#
Hello World
#>
You cannot do this ...
${yourcode} | powershell
... outside of a PowerShell session. The PowerShell pipeline is only available in a PowerShell session. If you are already in a session, then again, just type and run the code.
Whatever terminal you are in has to be able to send this to PowerShell to run via the normal PowerShell startup. Yet, again, this is just introducing unneeded complexity, vs just running the script of running the code in a PowerShell interactive session directly.
You could store the code from the variable in a temporary script file and then execute it:
$code_in_var = cat .\file1.ps1
$tmpScriptFile = New-TemporaryFile | Rename-Item -NewName {$_.Name -ireplace 'tmp$', 'ps1'} -PassThru
Set-Content -Path $tmpScriptFile -Value $code_in_var
& $tmpScriptFile
Remove-Item -Path $tmpScriptFile
But if the code already originates from a file, just directly execute that instead of course.

Redirect standard input (read-host) to a Powershell script

Here's a sample powershell script:
$in = read-host -prompt "input"
write-host $in
Here's a sample 'test.txt' file:
hello
And we want to pass piped input to it from powershell. Here's some I have tried:
.\test.ps1 < test.txt
.\test.ps1 < .\test.txt
.\test.ps1 | test.txt
.\test.ps1 | .\test.txt
test.txt | .\test.ps1
.\test.txt | .\test.ps1
get-content .\test.txt | .\test.ps1
even just trying to echo input doesn't work either:
echo hi | \.test.ps1
Every example above that doesn't produce an error always prompts the user instead of accepting the piped input.
Note: My powershell version table says 4.0.-1.-1
Thanks
Edit/Result: To those looking for a solution, you cannot pipe input to a powershell script. You have to update your PS file. See the snippets below.
The issue is that your script \.test.ps1 is not expecting the value.
Try this:
param(
[parameter(ValueFromPipeline)]$string
)
# Edit: added if statement
if($string){
$in = "$string"
}else{
$in = read-host -prompt "input"
}
Write-Host $in
Alternatively, you can use the magic variable $input without a param part (I don't fully understand this so can't really recommend it):
Write-Host $input
You can't pipe input to Read-Host, but there should be no need to do so.
PowerShell doesn't support input redirection (<) yet. In practice this is not a (significant) limitation because a < b can be rewritten as b | a (i.e., send output of b as input to a).
PowerShell can prompt for input for a parameter if the parameter's value is missing and it is set as a mandatory attribute. For example:
function test {
param(
[parameter(Mandatory=$true)] [String] $TheValue
)
"You entered: $TheValue"
}
If you don't provide input for the $TheValue parameter, PowerShell will prompt for it.
In addition, you can specify that a parameter accepts pipeline input. Example:
function test {
param(
[parameter(ValueFromPipeline=$true)] [String] $TheValue
)
process {
foreach ( $item in $TheValue ) {
"Input: $item"
}
}
}
So if you write
"A","B","C" | test
The function will output the following:
Input: A
Input: B
Input: C
All of this is spelled out pretty concisely in the PowerShell documentation.
Yes; in Powershell 5.1 "<" is not implemented (which sucks)
so, this won't work: tenkeyf < c:\users\marcus\work\data.num
but,
this will: type c:\users\marcus\work\data.num | tenkeyf
...
PowerShell doesn’t have a redirection mechanism, but.NET have.
you can use [System.Diagnostics.Process] implements the purpose of redirecting input.
The relevant Microsoft documents are as follows.
Process Class
This is a sample program that works perfectly on my windows 10 computer
function RunAndInput{
$pi = [System.Diagnostics.ProcessStartInfo]::new()
$pi.FileName ="powershell"
$pi.Arguments = """$PSScriptRoot\receiver.ps1"""
$pi.UseShellExecute = $false
$pi.RedirectStandardInput = $true
$pi.RedirectStandardOutput = $true
$p = [System.Diagnostics.Process]::Start($pi)
$p.StandardInput.WriteLine("abc"+ "`r`n");
$p.StandardOutput.ReadToEnd()
$p.Kill()
}
RunAndInput
# OutPut
Please enter the information: abc
Received:abc
# receiver.ps1
('Received:' + (Read-Host -Prompt 'Please enter the information'))
Hope to help you!

Executing Powershell script from command line with quoted parameters

I am automating the build of a legacy MS Access application, and in one of the steps, I am trying to make an Access executable (.ADE). I have come up with the following code, which is stored in a file (PSLibrary.ps1):
Add-Type -AssemblyName Microsoft.Office.Interop.Access
function Access-Compile {
param (
[Parameter(Mandatory=$TRUE,Position=1)][string]$source,
[Parameter(Mandatory=$TRUE,Position=2)][string]$destination
)
Write-Output "Starting MS Access"
$access = New-Object -ComObject Access.Application
$access.Visible = $FALSE
$access.AutomationSecurity = 1
if (!(Test-Path $source)) { Throw "Source '$source' not found" }
if ((Test-Path $destination)) {
Write-Output "File '$destination' already exists - deleting..."
Remove-Item $destination
}
Write-Output "Compiling '$source' to '$destination'"
$result = $access.SysCmd(603, $source, $destination)
$result
Write-Output "Exiting MS Access"
$access.quit()
}
If I go into the PowerShell ISE and run the command below, then everything works fine, and the expected output is created:
PS C:>& "C:\Temp\PSLibrary.ps1"
PS C:>Access-Compile "C:\Working\Project.adp" "C:\Working\Project.ade"
However, I can't seem to generate the right hocus-pocus to get this running from the command line, as I would in an automated build. For instance,
powershell.exe -command "& \"C:\\Temp\\PSLibrary.ps1\" Access-Compile \"C:\\Temp\\Project.adp\" \"C:\\Temp\\Project.ade\""
What am I doing wrong?
For complex parameters, you can use Powershell's -EncodedCommand parameter. It will accept a Base64 encoded string. No escaping is needed for quotes, slashes and such.
Consider a test function that will print its parameters. Like so,
function Test-Function {
param (
[Parameter(Mandatory=$TRUE,Position=1)][string]$source,
[Parameter(Mandatory=$TRUE,Position=2)][string]$destination
)
write-host "src: $source"
write-host "dst: $destination"
}
Create command to load the script and some parameters. Like so,
# Load the script and call function with some parameters
. C:\Temp\Calling-Test.ps1; Test-Function "some\special:characters?" "`"c:\my path\with\spaces within.ext`""
After the command syntax is OK, encode it into Base64 form. Like so,
[System.Convert]::ToBase64String([System.Text.Encoding]::UNICODE.GetBytes('. C:\Temp\Calling-Test.ps1; Test-Function "some\special:characters?" "`"c:\my path\with\spaces within.ext`""'))
You'll get a Base64 string. Like so,
LgAgAEMAOgBcAFQAZQBtAHAAXABDAGEAbABsAGkAbgBnAC0AVABlAHMAdAAuAHAAcwAxADsAIAAgAFQAZQBzAHQALQBGAHUAbgBjAHQAaQBvAG4AIAAiAHMAbwBtAGUAXABzAHAAZQBjAGkAYQBsADoAYwBoAGEAcgBhAGMAdABlAHIAcwA/ACIAIAAiAGAAIgBjADoAXABtAHkAIABwAGEAdABoAFwAdwBpAHQAaABcAHMAcABhAGMAZQBzACAAdwBpAHQAaABpAG4ALgBlAHgAdABgACIAIgA=
Finally, start Powershell and pass the encoded string as a parameter. Like so,
# The parameter string here is abreviated for readability purposes.
# Don't do this in production
C:\>powershell -encodedcommand LgAgA...
Output
src: some\special:characters?
dst: "c:\my path\with\spaces within.ext"
Should you later want to reverse the Base64 encoding, pass it into decoding method. Like so,
$str = " LgAgA..."
[Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String($str))
# Output
. C:\Temp\Calling-Test.ps1; Test-Function "some\special:characters?" "`"c:\my path\with\spaces within.ext`""
PowerShell like Bash can take single or double quotes
PS C:\Users\Steven> echo "hello"
hello
PS C:\Users\Steven> echo 'hello'
hello
this can alleviate some of the headache, also I think you can use the literal backslashes without escaping.
To run PowerShell, choose
Start Menu Programs Accessories
Windows Powershell Windows Powershell

Can I resolve PowerShell scriptblock parameters without invoking?

I'm looking at writing some PowerShell code that can either execute immediately, or produce the commands it would execute as generated scripts.
I'd like to avoid this scenario:
if($Generating){
write-Output "somecommand.exe"
}
else{
somecommand.exe
}
I got looking at ScriptBlocks, which at first looked promising because I can write the contents of the ScriptBlock to the console without executing it. Such as:
$sc = { somecommand.exe }
$sc
somecommand.exe
My specific question is, if my scriptblock contains parameters, can I get them to resolve when I'm writing the scriptblock contents to the console, but WITHOUT invoking the scriptblock?
For example given the following scriptblock:
$b2 = { Param([string]$P) Write-Host "$P" }
When I just type "$b2" at the console and hit enter I see this:
Param([string]$P) Write-Host "$P"
What I'd like to see is this (if the parameter value is "Foo"):
Param([string]$P) Write-Host "Foo"
I realize this can be done when it's invoked, either via "&" or using Invoke(), but would there be any way to get the parameters to resolve without invoking to make my script generation a little more elegant without needing a bunch of conditional statements throughout the code?
In PowerShell v3, you can get the param info via the AST property e.g.:
PS> $sb = {param($a,$b) "a is $a b is $b"}
PS> $sb.Ast.ParamBlock
Attributes Parameters Extent Parent
---------- ---------- ------ ------
{} {$a, $b} param($a,$b) {param($a,$b) "a...
Solution suitable for PowerShell v2:
# given the script block
$b2 = { Param([string]$P) Write-Host "$P" }
# make a function of it and "install" in the current scope
Invoke-Expression "function tmp {$b2}"
# get the function and its parameters
(Get-Command tmp).Parameters
When displaying a here-string with double quotes #" , it expands the variables. For the variables that should'nt expand, escape the variable with a backtick ( ` ).
So try this:
$P = "Foo"
$b2 = #"
{ Param([string]`$P) Write-Host "$P" }
"#
Test:
PS-ADMIN > $b2
{ Param([string]$P) Write-Host "Foo" }
If you want to convert it to scriptblock-type again:
#Convert it into scriptblock
$b3 = [Scriptblock]::Create($b2)
PS-ADMIN > $b3
{ Param([string]$P) Write-Host "Foo" }
PS-ADMIN > $b3.GetType().name
ScriptBlock
Using some of the suggestions I think I've found the best solution for my needs. Consider the following code
function TestFunc
{
Param(
[Parameter(Mandatory=$true)]
[string]$Folder,
[Parameter(Mandatory=$true)]
[string]$Foo
)
$code = #"
Write-Host "This is a folder $Folder"
Write-Host "This is the value of Foo $Foo"
"#
$block = [Scriptblock]::Create($code)
Write-Host "Running the block" -BackgroundColor Green -ForegroundColor Black
&$block
Write-Host "Displaying block code" -BackgroundColor Green -ForegroundColor Black
$block
}
And it's output:
Running the block
This is a folder c:\some\folder
This is the value of Foo FOOFOO
Displaying block code
Write-Host "This is a folder c:\some\folder"
Write-Host "This is the value of Foo FOOFOO"
By doing it this way, I still get all the benefit of keeping my existing functions and their parameters, parameter validation, CBH etc. I can also easily generate the code that the function would execute or just let it execute. Thanks for all the input, it's definitely been a good learning experience.
If you want to express your block as a block, not a string, the following works:
$printable = invoke-expression ('"' + ($block -replace '"', '`"') + '"')
Essentially, you're wrapping everything in quotes and then invoking it as an expression. The -replace call ensures any quotes in the block itself are escaped.
I'm using this in this handy function, which also halts execution if the invoked command failed.
# usage: exec { dir $myDir }
function exec($block)
{
# expand variables in block so it's easier to see what we're doing
$printable = invoke-expression ('"' + ($block -replace '"', '`"').Trim() + '"')
write-host "# $printable" -foregroundcolor gray
& $block
if ($lastExitCode -ne 0)
{
throw "Command failed: $printable in $(pwd) returned $lastExitCode"
}
}

Expanding variables in file contents

I have a file template.txt which contains the following:
Hello ${something}
I would like to create a PowerShell script that reads the file and expands the variables in the template, i.e.
$something = "World"
$template = Get-Content template.txt
# replace $something in template file with current value
# of variable in script -> get Hello World
How could I do this?
Another option is to use ExpandString() e.g.:
$expanded = $ExecutionContext.InvokeCommand.ExpandString($template)
Invoke-Expression will also work. However be careful. Both of these options are capable of executing arbitrary code e.g.:
# Contents of file template.txt
"EvilString";$(remove-item -whatif c:\ -r -force -confirm:$false -ea 0)
$template = gc template.txt
iex $template # could result in a bad day
If you want to have a "safe" string eval without the potential to accidentally run code then you can combine PowerShell jobs and restricted runspaces to do just that e.g.:
PS> $InitSB = {$ExecutionContext.SessionState.Applications.Clear(); $ExecutionContext.SessionState.Scripts.Clear(); Get-Command | %{$_.Visibility = 'Private'}}
PS> $SafeStringEvalSB = {param($str) $str}
PS> $job = Start-Job -Init $InitSB -ScriptBlock $SafeStringEvalSB -ArgumentList '$foo (Notepad.exe) bar'
PS> Wait-Job $job > $null
PS> Receive-Job $job
$foo (Notepad.exe) bar
Now if you attempt to use an expression in the string that uses a cmdlet, this will not execute the command:
PS> $job = Start-Job -Init $InitSB -ScriptBlock $SafeStringEvalSB -ArgumentList '$foo $(Start-Process Notepad.exe) bar'
PS> Wait-Job $job > $null
PS> Receive-Job $job
$foo $(Start-Process Notepad.exe) bar
If you would like to see a failure if a command is attempted, then use $ExecutionContext.InvokeCommand.ExpandString to expand the $str parameter.
I've found this solution:
$something = "World"
$template = Get-Content template.txt
$expanded = Invoke-Expression "`"$template`""
$expanded
Since I really don't like the idea of One More Thing To Remember - in this case, remembering that PS will evaluate variables and run any commands included in the template - I found another way to do this.
Instead of variables in template file, make up your own tokens - if you're not processing HTML, you can use e.g. <variable>, like so:
Hello <something>
Basically use any token that will be unique.
Then in your PS script, use:
$something = "World"
$template = Get-Content template.txt -Raw
# replace <something> in template file with current value
# of variable in script -> get Hello World
$template=$template.Replace("<something>",$something)
It's more cumbersome than straight-up InvokeCommand, but it's clearer than setting up limited execution environment just to avoid a security risk when processing simple template. YMMV depending on requirements :-)