Friday, August 15, 2014

PowerShell Single-Line Script for Useful Folder Modification Dates

Making Folder Dates Useful

I want to share an adventure I had after a DropBox rollback reset all of  my DropBox folder's modification dates on my Windows computers (Win 7 and XP). Why would I care about a folder modification dates? I'm glad you asked!

Folder modification dates allow you to view your projects in a useful archaeological manner (newest files and folders on top, oldest items at the bottom). At a glance you can see which projects are receiving attention, and which may have been put into a holding pattern for any number of reasons. You can simulate the reset of the folder dates by making a copy of an existing folder in Windows. The files in the newly created folder will have the correct modification dates, but all of the folders and sub-folders will have the current date for their modification date.

It was frustrating searching for recent projects by name from a list that includes all of my completed projects - (perhaps completed is too strong a word). View by name is even worse because you have to remember the name of the folder first. This applies to folders of photos as well. Did I name the folder "Beach Trip 2012" or was it "2012 Beach Trip" or was is "Myrtle Beach 2012"? If you have an idea of the date of interest, sorting your folders by date gives you a quick way to zoom in on what you are trying to find.

I needed to find a program or script that will scan my DropBox folder, find the newest file anywhere inside that folder or sub folder and then set the folder modification date of the folder to the date of that newest file. And then repeat this process for every sub-folder in the target folder.

Searching for a Solution

I always recommend searching for similar projects already published to avoid reinventing the wheel. In this case, my search turned up nothing close to what I want, so it looked like I was on my own. My first instinct was to try Python (which I recently used to for a quick fix to some Gerber files) but I found little information about using Python to change folder modification dates under Windows. Expanding my search beyond Python led me to this GREAT article about using PowerShell  to modify file times. This article was my introduction to PowerShell, and demonstrated the power of recursion and piping results from one command into the next command (or cmdlet's as they are known in PowerShell).

I found that PowerShell 2.0 was already installed on both my Windows 7 and XP computers. I only had to set the execution policy to remotesigned to allow me to run scripts from the IDE and start coding! Below is a clip from StackExchange with the steps needed to run your own PowerShell scripts. I clicked the Windows Start button and searched for PowerShell and then right-clicked on PowerShell and selected "Run as administrator".

  1. Start Windows PowerShell with the "Run as Administrator" option. Only members of the Administrators group on the computer can change the execution policy.
  2. Enable running unsigned scripts by entering:
    set-executionpolicy remotesigned
    
This will allow running unsigned scripts that you write on your local computer and signed scripts from Internet.
So I studied the PowerShell examples, explored the cmdlet's available, did a bunch of Google searches, and bought a copy of "Windows PowerShell Pocket Reference".

I used code fragments from the internet, and began building the function that I needed. Before long, I had a working script that was less than 10 lines! I was impressed at how little code PowerShell required to implement the desired functionality. 

As you can imagine, my first script was a bit of a kludge, with components thrown-together as I implemented each functional block. Once the script was working I went back and worked on clean up. As I began to understand the PowerShell methodology a little more, I saw places where I had extra steps, or code that could be eliminated. Eventually I optimized the entire function down to a single line of PowerShell script! I was amazed at what PowerShell can do with -recurse and the pipeline.

Notes. The PowerShell IDE  reports an error to the console terminal for each folder processed that contains no files in the folder or sub-folders. This error occurs because the script attempts to set a folder property ($_.LastWriteTime) equal to the last write time property of the null returned if there are no files found. And as you might expect, a null does not have a .LastWriteTime property! The script uses nested recursion so there are a lot of directory commands being issued which can take a while if you have a large collection of folders. A future post will describe how to fix the null last write time issue.

Working Single-Line Script

First things first, backup your files and work on a copy of your target folder until you are very happy with the results! I also recommend keeping an extra backup copy of your folder on another drive. Backups are good!

Below is the single-line script that process a folder and all sub-folders and changes all of the folder modification dates to the date of the most recently modified folder or sub-folder.
  Get-ChildItem "H:\DropBox\FolderDateFix\Test1" -recurse | Where-Object {$_.PsIsContainer} | ForEach-Object {$_.LastWriteTime = ($_ | Get-ChildItem -recurse | Where-Object {!$_.PsIsContainer} | Sort-Object LastWriteTime | Select-Object -last 1).LastWriteTime}

PowerShell allows you to break lines after a pipe without requiring a backtick. So the line below is equivalent to the single line above but a little easier to read in the browser because we started a new line after each pipe.

Get-ChildItem "H:\DropBox\FolderDateFix\Test1" -recurse | 
Where-Object {$_.PsIsContainer} | 
ForEach-Object {
  $_.LastWriteTime = ($_ | 
  Get-ChildItem -recurse | 
  Where-Object {!$_.PsIsContainer} | 
  Sort-Object LastWriteTime | 
  Select-Object -last 1).LastWriteTime



Walk through. 

We start off by using the Get-ChildItem cmdlet and the hard-coded path. You need to change text to reflect the path to your target folder. I used quotes for the path to allow a path with spaces to be used. An alias to Get-ChildItem (gci, ls or dir) could have been used but I wanted to use the native cmdlet names rather than aliases. The -recurse switch causes Get-ChildItem to return all of the items in the target folder and all sub-folders. By default hidden folders are ignored. Add the -force switch after -recurse if you want the script to update the date of hidden folders.

Next the results are piped to the Where-Object cmdlet where filter block {$_.PsIsContainer} tells the cmdlet to produce an array of only the folders. Here we see the first use of $_ which represents the object currently being processed in the pipeline. So now we have a collection that contains only folders.

Next the collection of folders is piped to the ForEach-Object and this is where things get interesting. Again we use $_ to indicate the current object or folder in the pipe but we are modifying the .LastWriteTime by setting the .LastWriteTime of the folder equal to the .LastWriteTime of the object returned from the last four cmdlets. Again, the results from each cmdlet are piped to the next cmdlet.

Next we find the newest file in the current folder or any sub folder. We get this by generating a list of all object  in the folder and sub-folder (using Get-ChildItem -recurse). By default hidden files are ignored. To include hidden files you need to add -force after the -recurse switch. We then select only the files using Where-Object {!$_.PsIsContainer} (is NOT a folder). Then we sort the files by date using Sort-Object LastWriteTime which orders the files from oldest to newest. Finally we select the last (newest) file using Select-Object -last and then the last write time of this file is applied to the current folder being processed in the pipe via  .LastWriteTime property of the file.

Dropping a new folder into a sub-folder will have no impact on the folder dates written by the script. But creating a new text file in a deep sub-directory will results in the script updating the date of the containing folder and every folder above the containing folder all the way up to the target folder.

In the end, I discovered PowerShell's amazing power and flexibility allows me to achieve my goals with a single-line script. I hope you find the script useful and that I have encouraged you to look into using PowerShell yourself.

Coming Soon: Putting a Graphical User Interface on a PowerShell Script

In a future post I will walk you through how I expanded the script to add a progress bar and prompt the user to select a target folder!

Adding a GUI to the PowerShell Script

No comments:

Post a Comment