Powershell pass command switches as variable - powershell

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

Related

Can you dynamically set an attribute in a Powershell cmdlet call?

I am not sure if this is possible, but I am wondering if there's an elegant "dynamic" way to use or not an attribute when using a cmdlet in Powershell.
For instance, in the code below, how can i set the -directory attribute to be present or not, depending on some conditions?
gci $folder_root -recurse -directory | ForEach{
# do something
}
You can conditionally add parameter arguments to a call through a technique called splatting.
All you need to do is construct a dictionary-like object and add any parameters you might want to pass to the call there:
# Create empty hashtable to hold conditional arguments
$optionalArguments = #{}
# Conditionally add an argument
if($somethingThatMightBeTrue){
# This is equivalent to having the `-Directory` switch present
$optionalArguments['Directory'] = $true
}
# And invoke the command
Get-ChildItem $folder_root -Recurse #optionalArguments
Notice that any variable that we splat is specified with a # instead of $ at the call site.

How to set PowerShell PSDefaultParameterValues conditionally based on other parameters

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.

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 $_ }

How to omit powershell parameter from cmdlet if value not provided?

I'm trying to execute a below Powershell command to create a new address list on exchange server with parameters like Name, Container, etc.
Container is an optional input/parameter, how do I omit it from cmdlet if its value is not provided?
I tried with IF conditionals but but does not seems working. Any help here?
New-AddressList -Name -Container \test MyAddressList5 -ConditionalStateOrProvince maha -IncludedRecipients MailboxUsers
You can pass needed parameters with their corresponding values via hashtable. Add If/Else conditions to include properties. Like so:
$Container = '\test MyAddressList5'
$Parameters = #{}
$Parameters.Add('ConditionalStateOrProvince','maha')
$Parameters.Add('IncludedRecipients','MailboxUsers')
if($Container){$Parameters.Add('Container',$Container)}
New-AddressList #Parameters
Also, when you need to include Switch parameter just pass $True. Like so:
$Parameters.Add('SomeSwitchParameter',$True)
Have a look at the documentation for the -Container parameter in New-AddressList: https://learn.microsoft.com/en-us/powershell/module/exchange/email-addresses-and-address-books/new-addresslist?view=exchange-ps#optional-parameters
Specifically:
If you don't use this parameter,the address list is created under the root (\).
...
Default value: None
$container = $null
New-AddressList -Container $container
# or...
$container = "\"
New-AddressList -Container $container
take a look at Get-Help *splatting to see a way to do this.
a bit more detail ... a "splat" is a hashtable of parameter = value pairs. once you have the basic always-there items in the splat, you can add others just as you would to any hashtable by $Param_Splat.Add(ParameterName, 'Value'). then when you call your cmdlet, you use Verb-Noun #Param_Splat. note the # symbol instead of the usual $. [grin]
take care,
lee

Why is my PowerShell function's parameter showing up as empty/blank?

On a whim, in order to learn PowerShell, which I know I'm woefully behind on, I decided to write a super simple script to use MSBuild to clean a bunch of solutions in code, then build them with a certain set of arguments. Here's the script:
$sourceDir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
$cleanArgs = '/t:clean /verbosity:q /nologo /P:Configuration=Debug'
$buildArgs = '/verbosity:q /nologo /P:Platform="Any CPU";Configuration=Debug'
$solutions = #("asdf.sln","qwer.sln","zxcv.sln","yuio.sln","hjkl.sln","nm.sln")
function buildWithArgs([string[]]$solutionsToBuild = #(), [string]$args = 'default')
{
Write-Host args: $args
foreach($sln in $solutionsToBuild)
{
& $env:WINDIR\Microsoft.NET\Framework64\v4.0.30319\msbuild.exe "$sourceDir\$sln $args" | Out-Default
}
}
buildWithArgs $solutions $buildArgs
buildWithArgs $solutions $cleanArgs
Now, my problem is that the function "buildWithArgs" doesn't respect the $args parameter, which is always empty when I run or debug the script, no matter what I pass in. The $solutionsToBuild parameter works flawlessly, however. Do I have improper syntax? Even if it were the case that my $cleanArgs string was somehow improperly formatted or not escaped, I can't even make the function work with simple strings like "asdf", as $args is always empty when I run it. Thanks for any help you can provide to what is assuredly some small, silly error!
First, $args is a special, built-in parameter in PowerShell, which represents all the arguments to a function. One of the side-effects to this is you can't see its value in the ISE debugger. Try changing its name to MSBuildArgs.
Make sure you define, buildArgs and cleanArgs as arrays. You're defining them as one single string. You do this so you can use PowerShell's splat operator (# instead of $ when referencing a variable), which sends each argument in an array as a parameter to a command.
Lastly, you don't need to quote parameters that are stored in variables. PowerShell will do any quoting for you.
After making these changes, you'll have something like this:
$sourceDir = Split-Path -Path $MyInvocation.MyCommand.Definition -Parent
$cleanArgs = #('/t:clean','/verbosity:q','/nologo','/P:Configuration=Debug')
$buildArgs = #('/verbosity:q','/nologo','/P:Platform="Any CPU";Configuration=Debug')
$solutions = #("asdf.sln","qwer.sln","zxcv.sln","yuio.sln","hjkl.sln","nm.sln")
function buildWithArgs([string[]]$solutionsToBuild = #(), [string[]]$MSBuildArgs= 'default')
{
Write-Host args: $args
foreach($sln in $solutionsToBuild)
{
$msbuildPath = Join-Path $env:WINDIR Microsoft.NET\Framework64\v4.0.30319\msbuild.exe -Resolve
& $msbuildPath (Join-Path $sourceDir $sln -Resolve) #MSBuildArgs | `
Out-Default
}
}
buildWithArgs $solutions $buildArgs
buildWithArgs $solutions $cleanArgs
$args is a special automatic variable in powershell functions. It contains an array of parameters which are not otherwise bound to the declared parameters of the function. By plain bad luck/inexperience, you happened to name one of your declared parameters the same, which will cause issues.
If you rename your parameter to something else, say $BuildArgs, it should be properly bound.
Note, however, that your current code will execute msbuild with a single parameter consisting of the solution path and args together. It is not the syntax to invoke msbuild with multiple arguments. If this causes issues for you I would suggest opening another question or searching online for that particular issue, just so this question does not get too big.