How to set PowerShell PSDefaultParameterValues conditionally based on other parameters - powershell

Without going much into details on why am I even trying this out, is it possible to set PSDefaultParameterValues conditionally based on other parameter values?
Let's say I would like to set -Force if ItemType is Directory in New-Item call.
$PSDefaultParameterValues = #{ "New-Item:Force" = {
# TODO: if Itemtype is Directory, return $true
# else return default: false
return $false
}
}
New-Item -ItemType Directory
Problem is, that I can get the parameters used in $args but I do not have access to their values.

As you've observed, the argument passed to your script block via the automatic $args variable contains the names of the bound parameters in the New-Item call at hand, but lacks their values.
This looks like an oversight, which GitHub proposal #16011 aims to correct.
The following workaround isn't foolproof, but may suffice in practice:
$PSDefaultParameterValues = #{
'New-Item:Force' = {
($false, $true)[
$args.BoundParameters.Contains('ItemType') -and
(Get-PSCallStack)[1].Position.Text -match '\bDirectory\b'
]
}
}
You could tweak the regex to be stricter, but note that PowerShell's elastic syntax and parameter aliases make it hard to match a parameter name reliably; e.g., -Type Directory, -it Directory and -ty Directory are all acceptable variations of -ItemType Directory.
A caveat is that this won't work if you pass the Directory argument to -ItemType in New-Item calls via a variable; e.g., $type='Directory'; New-Item -ItemType $type ... would not be recognized by the script block. Handling that case would require substantially more work.
Note:
The parent call-stack entry, which you can obtain as the 2nd element of the call-stack array returned by Get-PSCallStack, contains the raw command text of the New-Item call at hand (in property .Position.Text), which the solution above examines.
However, since it is the raw command text, it doesn't include the expanded argument values that are ultimately seen by the command; that is, what variable references and expression evaluate to isn't directly available.
You could perform your own expansion, assuming you've reliably identified the variable reference / subexpression of interest, but note that, at least in principle, evaluating a subexpression can have side effects (and possibly also take a long time to execute), so effectively executing it twice may be undesirable.

Related

Rename files & append Number in Powershell [duplicate]

I've never used powershell or any cmd line to try and rename files, nor so I really know much about script writing in general.
I've already had some success in renaming the files in question but am stuck on the last piece of the puzzle.
Original file names:
NEE100_N-20210812_082245.jpg
NEE101_E-20210812_083782.jpg
NEE102_W-20210812_084983.jpg
I successfully change those to AT-###-N-......jpg using:
Rename-Item -NewName {$_.name -replace "NEE\d\d\d_", "AT-112-"}
And this is what they looked like after:
AT-112-N-20210812_082245.jpg
AT-112-E-20210812_083782.jpg
AT-112-W-20210812_084983.jpg
Now however, I have a few files that look like this:
AT-112-NewImage-20210812_083782.jpg
AT-112-NewImage-20210812_093722.jpg
and I want to change them to:
AT-112-D1-20210812_083782.jpg
AT-112-D2-20210812_093722.jpg
...and so on.
I've tried a few things here to try and do that. Such as replacing "NewImage" with "D" and then using something like this (not exact, just an example):
$i = 1
Get-ChildItem *.jpg | %{Rename-Item $_ -NewName ('19981016_{0:D4}.jpg' -f $i++)}
But this did not work. I have seen scripts that use sequential numbering either added as a suffix or a prefix. But I can't figure out how to do this if what I want to have sequence numbering in the middle of the name.
Hopefully this make sense, if I need more elaboration, let me know. Thanks!
You need to use an expression (inside (...)) as your -replace substitution operand in order to incorporate a dynamic value, such as the sequence number in your case.
In order to use a variable that maintains state across multiple invocations of a delay-bind script block ({ ... }, the one being passed to the -NewName parameter in your first attempt), you need to create the variable in the caller's scope and explicitly reference it there:
This is necessary, because delay-bind script blocks run in a child scope, unfortunately,[1] so that any variables created inside the block go out of scope after every invocation.
Use Get-Variable to obtain a reference to a variable object in the caller's (parent) scope[2], and use its .Value property, as shown below.
$i = 1
Get-ChildItem *.jpg | Rename-Item -NewName {
$_.Name -replace '-NewImage-', ('-D{0}-' -f (Get-Variable i).Value++)
} -WhatIf
Note: The -WhatIf common parameter in the command above previews the operation. Remove -WhatIf once you're sure the operation will do what you want.
Note: The above solution is simple, but somewhat inefficient, due to the repeated Get-Variable calls - see this answer for more efficient alternatives.
[1] This contrasts with the behavior of script blocks passed to Where-Object and ForEach-Object. See GitHub issue #7157 for a discussion of this problematic discrepancy.
[2] Without a -Scope argument, if Get-Variable doesn't find a variable in the current scope, it looks for a variable in the ancestral scopes, starting with the parent scope - which in this case the caller's. You can make the call's intent more explicitly with -Scope 1, which starts the lookup from the parent scope.

Powershell rename files with sequential numbers in the middle of name

I've never used powershell or any cmd line to try and rename files, nor so I really know much about script writing in general.
I've already had some success in renaming the files in question but am stuck on the last piece of the puzzle.
Original file names:
NEE100_N-20210812_082245.jpg
NEE101_E-20210812_083782.jpg
NEE102_W-20210812_084983.jpg
I successfully change those to AT-###-N-......jpg using:
Rename-Item -NewName {$_.name -replace "NEE\d\d\d_", "AT-112-"}
And this is what they looked like after:
AT-112-N-20210812_082245.jpg
AT-112-E-20210812_083782.jpg
AT-112-W-20210812_084983.jpg
Now however, I have a few files that look like this:
AT-112-NewImage-20210812_083782.jpg
AT-112-NewImage-20210812_093722.jpg
and I want to change them to:
AT-112-D1-20210812_083782.jpg
AT-112-D2-20210812_093722.jpg
...and so on.
I've tried a few things here to try and do that. Such as replacing "NewImage" with "D" and then using something like this (not exact, just an example):
$i = 1
Get-ChildItem *.jpg | %{Rename-Item $_ -NewName ('19981016_{0:D4}.jpg' -f $i++)}
But this did not work. I have seen scripts that use sequential numbering either added as a suffix or a prefix. But I can't figure out how to do this if what I want to have sequence numbering in the middle of the name.
Hopefully this make sense, if I need more elaboration, let me know. Thanks!
You need to use an expression (inside (...)) as your -replace substitution operand in order to incorporate a dynamic value, such as the sequence number in your case.
In order to use a variable that maintains state across multiple invocations of a delay-bind script block ({ ... }, the one being passed to the -NewName parameter in your first attempt), you need to create the variable in the caller's scope and explicitly reference it there:
This is necessary, because delay-bind script blocks run in a child scope, unfortunately,[1] so that any variables created inside the block go out of scope after every invocation.
Use Get-Variable to obtain a reference to a variable object in the caller's (parent) scope[2], and use its .Value property, as shown below.
$i = 1
Get-ChildItem *.jpg | Rename-Item -NewName {
$_.Name -replace '-NewImage-', ('-D{0}-' -f (Get-Variable i).Value++)
} -WhatIf
Note: The -WhatIf common parameter in the command above previews the operation. Remove -WhatIf once you're sure the operation will do what you want.
Note: The above solution is simple, but somewhat inefficient, due to the repeated Get-Variable calls - see this answer for more efficient alternatives.
[1] This contrasts with the behavior of script blocks passed to Where-Object and ForEach-Object. See GitHub issue #7157 for a discussion of this problematic discrepancy.
[2] Without a -Scope argument, if Get-Variable doesn't find a variable in the current scope, it looks for a variable in the ancestral scopes, starting with the parent scope - which in this case the caller's. You can make the call's intent more explicitly with -Scope 1, which starts the lookup from the parent scope.

Test-Path fails but doesn't do else

I am trying to test several paths and if any fail, create the paths.
$subFolders = "$sortByProject$projectName\Originals", "$sortByProject$projectName\Pulled", "$sortByProject$projectName\Retouched", "$sortByProject$projectName\Uploaded"
if(!(Test-Path -Path "$sortByProject$projectName", "$subFolders")){
New-Item -ItemType Directory -Path "$sortByProject$projectName", "$subFolders"
}
The test finds "$sortByProject$projectName" exists but "$subFolders" fails, so output appears like:
True
False
I would think since a false is returned, it would move to the new-item command and build all four requested folders (.\originals, .\pulled, .\retouched and .\uploaded). All the variable are properly named and return the desired path when called independently. I think the mess up is because there are multiple items assigned to $subfolders, but I don't understand why.
I also think this code is sloppy and would love to learn a better way to do a multiple path test and create any of the missing paths.
You don't need to explicitly test the existence of your directory paths:
Just use New-Item's -Force switch in combination with -ItemType Directory, which will create the specified directories on demand - including parent directories - while leaving existing directories alone (and returning directory-info objects describing the preexisting / newly created directories).
New-Item -Force -ItemType Directory -Path $subFolders
As for what you tried:
Your Test-Path command returns an array of Booleans, and PowerShell considers any 2+-element array $true in a Boolean context - irrespective of its element values; see the bottom section of this answer for a summary of the PowerShell's to-Boolean coercion rules.
-Path "$sortByProject$projectName", "$subFolders" has two problems:
Not only is double-quoting variable values passed as command arguments never necessary in PowerShell, in the case of the array variable $subFolders using "$subFolders" turns the array into a single string containing the (stringified) elements separated with spaces.
If you correct this immediate problem - by using
-Path $sortByProject$projectName, $subFolders - another problem is revealed: you aren then in effect passing a jagged array, whose first element is a string, and whose second element is a nested array, which would break the invocation.
Correcting this problem requires a perhaps non-obvious approach: you must use + rather than , in order to construct a flat array, which in turn requires that the LHS already be an array, which requires using the unary form of ,, the array constructor operator, and switching to an expression, enclosed in (...), the grouping operator:
-Path (, $sortByProject$projectName + $subFolders)
$subfolders | where {-not (Test-Path $_)} | Foreach-Object { New-Item -ItemType Directory -Path $_ }

Get-ChildItem with Multiple Paths via Variable

This one stumps me a bit. I generally feel pretty advanced in powershell but I simply dont understand the nuance of this one.
This works
$LogFiles = Get-ChildItem -Path c:\windows\temp\*.log,c:\temp\*.log,C:\programdata\Microsoft\IntuneManagementExtension\Logs\*.log
Yet what I want to do (and doesnt work) is this:
$LogsToGather = "c:\windows\temp\*.log,c:\temp\*.log,C:\programdata\Microsoft\IntuneManagementExtension\Logs\*.log"
$LogFiles = Get-ChildItem -Path "$($LogsToGather)" -Recurse
I have tried making the VAR an array, I have tried a number of things with making string. I was able to write around the issue but I am uniquely interested in understanding what data type -path is accepting with that common delineation and be able to create it dynamically.
It seems like a trick that the cmdlet accepts comma delineation. Can it be recreated using some sort of array, hashtable, etc..?
Anyone know?
Yes, $LogsToGather must be an array of strings for your command to work:
$LogsToGather = 'c:\windows\temp\*.log', 'c:\temp\*.log', 'C:\programdata\Microsoft\IntuneManagementExtension\Logs\*.log'
Note how the array elements, separated by ,, must be quoted individually (see bottom section).
Get-Help with -Parameter is a quick way to examine the data type a given parameter expects:
PS> Get-Help Get-ChildItem -Parameter Path
-Path <String[]>
Specifies a path to one or more locations. Wildcards are permitted. The default location is the current directory (`.`).
Required? false
Position? 0
Default value Current directory
Accept pipeline input? True (ByPropertyName, ByValue)
Accept wildcard characters? false
String[] represents an array ([]) of [string] (System.String) instances - see about_Command_Syntax.
For more information on Get-ChildItem, see the docs.
As for what you tried:
$LogsToGather = "c:\windows\temp\*.log,c:\temp\*.log,C:\programdata\Microsoft\IntuneManagementExtension\Logs\*.log"
This creates a single string that, when passed to Get-ChildItem, is as a whole interpreted as a single path, which obviously won't work.
Note that specifying the elements of an array unquoted, as in:
Get-ChildItem -path c:\windows\temp\*.log, c:\temp\*.log, ...
is only supported if you pass an array as a command argument, not in the context of creating an array with an expression such as $LogsToGather = 'foo', 'bar', ..
The reason is that PowerShell has two fundamental parsing modes - argument mode and expression mode, as explained in this answer,

Powershell pass command switches as variable

i'm trying to create a script that handle copy of folders and files.
i need to pass the switches '-recures -container' if it's a folder and nothing is it's a file.
is there a way to create a variable that will hold the '-recurse -container' and pass it to the command like this:
$copy_args = '-Recurse -container '
Copy-Item $tmptmp\$file -Destination \\$server\d$\$tmpprd\ $copy_args -Force
thanks
Mor
The best way to do this is with a technique called splatting. You create a hashtable of the parameters you want to pass and then you use # with the variable name (instead of $) to indicate that you want to splat it in to the required cmdlets parameters:
$copy_args = #{
Recurse = $true
Container = $true
}
Copy-Item $tmptmp\$file -Destination \\$server\d$\$tmpprd\ #copy_args -Force
Mark Wragg's helpful answer recommends splatting, which gives you the most flexibility.
As an aside:
Setting -Recurse is sufficient in your case, because it implies -Container
In fact, you can even use -Recurse unconditionally, because it is simply ignored if the source path is a file.
On occasion you may want to conditionally pass a switch directly, without the added verbosity of splatting.
Given that the syntax - -SomeSwitch:$boolVar or -SomeSwitch:(<boolExpression>) (optionally with whitespace after :) - isn't obvious, let me demonstrate:
Using a Boolean variable:
# The source path.
$sourcePath = $tmptmp\$file
# Set the Boolean value that will turn the -Recurse switch on / off.
$doRecurse = Test-Path -PathType Container $sourcePath # $true if $sourcePath is a dir.
# Use -Recurse:$doRecurse
Copy-Item -Recurse:$doRecurse $sourcePath -Destination \\$server\d$\$tmpprd\ -Force
Alternatively, using a Boolean expression:
Copy-Item -Recurse:(Test-Path -PathType Container $sourcePath) $sourcePath -Destination \\$server\d$\$tmpprd\ -Force
Note that the : to separate the parameter name from the argument is a necessity in the case of a switch parameter, so as to indicate that the argument is intended for the switch (which normally do not take an argument) rather than being a separate, positional argument.
Caveat: Both in this case and with splatting passing an effective $false to a switch is technically not the same as omitting the switch, and there are situations where the difference matters.
Read on to learn more.
Technically, a cmdlet or advanced function can distinguish between an omitted switch and one with a $false argument via the automatic $PSBoundParameters variable, which contains a dictionary of all explicitly passed parameters.
In the case of the common -Confirm parameter, this distinction is used intentionally - which is atypical.
Here's a simple demonstration:
# Sample advanced function that supports -Confirm with a medium impact level.
function foo {
[CmdletBinding(SupportsShouldProcess, ConfirmImpact='Medium')]
param()
if ($PSCmdlet.ShouldProcess('dummy')) { 'do it' }
}
# Invocation *with -Confirm* prompts unconditionally.
foo -Confirm # ditto with -Confirm:$true
# Invocation *without -Confirm*:
# Whether you'll be prompted depends on the value of the $ConfirmPreference
# variable: If the value is 'Medium' or 'Low', you'll be prompted.
foo
# Invocation with *-Confirm:$false* NEVER prompts,
# irrespective of the $ConfirmPreference value.
foo -Confirm:$false