I have a script that I use to search for hundreds of files across multiple drives from a list. It works fine as it catches all the matches. The only issue is I need to see the file it matched along with the extension.
A bit of the back story...
We have programs that share the same name as a Copybook. Not too uncommon in the mainframe world. When searching for a file, I have to Wildcard the search in order to catch all the same name files (Minus the extension). I then have to manually search for the hits to determine if they are copybooks or programs.
When I try to add any logic to the script below, it displays the entire array of file names and not just the actual match.
Would anyone be able to assist in capturing and displaying just the matched file along with it's extension? Maybe it's location also?
Regards,
-Ron
#List containing file names must be wilcarded FILE.*
#Parent folder (Where to begin search)
$folder = 'C:\Workspace\src'
#Missing Artifacts Folder (Where Text file resides)
$Dir2 = 'C:\Workspace\Temp'
#Text File Name
$files=Get-Content $Dir2\FilesToSearchFor.txt
cd \
cd $folder
Write-Host "Folder: $folder"
# Get only files and only their names
$folderFiles = (Get-ChildItem -Recurse $folder -File).Name
foreach ($f in $files) {
#if ($folderFiles -contains $f) {
if ($folderFiles -like $f) {
Write-Host "File $f was found." -foregroundcolor green
} else {
Write-Host "File $f was not found!" -foregroundcolor red
}
}
Instead of testing whether the entire list of file names contains the target file name ($folderFiles -like $f), load all files into a hashtable and then test if the target file name exists as a key with ContainsKey():
$fileTable = #{}
Get-ChildItem -Recurse $folder -File |ForEach-Object {
# Create a new key-value entry for the given file name (minus the extension) if it doesn't already exist
if(-not $fileTable.ContainsKey($_.BaseName)){
$fileTable[$_.BaseName] = #()
}
# Add file info object to the hashtable entry
$fileTable[$_.BaseName] += $_
}
foreach($f in $files){
if($fileTable.ContainsKey($f)){
Write-Host "$($fileTable[$f].Count) file(s) matching '$f' were found." -ForegroundColor Green
foreach($file in $fileTable[$f]){
Write-Host "File with extension '$($file.Extension)' found at: '$($file.FullName)'"
}
}
else {
Write-Host "No files found matching '$f'"
}
}
Since the $fileTable contains not just the name, but also a reference to the original file info objects with that names as returned by Get-ChildItem, you can easily access relevant metadata (like the Extension property) now
1. Code Description alias how it is intended to work
User enters a path to a directory in PowerShell. Code checks if any folder within the declared directory contains no data at all. If so, the path of any empty folder will be shown on the prompt to the user and eventually removed from the system.
2. The Issue alias what I am struggling with
The code I just wrote doesn't count the depth of a folder hierarchy as I would expect (the column in the output table is blank). Besides that, the program works okay - I've still got to fix the issue where my code removes empty parent directories first and child directories later, which of course will cause an error in PowerShell; for instance, take
C:\Users\JohnMiller\Desktop\Homework
where Homework consists of Homework\Math\School Project and Homework\Computer Science\PowerShell Code. Note that all directories are supposed to be empty with the exception of PowerShell Code, the folder containing this script. (Side note: A folder is considered empty when no file dwells inside. At least that's what my code is based on for now.)
3. The Code
# Delete all empty (sub)folders in [$path]
[Console]::WriteLine("`n>> Start script for deleting all empty (sub)folders.")
$path = Read-Host -prompt ">> Specify a path"
if (test-path $path)
{
$allFolders = Get-ChildItem $path -recurse | Where {$_.PSisContainer -eq $True}
$allEmptyFolders = $allFolders | Where-Object {$_.GetFiles().Count -eq 0}
$allEmptyFolders | Select-Object FullName,#{Name = "FolderDepth"; Expression = {$_.DirectoryName.Split('\').Count}} | Sort-Object -descending FolderDepth,FullName
[Console]::WriteLine("`n>> Do you want do remove all these directories? Validate with [True] or [False].") #'#
$answer = Read-Host -prompt ">> Answer"
if ([System.Convert]::ToBoolean($answer) -eq $True)
{
$allEmptyFolders | Remove-Item -force -recurse
}
else
{
[Console]::WriteLine(">> Termination confirmed.`n")
exit
}
}
else
{
[Console]::WriteLine(">> ERROR: [$($path)] is an invalid directory. Program terminates.`n")
exit
}
The depth-count problem:
Your code references a .DirectoryName property in the calculated property passed to Select-Object, but the [System.IO.DirectoryInfo] instances output by Get-ChildItem have no such property. Use the .FullName property instead:
$allEmptyFolders |
Select-Object FullName,#{Name='FolderDepth'; Expression={$_.FullName.Split('\').Count}} |
Sort-Object -descending FolderDepth,FullName
Eliminating nested empty subfolders:
To recap your problem with a simple example:
If c:\foo is empty (no files) but has empty subdir. c:\foo\bar, your code outputs them both, and if you then delete c:\foo first, deleting c:\foo\bar next fails (because deleting c:\foo also removed c:\foo\bar).
If you eliminate all nested empty subdirs. up front, you not only declutter what you present to the user, but you can then safely iterative of the output and delete one by one.
With your approach you'd need a 2nd step to eliminate the nested empty dirs., but here's a depth-first recursive function that omits nested empty folders. To make it behave the same way as your code with respect to hidden files, pass -Force.
function Get-RecursivelyEmptyDirectories {
[cmdletbinding()]
param(
[string] $LiteralPath = '.',
[switch] $Force,
[switch] $DoNotValidatePath
)
$ErrorActionPreference = 'Stop'
if (-not $DoNotValidatePath) {
$dir = Get-Item -LiteralPath $LiteralPath
if (-not $dir.PSIsContainer) { Throw "Not a directory path: $LiteralPath" }
$LiteralPath = $dir.FullName
}
$haveFiles = [bool] (Get-ChildItem -LiteralPath $LiteralPath -File -Force:$Force | Select-Object -First 1)
$emptyChildDirCount = 0
$emptySubdirs = $null
if ($childDirs = Get-ChildItem -LiteralPath $LiteralPath -Directory -Force:$Force) {
$emptySubDirs = New-Object System.Collections.ArrayList
foreach($childDir in $childDirs) {
if ($childDir.LinkType -eq 'SymbolicLink') {
Write-Verbose "Ignoring symlink: $LiteralPath"
} else {
Write-Verbose "About to recurse on $($childDir.FullName)..."
try { # If .AddRange() fails due to exceeding the array list's capacity, we must fail too.
$emptySubDirs.AddRange(#(Get-RecursivelyEmptyDirectories -DoNotValidatePath -LiteralPath $childDir.FullName -Force:$Force))
} catch {
Throw
}
# If the last entry added is the child dir. at hand, that child dir.
# is by definition itself empty.
if ($emptySubDirs[-1] -eq $childDir.FullName) { ++$emptyChildDirCount }
}
} # foreach ($childDir ...
} # if ($childDirs = ...)
if (-not $haveFiles -and $emptyChildDirCount -eq $childDirs.Count) {
# There are no child files and all child dirs., if any, are themselves
# empty, so we only output the input path at hand, as the highest
# directory in this subtree that is empty (save for empty descendants).
$LiteralPath
} else {
# This directory is not itself empty, so output the (highest-level)
# descendants that are empty.
$emptySubDirs
}
}
Tips regarding your code:
Get-ChildItem -Directory is available in PSv3+, which is not only shorter but also more efficient than Get-ChildItem | .. Where { $_.PSisContainer -eq $True }.
Use Write-Host instead of [Console]::WriteLine
[System.Convert]::ToBoolean($answer) only works with the culture-invariant string literals 'True' and 'False' ([bool]::TrueString and [bool]::FalseString, although case variations and leading and trailing whitespace are allowed).
I am constructing a powershell script to query the existence of archived files within the folder that the files are archived to.
EMC's disk extender moves the file from the live storage to archive storage and leaves a stub/link to the file.
You can still list the archive stub in the directory tree and the file still appears to be the same size but in reality it is an archive stub probably 1k in size.
The file attribute(s) of an archive stub give the value O
I need to query the files in the live folder where attribute of the file = O
then compare the list to my archive directory on another server outputting the files that do not exist on my archive where an archive stub exists on the live storage.
basically I need to know how to query the file attribute to retrieve O and do a compare.
This function (taken from here) will return either true or false, depending on your O attribute
function Get-FileAttribute{
param($file,$attribute)
$val = [System.IO.FileAttributes]$attribute;
if((gci $file -force).Attributes -band $val -eq $val){$true;} else { $false; }
}
Get-FileAttribute "C:\PST\T.dll" "Offline"
You can use something like this:
function Get-FileAttribute{
param($file,$attribute)
$val = [System.IO.FileAttributes]$attribute;
if((gci $file -force).Attributes -band $val -eq $val){$true;} else { $false; }
}
# Get items both from folder and from archive
$folder = "C:\pst"
$archive = "C:\pst"
$itemsFromFolder = get-childitem $folder -recurse
$itemsFromArchive = get-childitem $archive -recurse
# Filter items from folder to have only offline files
$offlineItemsFromFolder = New-Object 'System.Collections.Generic.List[System.IO.FileInfo]'
foreach($item in $itemsFromFolder)
{
$isOffline = Get-FileAttribute $item.FullName "Offline"
if ($isOffline)
{
$offlineItemsFromFolder.Add($item)
}
}
# Compare each offline item from folder with item from archive
foreach($item in $offlineItemsFromFolder)
{
$result = $itemsFromArchive | Where-Object {$_.FullName -eq $item.FullName.Replace($folder, $archive)}
# Do some processing depending on $result being $true or $false
}
I want to write a PowerShell script that will recursively search a directory, but exclude specified files (for example, *.log, and myFile.txt), and also exclude specified directories, and their contents (for example, myDir and all files and folders below myDir).
I have been working with the Get-ChildItem CmdLet, and the Where-Object CmdLet, but I cannot seem to get this exact behavior.
I like Keith Hill's answer except it has a bug that prevents it from recursing past two levels. These commands manifest the bug:
New-Item level1/level2/level3/level4/foobar.txt -Force -ItemType file
cd level1
GetFiles . xyz | % { $_.fullname }
With Hill's original code you get this:
...\level1\level2
...\level1\level2\level3
Here is a corrected, and slightly refactored, version:
function GetFiles($path = $pwd, [string[]]$exclude)
{
foreach ($item in Get-ChildItem $path)
{
if ($exclude | Where {$item -like $_}) { continue }
$item
if (Test-Path $item.FullName -PathType Container)
{
GetFiles $item.FullName $exclude
}
}
}
With that bug fix in place you get this corrected output:
...\level1\level2
...\level1\level2\level3
...\level1\level2\level3\level4
...\level1\level2\level3\level4\foobar.txt
I also like ajk's answer for conciseness though, as he points out, it is less efficient. The reason it is less efficient, by the way, is because Hill's algorithm stops traversing a subtree when it finds a prune target while ajk's continues. But ajk's answer also suffers from a flaw, one I call the ancestor trap. Consider a path such as this that includes the same path component (i.e. subdir2) twice:
\usr\testdir\subdir2\child\grandchild\subdir2\doc
Set your location somewhere in between, e.g. cd \usr\testdir\subdir2\child, then run ajk's algorithm to filter out the lower subdir2 and you will get no output at all, i.e. it filters out everything because of the presence of subdir2 higher in the path. This is a corner case, though, and not likely to be hit often, so I would not rule out ajk's solution due to this one issue.
Nonetheless, I offer here a third alternative, one that does not have either of the above two bugs. Here is the basic algorithm, complete with a convenience definition for the path or paths to prune--you need only modify $excludeList to your own set of targets to use it:
$excludeList = #("stuff","bin","obj*")
Get-ChildItem -Recurse | % {
$pathParts = $_.FullName.substring($pwd.path.Length + 1).split("\");
if ( ! ($excludeList | where { $pathParts -like $_ } ) ) { $_ }
}
My algorithm is reasonably concise but, like ajk's, it is less efficient than Hill's (for the same reason: it does not stop traversing subtrees at prune targets). However, my code has an important advantage over Hill's--it can pipeline! It is therefore amenable to fit into a filter chain to make a custom version of Get-ChildItem while Hill's recursive algorithm, through no fault of its own, cannot. ajk's algorithm can be adapted to pipeline use as well, but specifying the item or items to exclude is not as clean, being embedded in a regular expression rather than a simple list of items that I have used.
I have packaged my tree pruning code into an enhanced version of Get-ChildItem. Aside from my rather unimaginative name--Get-EnhancedChildItem--I am excited about it and have included it in my open source Powershell library. It includes several other new capabilities besides tree pruning. Furthermore, the code is designed to be extensible: if you want to add a new filtering capability, it is straightforward to do. Essentially, Get-ChildItem is called first, and pipelined into each successive filter that you activate via command parameters. Thus something like this...
Get-EnhancedChildItem –Recurse –Force –Svn
–Exclude *.txt –ExcludeTree doc*,man -FullName -Verbose
... is converted internally into this:
Get-ChildItem | FilterExcludeTree | FilterSvn | FilterFullName
Each filter must conform to certain rules: accepting FileInfo and DirectoryInfo objects as inputs, generating the same as outputs, and using stdin and stdout so it may be inserted in a pipeline. Here is the same code refactored to fit these rules:
filter FilterExcludeTree()
{
$target = $_
Coalesce-Args $Path "." | % {
$canonicalPath = (Get-Item $_).FullName
if ($target.FullName.StartsWith($canonicalPath)) {
$pathParts = $target.FullName.substring($canonicalPath.Length + 1).split("\");
if ( ! ($excludeList | where { $pathParts -like $_ } ) ) { $target }
}
}
}
The only additional piece here is the Coalesce-Args function (found in this post by Keith Dahlby), which merely sends the current directory down the pipe in the event that the invocation did not specify any paths.
Because this answer is getting somewhat lengthy, rather than go into further detail about this filter, I refer the interested reader to my recently published article on Simple-Talk.com entitled Practical PowerShell: Pruning File Trees and Extending Cmdlets where I discuss Get-EnhancedChildItem at even greater length. One last thing I will mention, though, is another function in my open source library, New-FileTree, that lets you generate a dummy file tree for testing purposes so you can exercise any of the above algorithms. And when you are experimenting with any of these, I recommend piping to % { $_.fullname } as I did in the very first code fragment for more useful output to examine.
The Get-ChildItem cmdlet has an -Exclude parameter that is tempting to use but it doesn't work for filtering out entire directories from what I can tell. Try something like this:
function GetFiles($path = $pwd, [string[]]$exclude)
{
foreach ($item in Get-ChildItem $path)
{
if ($exclude | Where {$item -like $_}) { continue }
if (Test-Path $item.FullName -PathType Container)
{
$item
GetFiles $item.FullName $exclude
}
else
{
$item
}
}
}
Here's another option, which is less efficient but more concise. It's how I generally handle this sort of problem:
Get-ChildItem -Recurse .\targetdir -Exclude *.log |
Where-Object { $_.FullName -notmatch '\\excludedir($|\\)' }
The \\excludedir($|\\)' expression allows you to exclude the directory and its contents at the same time.
Update: Please check the excellent answer from msorens for an edge case flaw with this approach, and a much more fleshed out solution overall.
Recently, I explored the possibilities to parameterize the folder to scan through and the place where the result of recursive scan will be stored. At the end, I also did summarize the number of folders scanned and number of files inside as well. Sharing it with community in case it may help other developers.
##Script Starts
#read folder to scan and file location to be placed
$whichFolder = Read-Host -Prompt 'Which folder to Scan?'
$whereToPlaceReport = Read-Host -Prompt 'Where to place Report'
$totalFolders = 1
$totalFiles = 0
Write-Host "Process started..."
#IMP separator ? : used as a file in window cannot contain this special character in the file name
#Get Foldernames into Variable for ForEach Loop
$DFSFolders = get-childitem -path $whichFolder | where-object {$_.Psiscontainer -eq "True"} |select-object name ,fullName
#Below Logic for Main Folder
$mainFiles = get-childitem -path "C:\Users\User\Desktop" -file
("Folder Path" + "?" + "Folder Name" + "?" + "File Name " + "?"+ "File Length" )| out-file "$whereToPlaceReport\Report.csv" -Append
#Loop through folders in main Directory
foreach($file in $mainFiles)
{
$totalFiles = $totalFiles + 1
("C:\Users\User\Desktop" + "?" + "Main Folder" + "?"+ $file.name + "?" + $file.length ) | out-file "$whereToPlaceReport\Report.csv" -Append
}
foreach ($DFSfolder in $DFSfolders)
{
#write the folder name in begining
$totalFolders = $totalFolders + 1
write-host " Reading folder C:\Users\User\Desktop\$($DFSfolder.name)"
#$DFSfolder.fullName | out-file "C:\Users\User\Desktop\PoC powershell\ok2.csv" -Append
#For Each Folder obtain objects in a specified directory, recurse then filter for .sft file type, obtain the filename, then group, sort and eventually show the file name and total incidences of it.
$files = get-childitem -path "$whichFolder\$($DFSfolder.name)" -recurse
foreach($file in $files)
{
$totalFiles = $totalFiles + 1
($DFSfolder.fullName + "?" + $DFSfolder.name + "?"+ $file.name + "?" + $file.length ) | out-file "$whereToPlaceReport\Report.csv" -Append
}
}
# If running in the console, wait for input before closing.
if ($Host.Name -eq "ConsoleHost")
{
Write-Host ""
Write-Host ""
Write-Host ""
Write-Host " **Summary**" -ForegroundColor Red
Write-Host " ------------" -ForegroundColor Red
Write-Host " Total Folders Scanned = $totalFolders " -ForegroundColor Green
Write-Host " Total Files Scanned = $totalFiles " -ForegroundColor Green
Write-Host ""
Write-Host ""
Write-Host "I have done my Job,Press any key to exit" -ForegroundColor white
$Host.UI.RawUI.FlushInputBuffer() # Make sure buffered input doesn't "press a key" and skip the ReadKey().
$Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyUp") > $null
}
##Output
##Bat Code to run above powershell command
#ECHO OFF
SET ThisScriptsDirectory=%~dp0
SET PowerShellScriptPath=%ThisScriptsDirectory%MyPowerShellScript.ps1
PowerShell -NoProfile -ExecutionPolicy Bypass -Command "& {Start-Process PowerShell -ArgumentList '-NoProfile -ExecutionPolicy Bypass -File ""%PowerShellScriptPath%""' -Verb RunAs}";
A bit late, but try this one.
function Set-Files($Path) {
if(Test-Path $Path -PathType Leaf) {
# Do any logic on file
Write-Host $Path
return
}
if(Test-Path $path -PathType Container) {
# Do any logic on folder use exclude on get-childitem
# cycle again
Get-ChildItem -Path $path | foreach { Set-Files -Path $_.FullName }
}
}
# call
Set-Files -Path 'D:\myFolder'
Commenting here as this seems to be the most popular answer on the subject for searching for files whilst excluding certain directories in powershell.
To avoid issues with post filtering of results (i.e. avoiding permission issues etc), I only needed to filter out top level directories and that is all this example is based on, so whilst this example doesn't filter child directory names, it could very easily be made recursive to support this, if you were so inclined.
Quick breakdown of how the snippet works
$folders << Uses Get-Childitem to query the file system and perform folder exclusion
$file << The pattern of the file I am looking for
foreach << Iterates the $folders variable performing a recursive search using the Get-Childitem command
$folders = Get-ChildItem -Path C:\ -Directory -Name -Exclude Folder1,"Folder 2"
$file = "*filenametosearchfor*.extension"
foreach ($folder in $folders) {
Get-Childitem -Path "C:/$folder" -Recurse -Filter $file | ForEach-Object { Write-Output $_.FullName }
}
I'm new to PowerShell and been trying to get this script to work.
If ((Get-Date -UFormat %a) -eq "Mon") {$intSubtract = -3}
Else {$intSubtract = -1}
$datDate = (Get-Date).AddDays($intSubtract)
Write-Output "Find expected file --------------"
$strDate = ($datDate).ToString('yyyyMMdd')
Write-Host "strDate: $strDate"
$arrGetFile = Get-ChildItem -Path "\\Computer\Data\States\NorthDakota\Cities\*_Bismark_$strDate*.txt"
$strLocalFileName = $arrGetFile
If ($arrGetFile.count -ne 2)
{
Throw "No file or more than two files with today's date exists!"
}
Else {$strLocalFileName = $arrGetFile[0].Name}
Write-Output "Found file $strLocalFileName --------------"
#Encrypt each file
foreach ($arrGetFile in $strPath)
{
Write-Output "Start Encrypt --------------"
$strPath = "\\Computer\Data\States\NorthDakota\Cities\"
$FileAndPath = Join-Path $strPath $strLocalFileName
$Recipient = "0xA49B4B5D"
Import-Module \\JAMS\C$\PSM_PGP.psm1
Get-Module Encrypt
Encrypt $FileAndPath $Recipient
$strLocalFileNamePGP = $strLocalFileName + ".pgp"
Write-Output "End Encrypt --------------"
}
#Archive files
Write-Output "Archiving --------------"
move-item -path \\Computer\Data\States\NorthDakota\Cities\*_Bismark_$strDate*.txt -destination \\Computer\Data\States\NorthDakota\Cities\Archive
The Cities folder will contain two files. Example 2015_Bismark_20150626_183121.txt and 2015_Bismark_20150626_183121_Control.txt
I am trying to get both files encrypted however it is only finding and encrypting the file without _Control. It is archiving both files correctly.
Not sure what I am missing to also find the control file.
Your for loop is incorrect. You have foreach ($arrGetFile in $strPath), but $strPath doesn't seem to contain anything at that point.
Your for loop should be:
foreach ($LocalFile in $arrGetFile)
And you need to remove the following line:
$strLocalFileName = $arrGetFile
This is making $strLocalFileName an array of file objects, but later in the script you are treating it like a string. You may have more logical errors--you need to walk through the script very carefully and identify each variable and make sure it contains what you expect it to contain.
In general you seem to be treating arrays of non-string objects as if they are strings. Note that I changed your $strLocalFileName variable to $LocalFile. This is because it is a file object, not a string object.
Following is a sample that just shows that the for loop iterates through the both files.
If ((Get-Date -UFormat %a) -eq "Mon") {$intSubtract = -3}
Else {$intSubtract = -1}
$datDate = (Get-Date).AddDays($intSubtract)
Write-Output "Find expected file --------------"
$strDate = ($datDate).ToString('yyyyMMdd')
Write-Host "strDate: $strDate"
$arrGetFile = Get-ChildItem -Path "\\Computer\Data\States\NorthDakota\Cities\*_Bismark_$strDate*.txt"
If ($arrGetFile.count -ne 2)
{
Throw "No file or more than two files with today's date exists!"
}
Write-Output "Found files " ($arrGetFile | select Name | fl) "--------------"
#Process each file
foreach ($LocalFile in $arrGetFile)
{
$FileAndPath = Join-Path $LocalFile.DirectoryName $LocalFile
$FileAndPath
}
Start with this and then carefully add your encryption processing back into the loop.
Also, The line that assigns $FileAndPath could be removed. You can just use $LocalFile.FullName wherever you need the full path and filename.