Possible to use -WhatIf and the invocation operator (&)? - powershell

Is it possible to use the -WhatIf argument when executing external commands? I want to be able to run a script with -WhatIf and have it print out a full list of all the external commands and arguments it's going to run without actually running them.
I've tried doing stuff like the following:
Function Invoke-Checked
{
param([ScriptBlock]$s)
if ($PSCmdlet.ShouldProcess($s.ToString(), "Execute"))
{
Invoke-Command $s
}
}
But that won't expand any variables that are present in the scriptblock - doing something like:
$s = { & dir $test }
Invoke-Checked $s
just prints
Performing the operation "Execute" on target " & dir $test ".
not particularly helpful.
Is there any way to do what I want?

First of all - you need to make sure that your 'wrapper' function supports WhatIf.
Another thing: you can expand the scriptBlock, but I'm not really convinced that is smart thing to do: e.g. if $test = 'Some path with spaces', it would stop working after expansion.
That being said: here are two options that work for me: using GetNewClosure() method on scriptBlock, and expanding whole thing:
function Invoke-ExpandedChecked {
[CmdletBinding(
SupportsShouldProcess = $true,
ConfirmImpact = 'Medium'
)]
param([ScriptBlock]$ScriptBlock)
$expanded = $ExecutionContext.InvokeCommand.ExpandString($ScriptBlock)
$script = [scriptblock]::Create($expanded)
if ($PSCmdlet.ShouldProcess($script.ToString(), "Execute"))
{
& $script
}
}
function Invoke-Checked {
[CmdletBinding(
SupportsShouldProcess = $true,
ConfirmImpact = 'Medium'
)]
param([ScriptBlock]$ScriptBlock)
$newClosure = $ScriptBlock.GetNewClosure()
if ($PSCmdlet.ShouldProcess($newClosure.ToString(), "Execute"))
{
& $newClosure
}
}
$test = '.\DSCDemo.ps_'
$s = { cmd /c dir $test}
Invoke-Checked $s -WhatIf
Invoke-Checked $s
Invoke-ExpandedChecked $s -WhatIf
Invoke-ExpandedChecked $s
And an example of results for path with spaces:
$test = 'C:\Program Files'
Invoke-Checked $s
Invoke-ExpandedChecked $s
Works fine for one with new enclosure. With expanded:
cmd : File Not Found
At line:1 char:2
+ cmd /c dir C:\Program Files

I'm going to interpret the question to mean, "how do I use -whatif with running external commands?", since that's how I found this question.
# myscript.ps1
[cmdletbinding(SupportsShouldProcess=$True)]
Param($path) # put Param() if no parameters
if ($pscmdlet.ShouldProcess($Path, 'creating folder')) { # not -whatif
cmd /c mkdir $path
}
.\myscript foo -whatif
What if: Performing the operation "creating folder" on target "foo".

Related

Simple PowerShell parallel command execution

I have two scripts I wish to run in parallel. I have a driver script I wish to use to execute the two parallel scripts.
The method I'm using of course does not run in parallel. I have looked all over the google universe, and at using a workflow, but I can't get that to work either.
Driver Script:
Clear-Host
$a = 4
$b = 29
$command1 = "C:\JUNK\Code\PowerScript\CS1.ps1 $a"
$command2 = "C:\JUNK\Code\PowerScript\CS2.ps1 $b"
#Invoke-Expression $command1
#Invoke-Expression $command2
The scripts to get called resemble this:
#CS1.ps1
Param(
[string]$build
)
$ADATE = Get-Date
$DESTLOG1="C:\JUNK\Code\PowerScript\LOGS\status1.log"
$RunDefintion1 = ($build, $ADATE -join",")
Add-Content $DESTLOG1 $RunDefintion1
From powershell 2 upwards you could use the Start-Job command, something like this ( untested, might need some adjustments )
Start-Job { C:\JUNK\Code\PowerScript\CS1.ps1 $a }
Start-Job { C:\JUNK\Code\PowerScript\CS2.ps1 $b }

PowerShell: Invoking a script block that contains underscore variable

I normally do the following to invoke a script block containing $_:
$scriptBlock = { $_ <# do something with $_ here #> }
$theArg | ForEach-Object $scriptBlock
In effect, I am creating a pipeline which will give $_ its value (within the Foreach-Object function invocation).
However, when looking at the source code of the LINQ module, it defines and uses the following function to invoke the delegate:
# It is actually surprisingly difficult to write a function (in a module)
# that uses $_ in scriptblocks that it takes as parameters. This is a strange
# issue with scoping that seems to only matter when the function is a part
# of a module which has an isolated scope.
#
# In the case of this code:
# 1..10 | Add-Ten { $_ + 10 }
#
# ... the function Add-Ten must jump through hoops in order to invoke the
# supplied scriptblock in such a way that $_ represents the current item
# in the pipeline.
#
# Which brings me to Invoke-ScriptBlock.
# This function takes a ScriptBlock as a parameter, and an object that will
# be supplied to the $_ variable. Since the $_ may already be defined in
# this scope, we need to store the old value, and restore it when we are done.
# Unfortunately this can only be done (to my knowledge) by hitting the
# internal api's with reflection. Not only is this an issue for performance,
# it is also fragile. Fortunately this appears to still work in PowerShell
# version 2 through 3 beta.
function Invoke-ScriptBlock {
[CmdletBinding()]
param (
[Parameter(Position=1,Mandatory=$true)]
[ScriptBlock]$ScriptBlock,
[Parameter(ValueFromPipeline=$true)]
[Object]$InputObject
)
begin {
# equivalent to calling $ScriptBlock.SessionState property:
$SessionStateProperty = [ScriptBlock].GetProperty('SessionState',([System.Reflection.BindingFlags]'NonPublic,Instance'))
$SessionState = $SessionStateProperty.GetValue($ScriptBlock, $null)
}
}
process {
$NewUnderBar = $InputObject
$OldUnderBar = $SessionState.PSVariable.GetValue('_')
try {
$SessionState.PSVariable.Set('_', $NewUnderBar)
$SessionState.InvokeCommand.InvokeScript($SessionState, $ScriptBlock, #())
}
finally {
$SessionState.PSVariable.Set('_', $OldUnderBar)
}
}
}
This strikes me as a bit low-level. Is there a recommended, safe way of doing this?
You can invoke scriptblocks with the ampersand. No need to use Foreach-Object.
$scriptblock = {## whatever}
& $scriptblock
#(1,2,3) | % { & {write-host $_}}
To pass parameters:
$scriptblock = {write-host $args[0]}
& $scriptblock 'test'
$scriptBlock = {param($NamedParam) write-host $NamedParam}
& $scriptBlock -NamedParam 'test'
If you're going to be using this inside of Invoke-Command, you could also usin the $using construct.
$test = 'test'
$scriptblock = {write-host $using:test}

How to return the name of the calling script from a Powershell Module?

I have two Powershell files, a module and a script that calls the module.
Module: test.psm1
Function Get-Info {
$MyInvocation.MyCommand.Name
}
Script: myTest.ps1
Import-Module C:\Users\moomin\Documents\test.psm1 -force
Get-Info
When I run ./myTest.ps1 I get
Get-Info
I want to return the name of the calling script (test.ps1). How can I do that?
Use PSCommandPath instead in your module:
Example test.psm1
function Get-Info{
$MyInvocation.PSCommandPath
}
Example myTest.ps1
Import-Module C:\Users\moomin\Documents\test.psm1 -force
Get-Info
Output:
C:\Users\moomin\Documents\myTest.ps1
If you want only the name of the script that could be managed by doing
GCI $MyInvocation.PSCommandPath | Select -Expand Name
That would output:
myTest.ps1
I believe you could use the Get-PSCallStack cmdlet, which returns an array of stack frame objects. You can use this to identify the calling script down to the line of code.
Module: test.psm1
Function Get-Info {
$callstack = Get-PSCallStack
$callstack[1].Location
}
Output:
myTest.ps1: Line 2
Using the $MyInvocation.MyCommand is relative to it's scope.
A simple example (Of a script located : C:\Dev\Test-Script.ps1):
$name = $MyInvocation.MyCommand.Name;
$path = $MyInvocation.MyCommand.Path;
function Get-Invocation(){
$path = $MyInvocation.MyCommand.Path;
$cmd = $MyInvocation.MyCommand.Name;
write-host "Command : $cmd - Path : $path";
}
write-host "Command : $cmd - Path : $path";
Get-Invocation;
The output when running .\c:\Dev\Test-Script.ps1 :
Command : C:\Dev\Test-Script.ps1 - Path : C:\Dev\Test-Script.ps1
Command : Get-Invocation - Path :
As you see, the $MyInvocation is relative to the scoping. If you want the path of your script, do not enclose it in a function. If you want the invocation of the command, then you wrap it.
You could also use the callstack as suggested, but be aware of scoping rules.
I used this today after trying a couple of techniques.
$scriptPath = split-path -parent $MyInvocation.MyCommand.Definition
$ScriptName = $MyInvocation.MyCommand | select -ExpandProperty Name
Invoke-Expression ". $Script\$ScriptName"
To refer to the invocation info of the calling script, use:
#(Get-PSCallStack)[1].InvocationInfo
e.g.:
#(Get-PSCallStack)[1].InvocationInfo.MyCommand.Name
This provides the script path with trailing backslash as one variable and the script name as another.
The path works with Powershell 2.0 and 3.0 and 4.0 and probably 5.0
Where with Posershell $PSscriptroot is now available.
$_INST = $myinvocation.mycommand.path.substring(0,($myinvocation.mycommand.path.length - $MyInvocation.mycommand.name.length))
$_ScriptName = $myinvocation.mycommand.path.substring($MyInvocation.MyCommand.Definition.LastIndexOf('\'),($MyInvocation.mycommand.name.length +1))
$_ScriptName = $_ScriptName.TrimStart('\')
If you want a more reusable approach, you can use:
function Get-CallingFileName
{
$cStack = #(Get-PSCallStack)
$cStack[$cStack.Length-1].InvocationInfo.MyCommand.Name
}
The challenge I had was having a function that could be reused within the module. Everything else assumed that the script was calling the module function directly and if it was removed even 1 step, then the result would be the module file name. If, however, the source script is calling a function in the module which is, in turn, calling another function in the module, then this is the only answer I've seen that can ensure you're getting the source script info.
Of course, this approach is based on what #iRon and #James posted.
For you googlers looking for quick copy paste solution,
here is what works in Powershell 5.1
Inside your module:
$Script = (Get-PSCallStack)[2].Command
This will output just the script name (ScriptName.ps1) which invoked a function located in module.
I use this in my module:
function Get-ScriptPath {
[CmdletBinding()]
param (
[string]
$Extension = '.ps1'
)
# Allow module to inherit '-Verbose' flag.
if (($PSCmdlet) -and (-not $PSBoundParameters.ContainsKey('Verbose'))) {
$VerbosePreference = $PSCmdlet.GetVariableValue('VerbosePreference')
}
# Allow module to inherit '-Debug' flag.
if (($PSCmdlet) -and (-not $PSBoundParameters.ContainsKey('Debug'))) {
$DebugPreference = $PSCmdlet.GetVariableValue('DebugPreference')
}
$callstack = Get-PSCallStack
$i = 0
$max = 100
while ($true) {
if (!$callstack[$i]) {
Write-Verbose "Cannot detect callstack frame '$i' in 'Get-ScriptPath'."
return $null
}
$path = $callstack[$i].ScriptName
if ($path) {
Write-Verbose "Callstack frame '$i': '$path'."
$ext = [IO.Path]::GetExtension($path)
if (($ext) -and $ext -eq $Extension) {
return $path
}
}
$i++
if ($i -gt $max) {
Write-Verbose "Exceeded the maximum of '$max' callstack frames in 'Get-ScriptPath'."
return $null
}
}
return $null
}
You can grab the automatic variable MyInvocation from the parent scope and get the name from there.
Get-Variable -Scope:1 -Name:MyInvocation -ValueOnly
I did a basic test to check to see if it would always just get the direct parent scope and it worked like a treat and is extremely fast as opposed to Get-PSCallStack
function ScopeTest () {
Write-Information -Message:'ScopeTest'
}
Write-nLog -Message:'nLog' -Type:110 -SetLevel:Verbose
ScopeTest

How to get the parameter name in a powershell function

I would like to implement a function to output some debugging information.
I want the function, namely "Write-Foo", to work in the following way:
Write-Foo $SomeThing
# the output will be "SomeThing: (the value of Something)"
Is it possible to implement this function? I don't know how to get the name of the parameter of the function.
Thanks
This is close to what you want:
function Write-Foo($varName)
{
$varVal = $ExecutionContext.InvokeCommand.ExpandString("`$variable:$varName")
Write-Host "$($varName): $varVal"
}
Trying it out:
PS> $SomeThing = 'hello'
PS> Write-Foo SomeThing
SomeThing: hello
Another take:
function Write-Foo($varName)
{
$v = Get-Variable $varName
Write-Host ($v.Name + ": " + $v.Value)
}
Result:
PS C:\> $Something = "nothing"
PS C:\> Write-Foo SomeThing
Something: nothing
Another possibility (this assumes what you want to display is the name of the variable that was passed as an argument, not the name of the parameter it was passed to).
function Write-Foo($varName)
{
$var =
($MyInvocation.line -replace '\s*write-foo\s*\$','').trim()
'{0}: {1}' -f $var,$varName
}
$something = 'Hello'
write-foo $something
something: Hello
It's easier if you pass the name without the $
function Write-Foo($varName)
{
$var = Get-Variable $varName
'{0}: {1}' -f $var.Name,$var.Value
}
$something = 'Hello'
write-foo something
something: Hello
I'll also second #Ansgar Weicher's suggestion of writing that to the Debug stream so you don't pollute the pipeline with it.
You could define a function like this:
function Write-Foo($msg) {
Write-Host ("SomeThing: {0}" -f $msg)
}
or (if you want the output to be available for further processing) like this:
function Write-Foo($msg) {
"SomeThing: {0}" -f $msg
}
However, since you said you want to output debugging information, using the already existing Write-Debug cmdlet might be a better approach:
PS C:\> Write-Debug 'foo'
PS C:\> $DebugPreference
SilentlyContinue
PS C:\> $DebugPreference = 'Continue'
PS C:\> Write-Debug 'foo'
DEBUG: foo
You can access the function's parameters through $MyInvocation.BoundParameters, so this may be what you want:
function Write-Foo($varName) {
foreach ($key in $MyInvocation.BoundParameters.Keys) {
Write-Verbose ("{0}: {1}" -f ($key, $MyInvocation.BoundParameters[$key])) -Verbose
}
}
Write-Foo "Hello"
and the output looks like this:
VERBOSE: varName: Hello
If you want to be able to control when the debug information appears you can turn this into a cmdlet:
function Write-Foo {
[CmdletBinding()]
Param ($varName)
foreach ($key in $MyInvocation.BoundParameters.Keys) {
Write-Verbose ("{0}: {1}" -f ($key, $MyInvocation.BoundParameters[$key]))
}
}
Write-Foo "Hello"
Write-Foo "This will debug" -Verbose
and the first call produces no output while the second will show you:
VERBOSE: Verbose: True
VERBOSE: varName: This will debug
Naturally you can choose how exactly to output the debug information. Probably either Write-Debug (which usually prompts for each line of output) or Write-Verbose (which is usually suppressed) is appropriate here.

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