Downloads
101900.zip

Executive Summary:
The Windows PowerShell Get-ChildItem cmdlet uses the dir alias and is similar to Cmd.exe's dir command, although the PowerShell command doesn't have all the same features . The D.ps1 script rectifies this disparity by emulating dir's most useful features in a PowerShell script.

Using Windows PowerShell is a paradigm-shifting experience for Windows users who are accustomed to the Cmd.exe command shell. PowerShell is much more powerful and flexible, but most Cmd.exe commands don't have direct PowerShell equivalents. For example, PowerShell has a default dir alias that runs the Get-ChildItem cmdlet, but Get-ChildItem doesn't behave exactly the same as Cmd.exe's dir command.

Because dir is probably the command I use most frequently in Cmd.exe, I found myself missing some of dir's features when working in PowerShell—in particular, its /a (select attributes) and /o (sort order) parameters. Table 1 shows some example Cmd.exe dir commands and their PowerShell equivalents. As you can see, the PowerShell commands are all longer—and in many cases more complex—than the equivalent dir commands in Cmd.exe. To improve my productivity at the PowerShell command line, I wrote a script, D.ps1, which emulates a number of dir's most useful features.

Introducing D.ps1
You can download D.ps1 by clicking the Download the Code Here button at the top of this page. Table 2 describes D.ps1's command-line parameters. The main difference between using D.ps1 and using dir in Cmd.exe is that you use a dash (-) instead of a forward slash (/) for the parameters. All of the script's parameters are optional. If you run the script without parameters, it lists the contents of the current directory.

D.ps1 counts the number of files and directories, totals the lengths of the files, and reports them at the end of its output, as dir does. Figure 1 shows an example of D.ps1's output that displays the JScript script files in the current directory, sorted by date. Note that D.ps1's output doesn't include the displayed directory's path as Get-ChildItem and the dir command do. D.ps1 also displays each file's attributes, which Get-ChildItem does but dir doesn't do. Table 3 shows some sample D.ps1 commands and a description of each command.

By default, D.ps1 outputs formatted strings rather than file system objects. If you want to output objects or you want to list items from a location other than the file system, you must specify the -defaultoutput parameter, as noted in Table 2. If the current path isn't in the file system (e.g., HKCU:\) and you don't specify a path to list, D.ps1 outputs an error unless you use -defaultoutput.

The script is composed of a param statement, which defines the script's command-line parameters, and six functions: usage, iif, get-attributeflags, get-orderlist, get-providername, and main. The last line of the script executes the main function, which calls the other functions as needed.

The usage and iif Functions
If the -help parameter is present on the command line, the main function executes the usage function, which callout A in Listing 1 shows. The usage function simply outputs a usage message and ends the script with the exit statement.

The iif function provides a shortcut for the following frequently used syntax:

if (condition) \{<br>  $variable = truevalue<br>\} else \{<br>  $variable = falsevalue<br>\}

By using the iif function, you can write this instead:

$variable = iif \{ condition \} \{ truevalue \} \{ falsevalue \}

Callout B in the listing shows the iif function. The function uses three script blocks as parameters. It executes the first script block ($expr); if the result is true, the script executes the second script block; otherwise, it executes the third script block.

The get-attributeflags Function
The main function uses the get-attributeflags function to convert the -attributes parameter's argument (a string containing a list of file attributes to include or exclude) into two bitmap values. If you aren't familiar with bitmap values, see the sidebar "Understanding Bitmap Values."

The get-attributeflags function, which you can see at callout C, first creates a hash table that contains the attribute characters and their associated .NET bit mask values. It then builds a string based on the hash table's keys, iterates each character in the attribute string, and uses the switch statement to decide whether to enable or disable bits in the returned values. If the attribute character isn't a dash or a valid attribute character, the function throws an error, ending the script. The last line of the get-attributeflags function returns the two bitmap values to the main function; these values are used later in the script.

The get-orderlist Function
Callout D shows the get-orderlist function. The main function uses get-orderlist to output a list of hash tables that determines the sort order for the directory listing. The main function passes three parameters to the get-orderlist function: the -order parameter's argument (a string containing the desired sort order), the name field (the property to use when sorting by name), and the time field (the property to use when sorting by date). Each hash table has two keys: Expression and Ascending. The expression key in each hash table is the sort expression, and the Ascending key can contain either $TRUE (for an ascending sort) or $FALSE (for a descending sort).

The get-orderlist function works similarly to the get-attributeflags function: It creates a hash table containing the sort-order characters, builds a string based on the hash table's keys, iterates each character in the sort-order string, and uses the switch statement to output a hash table for each valid character. If the sort-order character isn't valid, the function throws an error. The main function uses the hash table (or tables, if it returns more than one) returned from the get-orderlist function with the Sort-Object cmdlet later in the script.

The get-providername Function
The main function uses the get-providername function, which is defined at callout E, to determine a path's provider (e.g., FileSystem, Registry, Certificate). The function returns an empty string if the path doesn't exist. First, the function sets the $result variable to an empty string; then, using the iif function, it sets the $pathArg variable to either -literalpath or -path according to the state of the script's -literalpath parameter. Then the function sets the $ErrorActionPreference variable to SilentlyContinue to prevent PowerShell from displaying error messages in case an error occurs.

Next, the function uses the Test-Path cmdlet to check whether the path exists. However, it uses Invoke-Expression rather than executing Test-Path directly so that it can support the script's -literalpath parameter. If the path exists, the script uses the Invoke-Expression cmdlet—again, to support -literalpath—to execute the Get-Item cmdlet and select the first returned object. The function then assigns the object's PSProvider object's Name property to the $result variable. The last line of the function outputs the $result variable.

The main Function
The main function contains the main body of the script. First, if the -help parameter is present, the main function calls the usage function, which would display the usage message and end the script. If -help isn't present, the function checks for the -path parameter. If it's missing, the function assumes the user wants to list the items in the current location.

Next, the main function uses the iif function to set the $pathArg variable to -path or -literalpath, depending on whether the script's -literalpath parameter is present or missing. After this, the function checks to see if the -attributes parameter is present. If it is, the main function calls the get-attributeflags function to retrieve the two bitmap values corresponding to the -attributes argument's parameter.

If the -timefield parameter is present, the main function then uses the switch statement to see if the parameter's argument (i.e., the $TimeField variable) starts with a, c, or w, which corresponds to LastAccessTime, CreationTime, or LastWriteTime, respectively. If the -timefield parameter's argument doesn't start with a, c, or w, the main function throws an error, ending the script. If the -timefield parameter is missing, the main function sets the $TimeField variable to LastWriteTime. The function uses the $TimeField variable to determine which file property to display or use when sorting.

If the -fullname or -recurse parameters are present, the main function uses the iif function to set the $nameField variable to FullName; otherwise it sets it to Name. The function uses the $nameField variable to decide which property to use when displaying or sorting files.

At this point, the main function has processed the script's command-line parameters and is ready to execute the Get-ChildItem cmdlet. However, the script can only implement the -attributes and -order parameters by piping Get-ChildItem's output to other cmdlets. The main function accomplishes this by building a string containing a pipeline. As the code in callout F shows, the main function constructs the pipeline string as follows:

  1. If the -recurse parameter is present, the function appends the -recurse parameter to the pipeline.
  2. If the -attributes parameter is present, the function appends the -force parameter to the pipeline.
  3. If either of the attribute bitmap values are non-zero, the main function appends a Where-Object script block to the pipeline; the Where-Object script block isn't necessary if the user wants to see all files, regardless of attributes. The Where-Object script block uses the comparison techniques described in the "Understanding Bitmap Values" sidebar to create a filter that includes or excludes file attributes based on the two bitmap values returned from the get-attributeflags function.
  4. If the -order parameter is present, the main function appends the Sort-Object cmdlet to the pipeline.

The function uses the backtick (`) escape character before some of the variable references in the pipeline string to prevent PowerShell from expanding the variables in the string.

Next, the main function checks whether the -defaultoutput parameter is present. If it is, the function executes Get-ChildItem by using the Invoke-Expression cmdlet, and then it uses the return statement to return from the main function, ending the script.

If the -defaultoutput parameter isn't present, the main function creates a string containing a formatted string expression that uses the -f operator. Later, the function uses the Invoke-Expression cmdlet to output this string for each file system item it displays. The formatted string expression contains the fields in the expression, the -f operator, and the following properties for each item:

  • Mode
  • Date
  • Time
  • Length
  • The file's owner (if -q is present)
  • Name

After this, the main function initializes three counter variables ($dirCount, $fileCount, and $sizeTotal) to zero and uses a foreach loop to iterate each path specified by the -path parameter. Inside the foreach loop, the function uses the switch statement to decide what to do with the results from the get-providername function. If the provider is FileSystem, the function uses the Invoke-Expression cmdlet to execute Get-ChildItem with the current path and the pipeline, then pipes the result to a ForEach-Object script block.

Inside the ForEach-Object script block, the main function checks for the -bare parameter. If the parameter is missing, the function invokes the formatted string expression it created earlier. If the item isn't a directory (i.e., if the item's Attributes property doesn't have the Directory bit set), the function increments the $fileCount and $sizeTotal variables; otherwise, the function increments the $dirCount variable.

Alternatively, if the -bare parameter is present, the main function only outputs the item's Name or FullName property (based on the $nameField variable). If the path isn't in the file system or if it doesn't exist, the main function uses the Write-Error cmdlet to output an error message and continues to the next path in the foreach loop.

After the ForEach-Object script block finishes, the main function again checks for the -bare parameter. If -bare is missing, the function outputs a formatted string containing the $fileCount, $sizeTotal, and $dirCount variables if either the $dirCount or $fileCount variables are non-zero.

It's All About Productivity
The limitations in PowerShell's Get-ChildItem cmdlet need not slow you down if you're used to Cmd.exe's dir command—let the D.ps1 script do the work for you. Put it in your Path and spend less time listing directories on your system.

Listing 1: D.ps1
# d.ps1
# Written by Bill Stewart (bill.stewart@frenchmortuary.com)
#
# Lists items like Cmd.exe's Dir command. I wrote this script because the
# get-childitem cmdlet lacks some of Dir's built-in functionality, and I wanted
# to quickly specify attributes and/or a sorting order without the tedium of
# constructing a where-object filter and/or sort-object hashtables.
#
# When passing parameters to the script, I recommend you use the form
# -parameter:argument (particularly with -attributes, -order, and -timefield)
# due to potential argument conflicts. For example, to list files without the
# archive attribute, you should write '-a:-a'. If you just write '-a -a',
# PowerShell's parser interprets this as the -a parameter specified twice. (You
# can also write -a '-a' or -a "-a", but -a:-a is shorter.)

param ($Path,
       $Attributes,
       $Order,
       $TimeField,
       \[Switch\] $FullName,
       \[Switch\] $Recurse,
       \[Switch\] $Bare,
       \[Switch\] $Q,
       \[Switch\] $LiteralPath,
       \[Switch\] $DefaultOutput,
       \[Switch\] $Help)
 <font color="c90000"># Begin Callout A</font>
# Outputs a usage message and exits.
function usage \{
  $scriptname = $SCRIPT:MYINVOCATION.MyCommand.Name

  "NAME"
  "    $scriptname"
  ""
  "SYNOPSIS"
  "    Lists items in one or more paths."
  ""
  "SYNTAX"
  "    $scriptname \[-path:<string>\] \[-attributes:<string>\] \[-order:<string>\]"
  "    \[-timefield:<string>\] \[-fullname\] \[-recurse\] \[-bare\] \[-q\] \[-literalpath\]"
  "    \[-defaultoutput\]"
  ""
  "PARAMETERS"
  "    -path:<string>"
  "        The path(s) to the item(s) to list. Without -defaultoutput, the path(s)"
  "        must be in the file system."
  ""
  "    -attributes:<string>"
  "        Displays items matching any one or more of the following attributes:"
  "            A  Files ready for archiving  L  Links (reparse points)"
  "            D  Directories                N  Normal (no other attributes)"
  "            H  Hidden files/directories   R  Read-only files/directories"
  "            I  Not content-indexed        S  System files/directories"
  "        Prefix an attribute character with '-' to exclude it. Use an empty"
  "        string (') to include all attributes."
  ""
  "    -order:<string>"
  "        Displays items in sorted order."
  "            D  Date (oldest first)     N  Name (alphabetic)"
  "            E  Extension (alphabetic)  S  Size (smallest first)"
  "            G  Group directories"
  "        Prefix a sort order character with '-' to reverse the order. Items are"
  "        sorted in the order specified."
  ""
  "    -timefield:<string>"
  "        Controls which time field is displayed and/or used for sorting."
  "            A  Last access time  W  Last write time"
  "            C  Creation time"
  ""
  "    -fullname"
  "        Displays items' full names."
  ""
  "    -recurse"
  "        Recurse through subdirectories. Note: -recurse enables -fullname. When"
  "        using -recurse, -path must contain only directory names."
  ""
  "    -bare"
  "        Displays items' names only."
  ""
  "    -q"
  "        Displays the owner for each item."
  ""
  "    -literalpath"
  "        Specifies that paths are literal (i.e., no characters are interpreted"
  "        as wildcards)."
  ""
  "    -defaultoutput"
  "        Outputs objects instead of formatted strings."

  exit
\}
# End Callout A


</string></string></string></string></string></string></string></string>
<font color="c90000"># Begin Callout B</font>
# If $expr is True, execute $t; otherwise, execute $f.
function iif(\[ScriptBlock\] $expr, \[ScriptBlock\] $t, \[ScriptBlock\] $f) \{
  if (& $expr) \{
    & $t
  \} else \{
    & $f
  \}
\}
# End Callout B
<font color="c90000"># Begin Callout C</font>
# Based on the specified attribute string, this function returns two bitmap
# values. The first bitmap contains the attributes to be included, and the
# second bitmap contains the attributes to be excluded.
function get-attributeflags($attrString) \{
  # Create hash table containing the list of file system attributes.
  $attrHash = @\{"A" = \[System.IO.FileAttributes\]::Archive;
                "D" = \[System.IO.FileAttributes\]::Directory;
                "H" = \[System.IO.FileAttributes\]::Hidden;
                "I" = \[System.IO.FileAttributes\]::NotContentIndexed;
                "L" = \[System.IO.FileAttributes\]::ReparsePoint;
                "N" = \[System.IO.FileAttributes\]::Normal;
                "R" = \[System.IO.FileAttributes\]::ReadOnly;
                "S" = \[System.IO.FileAttributes\]::System\}

  $includeFlags = 0    # Attributes to be included
  $excludeFlags = 0    # Attributes to be excluded

  # Create a string containing a list of valid attribute characters.
  $attrChars = ""
  $attrHash.Keys | foreach-object \{ $attrChars += $_ \}

  # Keep track of whether '-' appears before an attribute character.
  $enableFlag = $TRUE

  # Iterate the attribute string as a character array.
  foreach ($attrChar in \[Char\[\]\] $attrString) \{
    switch -wildcard ($attrChar) \{
      "-" \{
        if ($enableFlag) \{
          $enableFlag = $FALSE
        \}
      \}
      "\[$attrChars\]" \{
        $flag = $attrHash\["$_"\]
        if ($enableFlag) \{
          # Set the bit in the "include" bits.
          $includeFlags = $includeFlags -bor $flag
          # Clear the bit in the "exclude" bits.
          $excludeFlags = $excludeFlags -band (-bnot $flag)
        \} else \{
          $enableFlag = $TRUE
          # Set the bit in the "exclude" bits.
          $excludeFlags = $excludeFlags -bor $flag
          # Clear the bit in the "include" bits.
          $includeFlags = $includeFlags -band (-bnot $flag)
        \}
      \}
      default \{
        # Throw an error if the attribute character is not valid.
        throw "Invalid attribute character ('$_'). Use -help for help."
      \}
    \}
  \}

  # Output both bit flags.
  $includeFlags,$excludeFlags
\}
# End Callout C
<font color="c90000">#Begin Callout D</font>
# Outputs a list of sort-order hash tables based on the specified sort-order
# string, name field, and time field.
function get-orderlist($orderString, $nameField, $timeField) \{
  $orderHash = @\{"D" = $timeField;
                 "E" = "Extension";
                 "N" = $nameField;
                 "S" = "Length"\}

  # Create string containing a list of valid sort-order characters.
  $orderChars = ""
  $orderHash.Keys | foreach-object \{ $orderChars += $_ \}

  # Keep track of whether '-' appears before a sort-order character.
  $ascendingSort = $TRUE

  # Iterate the sort-order string as a character array.
  foreach ($orderChar in \[Char\[\]\] $orderString) \{
    switch -wildcard ($orderChar) \{
      "-" \{
        if ($ascendingSort) \{
          $ascendingSort = $FALSE
        \}
      \}
      "\[$orderChars\]" \{
        # Output a hashtable containing the requested sort order.
        @\{"Expression" = $orderHash\["$_"\];
          "Ascending"  = $ascendingSort\}
        $ascendingSort = $TRUE
      \}
      "G" \{
        # Group directories: Sort by the Directory attribute.
        @\{"Expression" = \{($_.Attributes -band
                          \[System.IO.FileAttributes\]::Directory) -ne 0\};
          "Ascending"  = -not $ascendingSort\}
        $ascendingSort = $TRUE
      \}
      default \{
        throw "Invalid sort-order character ('$_'). Use -help for help."
      \}
    \}
  \}
\}
# End Callout D
<font color="c90000"># Begin Callout E</font>
# Returns the provider name for the specified path. If the path doesn't exist,
# the function returns a blank string.
function get-providername($path) \{
  $result = ""
  $pathArg = iif \{ $LiteralPath \} \{ "-literalpath" \} \{ "-path" \}
  $ErrorActionPreference = "SilentlyContinue"
  if (invoke-expression "test-path $pathArg `$path") \{
    $result = (invoke-expression ("get-item $pathArg `$path -force |" +
      " select-object -f 1")).PSProvider.Name
  \}
  $result
\}
# End Callout E


function main \{
  # Display the usage message if -help exists.
  if ($Help) \{
    usage
  \}

  # If -path is missing, assume the current location.
  if ($Path -eq $NULL) \{
    $Path = (get-location).Path
  \}

  # Use -literalpath if requested; otherwise, just use -path.
  $pathArg = iif \{ $LiteralPath \} \{ "-literalpath" \} \{ "-path" \}

  # If -attributes exists, retrieve the bitmap values.
  if ($Attributes -ne $NULL) \{
    $attrInclude,$attrExclude = get-attributeflags $Attributes
  \}

  # If -timefield exists, make sure it's valid. LastWriteTime is the default.
  if ($TimeField -ne $NULL) \{
    switch -wildcard ($TimeField) \{
      "A*" \{ $TimeField = "LastAccessTime" \}
      "C*" \{ $TimeField = "CreationTime" \}
      "W*" \{ $TimeField = "LastWriteTime" \}
      default \{
        throw "Invalid time field ('$TimeField'). Use -help for help."
      \}
    \}
  \} else \{
    $TimeField = "LastWriteTime"
  \}

  # Use the FullName property if requested or if using -recurse.
  $nameField = iif \{ $FullName -or $Recurse \} \{ "FullName" \} \{ "Name" \}

  # If -order exists, retrieve the sort order.
  if ($Order -ne $NULL) \{
    $Order = get-orderlist $Order $nameField $TimeField
  \}
<font color="c90000"># Begin Callout F</font>
  # Create the pipeline for the get-childitem cmdlet.
  $pipeline = ""

  # Add -recurse if requested.
  if ($Recurse) \{
    $pipeline += " -recurse"
  \}

  # If -attributes exists, use -force.
  if ($Attributes -ne $NULL) \{
    $pipeline += " -force"
    # If any attributes were specified, pipe to a where-object scriptblock.
    if (($attrInclude -ne 0) -or ($attrExclude -ne 0)) \{
      $pipeline += " | where-object \{ "
      if (($attrInclude -ne 0) -and ($attrExclude -ne 0)) \{
        $pipeline += "((`$_.Attributes -band $attrInclude) -eq $attrInclude) -and " +
                     "((`$_.Attributes -band $attrExclude) -eq 0)"
      \} elseif ($attrInclude -ne 0) \{
        $pipeline += "(`$_.Attributes -band $attrInclude) -eq $attrInclude"
      \} else \{
        $pipeline += "(`$_.Attributes -band $attrExclude) -eq 0"
      \}
    $pipeline += " \}"
    \}
  \}

  # Pipe to sort-object if needed.
  if ($Order -ne $NULL) \{
    $pipeline += " | sort-object `$Order"
  \}
# End Callout F

  # If -defaultoutput exists, execute the expression and return.
  if ($DefaultOutput) \{
    invoke-expression "get-childitem $pathArg `$Path $pipeline"
    return
  \}

  # Create the formatted string expression.
  $formatStr = "`"\{0,5\} \{1,17\}  \{2,8\} \{3,15:N0\}"
  $formatStr += iif \{ -not $Q \} \{ " \{4\}" \} \{ " \{4,-22\} \{5\}" \}
  $formatStr += "`" -f `$_.Mode," +
    "`$_.$TimeField.ToString('d')," +
    "`$_.$TimeField.ToString('t')," +
    "`$_.Length"
  if ($Q) \{
    $formatStr += ",(get-acl `$_.FullName).Owner"
  \}
  $formatStr += ",`$_.$nameField"

  # Initialize the counters.
  $dirCount = $fileCount = $sizeTotal = 0

  # Iterate each path. Paths must be in the file system.
  foreach ($item in $Path) \{
    switch (get-providername $item) \{
      "FileSystem" \{
        invoke-expression "get-childitem $pathArg `$item $pipeline" |
          foreach-object \{
          if (-not $Bare) \{
            invoke-expression $formatStr
            if (($_.Attributes -band \[System.IO.FileAttributes\]::Directory) -eq 0) \{
              $fileCount += 1
              $sizeTotal += $_.Length
            \} else \{
              $dirCount += 1
            \}
          \} else \{
            $_.$nameField
          \}
        \}
      \}
      "" \{
        write-error "Cannot find path '$item' because it does not exist."
      \}
      default \{
        write-error "The path '$item' is not in the file system."
      \}
    \}
  \}

  # Output footer information when not using -bare.
  if (-not $Bare) \{
    if (($fileCount -gt 0) -or ($dirCount -gt 0)) \{
      "\{0,16:N0\} file(s) \{1,16:N0\} byte(s)`n\{2,16:N0\} dir(s)" -f
        $fileCount,$sizeTotal,$dirCount
    \}
  \}

\}

main