PowerShell - ShouldProcess not working when called from inside Invoke-Command - powershell

I am trying to add ShouldProcess logic to a script that deletes files on a remote server to I can use the -WhatIf parameter, but it is returning an error. Here is the function:
function testshouldprocess {
[CmdletBinding(SupportsShouldProcess = $true]
param(
$server
)
invoke-command $server {
Get-ChildItem c:\temp\ | ForEach-Object {
if($pscmdlet.ShouldProcess($Server)) {
remove-item $_.fullname
}
}
}
}
testshouldprocess 'Server1' -WhatIf
When the script is run, it returns error
InvalidOperation: You cannot call a method on a null-valued expression.
as each file passes through the pipeline. If I change the code to
if ($pscmdlet.ShouldProcess($server)) {
invoke-command $server {
Get-ChildItem c:\temp\ | ForEach-Object {
remove-item $_.fullname
}
}
}
it works, but the WhatIf only executes one time for the entire directory listing. If I change the code to
Get-ChildItem \\$server\c$\temp\ | ForEach-Object {
if ($pscmdlet.ShouldProcess($server)) {
remove-item $_.fullname
}
}
it works, but I would would much prefer to use Invoke-Command.
Is ShouldProcess not compatible with Invoke-Command?
Any insight is appreciated.

Hazrelle's answer provides the crucial pointer regarding the need to use the $using: scope in order for the remotely executing script block to have access to values from the caller's scope.
To fully support your scenario - both for -WhatIf and for -Confirm functionality, both of which are implied by turning SupportShouldProces on - you must:
Make your remotely executing script block an advanced one too, with its own [CmdletBinding(SupportsShouldProcess)] attribute above the param() block, and therefore its own $PSCmdlet instance.
Refer to the what-if/confirm-relevant values from the caller's scope via $using:WhatIfPreference and $using:ConfirmPreference
Note that for advanced functions and scripts PowerShell translates the -WhatIf and -Confirm switches into the equivalent preference-variable values, using function-local variables; that is, passing -WhatIf creates a function-local $WhatIfPreference variable with value $true, and passing -Confirm creates a function-local $ConfirmPreference with value High.
function testshouldprocess {
[CmdletBinding(SupportsShouldProcess)]
param(
$server
)
Invoke-Command $server {
[CmdletBinding(SupportsShouldProcess)]
param()
# Use the caller's WhatIf / Confirm preferences.
$WhatIfPreference = $using:WhatIfPreference
$ConfirmPreference = $using:ConfirmPreference
Get-ChildItem c:\temp\ | ForEach-Object {
if ($pscmdlet.ShouldProcess($using:server, "delete file: $($_.FullName)")) {
Remove-Item $_.FullName
}
}
}
}
testshouldprocess 'Server1' -WhatIf

The remote server knows only the command you execute. Not the values from the remote caller. Try with remove-item $_.fullname -Whatif:$($using:pscmdlet.ShouldProcess($server)). See Remote variables
Another option is to specify $WhatIfPreference on the remote server and use that in next statements
$WhatIfPreference = $using:pscmdlet.ShouldProcess($server);
Then remove-item $_.fullname -WhatIf:$WhatIfPreference

Related

How to pass the full path of a file as an argument to a new instance of Powershell?

This is the code of my script that calls another script:
$LocalFolder = "$env:USERPROFILE\Desktop\Banana Productions"
$RenderClient = "$LocalFolder\Render\Modelo 02\Cliente 02"
$CutFolder = "$RenderClient\Cortar"
$FFMpegScript = "$CutFolder\ffmpeg crop.ps1"
gci "$RenderClient\Cortar" *.mp4 -File -Recurse | ForEach-Object {
$FilePath = $_.FullName
start PowerShell "-NoExit", "-File", "`"$FFMpegScript`"", "$FilePath"
Write-Host $FilePath
}
The issue is that I am not able to pass the argument with the value of $_.FullName to the new instance, I get an error message in the new instance with the message:
Cannot process argument because the value of argument "name" is not valid
This is all that's in the script I'm calling:
param($args)
Write-Host $args[0]
Read-Host -Prompt "Press Enter to continue"
How can I resolve this?
Due to a long-standing bug in Start-Process - see GitHub issue #5576 - it is best to pass a single string argument to the (positionally implied) -ArgumentList parameter, which allows you to control the process command line explicitly:
The following uses an expandable here-string for syntactic convenience:
Get-ChildItem "$RenderClient\Cortar" -Filter *.mp4 -File -Recurse |
ForEach-Object {
$FilePath = $_.FullName
Start-Process PowerShell #"
-NoExit -File "$FFMpegScript" "$FilePath"
"# # Note: This must be at the very start of the line.
}
Additionally, do not use the automatic $args variable as a custom variable.
In fact, if you want your script to receive positional arguments only, there is no need for a formal param(...) declaration - just use the array-valued $args variable as-is:
# Without a param(...) declaration, $args *implicitly* contains
# any arguments passed, as an array.
$args[0] # output the 1st argument passed.
Read-Host -Prompt "Press Enter to continue"

Pass Variables to Powershell Pester Tests v5

I am at the last stages with my pester tests, but am having issues with the variable scoping.
I have quite a few tests and would like use something like Global scoping in order to declare variables such as $ModuleName, $ProjectRoot, and other similar things that are needed in each test.
I have tried using New-Variable -Scope Global and $Global:ModuleName but these don't seem to be visible in the Pester Tests.
I am calling Invoke-Pester from a build.ps1 file which has these declared.
Has anyone seen any good ways to use centrally defined variables, without using $env: variables?
I have found using $Env: variables to be the best approach. Thanks to the BuildHelpers module for showing me this.
This is now what the beginning of my Pester Test looks like:
Describe "Core Module Validation" {
BeforeAll {
$Script:moduleName = $ENV:BHProjectName
$Script:modulePath = $ENV:BHModulePath
Remove-Module -Name $moduleName -Force -ErrorAction SilentlyContinue
$Script:manifestTestResult = Test-ModuleManifest -Path $Env:BHPSModuleManifest
$Script:publicScriptFiles = Get-ChildItem -Path "$modulePath\Public" -Recurse
}
It "should cleanly import Module '$ENV:BHProjectName'" {
{ Import-Module -Name $modulePath -Force } | Should -Not -Throw
}
It "should export one or more functions" {
Import-Module -Name $modulePath -Force
Get-Command -Module $moduleName | Measure-Object | Select-Object -ExpandProperty Count | Should -BeGreaterThan 0
}
It "should have a valid PSD1 Module Manifest" {
{ Test-ModuleManifest -Path $ENV:BHPSModuleManifest } | Should -Not -Throw
}
These $Env: variables are set in my build.ps1 file, and are only temporary for the session.

Proxy function in PowerShell not accepting pipeline input

I've created a proxy function for Remove-Item, which deletes to the recycle bin instead of permanently (using the proxy so that I can seamlessly replace the rm alias, without breaking 3rd party scripts).
However, it doesn't work when a file is piped into the function. The heart of the proxy function is this:
if ($PSBoundParameters['DeletePermanently'] -or $PSBoundParameters['LiteralPath'] -or $PSBoundParameters['Filter'] -or $PSBoundParameters['Include'] -or $PSBoundParameters['Exclude'] -or $PSBoundParameters['Recurse'] -or $PSBoundParameters['Force'] -or $PSBoundParameters['Credential']) {
if ($PSBoundParameters['DeletePermanently']) { $PSBoundParameters.Remove('DeletePermanently') | Out-Null }
$scriptCmd = {& $wrappedCmd #PSBoundParameters }
} else {
$scriptCmd = {& Recycle-Item -Path $PSBoundParameters['Path'] }
}
So, my custom Recycle-Item function is only called if Path is the only parameter. So, something like Get-ChildItem .\temp\ | rm -DeletePermanently works just fine, but Get-ChildItem .\temp\ | rm has an error because the Path passed to Recycle-Item is $null.
I've tried passing $Path instead of $PSBoundParameters['Path'] and tried splatting #PSBoundParameters like the call to $wrappedCmd above, but none of it appears to do much good. I've copied the params from this function to Recycle-Item, to ensure that it is expecting input from the pipeline, but that doesn't seem to help either. Some of those changes appear to pass along the file name, but not the full path, so I don't know if there's some magic inside Remove-Item that I need to replicate to handle a file object from the pipeline.
Recycle-Item is just a basic function:
function Recycle-Item($Path) {
$item = Get-Item $Path
$directoryPath = Split-Path $item -Parent
$shell = new-object -comobject "Shell.Application"
$shellFolder = $shell.Namespace($directoryPath)
$shellItem = $shellFolder.ParseName($item.Name)
$shellItem.InvokeVerb("delete")
}
As mentioned in the comments, the provider cmdlets usually bind on LiteralPath when you pipe objects between them. This way allows Path to support wildcard globbing without the chance of passing ambiguous item paths between cmdlets.
Remove-Item has only two parameter sets, and they are named after their mandatory parameters, Path and LiteralPath
To solve your problem, simply check for all defined parameters that are not one of these two, then pass the appropriate value to Remove-Item based on the $PSCmdlet.ParameterSetName value:
if(#($PSBoundParameters.Keys |Where-Object {#('DeletePermanently','Filter','Include','Exclude','Recurse','Force','Credential') -contains $_}).Count -ge 1){
# a parameter other than the Path/LiteralPath or the common parameters was specified, default to Remove-Item
if ($PSBoundParameters['DeletePermanently']) {
$PSBoundParameters.Remove('DeletePermanently') | Out-Null
}
$scriptCmd = {& $wrappedCmd #PSBoundParameters }
} else {
# apart from common parameters, only Path/LiteralPath was specified, go for Recycle-Item
$scriptCmd = {& Recycle-Item -Path $PSBoundParameters[$PSCmdlet.ParameterSetName] }
}

Is it possible to create a user defined PowerShell environment?

I would like to clear a PowerShell session of mostly all alias definitions, except for common aliases like cd, sort, mkdir, ...
After I finished my session, I would like to restore all previous known the aliases.
There is no need to unload modules or to unregister CmdLets. I just want to clear the alias namespace for my session.
I could specify the allowed aliases in a list like this:
$AllowedAliases = #(
"cd", "mkdir", "rm", "rmdir",
"cd", "mkdir", "rm", "rmdir",
"where", "select",
"sort"
)
How can I save the aliases and restore them?
or
How can I start a clean PoSh and load only basic aliases?
What I have tested so far:
The following lines are from my example module called poc.psm1.
$Aliases = #()
function Register-PoC
{ foreach ($a in (Get-Item Alias:))
{ $script:Aliases += $a
Write-Host "$($a.Name) => $($a.ReferencedCommand) ($($a.Visibility))"
Remove-Item "Alias:$($a.Name)" -Force
}
}
function Unregister-PoC
{ foreach ($a in $script:Aliases)
{ Write-Host "$($a.Name) <= $($a.ReferencedCommand)"
if (Test-Path "Alias:$($a.Name)")
{ Write-Host "$($a.Name) exists." }
else
{ Set-Alias -Name $a.Name -Value $a.ReferencedCommand -Scope $a.Visibility }
}
if (Test-Path Alias:quit) { Remove-Item Alias:quit }
Remove-Module PoC
}
Export-ModuleMember -Function 'Register-PoC'
Export-ModuleMember -Function 'Unregister-PoC'
Register-PoC
Set-Alias -Name quit -Value Unregister-PoC -Description "Unload this module." -Scope Global
Usage example:
Import-Module .\poc.psm1
dir Alias:
quit
dir Alias:
Unfortunately, dir Alias: is not empty after invoking my script...
Another thing is, that I should preserve some settings of these aliases, because manual test showed, that dir does not behave like dir in before:
Remove-Item dir
Set-Alias dir Get-Item
dir
Cmdlet Get-Item an der Befehlspipelineposition 1
Geben Sie Werte für die folgenden Parameter an:
Path[0]:
So dir seams to append a default path to Get-Item if non is set to the alias.
Aliases are scoped. When you remove all aliases within a function, the aliases in the global scope aren't affected. Here is code that worked for me (simplifies your code a bit, although I didn't touch unregister-PoC, which could also be simplified I think):
function Register-PoC {
$script:aliases = get-item alias:
remove-item alias:* -force
}
function Unregister-PoC
{ foreach ($a in $script:Aliases)
{ Write-Host "$($a.Name) <= $($a.ReferencedCommand)"
if (Test-Path "Alias:$($a.Name)")
{ Write-Host "$($a.Name) exists." }
else
{ Set-Alias -Name $a.Name -Value $a.ReferencedCommand -Scope $a.Visibility }
}
if (Test-Path Alias:quit) { Remove-Item Alias:quit }
Remove-Module PoC
}
. Register-PoC
Set-Alias -Name quit -Value Unregister-PoC -Description "Unload this module." -Scope Global
Note the dot operator on Register-PoC. You will need to dot source quit to restore the aliases to global scope.
BTW, rather than the foreach loop in Unregister-PoC you could use copy-item.
For your situation, I would recommend using PowerShell profiles. These can be defined per user, per machine, and other situations. You can automatically run functions stored in the profile just by calling the function after it's defined in the profile.
For just your current user on the current machine, see Example 3 here.
New-Item -Path $PROFILE -ItemType File -Force
For other profile options, check out Understanding the Six PowerShell Profiles.
To ignore a profile, you can do that by directly running powershell.exe -NoProfile -NoExit but beware of nested sessions when doing that in another PowerShell session.
For a way to wipe all aliases except your desired list, you can export the aliases and re-import them after wiping all aliases. This can be added to the profile, if desired. Otherwise assign it to a function in the profile and call as needed. Change path if not using the profile folder(or wherever you like to keep things):
$allowedaliases = "cd","dir","rm","rmdir","where","select","sort"
Export-Alias -Path "$((Get-ChildItem $PROFILE).DirectoryName)\aliases.csv" -Name $allowedaliases
Remove-Item Alias:* -Force
Import-Alias -Path "$((Get-ChildItem $PROFILE).DirectoryName)\aliases.csv" -Force
Note: One of the original listed aliases(mkdir) is a function, not an alias, at least in Powershell v5.0. Also added dir into the list since that was mentioned by the OP.

Automated profile update Powershell - drive not found

I am struggling with my script - for some reason, the PSDrive that my script creates is not accessible for Resolve-Path.
In general, in the script there is "Start-RDP" function which starts RDP with preloaded credentials (autologon), and then checks if the Powershell profile on the target host is up to date (by comparing the filehashes). However, in order for the script to access the remote filesystem I need to mount it as PSDrive.
Here is the script that is offending. All the variables are set properly during that time, above in the script.
New-PSDrive -name "$computername" -Root "\\$computername\c$" -Credential $CurrentCred -PSProvider FileSystem | out-null
Start-Sleep -Seconds 10
while (!(Test-Path -Path ${Computername}:\$Userpath\$Documents\)) { Write-host "UserDir not created yet!" ; start-sleep -Seconds 5 }
if (Test-Path -Path ${Computername}:\$Userpath\$Documents\WindowsPowerShell) {
$ProfileHash = Get-FileHash $Profile.CurrentUserAllHosts
if (!(Test-Path "${computername}:\$Userpath\$Documents\WindowsPowerShell\profile.ps1")) { Copy-Item -Force -Path "$env:userprofile\WindowsPowershell\profile.ps1" -Destination "${computername}:\$Userpath\$Documents\WindowsPowerShell\" }
$RemoteProfileHash = Get-FileHash "${computername}:\$Userpath\$Documents\WindowsPowerShell\profile.ps1"
if ($ProfileHash -ne $RemoteProfileHash) { Copy-Item -Force -Path "$env:userprofile\$Documents\WindowsPowershell\profile.ps1" -Destination "${computername}:\$userpath\$Documents\WindowsPowerShell\" }
}
The error I am getting is at second Test-Path (where I check if WindowsPowerShell directory exists).
Resolve-Path : Cannot find drive. A drive with the name 'server01' does not exist.
At C:\windows\system32\windowspowershell\v1.0\Modules\Microsoft.PowerShell.Utility\Microsoft.PowerShell.Utility.psm1:35 char:32
+ $pathsToProcess += Resolve-Path $Path | Foreach-Object ProviderPath
+ ~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (server01:String) [Resolve-Path], DriveNotFoundException
+ FullyQualifiedErrorId : DriveNotFound,Microsoft.PowerShell.Commands.ResolvePathCommand
I am unable to trace down the specific reason this error occurs. The drive is there (I checked using PSBreakpoint)
I'm kind of stuck at this for some time now, do you have any ideas on that one?
I see what you did there.
The problem is that you are using the variable $Profile.CurrentUserAllHosts which powershell is trying to resolve as a complete variable name. $Profile is a string, which has no property called CurrentUserAllHosts. To fix, use the following:
$ProfileHash = Get-FileHash "${Profile}.CurrentUserAllHosts"
After some more investigation, I found this snippet on a blog
commands like Resolve-Path and $PSCmdlet.GetUnresolvedProviderPathFromPSPath() don’t normalize UNC paths properly, even when the FileSystem provider handles them.
Which then links to the Get-NormalizedFileSystemPath script on technet.
Since Get-FileHash is a system provided method, you'll want to Get-NormalizedFileSystemPath before passing it to Get-FileHash
And for posterity sake, here's the script:
function Get-NormalizedFileSystemPath
{
<#
.Synopsis
Normalizes file system paths.
.DESCRIPTION
Normalizes file system paths. This is similar to what the Resolve-Path cmdlet does, except Get-NormalizedFileSystemPath also properly handles UNC paths and converts 8.3 short names to long paths.
.PARAMETER Path
The path or paths to be normalized.
.PARAMETER IncludeProviderPrefix
If this switch is passed, normalized paths will be prefixed with 'FileSystem::'. This allows them to be reliably passed to cmdlets such as Get-Content, Get-Item, etc, regardless of Powershell's current location.
.EXAMPLE
Get-NormalizedFileSystemPath -Path '\\server\share\.\SomeFolder\..\SomeOtherFolder\File.txt'
Returns '\\server\share\SomeOtherFolder\File.txt'
.EXAMPLE
'\\server\c$\.\SomeFolder\..\PROGRA~1' | Get-NormalizedFileSystemPath -IncludeProviderPrefix
Assuming you can access the c$ share on \\server, and PROGRA~1 is the short name for "Program Files" (which is common), returns:
'FileSystem::\\server\c$\Program Files'
.INPUTS
String
.OUTPUTS
String
.NOTES
Paths passed to this command cannot contain wildcards; these will be treated as invalid characters by the .NET Framework classes which do the work of validating and normalizing the path.
.LINK
Resolve-Path
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[Alias('PSPath', 'FullName')]
[string[]]
$Path,
[switch]
$IncludeProviderPrefix
)
process
{
foreach ($_path in $Path)
{
$_resolved = $_path
if ($_resolved -match '^([^:]+)::')
{
$providerName = $matches[1]
if ($providerName -ne 'FileSystem')
{
Write-Error "Only FileSystem paths may be passed to Get-NormalizedFileSystemPath. Value '$_path' is for provider '$providerName'."
continue
}
$_resolved = $_resolved.Substring($matches[0].Length)
}
if (-not [System.IO.Path]::IsPathRooted($_resolved))
{
$_resolved = Join-Path -Path $PSCmdlet.SessionState.Path.CurrentFileSystemLocation -ChildPath $_resolved
}
try
{
$dirInfo = New-Object System.IO.DirectoryInfo($_resolved)
}
catch
{
$exception = $_.Exception
while ($null -ne $exception.InnerException)
{
$exception = $exception.InnerException
}
Write-Error "Value '$_path' could not be parsed as a FileSystem path: $($exception.Message)"
continue
}
$_resolved = $dirInfo.FullName
if ($IncludeProviderPrefix)
{
$_resolved = "FileSystem::$_resolved"
}
Write-Output $_resolved
}
} # process
} # function Get-NormalizedFileSystemPath