as an adjunct to this issue: How do I force declared parameters to require explicit naming? I am struggling with pipelines. Suppose I want the behaviour that this declares:
param(
$installdir,
$compilemode,
[Parameter(Position=0, ValueFromRemainingArguments=$true)] $files
)
namely, that I can call my script like this:
c:\> MyScript -installdir c:\ file-1.txt file-2.txt file-3.txt
but now I want to also be able to do it this way:
c:\> gi file-*.txt |MyScript -installdir c:\
I might think of adding a decoration to the parameter like this:
param(
$installdir,
$compilemode,
[Parameter(
Position=0,
ValueFromRemainingArguments=$true,
ValueFromPipeline=$true
)] $files
)
but what actually happens is I only get 1 argument into my parameter i.e. instead of getting an array with all the files that gi produced, I get only the first in the list.
A second way I attempted this was by using the $input variable (instead of using the ValueFromPipeline decorator), but then in trying to call the script I get the error:
The input object cannot be bound to any parameters for the command
either because the command does not take pipeline input or the input
and its properties do not match any of the parameters that take
pipeline input.
where can I go from here?
You could declare it without ValueFromRemainingArguments:
param(
[Parameter(
Position=0,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true)]
[Alias('PSPath')]
[string[]]
$files,
$installdir,
$compilemode
)
And then pass in multiple files as an array using the comma operator e.g.:
MyScript -installdir c:\ file-1.txt,file-2.txt,file-3.txt
Note: In order to accept input from commands like Get-Item and Get-ChildItem, use ValueFromPipelineByPropertyName and add a parameter alias "PSPath" that will find the PSPath property on the objects output by Get-Item/Get-ChildItem.
I have test this in ISE and it works fine:
function foo
{
param(
[Parameter(
Position=0,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true)]
[Alias('PSPath')]
[string[]]
$files,
$installdir,
$compilemode
)
process {
foreach ($file in $files) {
"File is $file, installdir: $installdir, compilemode: $compilemode"
}
}
}
foo a,b,c -installdir c:\temp -compilemode x64
ls $home -file | foo -installdir c:\bin -compilemode x86
FYI, this is a template I use all the time to create commands that take pipeline input or array input, as well as wildcard paths:
function Verb-PathLiteralPath
{
[CmdletBinding(DefaultParameterSetName="Path",
SupportsShouldProcess=$true)]
#[OutputType([output_type_here])] # Uncomment this line and specify the output type of this
# function to enable Intellisense for its output.
param(
[Parameter(Mandatory=$true,
Position=0,
ParameterSetName="Path",
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true,
HelpMessage="Path to one or more locations.")]
[ValidateNotNullOrEmpty()]
[SupportsWildcards()]
[string[]]
$Path,
[Alias("PSPath")]
[Parameter(Mandatory=$true,
Position=0,
ParameterSetName="LiteralPath",
ValueFromPipelineByPropertyName=$true,
HelpMessage="Literal path to one or more locations.")]
[ValidateNotNullOrEmpty()]
[string[]]
$LiteralPath
)
Begin
{
Set-StrictMode -Version Latest
}
Process
{
if ($psCmdlet.ParameterSetName -eq "Path")
{
if (!(Test-Path $Path)) {
$ex = new-object System.Management.Automation.ItemNotFoundException "Cannot find path '$Path' because it does not exist."
$category = [System.Management.Automation.ErrorCategory]::ObjectNotFound
$errRecord = new-object System.Management.Automation.ErrorRecord $ex, "PathNotFound", $category, $Path
$psCmdlet.WriteError($errRecord)
}
# In the -Path (non-literal) case, resolve any wildcards in path
$resolvedPaths = $Path | Resolve-Path | Convert-Path
}
else
{
if (!(Test-Path $LiteralPath)) {
$ex = new-object System.Management.Automation.ItemNotFoundException "Cannot find path '$LiteralPath' because it does not exist."
$category = [System.Management.Automation.ErrorCategory]::ObjectNotFound
$errRecord = new-object System.Management.Automation.ErrorRecord $ex, "PathNotFound", $category, $LiteralPath
$psCmdlet.WriteError($errRecord)
}
# Must be -LiteralPath
$resolvedPaths = $LiteralPath | Convert-Path
}
foreach ($rpath in $resolvedPaths)
{
if ($pscmdlet.ShouldProcess($rpath, "Operation"))
{
# .. process rpath
}
}
}
End
{
}
}
Related
Consider the following code:
# create test group
New-LocalGroup -Name 'Group1' -Description 'xxx'
# update test group description to blank
Set-LocalGroup -Name 'Group1' -Description '' # fails
Set-LocalGroup -Name 'Group1' -Description $null # fails
On the contrary it is possible to create a group without description:
New-LocalGroup -Name 'Group2'
How is it possible to update the group description of a local group to blank without removing the group first? This happens on PowerShell 5.1.
Although there are some AD attributes that requires the Putex method, that doesn't count for the Description attribute. Meaning my assumption in the initial comment to the question is wrong, and it is possible to clear the Description attribute with just the Put method`:
$Name = 'Group1'
New-LocalGroup -Name $Name -Description 'xxx'
$Group = [ADSI]"WinNT://./$Name,group"
$Group.Put('Description', '')
$Group.SetInfo()
Get-LocalGroup -Name $Name
Name Description
---- -----------
Group1
The issue lays purely in the cmdlet parameter definitions. Without going into the C# programming, you might just pull this from the proxy command:
$Command = Get-Command Set-LocalGroup
$MetaData = [System.Management.Automation.CommandMetadata]$Command
$ProxyCommand = [System.Management.Automation.ProxyCommand]::Create($MetaData)
$ProxyCommand
[CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium', HelpUri='https://go.microsoft.com/fwlink/?LinkId=717979')]
param(
[Parameter(Mandatory=$true)]
[ValidateNotNull()] # <-- Here is the issue
[ValidateLength(0, 48)]
[string]
${Description},
...
In other words, to quick and dirty workaround this with a proxy command:
function SetLocalGroup {
[CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact='Medium', HelpUri='https://go.microsoft.com/fwlink/?LinkId=717979')]
param(
[Parameter(Mandatory=$true)]
[AllowEmptyString()] # <-- Modified
[ValidateLength(0, 48)]
[string]
${Description},
[Parameter(ParameterSetName='InputObject', Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
[ValidateNotNull()]
[Microsoft.PowerShell.Commands.LocalGroup]
${InputObject},
[Parameter(ParameterSetName='Default', Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
[ValidateNotNull()]
${Name},
[Parameter(ParameterSetName='SecurityIdentifier', Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
[ValidateNotNull()]
[System.Security.Principal.SecurityIdentifier]
${SID})
end {
if ($Description) { Set-LocalGroup #PSBoundParameters }
elseif ($Name) {
$Group = [ADSI]"WinNT://./$Name,group"
$Group.Put('Description', '')
$Group.SetInfo()
}
}
}
SetLocalGroup -Name 'Group1' -Description ''
Related bug report: #16049 AllowEmptyString()] for -Description in Set-LocalGroup/SetLocalUser
As Set-LocalGroup fails on that, the only other way I can think of is using ADSI:
$group = [ADSI]"WinNT://$env:COMPUTERNAME/Group1,group"
$group.Description.Value = [string]::Empty
$group.CommitChanges()
It's a workaround of course and I agree with iRon you should do a bug report on this.
I write own powershell func for debug like:
function StartDebug {
param (
[PARAMETER(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
$FunctionName,
[PARAMETER(Mandatory = $false)]
$OtherArg
)
try {& $FunctionName $OtherArg} catch {...} finally {...}
and use it everyway, but i need more arg after $FunctionName. is it realistic to pass many arguments in this case bec use from 0 to 10 arg. do I have to list all the arguments that can be in the parameters of the function? like:
function StartDebug {
param (
[PARAMETER(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
$FunctionName,
[PARAMETER(Mandatory = $false)]
$OtherArg,
[PARAMETER(Mandatory = $false)]
$OtherArg1,
[PARAMETER(Mandatory = $false)]
$OtherArg2,
[PARAMETER(Mandatory = $false)]
$OtherArg3
)
try {& $FunctionName $OtherArg OtherArg1 OtherArg2 OtherArg3 } catch {...} finally {...}
but i dont use positional parameters in code and too many named parameters in code (~100)
Interested in any ideas about this. tnx!
The magic word is Splatting. You can provide an array or a hashtable containing your arguments to a function. The splatter is written with an #VariableName instead of the $:
function StartDebug {
param (
[PARAMETER(Mandatory = $true)]
[ValidateNotNullOrEmpty()]
$FunctionName,
[PARAMETER(Mandatory = $false)]
$OtherArg
)
try {& $FunctionName #OtherArg # Watch out for the # in the OtherArg
} catch {$_} finally {}
}
$FunctionName = 'Get-ChildItem'
$Splatter = #{
Path = 'C:\'
Filter = 'Users'
Directory = $true
}
$Splatter2 = #('c:\')
StartDebug $FunctionName $Splatter
StartDebug $FunctionName $Splatter2
However if you want to use single items as $OtherArg you will have to provide them as single element array as can be seen with $Splatter2. Or extend your function to transform single arguments in arrays automatically, but thats up to you.
I think you better run it using scriptblock:
$result = Invoke-DeepDebug { Get-ChildItem -Path 'C:\' ; Get-Service -InformationAction Continue}
And in Invoke-DeepDebug you can work with $Command.AST as deep and detailed as you want.
Function Invoke-DeepDebug {
param(
[Parameter(Mandatory=$true, Position=0)]
[Scriptblock]$Command
)
Write-Host -f Cyan "Executing " -n
Write-Host -f Magenta $Command.Ast.Extent.Text -n
Write-Host -f Yellow " ... " -n
$result = $null
try {
$result = Invoke-Command $Command -ErrorAction Stop
Write-Host -f Green "OK!"
} catch {
Write-Host -f Red "Error"
Write-Host -f Red "`t$($_.Exception.Message)"
}
return $result
}
I want to make rm -rf an alias for Remove-Item, since I keep accidentally typing it when using PowerShell.
I had guessed maybe I could do something like this, but that doesn't work.
Set-Alias -name 'rm -rf' -value Remove-Item
You could also remove the default alias and then replace it with a custom function.
# remove default alias
if (Test-Path Alias:rm) {
Remove-Item Alias:rm
}
# custom function for 'rm'
function rm {
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[switch]$rf,
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[string]$Path
)
process {
Remove-Item -Path $Path -Recurse:$rf -Force:$rf
}
}
Then call it like this:
rm -rf "C:\Temp\dir"
If course, so far this function doesn't have the full functionality of Remove-Item, but you can extend it as you like.
Note: Even though this "solves" your problem in the short-run, you should not revert to these kinds of workarounds. Better get accustomed to the actual PowerShell commands and syntax, or you're bound to run into more problems sooner or later.
You already have a solution to your problem but as I mentioned, a proxy function could be suitable in this particular scenario. Here's a working example (atleast for PSVersion 5.1).
Adding the following to your $profile should work and you'd be able to run rm -rf "path" to recursively and forcefully remove a directory. Bear in mind that this hasn't been tested extensively but it does take into account wether you specified -rf or not on the command line. It also supports the common parameters such as -Confirm:$true.
if(Test-Path Alias:rm) { Remove-Item Alias:rm }
function rm
{
[CmdletBinding(DefaultParameterSetName='Path', SupportsShouldProcess=$true, ConfirmImpact='Medium', SupportsTransactions=$true, HelpUri='https://go.microsoft.com/fwlink/?LinkID=113373')]
param(
[Parameter(ParameterSetName='Path', Mandatory=$true, Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
[string[]]
${Path},
[Parameter(ParameterSetName='LiteralPath', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
[Alias('PSPath')]
[string[]]
${LiteralPath},
[string]
${Filter},
[string[]]
${Include},
[string[]]
${Exclude},
[switch]
${Recurse},
[switch]
${Force},
[switch]
${rf},
[Parameter(ValueFromPipelineByPropertyName=$true)]
[pscredential]
[System.Management.Automation.CredentialAttribute()]
${Credential})
begin
{
try {
$outBuffer = $null
if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
{
$PSBoundParameters['OutBuffer'] = 1
}
if($rf)
{
$PSBoundParameters.Remove('rf') | Out-Null
$PSBoundParameters.Add('Recurse', $true) | Out-Null
$PSBoundParameters.Add('Force', $true) | Out-Null
}
$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Management\Remove-Item', [System.Management.Automation.CommandTypes]::Cmdlet)
$scriptCmd = {& $wrappedCmd #PSBoundParameters }
$steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
$steppablePipeline.Begin($PSCmdlet)
} catch {
throw
}
}
process
{
try {
$steppablePipeline.Process($_)
} catch {
throw
}
}
end
{
try {
$steppablePipeline.End()
} catch {
throw
}
}
<#
.ForwardHelpTargetName Microsoft.PowerShell.Management\Remove-Item
.ForwardHelpCategory Cmdlet
#>
}
It will work. Set the alias as below.
Set-Alias -Name 'rm -rf' -Value Remove-Item
For calling it, you can use the call operator (&) operator like this -
& 'rm -rf' \\PathToYourFileWhichYouWantToDelete\FileName.extension
I have a function that takes a powershell psd1 file as argument
function Transform-DataFile
[CmdletBinding()]
[Alias()]
[OutputType([int])]
Param
(
# Param1 help description
[Parameter(Mandatory=$true,
ValueFromPipelineByPropertyName=$true,
Position=0)]
[Microsoft.PowerShell.DesiredStateConfiguration.ArgumentToConfigurationDataTransformation()]
$DataFile,
[String]
$OutFile
)
Begin
{
}
Process
{
$DataFile.NodeName = "Kalle"
Set-Content -Path $OutFile -Value $DataFile
}
End
{
}
I want to edit this file and then save it with another filename. The result file should be a valid powershell psd1 file. What can I do to achieve this?
I want to implement a "Log" library in common.ps1 and then use dot souring to load it.
but it does not work as I expected I thought I can get the different value after calling SetLogConfiguratoion, but the value is not changed. then the "Log" function does not work becasue the log path is $null.
Do I miss-understand dot-sourcing?
write-host $g_nodeName ==> show $null
. 'C:\Test\Common.ps1'
write-host $g_nodeName ==> show "unkown"
SetLogConfiguratoion $sqlInstance (join-path $BackupShare 'RemvoeAgent.log')
write-host $g_nodeName ==> still "unkown"
Log 'ERROR' 'Test'
and Common.ps1 is as below
$g_logPath = $null
$g_nodeName = "Unknown"
function SetLogConfiguratoion
{
param
(
[Parameter(
Mandatory=$true,
HelpMessage='NodeName')]
[ValidateNotNullOrEmpty()]
[string]$NodeName,
[Parameter(
Mandatory=$true,
HelpMessage='LogPath')]
[ValidateNotNullOrEmpty()]
[string]$LogPath
)
if($LogPath.StartsWith('Microsoft.PowerShell.Core\FileSystem::'))
{
$g_logPath = $LogPath;
}
else
{
$g_logPath = 'Microsoft.PowerShell.Core\FileSystem::' + $LogPath;
}
$g_NodeName = $NodeName;
}
function Log
{
param
(
[Parameter(
Mandatory=$true,
HelpMessage='Log level')]
[ValidateNotNullOrEmpty()]
[ValidateSet(
'Error',
'Warning',
'Info',
'Verbose'
)]
[string]$level,
[Parameter(
Mandatory=$true,
HelpMessage='message')]
[ValidateNotNull()]
[string]$message
)
if($g_logPath -eq $null)
{
return
}
$time = Get-Date –format ‘yyyy/MM/dd HH:mm:ss’
$msg = "$time :: $level :: $nodeName :: $message"
Add-content $LogPath -value $message + '\n'
}
Dot sourcing the script will make it run in the local scope, and create the functions there, but you're still invoking the function in it's own scope. It's going to set $g_nodename in that scope. If you want all of that to run in the local scope, you need to dot source the script into the local scope to create the functions, then call the functions in the local scope (by preceding them with '. ' (note the space after the dot - that has to be there).
. SetLogConfiguratoion $sqlInstance (join-path $BackupShare 'RemvoeAgent.log')