dir –a:d

PowerShell Team

In cmd, listing files based on attributes was simple:

only directories dir /a:d
only files (no directories) dir /a:-d
only hidden files dir /a:h

In PowerShell, it’s not so easy:

only directories dir | ? { $_.PSIsContainer }
only files (no directories) dir | ? { !$_.PSIsContainer }
only hidden files dir -force | ? { $_.Attributes –band [IO.FileAttributes]::Hidden }

We have had requests to cover the first case better, for example:

https://connect.microsoft.com/feedback/ViewFeedback.aspx?SiteID=99&FeedbackID=252549&wa=wsignin1.0

https://connect.microsoft.com/feedback/ViewFeedback.aspx?SiteID=99&FeedbackID=308796&wa=wsignin1.0

We haven’t added such a parameter to Get-ChildItem yet, but with some new features in V2, you can add them yourself.

The techniques I describe below can be used to augment any cmdlet, or any script or function for that matter.

If you don’t care much about the actual implementation details, just skip to the bottom and download the module.  To start using it, you can use:

import-module path\to\Get-ChildItem.psm1

(Note that there isn’t any particularly good reason I implemented this as a module, other than as a way of testing multiple features.  It could work equally well as a script or function.)

To get started, we need a proxy to Get-ChildItem.  It should support all of the parameters that Get-ChildItem supports, plus it should support pipelining in the same way as the cmdlet.  We can use the following code to generate a proxy:

$md = new-object System.Management.Automation.CommandMetadata (get-command get-childitem)
[System.Management.Automation.ProxyCommand]::Create($md)

This will return a rather long string, we can capture it in a file and make our changes.

The first thing we need is an additional parameter.  We’ll call it Attribute.  We just add:

[System.String]
$Attribute = ,

to the param block.

Next, we need to add the logic for our new parameter.  To do this, we will replace the Begin block that was generated with the following code:

 

[void]$PSBoundParameters.Remove(‘Attribute’)
$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand(‘Get-ChildItem’,
        [System.Management.Automation.CommandTypes]
::Cmdlet)
if ($Attribute -ne )
{
    $includeMask = 0
    $excludeMask = 0
    [bool]$onOrOff = $true
    foreach ($char in $Attribute.GetEnumerator())
    {
        if ($char -eq ‘-‘) { $onOrOff = $false }
        else {
            if ($flags[$char] -eq $null) {
                throw “Attribute ‘$char’ not supported”
            }
            if ($onOrOff) {
                $includeMask = $includeMask -bor $flags[$char]
            }
            else {
                $excludeMask = $excludeMask -bor $flags[$char]
            }
            $onOrOff = $true
        }
    }

    if ($includeMask -band [IO.FileAttributes]::Hidden) {
        $PSBoundParameters.Force = $true
    }

    $scriptCmd = {& $wrappedCmd @PSBoundParameters |
        ? { $_.PSProvider.Name -ne ‘FileSystem’ -or
            ((($_.Attributes -band $includeMask) -eq $includeMask) -and
             (($_.Attributes -band $excludeMask) -eq 0))
        }
    }
}
else {
    $scriptCmd = {& $wrappedCmd @PSBoundParameters }
}

$steppablePipeline = $scriptCmd.GetSteppablePipeline()
$steppablePipeline.Begin($PSCmdlet)

 

Note the first thing we do is remove our parameter from PSBoundParameters.  If we don’t do that, the real cmdlet Get-ChildItem will complain about the unknown parameter when it is specified.

I wanted functionality and syntax as close to cmd.exe as possible.  So we should support things like ‘dir –a:h-sr’, which would include hidden read-only files that are not system files.  To do this, I generate bitmasks representing the attributes requested, then filter the output of the actual Get-ChildItem command based on these bitmasks.

Note the script block assigned to $scriptCmd is similar to the one generated by ProxyCommand.Create, but it includes some filtering.  Steppable pipelines can’t be created for arbitrary script blocks, only when there is a single pipeline in the script block.  Fortunately we can do all our filtering in one pipeline.

If you wanted to add sorting to the output, you could extend this pipeline to do the appropriate sorting.  I leave it as an exercise for the reader to add the cmd.exe sorting flags like /OD, /O-D, /OS, /O-S, etc.

ProxyCommand.Create works pretty good, but it doesn’t generate code to support dynamic parameters.  Most cmdlets don’t have dynamic parameters, but Get-ChildItem definitely does, so we want our proxy to support them as well.  Different providers can add dynamic parameters to Get-ChildItem, so the optimal solution is to, at runtime, ask the Get-ChildItem cmdlet what dynamic parameters it has.  We can do that this way:

[void]$PSBoundParameters.Remove('Attribute')
$argList = @($psboundparameters.getenumerator() | % { "-$($_.Key)"; $_.Value })
$wrappedCmd = Get-Command Get-ChildItem -Type Cmdlet -ArgumentList $argList
$providerParams = @($wrappedCmd.Parameters.GetEnumerator() |
                                                  Where-Object { $_.Value.IsDynamic })
if ($providerParams.Length -gt 0)
{
    $paramDictionary = new-object Management.Automation.RuntimeDefinedParameterDictionary
    foreach ($param in $providerParams)
    {
        $param = $param.Value
        $dynParam1 = new-object Management.Automation.RuntimeDefinedParameter `
                                  $param.Name, $param.ParameterType, $param.Attributes
        $paramDictionary.Add($param.Name, $dynParam1)
    }
    return $paramDictionary
}

This code generates an argument list from the automatic variable $PSBoundParameters.  $argList won’t necessarily look exactly like the arguments passed in as it will add the parameter names to positional arguments.

Jason Shirk [MSFT]

 

Get-ChildItem.psm1

0 comments

Discussion is closed.

Feedback usabilla icon