Submitted By: Bob Landau

Adds additional capabilities found in the DIR command to the Get-ChildItem cmdlet.

PowerShell
Edit|Remove
#********************************************************************************************************************#
#
#   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}