Prof. Powershell

PowerShell File Frontier, Part 3: Right Tool for the Job

This week Prof. PowerShell will show you a custom tool for filtering files.

Over the last few lessons we've been exploring different ways of filtering for files. On one hand using separate cmdlets in a pipelined expression is the "right" way, I also recognize the need for an easier to use tool. So I created a wrapper function to Get-ChildItem called Get-MyFile. Here is the code:

#requires -version 4.0

Function Get-MyFile {

<#
.Synopsis
Get filtered file results
.Description

This function is a wrapper to Get-ChildItem that makes it easier to filter files by the last write time or their size.

.Example PS C:\> get-myfile c:\scripts\*.ps1 -after 7/1/2014 -largerthan 10KB -recurse

    Directory: C:\scripts

Mode                LastWriteTime     Length Name
----                -------------     ------ ----          
-a---          7/1/2014   3:53 PM      11069 Get-ProcessTotals.ps1
-a---         7/16/2014   5:31 PM      13922 New-PSCommand.ps1
-a---          7/2/2014   3:00 PM      11316 UserGroupComparison.ps1

Get all PS1 files modified after 7/1/2014 and at least 10KB in size
.Notes
Last Updated:
Version     : 0.9

Learn more:
Learn PowerShell in a Month of Lunches (http://manning.com/jones3/)
Learn PowerShell Toolmaking in a Month of Lunches (http://manning.com/jones4/)
PowerShell in Depth: An Administrator's Guide (http://www.manning.com/jones2/)
PowerShell Deep Dives (http://manning.com/hicks/)

  ****************************************************************
* DO NOT USE IN A PRODUCTION ENVIRONMENT UNTIL YOU HAVE TESTED *
* THOROUGHLY IN A LAB ENVIRONMENT. USE AT YOUR OWN RISK.  IF   *
* YOU DO NOT UNDERSTAND WHAT THIS SCRIPT DOES OR HOW IT WORKS, *
* DO NOT USE IT OUTSIDE OF A SECURE, TEST SETTING.             *
****************************************************************

.Link
Get-ChildItem
#>

[CmdletBinding(DefaultParameterSetName='Items', SupportsTransactions=$true)]
param(
[Parameter(ParameterSetName='Items', Position=0, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]

[ValidateScript({
if ( (Resolve-Path $_).Provider.Name -eq 'FileSystem') {
$True
}
else {
Throw "This command only supports FileSystem paths."
}

})]

    [string[]]$Path=".",

    [Parameter(ParameterSetName='LiteralItems', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
[Alias('PSPath')]
[ValidateScript({
if ( (Resolve-Path $_).Provider.Name -eq 'FileSystem') {
$True
}
else {
Throw "This command only supports FileSystem paths."
}

})]
[string[]]$LiteralPath,

    [Parameter(Position=1)]
[string]$Filter,
[string[]]$Include,
[string[]]$Exclude,
[Alias('s')]
[switch]$Recurse,
[switch]$Force,
[switch]$Name,   
[datetime]$After,
[datetime]$Before,
[int]$LargerThan,
[int]$SmallerThan

)

begin {
Write-Verbose -Message "Ending $($MyInvocation.Mycommand)"
try {
$outBuffer = $null
if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
{
$PSBoundParameters['OutBuffer'] = 1
}

#add -File to PSBoundParameters
$PSBoundParameters.Add("File",$True)

        Write-Verbose "PSBoundParameters are: $($PSBoundParameters | Out-String)"

        #My modification
if ($After -OR $Before -OR $LargerThan -OR $SmallerThan) {
#construct a filter
$filters = @()
if ($after) {
$filters+='$_.lastwritetime -ge $after'
}
if ($Before) {
$filters+='$_.lastwritetime -le $before'
}
if ($SmallerThan) {
$filters+= ' $_.length -le $smallerthan'
}
if ($LargerThan) {
$filters+= ' $_.length -ge $largerthan'
}
$f = $filters -join " -AND"
Write-Verbose $f
$filterblock = [scriptblock]::Create($f)

          <#
remove parameters to avoid errors when I splat parameters to
Get-ChildItem, or in the case of -Name, which I will manually
add back in at the end
#>

"After","Before","LargerThan","SmallerThan","Name" | foreach {
if ($PSBoundParameters.ContainsKey("$_")) { $PSBoundParameters.Remove($_) | Out-Null }
}

}       

} catch {
throw
}

    $data=@()
} #begin

process {
try {
if ($filterblock) {
$data+= (Get-ChildItem @PSBoundParameters).Where($filterblock)
}
else {
$data+= Get-Childitem @PSBoundParameters
}

} catch {
throw
}

} #process

End {

    if ($name) {
Write-Verbose "send only the filename"

$data | Select-Object -ExpandProperty Name
}
else {
#write the data to the pipeline
$data
}
Write-Verbose -Message "Ending $($MyInvocation.Mycommand)"
} #end
} #end function

I originally started creating a proxy function for Get-ChildItem but then I decided to simply make a standalone function that will call Get-ChildItem and uses the same file system parameters plus some filtering parameters.

I added parameters so you could find files modified before or after a certain date as well as files smaller or larger than a given value. You can even use multiple parameters.

 

PS C:\> get-myfile c:\scripts\*.ps1 -after  7/1/2014 -largerthan 10KB -recurse

    Directory: C:\scripts

 

Mode                LastWriteTime     Length Name
----                -------------     ------ ----    
-a---          7/1/2014   3:53 PM      11069 Get-ProcessTotals.ps1
-a---         7/16/2014   5:31 PM      13922 New-PSCommand.ps1
-a---          7/2/2014   3:00 PM      11316 UserGroupComparison.ps

This command will get all PS1 files modified after 7/1/2014 and at least 10KB in size. If you look through the script, you will see the main part of the command uses the Where() method which was introduced in PowerShell v4.

$data+= (Get-ChildItem  @PSBoundParameters).Where($filterblock)

The filterblock is built on the fly based on your parameters. In the example above this means a filtering scripblock like this:

{$_.lastwritetime -ge '7/1/2014' -AND $_.length -ge 10kb}

If I were to manually type everything my expression would normally look like this:

get-childitem c:\scripts\*.ps1 | where {$_.lastwritetime  -ge '7/1/2014' -AND $_.length -ge 10kb} 

But in v4 I can use the Where() method

(get-childitem c:\scripts\*.ps1).where({$_.lastwritetime  -ge '7/1/2014' -AND $_.length -ge 10kb})

I opted for this technique because it is much, much faster than piping to Where-Object.

But you could use the function without having to worry about the internals. That is the great thing about PowerShell is that with a little work you can make tools to simplify your work or to share with others. You could even define an alias for the function:

PS C:\> set-alias -Name mydir -Value Get-MyFile 

I wouldn't redefine dir because my function only works with filesystem drives. If you try to run Get-MyFile against something like HKLM: you will get an error. I hope you recognize the value in learning PowerShell. Once you get a handle on some basic handles and concepts it doesn't take much work to get to the next level.

About the Author

Jeffery Hicks is an IT veteran with over 25 years of experience, much of it spent as an IT infrastructure consultant specializing in Microsoft server technologies with an emphasis in automation and efficiency. He is a multi-year recipient of the Microsoft MVP Award in Windows PowerShell. He works today as an independent author, trainer and consultant. Jeff has written for numerous online sites and print publications, is a contributing editor at Petri.com, and a frequent speaker at technology conferences and user groups.

comments powered by Disqus
Most   Popular