How To Find With PowerShell

Finding files, do I even need to describe how important this is?

From finding README files in a project

$ find . -name README*
./app/README.md
./README.md
./ipsum.md/README.md

to locating that one config file you never can remember the location of.

$ find . -name dnsconf.yaml
./app/module1/dnsconf.yaml

Info

All commands in this post are run against a open source repository to allow you to reproduce output & play with the commands.

find#

$ find . -name README*
./app/README.md
./README.md
./ipsum.md/README.md

This will list all files matching the README* pattern in my current working directory.

With PowerShell we will use Get-ChildItem and define our search term in the -Filter parameter. Additionally we give it the parameter to recurse, wich it would not do by default.

PS> Get-ChildItem -Path "." -Recurse -Filter "README*"
    Directory: /Users/mkamner/projects/mkamner/scripts

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-----          12/06/2020    07:23           1397 README.md

    Directory: /Users/mkamner/projects/mkamner/scripts/app

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-----          12/31/2020    14:29            115 README.md

    Directory: /Users/mkamner/projects/mkamner/scripts/app/module3

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-----          12/31/2020    14:33              0 README

    Directory: /Users/mkamner/projects/mkamner/scripts/ipsum.md

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-----          12/31/2020    14:15             55 README.md

As you can see PowerShell found one additional file: ./app/module3/README, this is because of the different behavior of * in Bash and PowerShell. To be honest, the PowerShell behavior actually works in our favor here.

Either way, the output is far from great! Let’s fix this with Format-Table.

PS> Get-ChildItem -Path "." -Recurse -Filter "README*" | Format-Table FullName
FullName
--------
/Users/mkamner/projects/mkamner/scripts/README.md
/Users/mkamner/projects/mkamner/scripts/app/README.md
/Users/mkamner/projects/mkamner/scripts/app/module3/README
/Users/mkamner/projects/mkamner/scripts/ipsum.md/README.md

find -type#

By default find returns files and folders alike.

$ find . -name "*.md"
./app/README.md
./README.md
./ipsum.md
./ipsum.md/5.md
./ipsum.md/1.md
./ipsum.md/0.md
./ipsum.md/4.md
./ipsum.md/README.md
./ipsum.md/9.md
./ipsum.md/8.md
./ipsum.md/3.md
./ipsum.md/7.md
./ipsum.md/6.md
./ipsum.md/2.md

PowerShell does the same.

PS> Get-ChildItem -Path "." -Recurse -Filter "*md" | Format-Table FullName
FullName
--------
/Users/mkamner/projects/mkamner/scripts/ipsum.md
/Users/mkamner/projects/mkamner/scripts/README.md
/Users/mkamner/projects/mkamner/scripts/app/README.md
/Users/mkamner/projects/mkamner/scripts/ipsum.md/0.md
/Users/mkamner/projects/mkamner/scripts/ipsum.md/1.md
/Users/mkamner/projects/mkamner/scripts/ipsum.md/2.md
/Users/mkamner/projects/mkamner/scripts/ipsum.md/3.md
/Users/mkamner/projects/mkamner/scripts/ipsum.md/4.md
/Users/mkamner/projects/mkamner/scripts/ipsum.md/5.md
/Users/mkamner/projects/mkamner/scripts/ipsum.md/6.md
/Users/mkamner/projects/mkamner/scripts/ipsum.md/7.md
/Users/mkamner/projects/mkamner/scripts/ipsum.md/8.md
/Users/mkamner/projects/mkamner/scripts/ipsum.md/9.md
/Users/mkamner/projects/mkamner/scripts/ipsum.md/README.md

For find this can be altered by using the -type parameter and setting it to either

  • d for directories

    $ find . -name "*.md" -type d
    ./ipsum.md
    
  • or f for files

    $ find . -name "*.md" -type f
    ./app/README.md
    ./README.md
    ./ipsum.md/5.md
    ./ipsum.md/1.md
    ./ipsum.md/0.md
    ./ipsum.md/4.md
    ./ipsum.md/README.md
    ./ipsum.md/9.md
    ./ipsum.md/8.md
    ./ipsum.md/3.md
    ./ipsum.md/7.md
    ./ipsum.md/6.md
    ./ipsum.md/2.md
    

PowerShell can replicate this with two unique parameters.

  • -Directory

    PS> Get-ChildItem -Path "." -Recurse -Filter "*md" -Directory | Format-Table FullName
    FullName
    --------
    /Users/mkamner/projects/mkamner/scripts/ipsum.md
    
  • and File

    PS> Get-ChildItem -Path "." -Recurse -Filter "*md" -File | Format-Table FullName
    FullName
    --------
    /Users/mkamner/projects/mkamner/scripts/README.md
    /Users/mkamner/projects/mkamner/scripts/app/README.md
    /Users/mkamner/projects/mkamner/scripts/ipsum.md/0.md
    /Users/mkamner/projects/mkamner/scripts/ipsum.md/1.md
    /Users/mkamner/projects/mkamner/scripts/ipsum.md/2.md
    /Users/mkamner/projects/mkamner/scripts/ipsum.md/3.md
    /Users/mkamner/projects/mkamner/scripts/ipsum.md/4.md
    /Users/mkamner/projects/mkamner/scripts/ipsum.md/5.md
    /Users/mkamner/projects/mkamner/scripts/ipsum.md/6.md
    /Users/mkamner/projects/mkamner/scripts/ipsum.md/7.md
    /Users/mkamner/projects/mkamner/scripts/ipsum.md/8.md
    /Users/mkamner/projects/mkamner/scripts/ipsum.md/9.md
    /Users/mkamner/projects/mkamner/scripts/ipsum.md/README.md
    

Tipps & Tricks#

  • To include hidden files and folders (starting with .) you should add the parameter -Force
  • You can control how deep the folder structure should be searched with -Depth

Shortcuts#

Writing out Get-ChildItem every time is nothing to be desired, thankfully we can take some shortcuts.

  • Shorten Get-ChildItem by using it’s alias gci
  • Positional arguments
    • -Path can be provided as positional argument 0
    • -Filter can be provided as positional argument 1
  • Omit "" around your strings as long as they have no spaces inside
  • Commands are always case insensitive, this is more of a personal preference
  • Although not required Format-Table can also be shortened to ft.

With this shortcuts our commands are way shorter.

Get-ChildItem -Path "." -Recurse -Filter "README*"
gci . README* -Recurse

Get-ChildItem -Path "." -Recurse -Filter "README*" | Format-Table FullName
gci . README* -Recurse | ft FullName

Get-ChildItem -Path "." -Recurse -Filter "*md" | Format-Table FullName
gci . *md -Recurse | ft FullName

Get-ChildItem -Path "." -Recurse -Filter "*md" -Directory | Format-Table FullName
gci . *md -Recurse -Directory | ft FullName

Get-ChildItem -Path "." -Recurse -Filter "*md" -File | Format-Table FullName
gci . *md -Recurse -File | ft FullName

Taking It Further#

This covers basic examples for replacing find in PowerShell.

Thanks to PowerShells object oriented piping system the information we see on screen is not everything we get to work with.

Lets take a look with one on the previous examples and plug it into the fictional pipeline to search all config files for occurrences of 1.1.1.1. We will use commands from my How To Grep With PowerShell post to create a report of all files matching *conf* and containing 1.1.1.1.

Get-ChildItem -Path "." -Recurse -Filter "*conf*" |
Foreach-Object {Select-String -Path $_.PSPath -Pattern "1.1.1.1" | Select-Object Path,LineNumber,Line} |
Export-Csv -Path cloudflare_dns.csv -NoTypeInformation

Obviously this is just a example demonstrating the many possibilities in having a rich object returned. You could write them to a variable for later use, loop over them or any other thing a real programming language allows you to do.

For quick reference this are all values accessible inside every object returned.

PSPath              : Microsoft.PowerShell.Core\FileSystem::/Users/mkamner/projects/mkamner/scripts/app/conf
PSParentPath        : Microsoft.PowerShell.Core\FileSystem::/Users/mkamner/projects/mkamner/scripts/app
PSChildName         : conf
PSDrive             : /
PSProvider          : Microsoft.PowerShell.Core\FileSystem
PSIsContainer       : False
Mode                : -----
ModeWithoutHardLink : -----
VersionInfo         : File:             /Users/mkamner/projects/mkamner/scripts/app/conf
                      InternalName:
                      OriginalFilename:
                      FileVersion:
                      FileDescription:
                      Product:
                      ProductVersion:
                      Debug:            False
                      Patched:          False
                      PreRelease:       False
                      PrivateBuild:     False
                      SpecialBuild:     False
                      Language:

BaseName            : conf
Target              :
LinkType            :
Length              : 27
DirectoryName       : /Users/mkamner/projects/mkamner/scripts/app
Directory           : /Users/mkamner/projects/mkamner/scripts/app
IsReadOnly          : False
FullName            : /Users/mkamner/projects/mkamner/scripts/app/conf
Extension           :
Name                : conf
Exists              : True
CreationTime        : 12/31/2020 14:32:17
CreationTimeUtc     : 12/31/2020 13:32:17
LastAccessTime      : 12/31/2020 14:56:07
LastAccessTimeUtc   : 12/31/2020 13:56:07
LastWriteTime       : 12/31/2020 14:56:05
LastWriteTimeUtc    : 12/31/2020 13:56:05
Attributes          : Normal

The LastWriteTime value is usually the most interesting to me, as it allows me to quickly filter on only files changed in the last X days or hours.

PS> Get-ChildItem -Path "." -Recurse -Filter "*conf*" | Where-Object {$_.LastWriteTime -gt (Get-Date).addDays(-1)} | Format-Table FullName

FullName
--------
/Users/mkamner/projects/mkamner/scripts/app/conf
/Users/mkamner/projects/mkamner/scripts/app/module2/conf.yaml
/Users/mkamner/projects/mkamner/scripts/app/module1/dnsconf.yaml

Replacing find is not all Get-ChildItem can do, a great ressource to dive in even deeper is the official documentation from Microsoft.

If you want to further level up your file interactions with PowerShell I would recommend two posts:

Feel free to ask me about your PowerShell problem over on Twitter!

This is a multi part series, read more.