How to execute powershell code which is in variable - powershell

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.

Related

In powershell, Have Out-Host with conditional statement

I have a requirement to append | Out-Host with each Powershell execution.
But the response with out-host changes as below
>> some crap
>> $? --> returns false
>> some crap | Out-Host
>> $? --> returns false as expected
>> $(some crap) | Out-Host
>> $? --> returns true not expected
I understand that the true return might be because of the subexpression that I have introduced. But I see it needed in scenarios where I have a conditional script. There simple appending Out-Host doesn't work. For example,
$condition = $true; if ( $condition ) {echo "The condition was true"} | Out-Host
The above fails that an empty pipe is not allowed
If I change it to the below, it works
$($condition = $true; if ( $condition ) {echo "The condition was true"} )| Out-Host
I basically want to append Out-Host such that my output/response of run doesn't get affected. Since Out-Host is said to be the default, there should be a way to handle it for conditional statements as well.
Any help is appreciated.
It's still not entirely clear to me why you need to add Out-Host all over the place, but the safest option for programmatically adding | Out-Host to all your pipelines is to parse the existing script using the Parser class:
function Add-TrailingOutHost {
param(
[string]$Script
)
# Start by parsing the script
$parserErrors = #()
$AST = [System.Management.Automation.Language.Parser]::ParseInput($Script, [ref]$null, [ref]$parserErrors)
if($parserErrors){
Write-Error 'Errors encountered parsing script'
return
}
# Locate all pipeline expressions in the AST returned by the parser
$pipelines = $AST.FindAll({
param($Tree)
# We only want "free-standing" pipelines - ignore pipelines in flow control statements and assignments
$Tree -is [System.Management.Automation.Language.PipelineAst] -and $Tree.Parent -isnot [System.Management.Automation.Language.StatementAst]
}, $true)
# We'll be injecting code into the script, thereby modifying the offsets of the existing statements
# To avoid inject code at the wrong offset we need to traverse the script bottom-up
for($i = $pipelines.Count - 1; $i -ge 0; $i--){
$pipeline = $pipelines[$i]
# Check if the last command in the pipeline is already `Out-*` something - ignore those
$lastCommand = $pipeline.PipelineElements[-1]
if($lastCommand.CommandElements[0] -like 'Out-*'){
continue
}
# Otherwise, inject the string `| Out-Host` at the end of the pipeline
$Script = $Script.Insert($lastCommand.Extent.EndOffset, ' | Out-Host')
}
# return modified script
return $Script
}
Now you can add Out-Host to the relevant parts of any script with:
Add-TrailingOutHost -Script #'
echo "Hello there"
if(Test-Path .){
echo "Something else"
}
'#
The output of which is:
echo "Hello there" | Out-Host
if(Test-Path .){
echo "Something else" | Out-Host
}

Submitting parameters to Invoke-Expression

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"

"echo on" in powershell or how do I make Powershell output the command lines of all the commands, INCLUDING the native ones invoked by the script?

My question may seem duplicate of PowerShell "echo on", but it is not.
I am not interested in capturing the command output, but in the command line itself of every command executed by the script, including the native commands.
This is what "echo on" in cmd does and this is what I am looking for. Set-PSDebug -Trace 1 does not do it and neither passing the -Verbose flag.
So far I have not see a way except outputing them myself, which is a huge pain in itself.
So, can Powershell do what "echo on" does in cmd?
EDIT 1
Not ideal, but I would accept an answer suggesting to use a wrapper function which would receive a command (native or powershell) with parameters and run the command while faithfully logging the respective command line. Of course, the wrapper function code should be part of the answer.
EDIT 2
The following trivial example demonstrates why Set-PSDebug -Trace 1 does not do it:
tasklist `
/fi "status eq running" | Select-Object -First 4
Please, observe:
C:\> cat C:\temp\1.ps1
tasklist `
/fi "status eq running" | Select-Object -First 4
C:\> Set-PSDebug -Trace 1
C:\> C:\temp\1.ps1
DEBUG: 1+ >>>> C:\temp\1.ps1
DEBUG: 1+ >>>> tasklist `
Image Name PID Session Name Session# Mem Usage
========================= ======== ================ =========== ============
csrss.exe 756 Console 1 2,816 K
C:\>
EDIT 3
For comparison, observe an equivalent script in cmd with echo on:
C:\>type c:\temp\1.cmd
#echo on
tasklist ^
/fi "status eq running" |findstr/n ^^|findstr "^[1-4]:"
C:\>c:\temp\1.cmd
C:\>tasklist /fi "status eq running" | findstr/n ^ | findstr "^[1-4]:"
1:
2:Image Name PID Session Name Session# Mem Usage
3:========================= ======== ================ =========== ============
4:csrss.exe 756 Console 1 2,328 K
C:\>
EDIT 4
start-transcript does not do it either:
C:\WINDOWS\system32> cat c:\temp\1.ps1
tasklist `
/fi "status eq running" | Select-Object -First 4 | Out-Default
C:\WINDOWS\system32> Start-Transcript
Transcript started, output file is ~\Documents\PowerShell_transcript.L-PF0TBKV7.Sr1ntThx.20190611143800.txt
C:\WINDOWS\system32> c:\temp\1.ps1
Image Name PID Session Name Session# Mem Usage
========================= ======== ================ =========== ============
csrss.exe 756 Console 1 2,936 K
C:\WINDOWS\system32> Stop-Transcript
Transcript stopped, output file is ~\Documents\PowerShell_transcript.L-PF0TBKV7.Sr1ntThx.20190611143800.txt
C:\WINDOWS\system32> cat ~\Documents\PowerShell_transcript.L-PF0TBKV7.Sr1ntThx.20190611143800.txt
**********************
Windows PowerShell transcript start
Start time: 20190611143800
Username: xyz\me
RunAs User: xyz\me
Configuration Name:
Machine: L-PF0TBKV7 (Microsoft Windows NT 10.0.16299.0)
Host Application: C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe
Process ID: 25508
PSVersion: 5.1.16299.1004
PSEdition: Desktop
PSCompatibleVersions: 1.0, 2.0, 3.0, 4.0, 5.0, 5.1.16299.1004
BuildVersion: 10.0.16299.1004
CLRVersion: 4.0.30319.42000
WSManStackVersion: 3.0
PSRemotingProtocolVersion: 2.3
SerializationVersion: 1.1.0.1
**********************
Transcript started, output file is ~\Documents\PowerShell_transcript.L-PF0TBKV7.Sr1ntThx.20190611143800.txt
C:\WINDOWS\system32
>
PS>c:\temp\1.ps1
Image Name PID Session Name Session# Mem Usage
========================= ======== ================ =========== ============
csrss.exe 756 Console 1 2,936 K
C:\WINDOWS\system32
>
PS>Stop-Transcript
**********************
Windows PowerShell transcript end
End time: 20190611143810
**********************
C:\WINDOWS\system32>
As you can see it does not contain the command line.
Firstly, the reason you're dissatisfied with the built-in options is because you're going against the grain; your requirement is like asking how to put sacks of gravel in the back of a Porsche. Powershell comes with Verbose and Debug output streams and a fantastic debugger.
If you have any ability to influence coding standards, look at splatting as an alternative to backtick-continuations.
If you can count on versions of Windows that are not years past EoL, consider Get-ScheduledTask | Where-Object State -eq 'Ready' instead of tasklist.
That said, yes, what you want is possible. Here's a script that will echo across line continuations:
# Echo.ps1
function Disable-Echo
{
param
(
[Parameter(Mandatory)]
[string]$Path
)
$Path = ($Path | Resolve-Path -ErrorAction Stop).Path
Get-PSBreakpoint -Script $Path | Remove-PSBreakpoint
}
function Enable-Echo
{
param
(
[Parameter(Mandatory)]
[string]$Path
)
$Path = ($Path | Resolve-Path -ErrorAction Stop).Path
Disable-Echo $Path
$Ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref]$null, [ref]$null)
$Statements = $Ast.BeginBlock, $Ast.ProcessBlock, $Ast.EndBlock |
Select-Object -ExpandProperty Statements |
Write-Output |
Where-Object {$_.Extent}
foreach ($Statement in $Statements)
{
$Action = {
$Text = $Statement.Extent.Text
$Text = $Text -replace '`\r?\n' # Concatenate lines that are escaped with backtick
# Alternative to remove superfluous whitespace:
# $Text = $Text -replace '\s+`\r?\n\s*', ' '
Write-Host "ECHO: $Text" -ForegroundColor Yellow
continue # or 'break' to stop on the line
}.GetNewClosure() # Create a closure, to capture the value of $Statement
$Params = #{
Script = $Path
Line = $Statement.Extent.StartLineNumber
Column = $Statement.Extent.StartColumnNumber
Action = $Action
}
$null = Set-PSBreakpoint #Params
}
}
Sample script:
# foo.ps1
gci `
-Name `
-File `
-Filter Victor.*
gci -File -Name *.md; gci -File -Name *.psd1
Usage:
# Without echo
❯ .\foo.ps1
Victor.build.ps1
Victor.psd1
Victor.psm1
README.md
Victor.psd1
❯ . .\Echo.ps1
❯ Enable-Echo .\foo.ps1
❯ .\foo.ps1
ECHO: gci -Name -File -Filter Victor.*
Victor.build.ps1
Victor.psd1
Victor.psm1
ECHO: gci -File -Name *.md
README.md
ECHO: gci -File -Name *.psd1
Victor.psd1
Tested on PSv5 and PSv7. Should work on PSv2, although the sample foo.ps1 is PSv3+ (IIRC).
This will not echo calls to other scripts. For that, you'd probably want to do more AST inspection, identify CommandAsts that call scripts, and recursively enable echo on those scripts too. Alternatively, there might be joy in Set-PSBreakpoint -Variable - the $$ variable might be a good place to start - but this would likely be a PITA to work with as it would invoke while you're trying to debug the echo function. You could inspect Get-PSCallStack to skip the action while you're at the command prompt.
I expect four answers and you have already mentioned three that do not work for you (Set-PSDebug, Start-Transaction, -Verbose). As much as they may be viable but not in the format you are looking for, I will not talk more of them.
For the third option, try using Get-History. Now, this will not print out each command as you execute it (only when you call it) like I assume you want. It will also likely not print out each of the lines inside another script (you would want a trace but you did not like that because it prints out more than just the execution).
You can try asking around the PowerShell repository but I do not expect you to find what you are seeking.
If Event logs is an option, start tracing by enabling this Group Policy.
Administrative Templates -> Windows Components -> Windows PowerShell
See Microsoft Docs - Script Tracing and Logging
Then you would of course need to parse the Event logs accordingly...

redirect stdout, stderr from powershell script as admin through start-process

Inside a powershell script, I'm running a command which starts a new powershell as admin (if I'm not and if needed, depending on $arg) and then runs the script.
I'm trying to redirect stdout and stderr to the first terminal.
Not trying to make things easier, there are arguments too.
param([string]$arg="help")
if($arg -eq "start" -Or $arg -eq "stop")
{
if(![bool](([System.Security.Principal.WindowsIdentity]::GetCurrent()).groups -match "S-1-5-32-544"))
{
Start-Process powershell -Verb runas -ArgumentList " -file servicemssql.ps1 $arg"
exit
}
}
$Services = "MSSQLSERVER", "SQLSERVERAGENT", "MSSQLServerOLAPService", "SSASTELEMETRY", "SQLBrowser", `
"SQLTELEMETRY", "MSSQLLaunchpad", "SQLWriter", "MSSQLFDLauncher"
function startsql {
"starting SQL services"
Foreach ($s in $Services) {
"starting $s"
Start-Service -Name "$s"
}
}
function stopsql {
"stopping SQL services"
Foreach ($s in $Services) {
"stopping $s"
Stop-Service -Force -Name "$s"
}
}
function statussql {
"getting SQL services status"
Foreach ($s in $Services) {
Get-Service -Name "$s"
}
}
function help {
"usage: StartMssql [status|start|stop]"
}
Switch ($arg) {
"start" { startsql }
"stop" { stopsql }
"status" { statussql }
"help" { help }
"h" { help }
}
Using the following answers on SO doesn't work:
Capturing standard out and error with Start-Process
Powershell: Capturing standard out and error with Process object
How to deal with the double quote inside double quote while preserving the variable ($arg) expansion ?
PowerShell's Start-Process cmdlet:
does have -RedirectStandardOut and -RedirectStandardError parameters,
but syntactically they cannot be combined with -Verb Runas, the argument required to start a process elevated (with administrative privileges).
This constraint is also reflected in the underlying .NET API, where setting the .UseShellExecute property on a System.Diagnostics.ProcessStartInfo instance to true - the prerequisite for being able to use .Verb = "RunAs" in order to run elevated - means that you cannot use the .RedirectStandardOutput and .RedirectStandardError properties.
Overall, this suggests that you cannot directly capture an elevated process' output streams from a non-elevated process.
A pure PowerShell workaround is not trivial:
param([string] $arg='help')
if ($arg -in 'start', 'stop') {
if (-not (([System.Security.Principal.WindowsPrincipal] [System.Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole('Administrators'))) {
# Invoke the script via -Command rather than -File, so that
# a redirection can be specified.
$passThruArgs = '-command', '&', 'servicemssql.ps1', $arg, '*>', "`"$PSScriptRoot\out.txt`""
Start-Process powershell -Wait -Verb RunAs -ArgumentList $passThruArgs
# Retrieve the captured output streams here:
Get-Content "$PSScriptRoot\out.txt"
exit
}
}
# ...
Instead of -File, -Command is used to invoke the script, because that allows appending a redirection to the command: *> redirects all output streams.
#soleil suggests using Tee-Object as an alternative so that the output produced by the elevated process is not only captured, but also printed to the (invariably new window's) console as it is being produced:
..., $arg, '|', 'Tee-Object', '-FilePath', "`"$PSScriptRoot\out.txt`""
Caveat: While it doesn't make a difference in this simple case, it's important to know that arguments are parsed differently between -File and -Command modes; in a nutshell, with -File, the arguments following the script name are treated as literals, whereas the arguments following -Command form a command that is evaluated according to normal PowerShell rules in the target session, which has implications for escaping, for instance; notably, values with embedded spaces must be surrounded with quotes as part of the value.
The $PSScriptRoot\ path component in output-capture file $PSScriptRoot\out.txt ensures that the file is created in the same folder as the calling script (elevated processes default to $env:SystemRoot\System32 as the working dir.)
Similarly, this means that script file servicemssql.ps1, if it is invoked without a path component, must be in one of the directories listed in $env:PATH in order for the elevated PowerShell instance to find it; otherwise, a full path is also required, such as $PSScriptRoot\servicemssql.ps1.
-Wait ensures that control doesn't return until the elevated process has exited, at which point file $PSScriptRoot\out.txt can be examined.
As for the follow-up question:
To go even further, could we have a way to have the admin shell running non visible, and read the file as we go with the Unix equivalent of tail -f from the non -privileged shell ?
It is possible to run the elevated process itself invisibly, but note that you'll still get the UAC confirmation prompt. (If you were to turn UAC off (not recommended), you could use Start-Process -NoNewWindow to run the process in the same window.)
To also monitor output as it is being produced, tail -f-style, a PowerShell-only solution is both nontrivial and not the most efficient; to wit:
param([string]$arg='help')
if ($arg -in 'start', 'stop') {
if (-not (([System.Security.Principal.WindowsPrincipal] [System.Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole('Administrators'))) {
# Delete any old capture file.
$captureFile = "$PSScriptRoot\out.txt"
Remove-Item -ErrorAction Ignore $captureFile
# Start the elevated process *hidden and asynchronously*, passing
# a [System.Diagnostics.Process] instance representing the new process out, which can be used
# to monitor the process
$passThruArgs = '-noprofile', '-command', '&', "servicemssql.ps1", $arg, '*>', $captureFile
$ps = Start-Process powershell -WindowStyle Hidden -PassThru -Verb RunAs -ArgumentList $passThruArgs
# Wait for the capture file to appear, so we can start
# "tailing" it.
While (-not $ps.HasExited -and -not (Test-Path -LiteralPath $captureFile)) {
Start-Sleep -Milliseconds 100
}
# Start an aux. background that removes the capture file when the elevated
# process exits. This will make Get-Content -Wait below stop waiting.
$jb = Start-Job {
# Wait for the process to exit.
# Note: $using:ps cannot be used directly, because, due to
# serialization/deserialization, it is not a live object.
$ps = (Get-Process -Id $using:ps.Id)
while (-not $ps.HasExited) { Start-Sleep -Milliseconds 100 }
# Get-Content -Wait only checks once every second, so we must make
# sure that it has seen the latest content before we delete the file.
Start-Sleep -Milliseconds 1100
# Delete the file, which will make Get-Content -Wait exit (with an error).
Remove-Item -LiteralPath $using:captureFile
}
# Output the content of $captureFile and wait for new content to appear
# (-Wait), similar to tail -f.
# `-OutVariable capturedLines` collects all output in
# variable $capturedLines for later inspection.
Get-Content -ErrorAction SilentlyContinue -Wait -OutVariable capturedLines -LiteralPath $captureFile
Remove-Job -Force $jb # Remove the aux. job
Write-Verbose -Verbose "$($capturedLines.Count) line(s) captured."
exit
}
}
# ...

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"
}
}