Script Center > Gallery > Storage > Add DIR Capabilities to the Get-ChildItem Cmdlet
TechNet Script Center logo

Welcome to the TechNet Script Center Gallery!

Each contribution is licensed to you under a License Agreement by its owner, not Microsoft. Microsoft does not guarantee the contribution or purport to grant rights to it.

Add DIR Capabilities to the Get-ChildItem Cmdlet

(Community)
Rate it:
 
 
 
 
 
Script Code
Windows PowerShell
#********************************************************************************************************************#
#
#   Written Bob Landau (robert_landau@msn.com)
#   Version 1.0 
#   Date 10/22/2007
#
#
#   Dir2 started out in life as a function which would be loaded into memory once however dot-sourcing a script loads
#   all script variables into Powershell's global scope which I do not want. Now there are two choices
#
#   1) dot-source the script and call the function directly (slightly better performance)
#   2) call the function indirectly by aliasing the script (no exposed internal variables)
#
#
#   function Dir2: 
#
#   Purpose: provide one of the syntacical ease which the veneriable DIR gives with the power of Get-ChildItem
#
#   Parameters: The common and useful parameters (in my opinion) that DIR understands
#
#   /A[adhrs-] plus
#   *    Inclusive a file must match all of the specified attributes other 
#        attributes are ignored.
#        This is the default default behavior which matches what DIR determines what to return
#   /O[dens-] plus 
#        a    LastAccessTime
#        c    CreationTime
#        d    same as (w)
#        w    LastWriteTime
#        /B   This option results in raw strings being returned so anything that requires
#             a FileInfo class such as sorting or attributes will not work	
#        /P
#        /W
#        /S
#
#   All of the parameters that Get-ChildItem understands with the following additions
#
#   -Search < Character Expression to Include in Search > < optional Character Expression to Exclude in Search >
#   Like Get-ChildItem; the current directory will be used unless the -Path parameter is specified
#
#   This option is similar to the WHERE command that some might be familar with.
#   This is simply a short cut for the following 
#
#   Get-ChildItem -Path <...>  -Include <...>  -Exclude <   >  -Recurse
#   Both -Path and -Exclude are optional.
#
#   -Escape < [System.Collections.HashTable[]] > 
#   where the Hashtable has the following two fields @{Name=<>; Expression=<>}. In addition the
#   Expression must return a FileInfo/DirInfo object.
#
#   This allows you to inject a pipeline expression into this function. Most of the Pipelines that
#   I write "proecess" the "finished" data; they are not "filters"; -Escape should only be used for
#   the "filter" category of pipeline expressions. Originally I was going to add a "date" filter so 
#   that only files "touched" after/before a certain date would be processed further. Rather than 
#   hard coding this I've decieded to generalize this ability.
#
#   One nice side effect is it actually took less lines of code to do this than the hard-coded algorithm.
#
#   Here is an example:
#
#   Dir2 \users -search * -escape @{Name='Now'; Expression='| ? { $_.LastWriteTime -gt [DateTime]::Now.AddDays(-10) }'}
#
#   Lastly when -LiteralPath is specified Dir2 unlike Get-ChildItem will filter the output by processing
#   the expressions in both Include/Exclude. This workaround can be removed once -LiteralPath has been fixed.
#
#
#   Examples:
#
#   'c:\windows\system32\*', 'c:\windows\*' | dir2 /p  /a-da /o-sn -include *.dll -exclude [a-m]*
#
#   '\\picard\c$'  | dir2 *.ps1 /o-sn -exclude [a-m]* /p
#
#   dir2 -Literalpath c:\windows\system32\  /p  /a-da /o-sn -include *.dll -exclude [a-m]*
#   NOTE: Wildcards are NOT allowed in the path when -Literalpath is used. 
#   However both Include and Exclude require the path to end with a * (container)
#   so for -LiteralPath I've made an exception. Once -LiteralPath works correctly 
#   this code marked "LiteralPath Work Around" should be eliminated
#
#   dir2 \\klingons\Reviews, \\Romuians\Reviews -Search 'Aug??Review.doc', '<email_addr>_Aug*.doc'
#
#   This function is by design noisy. It will print out each step and all parameters past into each
#   function. Once you are satisfied with the behavior of this function you can quiet it down by 
#   Change the alias OUTPUT from Out-Default to Out-Null and UseConsole to $false
#
#
#   While I have no doubt there are bugs in this function prior to assuming there is a  bug check the documentation
#   for both this function and Get-ChildItem. If you still believe that function is incorrect paste the expression 
#   that is being passed Get-ChildItem _directly_ (This can be found by cut n pasting the last DEBUG-OUT statement in Dir2)
#
#
#********************************************************************************************************************#



## Comment out this prior to release
#Set-PSDebug -Strict


#********************************************************************************************************************#
#
#   PowerShell does not have the capability that I know of to specify a namespace that a function is part of
#   Both Parse_Arguments and Process_Arguments as well as the various utility functions are internal 
#   to this script so should not be exposed externally
#
#   The only way I know to eliminate these internal functions from being promoted to global scope is use nested functions
#
#   This _still_ doesn't solve the problem of public functions names colliding 
#
#********************************************************************************************************************#



function Dir2 ( [object[]] $args_ ) {
## function defined at the end of this script



#******************************************           LiteralPath Work Around   **********************************#
#
#   The $Script:LiteralPath flag and ALL logic which uses this is here ONLY because of the current limitation 
#   that Get-ChildItem has when passed in a literal path. The current implementation does not allow you to search for 
#   a set of files in a directory specified as part of the literal path
#   Once Get-ChildItem is fixed this logic should be removed
#
#**********************************************       LiteralPath Work Around       *********************#
$Script:LiteralPath = $false





## Comment out this prior to release
# $DebugPreference = 'Continue'
# $DebugPreference = 'SilentlyContinue'


#********************************************************************************************************************#
#
#   These are a small set of Debug Routines that I've translated from a C library eventually these 
#   should be in their own library
#
#********************************************************************************************************************#

Set-Alias -name DEBUG-OUT -value DEBUG-OUT_

Set-Alias -name ENTER -value Enter_Function
Set-Alias -name LEAVE -value Leave_Function

Set-Alias -name OUTPUT -value Out-Default
#Set-Alias -name OUTPUT -value Out-Null

$Script:UseConsole = $true
#$Script:UseConsole = $false


$Script:IndentBy = 0
$Script:Indent = '   '

function Enter_Function ( [string] $msg ) 
{
   $msg_ =  ($Script:Indent * $Script:IndentBy++) + 'Entering ' + $msg; 
   DEBUG-OUT__ $msg_ $Script:UseConsole '-ForeGroundColor yellow -BackGroundColor black'
}

function Leave_Function ( [string] $msg ) 
{
   $msg_ =  ($Script:Indent * --$Script:IndentBy) + 'Leaving ' + $msg; 
   DEBUG-OUT__ $msg_ $Script:UseConsole '-ForeGroundColor yellow -BackGroundColor black'
}

function DEBUG-OUT_ ( [string] $msg ) 
{
   $msg_ =  ($Script:Indent * $Script:IndentBy) + $msg; 
   DEBUG-OUT__ $msg_ $Script:UseConsole
}

function DEBUG-OUT__ ( [string] $msg, [bool] $Console = $true, [string] $outputAttributes = $null )
{

   if ( $Console -eq $true )
   {
## Send output to the screen
      Invoke-Expression  "Write-Host '$msg' $outputAttributes"
   }
   else
   {
## Send output to the default device
      Write-Output $msg
   }
}
#********************************************************************************************************************#



#********************************************************************************************************************#
#
#   Usage: Display extended functionality and Get-ChildItem help
#
#********************************************************************************************************************#

function Usage ( [string] $ExtendedHelp = $null )
{

    [string] $Script:DirHelp = `
    "              These are the Standard options available in DIR`n
    /A          Displays files with specified attributes.
    attributes   D  Directories                R  Read-only files
                H  Hidden files               A  Files ready for archiving
                S  System files               I  Not content indexed files
                L  Reparse Points             -  Prefix meaning not
    /B          Uses bare format (no heading information or summary)
    /O          List by files in sorted order
    sortorder    N  By name (alphabetic)       S  By size (smallest first)
                E  By extension (alphabetic)  D  By date/time (oldest first)
                A  LastAccessTime             C  Same as D
                W  LastWriteTime              -  Prefix to reverse order
    /P          Pauses after each screenful of information
    /S          Displays files in specified directory and all subdirectories
    /W          Uses wide list format
    "


    [string] $Script:DirExamples = `
    "`tReturn all text files in the current directory
    `n`t`tDir2 *.txt /p /w
      
    `tReturn all DLL's in the Windows directory that have a name which starts with [N-Z]
    `tSorted by size first in descending order and then by name.
    `n`t`t'c:\windows\system32\*', 'c:\windows\*' | Dir2 /p  /a-da /o-sn -include *.dll -exclude [a-m]*

    `tSearch for you last review to refresh your memory on what rubbish you had promised to your manager
    `n`t`tDir2 \\Klingons\Reviews, \\Romuians\Reviews -Search 'Aug??Review.doc', 'Worf*.doc' `
     -escape @{Name='Aug'; Expression='| ? { `$_.LastWriteTime -gt `"07/20/2007`" -and `$_.LastWriteTime -lt `"8/15/2007`" }'}

    `tCopy all PS scripts which are not `"test scripts`" to a variable for further processing
    `n`t`t`$files = Dir2 -LiteralPath \test[123] -include *.ps1 -exclude temp*
    "



    [string] $Script:ExtendedOptions = `
    "    -Search <character expression to include in search> <optional character expression to exclude in search>
    `n`tLike Get-ChildItem/Dir: the current directory will be used if no -Path parameter is specified

    `tThis option is similar to the WHERE command that some might be familar with.
    `tThis is simply a short cut for the following 

    `tGet-ChildItem -Path <...>  -Include <...>  -Exclude <   >  -Recurse
    `tBoth -Path and -Exclude are optional.

    -LiteralPath now will filter the output by processing the expressions in both Include/Exclude arguments. 
     `n`tThis workaround can be removed once -LiteralPath has been fixed.

    -Escape < [System.Collections.HashTable[]] > with the following signature:
    `n`t@{Name='Name'; Expression=' User defined Filter pipe : returning either a File/Directory Info class '}
    `n`tThis gives one the ability to inject a pipe expression into this script.
    `n`tThis option is really ONLY benefitial to 5 percent of the Pipeline expressions. Unless this is
    `tused to `"filter`" the output for the next Pipe; this should not be used.
    "

   Write-Host "`n$DirHelp `n`n`n"
   Write-Host "$ExtendedOptions `n`n"
   if ( $ExtendedHelp -eq '-full' ) { Write-Host "    Examples:`n`n$DirExamples `n`n" }
   Read-Host "`n`nPress <RETURN> for Get-ChildItem help "
   Write-Host "`n`n"
   Invoke-Expression "Help Get-ChildItem $ExtendedHelp | Out-Host -p"
}

#********************************************************************************************************************#
#
#   Utility functions
#
#********************************************************************************************************************#

function Build-HashTable ( [string] $name, [object] $expression )
{
$(
   ENTER "$($MyInvocation.MyCommand.name)" 
   $e = @{ArgumentType = $name; ArgumentValue = $expression}
   DEBUG-OUT "Argument = $($e[`"ArgumentType`",`"ArgumentValue`"])"
   LEAVE "$($MyInvocation.MyCommand.name)"
) | OUTPUT
   $e
}

$Script:Empty = '  '
function Parse_Legacy_Arguments( [string] $flag = $(throw 'flag must be specified'), [string] $value = $Script:Empty )
{
$(
   ENTER "$($MyInvocation.MyCommand.name)" 
   $e = Build-HashTable $flag $value.SubString(2)                       ## Strip off the flag
   DEBUG-OUT "Argument = $($e[`"ArgumentType`",`"ArgumentValue`"])"
   LEAVE "$($MyInvocation.MyCommand.name)"
) | OUTPUT
   $e
}


function Parse_Cmdlet_Switch_Argument( [string] $flag = $(throw 'flag must be specified') )
{
$(
   ENTER "$($MyInvocation.MyCommand.name)" 
   $e = Build-HashTable $flag $null
   DEBUG-OUT "Argument = $($e[`"ArgumentType`",`"ArgumentValue`"])"
   LEAVE "$($MyInvocation.MyCommand.name)"
) | OUTPUT
   $e
}



function Parse_Cmdlet_Multi_Value_Argument( [string] $flag = $(throw 'flag must be specified'), `
    [object[]] $value = $(throw 'flag must be specified'), [bool] $quote = $false )
{
$(
   ENTER "$($MyInvocation.MyCommand.name)" 

   $p=$null

   if ( $quote )
   {
      for ($i=0; $i -lt $value.Length; $i++) { $p += '"' + $value[$i] + '"' + ', ' }
      $e = Build-HashTable $flag $p.TrimEnd(', ')
   }
   else
   {
      if ( $value.gettype() -eq [String] ) {
           for ($i=0; $i -lt $value.Length; $i++) { $p += $value[$i] + ', ' }
           $e = Build-HashTable $flag $p.TrimEnd(', ')
      } else {
           for ($i=0; $i -lt $value.Length; $i++) { [array]$p += $value[$i] }
           $e = Build-HashTable $flag $p
      }
   }

   DEBUG-OUT "Argument = $($e[`"ArgumentType`",`"ArgumentValue`"])"

   LEAVE "$($MyInvocation.MyCommand.name)"
) | OUTPUT
   $e
}

function PutBack( [System.Collections.IEnumerator] $enum, [object] $p )
{
$(
   ENTER "$($MyInvocation.MyCommand.name)" 

   $enum.reset()
   $i = 0

   while ($enum.movenext())
   {
      if ($p -eq $switch.current)
      {
         break;
      }
      $i++
   }

   $enum.reset()
   for ($j = 0; $j -lt $i; $j++)
   {
      [void]$enum.movenext()
   }

   LEAVE "$($MyInvocation.MyCommand.name)"
) | OUTPUT
}
#********************************************************************************************************************#


function Parse_Arguments( [object[]] $args_ )
{

$(  ### This prevents Powershell from sending the output any place other than where it is directed to

   ENTER "$($MyInvocation.MyCommand.name)" 

## Dump parameters
   $i=0;  foreach ($arg in $args_) {DEBUG-OUT "`$args_[$i] = $arg"; $i++}

## These are the command-line pre-processed arguments that will be passed to Process-Arguments
   [System.Collections.HashTable[]]$Params = $()


## Loop through appending the parameters to the hashtable array: $Params
    switch -wildcard ($args_)
    {

#*******************************************    These are the standard parameters associated with DIR   ***************#
        /A*  {
                $Params += Parse_Legacy_Arguments 'Attr' $_
                continue
             }
        /O*  {
                $Params += Parse_Legacy_Arguments 'Sort' $_
                continue
             }
        /S  {
                $Params += Parse_Legacy_Arguments 'SubDir'
                continue
            }
        /W  {
                $Params += Parse_Legacy_Arguments 'Wide'
                continue
            }
        /B  {
                $Params += Parse_Legacy_Arguments 'Bare'
                continue
            }
        /P  {
                $Params += Parse_Legacy_Arguments 'Pause'
                continue
            }


#************************************************    These parameters are known to Get-ChildItem   **********************#

## These are switch parameters so do not have any arguments
        -Force
            {
                $Params += Parse_Cmdlet_Switch_Argument $_
                continue
            }
        -Name
            {
                $Params += Parse_Cmdlet_Switch_Argument $_
                continue                
            }
        -Recurse
            {
                $Params += Parse_Cmdlet_Switch_Argument $_
                continue
            }
## The -Path, -Include and -Exclude parameters are complex the an be a single value string or an array or 
## strings either of these valus may contain spaces
        -Include
            {
                [void]$switch.movenext()
                $Params += Parse_Cmdlet_Multi_Value_Argument $_  @($switch.current) $true
                continue
            }
        -Exclude
            {
                [void]$switch.movenext()
                $Params += Parse_Cmdlet_Multi_Value_Argument $_  @($switch.current) $true
                continue
            }
        -Path
            {
                [void]$switch.movenext()
                $Params += Parse_Cmdlet_Multi_Value_Argument $_  @($switch.current) $true
                continue
            }


        -LiteralPath
            {
                [void]$switch.movenext()
                $Params += Parse_Cmdlet_Multi_Value_Argument $_  @($switch.current) $true
                $Script:LiteralPath = $true     ## ***    LiteralPath Work Around    *** ##
                continue
            }
        -Search
            {
                [void]$switch.movenext()
                $Params +=  Parse_Cmdlet_Multi_Value_Argument -'Include'  @($switch.current) $true

                [void]$switch.movenext()
                if ( @($switch.current) -like '-*' -or @($switch.current) -like '/*' )  
                {
                   PutBack $switch @($switch.current)
                }
                elseif ( @($switch.current) -ne $null )
                { 
                   $Params +=  Parse_Cmdlet_Multi_Value_Argument '-Exclude'  @($switch.current) $true
                }

                $Params += Parse_Cmdlet_Switch_Argument '-Recurse'

                continue
            }

## These arguments can be either a single string or an array of strings so they must be handled accodingly
        -*  {
                [void]$switch.movenext()
                $Params += Parse_Cmdlet_Multi_Value_Argument $_  @($switch.current)
                continue
            }
        Default
            { 
# Set-PSDebug -step
                $Params += Build-HashTable 'Unknown' $_
            }
    }

## Loop through and print out the hashtable representing the parameters to pass to Get-ChildItem
   if ( $Params -ne $null )
   {
      DEBUG-OUT "`$Params.count is $($Params.Count) and the type is $($Params.gettype().fullname)"
      foreach ($p in $Params) { DEBUG-OUT "`$p[ArgumentType,ArgumentValue] is $($p[`"ArgumentType`",`"ArgumentValue`"])"}
   }

   LEAVE "$($MyInvocation.MyCommand.name)"
) | OUTPUT

## return the hashtable array
   $Params 
}


function Process_Arguments ( [object[]] $Params )
{
$(  ### This prevents Powershell from sending the output any place other than where it is directed to

   ENTER "$($MyInvocation.MyCommand.name)" 

## This will be returned to the caller which in turn will be the parameters passed to Invoke-Expression to do the final work
   [System.Collections.HashTable[]]$Results = @()
   $Pipe_Expression = $Parameters = $null

## ***   LiteralPath Work Around    *** ##
   $Include_Pipe = $Exclude_Pipe = '$true'

## These pipeline expressions _must_ be at the very end of the pipeline
   $Wide_Pipe_Expression = $More_Pipe_Expression = $Sort_Pipe_Expression = $null
   
## Loop through mapping the command line parameters to the parameter or pipeline expressions that Get-ChildItem understnads
   foreach ($p in  $Params) 
   {
#       DEBUG-OUT "`$p.ArgumentType is $($p.ArgumentType)"
       switch -wildcard ($p.ArgumentType)
       {
          Attr 
             { 
#                DEBUG-OUT "`$p.ArgumentValue is $($p.ArgumentValue)"

                $attributes = $nattr = 0
                $negate = $Inclusive = $false
                
                
                $StandardAttributes = [System.IO.FileAttributes]::Hidden -bor [System.IO.FileAttributes]::System -bor `
                [System.IO.FileAttributes]::ReadOnly    `
                             -bor [System.IO.FileAttributes]::Archive -bor [System.IO.FileAttributes]::Directory
                             
:NextRound_Attr for ($i=0; $i -lt $p.ArgumentValue.Length; $i++)
                {
                   switch ($p.ArgumentValue[$i])
                   {
                      a { $FileAttr = [System.IO.FileAttributes]::Archive }

                      d { $FileAttr = [System.IO.FileAttributes]::Directory }

                      h { $FileAttr = [System.IO.FileAttributes]::Hidden }

                      l {$FileAttr = [System.IO.FileAttributes]::ReparsePoint; `
                      $StandardAttributes = $StandardAttributes -bor $FileAttr}

                      r { $FileAttr = [System.IO.FileAttributes]::Readonly }

                      s { $FileAttr = [System.IO.FileAttributes]::System }

                      * { $Inclusive = $true; continue NextRound_Attr }

                      - { $negate = $true; continue NextRound_Attr }
                   }

                   if ( $negate ) {
                      $nattr = $nattr -bor $FileAttr; $negate=$false
                   } else {
                      $attributes = $attributes -bor $FileAttr 
                   }

                }

#                DEBUG-OUT "`$attributes is $attributes and `$nattr is $nattr"


#********************************************************************************************************************#
#
#   This expression will pass to the next pipeline expression each FileInfo or DirectoryInfo class which 
#   is in the set of  [$attribues] and is not in the set [$nattr]
#
#   The expresssion ( $($_.get_Attributes() -band $StandardAttributes ) is used to mask out what is returned by 
#   get_Attributes() to only set of attributes specified.
#
#   This expresssion  "-not ( ( $attr -bxor ' + "$attributes" + ' ) -band '+ "$attributes" + ' )"  
#   will return true ONLY if all the attributes are set
#
#********************************************************************************************************************#



## By default the behavior is to be compatable with DIR
##  1) Every attributes specified must be set for a result to be returned
##  2) -Force is the default
                 $Inclusive = $true
                 
                 if ( $Inclusive -eq $true )
                 {
                    $Pipe_Expression += ' | ? { ( ( $_.Attributes -band ' + "$attributes" + ' ) -eq ' + `
                    "$attributes" + ' ) -and -not ( $_.Attributes -band ' + "$nattr" + ' ) } '
                    $Parameters += ' -force '
                 }
                 else
                 {
                    $Pipe_Expression += ' | ? { ( ( $_.Attributes -band ' + "$StandardAttributes" +  ' ) `
                    -band ' + "$attributes" + ' ) -and -not ( $_.Attributes -band ' + "$nattr" + ' ) } '
                 }

## This is only needed if compatiability with DIR is NOT desired
##                 if ( $attributes -band [System.IO.FileAttributes]::Hidden )  { $Parameters += ' -force ' }

             }
          Sort
             {
#                DEBUG-OUT "`$p.ArgumentValue is $($p.ArgumentValue)"

                $SortOrder = ' -Property '
                $Reverse = $false


#********************************************************************************************************************#
#
#   Build the set of hashtable elements which represent predicates used by Sort-Object
#
#********************************************************************************************************************#

:NextRound_Sort for ($i=0; $i -lt $p.ArgumentValue.Length; $i++)
                {
                   switch ($p.ArgumentValue[$i])
                   {
                      a { 
                           if ( $Reverse ) {
                              $SortOrder += ' @{e={$_.LastAccessTime}; Descending=$true}, '
                              $Reverse = $false
                           } else {
                              $SortOrder += ' @{e={$_.LastAccessTime}; Ascending=$true}, '
                           }
                        }

                      c { 
                           if ( $Reverse ) {
                              $SortOrder += ' @{e={$_.CreationTime}; Descending=$true}, '
                              $Reverse = $false
                           } else {
                              $SortOrder += ' @{e={$_.CreationTime}; Ascending=$true}, '
                           }
                        }

                      d { 
                           if ( $Reverse ) {
                              $SortOrder += ' @{e={$_.CreationTime}; Descending=$true}, '
                              $Reverse = $false
                           } else {
                              $SortOrder += ' @{e={$_.CreationTime}; Ascending=$true}, '
                           }
                        }

                      w { 
                           if ( $Reverse ) {
                              $SortOrder += ' @{e={$_.LastWriteTime}; Descending=$true}, '
                              $Reverse = $false
                           } else {
                              $SortOrder += ' @{e={$_.LastWriteTime}; Ascending=$true}, '
                           }
                        }

                      e { 
                           if ( $Reverse ) {
                              $SortOrder += ' @{e={$_.Extension}; Descending=$true}, '
                              $Reverse = $false
                           } else {
                              $SortOrder += ' @{e={$_.Extension}; Ascending=$true}, '
                           }
                        }

                      g { }

                      n { 
                           if ( $Reverse ) {
                              $SortOrder += ' @{e={$_.Name}; Descending=$true}, '
                              $Reverse = $false
                           } else {
                              $SortOrder += ' @{e={$_.Name}; Ascending=$true}, '
                           }
                        }

                      s { 
                           if ( $Reverse ) {
                              $SortOrder += ' @{e={$_.Length}; Descending=$true}, '
                              $Reverse = $false
                           } else {
                              $SortOrder += ' @{e={$_.Length}; Ascending=$true}, '
                           }
                        }

                      - {$Reverse=$true; continue NextRound_Sort }
                   }
                }


                $SortOrder = $SortOrder.TrimEnd(', ')
#                DEBUG-OUT "`$SortOrder is $SortOrder "

### BUGBUG
### When $DebugPreference is set to anything other than Silently Continue sorting on the length
### of a Directory will spit out a warning

### DEBUG: "Sort-Object" - "Length" cannot be found in "InputObject".

### The below pipe will eliminate the directories for the sort

### Get-Childitem  -path .\*  | ? { -not ($_.get_Attributes() -band [System.IO.FileAttributes]::Directory)} | 
### Sort-Object  -property  Length



#********************************************************************************************************************#
#
#   This expression will pass along each FileInfo or DirectoryInfo class sorted according to the [$SortOrder]. 
#   The sort order of each succesive property is dependant of the previous properities
#
#********************************************************************************************************************#
               $Sort_Pipe_Expression = " | Sort-Object $SortOrder "
             }


#********************************************************************************************************************#
#
#   This pipeline expression removes all structure and collapses the output into a [string] type so all 
#   processing must be done prior to invoking this
#
#********************************************************************************************************************#
          Wide { $Wide_Pipe_Expression = ' | Format-Wide ' }



#********************************************************************************************************************#
#
#   This pipeline expression stops the execution until a key has been pressed so this must be at the very 
#   end of the pipeline
#
#********************************************************************************************************************#
          Pause { $More_Pipe_Expression = ' | Out-Host -p '}



#********************************************************************************************************************#
#
#   This parameter expression removes all structure and collapses the output into a [string] type
#
#********************************************************************************************************************#
          Bare { $Parameters += ' -name ' }

          SubDir { $Parameters += ' -recurse ' }


        -Escape
            {
#                foreach ( $h in $p.ArgumentValue ) { DEBUG-OUT "`$h[`"Name`",`"Expression`"] = $($h[`"Name`",`"Expression`"])" }
                foreach ( $arg in $p.ArgumentValue ) { $Pipe_Expression += $arg.Expression + '  '}
                continue
            }


#***********************************************           LiteralPath Work Around       ***********************************#
#
#   The $Script:LiteralPath flag and ALL logic which uses this is here ONLY because of the current limitation that 
#   Get-ChildItem has when passed in a literal path. The current implementation does not allow you to search for a set of 
#   files in a directory specified as part of the literal path
#   Once Get-ChildItem is fixed this logic should be removed
#
#***********************************************           LiteralPath Work Around       *****************************#
          -Include
             {
                if ($Script:LiteralPath) 
                { 
                   $Include_Pipe = '$_ -like ' + "$($p.argumentvalue)"
                   continue
                } 
#               else 
#               { 
#                  fall through 
#               }
             }
          -Exclude
             {
                if ($Script:LiteralPath) 
                {
                   $Exclude_Pipe = '$_ -notlike ' + "$($p.argumentvalue)"
                   continue
                }
             }
#***********************************************           LiteralPath Work Around       ********************************#




#********************************************************************************************************************#
#
#   Pass the native Get-ChildItem parameters through without processing
#
#********************************************************************************************************************#
          -* { $Parameters += " $_  $($p.ArgumentValue) " }
       
       }
   }


#***********************************************           LiteralPath Work Around       *******************************#
   if ( $Include_Pipe -ne '$true' -or $Exclude_Pipe -ne '$true' )
   {
      $LiteralPath_Pipe = '| % {if (' + "$Include_Pipe" + ' -and ' + "$Exclude_Pipe" + ' ) {$_}}'
      $Pipe_Expression = $LiteralPath_Pipe + $Pipe_Expression
   }
#***********************************************           LiteralPath Work Around       *******************************#




#********************************************************************************************************************#
#
#   There is no need to sort on an object which will be thrown away later so sorting will be done after all other 
#   processing other than the formating or pagnation
#
#********************************************************************************************************************#
   $Pipe_Expression += $Sort_Pipe_Expression



#********************************************************************************************************************#
#
#   Both of these if used must be the last two pipes in the series otherwise they'll effect the processing of the 
#   File and Directory Info class
#
#********************************************************************************************************************#

   $Pipe_Expression += $Wide_Pipe_Expression
   $Pipe_Expression += $More_Pipe_Expression
   

   DEBUG-OUT "`$Pipe_Expression is $Pipe_Expression"
   DEBUG-OUT "`$Parameters is $Parameters"
   


#********************************************************************************************************************#
#
#   Return the results of processing the parameters to the caller via a collection
#
#********************************************************************************************************************#

   $Results += Build-HashTable 'Pipe' $Pipe_Expression
   $Results += Build-HashTable 'Args' $Parameters


   LEAVE "$($MyInvocation.MyCommand.name)" 
) | OUTPUT

   $Results

}



# function Dir2 ( [object[]] $args_ ) {

#********************************************************************************************************************#
#
#   PowerShell has an annoying habit of writing all output directed to stdout to a pipe 
#   Even though the pipe is created much later within the function 
#
#********************************************************************************************************************#
$(   ### This prevents Powershell from sending the output any place other than where it is directed to


#Set-PSDebug -step

   ENTER "$($MyInvocation.MyCommand.name)" 

   $index = 0

## flags
   $PositionalParameter = 0
   $Path = 1
   $Filter = 2
   $SearchArgNotFound = $true

   $Param_Pos = @($null,$null,$null)
   $Param_Name = @($null,$null,$null)
   $Names = @($null,$null,$null)
   
   $dirArgs = @()
   $args__ = @()
   $PipeArgs= @()


#################################  BUGBUG: dot-Sourcing will flatten out the -Path array  ############################## 
#
#   This workaround is required because Powershell packages up <array of *ANY* type> parameters differently depending on
#
#       1) dot-sourced
#       2) called within a function
#
#   In the former case the 2D array will be flatten into a 1D array. The correct behavior is to keep it as a 2D array
#   which is the behavior you see in cmdlets and when the function is called within a script.
#
#   This is a very fragile workaround which depends on ScriptName being an empty string in the context of a function called
#   directly. Currently this is true when called on the command line however if any function or script
#   calls this function calls this assumption is false.
#
#################################  BUGBUG: dot-Sourcing will flatten out the -Path array  ############################### 
   if ( $MyInvocation.ScriptName.length -eq 0 ) { $args_ =  ,$args_ }

## Regardless of whether Dir2 was invoked directly or via the script the variable $args_ will be used

   if ($args.Count -gt 0) { DEBUG-OUT  "appending $($args.count) parmaeters to `$args"; $args_ += $args }

   
   if ( $args_ -ne $null -and $args_[0] -eq '-?' ) 
   {
      Usage $args_[1]
      return
   }
   
#********************************************************************************************************************#
#
#   Regardless of how the parameters are passed in by name, by position or via a pipeline; the array passed to
#   Parse_Arguments be will identical for the same set of parameter values
#
#********************************************************************************************************************#

   foreach ($arg in $input) { $PipeArgs += $arg }

   if ($PipeArgs.Count -gt 0) { 
      DEBUG-OUT "There are $($PipeArgs.Count) parameters in the pipe"      
      $i=0; foreach ($arg in $PipeArgs) {DEBUG-OUT "`$args[$i] = $arg" ; $i++}
   }


   if ($args.Count -gt 0) { 
      DEBUG-OUT "There are $($args.Count) command line parameters"
      $i=0; foreach ($arg in $args) {DEBUG-OUT "`$args[$i] = $arg" ; $i++}
   }



## Copy the pipeline into the parameter array as a name/value pair. 
   if ($PipeArgs.Count -gt 0)
   {
## Test to see if this is the named LiteralPath named property/value being passed via the pipe
      if ($PipeArgs[0].LiteralPath -eq $null)
      {
         $args__ += '-Path'
## Do not let Powershell flatten out the array
         $args__ += (,$PipeArgs)
      }
      else
      {
         $args__ += '-LiteralPath'
         $p = @()
         for ($i=0; $i -lt $PipeArgs.Count; $i++) { $p += $PipeArgs[$i].LiteralPath }
         $args__ += ( ,$p )
      }

   }

## Copy the commandline parameters into the paramter array
   if ($args_.Count -gt 0)
   {
      $args__ += $args_
   }


   DEBUG-OUT "There are a total of $($args__.Count) parameters"
   if ($args__.Count -gt 0) { $i=0; foreach ($arg in $args__) {DEBUG-OUT "`$args[$i] = $arg" ; $i++} }


#********************************************************************************************************************#
#
#   Prior to calling Parse_Arguments any positional arguments need to be changed to named arguments
#
#********************************************************************************************************************#


## The Path parameter can be a multi-valued string so it needs to be handled carefully
   if ( ( $args__[$index] -ne $null ) -and ( @($args__[$index])[0][0] -ne '-') -and ( @($args__[$index])[0][0] -ne '/') )
   {
       $PositionalParameter = $PositionalParameter -bor $Path
       $Param_Pos[$Path] = @($args__[$index])
   }
   elseif ( ( $args__[$index] -ne $null ) -and ( @($args__[$index])[0][0] -eq '-') )
   {
## This is a native Get-ChildItem named parameter assume it's mulit-valued
       $Names[$Path]= $args__[$index]
## This must be special cased because unlike other parameters it can have either one or two arguments
       if ( $Names[$Path] -eq '-Search' ) { $SearchArgNotFound = $false }
       $index++
       $Param_Name[$Path] = @($args__[$index])
   }
   elseif ( ( $args__[$index] -ne $null ) -and ( @($args__[$index])[0][0] -eq '/') )
   {
## This is a native DIR parameter it must be single valued
       $Names[$Path] = $args__[$index]
   }

   $index++

## The -Filter parameter must be a single valued string
   if (  ( $args__[$index] -ne $null ) -and ( @($args__[$index])[0][0] -ne '-') -and ( @($args__[$index])[0][0] -ne '/') )
   {
       $PositionalParameter = $PositionalParameter -bor $Filter
       $Param_Pos[$Filter] = $args__[$index]
   }
   elseif ( ( $args__[$index] -ne $null ) -and ( @($args__[$index])[0][0] -eq '-') )
   {
## This is a native Get-ChildItem named parameter assume it's mulit-valued

       $Names[$Filter]= $args__[$index]
       $index++
       $Param_Name[$Filter] = @($args__[$index])
   }
   elseif ( ( $args__[$index] -ne $null ) -and ( @($args__[$index])[0][0] -eq '/') )
   {
## This is a native DIR parameter it must be single valued
       $Names[$Filter] = $args__[$index]
   }



#********************************************************************************************************************#
#
#   Now all the special cases have been dealt with. Build up the parameter array
#
#********************************************************************************************************************#


## copy the first two parameters into the parameter array as name/value pairs
   if ($PositionalParameter -band $Path)
   {
       $dirArgs += '-Path'
       $dirArgs += (,$Param_Pos[$Path])
   }
   else 
   {
       if ($Names[$Path] -ne $null)
       {
           $dirArgs += $Names[$Path]
       }
       if ($Param_Name[$Path] -ne $null)
       {
           $dirArgs += (,$Param_Name[$Path])
       }
   }


   if ($PositionalParameter -band $Filter)
   {
## Verify that -Search was not the first parameter before assuming this is a positional arugment
       if ( $SearchArgNotFound -eq $true )
       {
          $dirArgs += '-Filter'
       }
       $dirArgs += (,$Param_Pos[$Filter])
   }
   else
   {
       if ($Names[$Filter] -ne $null)
       {
           $dirArgs += $Names[$Filter]
       }
       if ($Param_Name[$Filter] -ne $null)
       {
           $dirArgs += (,$Param_Name[$Filter])
       }
   }


## copy any remaining parameters into the parameter arrray
   if ( ($index+1) -le $args__.GetUpperBound(0) )
   {
       $dirArgs += $args__[($index+1)..$args__.GetUpperBound(0)]
   }
   
   if ($dirArgs.Count -gt 0) { 
      DEBUG-OUT "There are $($dirArgs.Count) parameters to process"
      $i=0; foreach ($arg in $dirArgs) {DEBUG-OUT "`$args[$i] = $arg" ; $i++}
   }

   $Params = Parse_Arguments $dirArgs

#   $Params

   $Results = Process_Arguments $Params

#   $Results



   foreach ($result in $Results) 
   {
       switch ($result.ArgumentType)
       {
           Pipe   { $Pipe_Expression = $result.ArgumentValue }
           Args   { $Parameters  = $result.ArgumentValue }
       }
   }

   DEBUG-OUT "`$Pipe_Expression = $Pipe_Expression"
   DEBUG-OUT "`$Parameters = $Parameters"

   DEBUG-OUT "Passing the following string to Invoke-Expression: `"Get-Childitem `$Parameters `$Pipe_Expression is: `
   Get-Childitem $Parameters $Pipe_Expression`""

   LEAVE "$($MyInvocation.MyCommand.name)" 
) | OUTPUT


# Set-PSDebug -step
# Pipe the Access denied errors to the bit bucket
  Invoke-Expression -Command "Get-Childitem $Parameters $Pipe_Expression"
}  # End of script





## This will be used if the function is invoked indirectly via the script Dir2.ps1
$input | Script:Dir2 $args




#********************************************************************************************************************#
#
#   These are only used while testing the function.
#
#********************************************************************************************************************#


# DEBUG-OUT 'hello world'
# ENTER 'goodbye'

# [System.Collections.HashTable[]]$Param = @()
# Set-PSDebug -step
# $Param += Parse_Legacy_Arguments 'Attr' '/adsf' 
# $Param += Parse_Legacy_Arguments 'Wide'
# $Param += Parse_Cmdlet_Switch_Argument '-Force'
# $Param += Parse_Cmdlet_Multi_Value_Argument '-Include' 'c:\', 'd:\'
# echo $Param

# [System.Collections.HashTable[]]$Param = @()
# $Param =  Parse_Arguments $args
# echo $Param

#$a = '-path', 'c:' , '-search', '[a-f]?*', '-name'
#echo "output should be '-path', 'c:' , '-name'"
#switch -wildcard ($a) {
#        -Search {
#                [void]$switch.movenext(); [void]$switch.movenext()
# push back -name                
#                if (@($switch.current) -like "-*") {PutBack $switch @($switch.current)}
#                }
#        default { echo $_ }
#}


# Measure-Command { Dir $args }

# Dir2 $args

# 'C:\*', 'C:\Windows\*', 'C:\Windows\System32\*', 'C:\Windows\System32\WindowsPowerShell\v1.0\*'  | Dir2 $args


############################       All these should be equal        ############################
# CMD /c Dir \windows /a-r-s /ong /b | Tee -filePath c:out2 | Measure-Object
# Dir2 \windows /a-r-s /on | % {$_.Name} | Tee -filePath c:out1 | measure-object
# Compare-Object ${c:out1} ${c:out2}

# CMD /c Dir \windows /ars /ong /b | Tee -filePath c:out2 | Measure-Object
# Dir2 \windows /ars /on | % {$_.Name} | Tee -filePath c:out1 | measure-object
# Compare-Object ${c:out1} ${c:out2}

# CMD /c Dir \windows /as-h /ong /b | Tee -filePath c:out2 | Measure-Object
# Dir2 /as-h /on -Path \windows | % {$_.Name} | Tee -filePath c:out1 | Measure-Object
# Compare-Object ${c:out1} ${c:out2}

# CMD /c Dir \windows /ahr-d /ong /b | Tee -filePath c:out2 | Measure-Object
# Dir2 \windows /ahr-d /on | % {$_.Name} | Tee -filePath c:out1 | Measure-Object
# Compare-Object ${c:out1} ${c:out2}

# CMD /c Dir \windows /aa-r-d /ong /b | Tee -filePath c:out2 | Measure-Object
# Dir2 \windows /aa-r-d /on | % {$_.Name} | Tee -filePath c:out1 | Measure-Object
# Compare-Object ${c:out1} ${c:out2}

# CMD /c Dir \windows /a-d /osg /b | Tee -filePath c:out2 | Measure-Object
# Dir2 \windows /a-d /osn | % {$_.Name} | Tee -filePath c:out1 | Measure-Object
# Compare-Object ${c:out1} ${c:out2}

# CMD /c Dir \windows /a-d /osg /b | Tee -filePath c:out2 | Measure-Object
# Dir2 \windows /a-d /os | % {$_.Name} | Tee -filePath c:out1 | measure-object
# Compare-Object ${c:out1} ${c:out2}

# CMD /c Dir \windows /o-sng /b | Tee -filePath c:out2 | Measure-Object
# Dir2 \windows /a-s-h /o-sng | % {$_.Name} | Tee -filePath c:out1 | measure-object
# Compare-Object ${c:out1} ${c:out2}

# CMD /c Dir \windows /odn /tc /b | Tee -filePath c:out2 | Measure-Object
# Dir2 \windows /a-s-h /oc | % {$_.Name} | Tee -filePath c:out1 | measure-object
# Compare-Object ${c:out1} ${c:out2}

# CMD /c Dir \windows /odn /ta /b | Tee -filePath c:out2 | Measure-Object
# Dir2 \windows /a-s-h /oa | % {$_.Name} | Tee -filePath c:out1 | measure-object
# Compare-Object ${c:out1} ${c:out2}

# CMD /c Dir /a-d \windows /oen /b | Tee -filePath c:out2 | Measure-Object
# Dir2 \windows /a-d /oen | % {$_.Name} | Tee -filePath c:out1 | measure-object
# Compare-Object ${c:out1} ${c:out2}


# These should be return the same information regardless of whether the function or script is called
# 'c:\', 'c:\windows' | c:\temp\dir2.ps1  /aa-d /os | Tee -filePath c:\temp\out2 | Measure-Object
# 'c:\', 'c:\windows' | dir2  /aa-d /os | Tee -filePath c:\temp\out1 | measure-object
# Compare-Object ${c:\temp\out1} ${c:\temp\out2}


# Validate that you can pass an array of directories as the first parameter regardless of how called
# c:\temp\dir2.ps1 c:\, c:\windows /aa-d /os | Tee -filePath c:\temp\out2 | Measure-Object
# dir2 c:\, c:\windows /aa-d /os | Tee -filePath c:\temp\out1 | Measure-Object
# Compare-Object ${c:\temp\out1} ${c:\temp\out2}

############################       All these should be equal        ############################


# Search for any recent text file written within the last 10 days
# Dir2 \users -search * -escape @{Name='Now'; Expression='| ? { $_.LastWriteTime -gt [DateTime]::Now.AddDays(-10) }'}


# sorts by name but does not group by size equivalent to dir /on-s
# gci | sort  name, @{e={$_.Length}; Descending=$true}
 
# sorts by length and within this group will sort by name equivalent to dir /o-sn
# gci | sort  @{e={$_.Length}; Descending=$true}, name
# gci | sort  -property  @{e={$_.Length}; Descending=$true}, @{e={$_.Name}; Ascending=$true}
 
# Both these are equivalent to dir /osn
# gci | sort -property length, name

# gci | sort  -property  @{e={$_.Length, $_.extension}; Descending=$true}, @{e={$_.Name}; Descending=$false}
Platforms
Windows Server 2008 R2 No
Windows Server 2008 No
Windows Server 2003 No
Windows 7 No
Windows Vista No
Windows XP No
Windows 2000 No
For online peer support, join The Official Scripting Guys Forum! To provide feedback or report bugs in sample scripts, please start a new discussion on the Discussions tab for this script.
Disclaimer The sample scripts are not supported under any Microsoft standard support program or service. The sample scripts are provided AS IS without warranty of any kind. Microsoft further disclaims all implied warranties including, without limitation, any implied warranties of merchantability or of fitness for a particular purpose. The entire risk arising out of the use or performance of the sample scripts and documentation remains with you. In no event shall Microsoft, its authors, or anyone else involved in the creation, production, or delivery of the scripts be liable for any damages whatsoever (including, without limitation, damages for loss of business profits, business interruption, loss of business information, or other pecuniary loss) arising out of the use of or inability to use the sample scripts or documentation, even if Microsoft has been advised of the possibility of such damages.
Be the first to create a discussion.