The 2013 Scripting Games, Beginner Event #1 follow-up

It’s been a while since I blogged and i have the skeletons of multiple blogs post waiting to be edited. But now that I’ve settled into my new job and the scripting games have died down I can finally start posting my notes and learning points from the rest of the scripting games events. So here is the 1st in a series of follow-ups on the scripting games. Each entry the following:

  • The original question
  • My submitted answer
  • A revised answer after reviewing other entries and judges notes
  • A summary of learning points I took away from the event

Question 1

Dr. Scripto is in a tizzy! It seems that someone has allowed a series of application log
files to pile up for around two years, and they’re starting to put the pinch on free
disk space on a server. Your job is to help get the old files off to a new location.
The log files are located in C:\Application\Log. There are three applications that
write logs here, and each uses its own subfolder. For example,
C:\Application\Log\App 1, C:\Application\Log\OtherApp, and
C :\Application\Log\ThisAppAlso. Within those subfolders, the filenames are
random GUIDs with a .LOG filename extension. Once created on disk, the files are
never touched again by the applications.
Your goal is to grab all of the files older than 90 days and move them to
\\NASServer\Archives. You need to maintain the subfolder structure, so that files
from C:\Application\Log\Appl get moved to \\NASServer\Archives\Appi, and so
forth.
You want to ensure that any errors that happen during the move are clearly
displayed to whoever is running your command. You also want your command to be
as concise as possible — Dr. Scripto says a one-liner would be awesome, if you can
pull it off, but it’s not mandatory that your command be that concise. It’s also okay to
use full command and parameter names. If no errors occur, your commaid doesn’t
need to display any output — “no news is good news.”

My Answer

Get-ChildItem -Path "C:\Application\Log" -Recurse -Filter *.log | Where-object {$_.CreationTime -le (get-date).AddDays(-90)} | Select Name,Directory,FullName  | ForEach-Object {Move-Item $_.FullName -Destination ("\\NASServer\Archives\"+($_.Directory.Name)+"\"+$_.Name)}

After reviewing other entries my revised entry is

Get-ChildItem -Path "C:\Application\Log" -Recurse -Filter "*.log" | Where-object {$_.CreationTime -le (get-date).AddDays(-90)} | ForEach-Object {Move-Item $_.FullName -Destination ("\\NASServer\Archives\"+$_.Directory.Name)}

Removed:

  • Redundant select statement, for some reason when I 1st did the script I thought this was the only way to get the Directory Name info
  • Didn’t need the extra $_.Name portion since I’m coming to a directory not an actual file
  • I didn’t incase *.log in quotations so it will find *.log123 etc. in addition to *.log

Learning Points

Think before using -recurse (Ann Hershel)

If you are working with a deep folder structure you can use wildcards to specify a at just a few levels, e.g.

Get-ChildItem -Path C:\Application\Log\*\*.log

Foreach() versus ForEach-Object (Ann Hershel)

foreach() is faster but it requires the data to be collected in memory first, so large sets can chew up a lot of memory. If the collection gathering fails then the whole command fails. Foreach-Object processes objects as they appear, so it uses less memory and is potentially better for large data sets

Create directories while moving them

You can make a check for the destination folders and create them using New-item If they weren’t present. Using New-Item alone will create the destination files and directories but will not remove them from the source. But you can get around this with a simple if statement

if (-not(Test-Path $ArchiveDirectory)) {New-Item $ArchiveDirectory -ItemType Directory | Out-Null} Move-Item $file.FullName $ArchiveDirectory }

Honor Verb Hyphen Noun when creating functions (Bartek Bielawski)

Use approved verbs, you should rarely deviate from this. And your nouns should not be plural.

Don’t use Boolean if you don’t need to (Bartek Bielawski)

You can test for a value by using if ($Value) and the opposite by if (-not $Value)

Don’t repeat calculations in your pipeline (June Blender)

For example the following will do the math for Get-Date for every file passed down the pipeline

Get-ChildItem C:\Application\Log\*\*.log | 
 Where-Object {$_.LastWriteTime -lt (Get-Date).AddDays(-90)} | 
 Move-Item -Destination ...

It would be better do it once, save it as a variable then and re-use it

$ArchiveDate = (Get-Date).AddDays(-90)
Get-ChildItem C:\Application\Log\*\*.log | 
 Where-Object {$_.LastWriteTime -lt $ArchiveDate} | 
 Move-Item -Destination ...

Another issue with doing the calculation every time an object is passed down the pipeline is that the date is changing each time. What if you ran this command at 11:59PM? The files that hit the pipeline at 12:00AM are being compared using a different date.

Validate your parameters before running the script (Glenn Sizemore)

Example

Param ( 
    [Parameter(Mandatory=$true, ValuefrompipelineByPropertyName=$true)]
  [ValidateScript({Test-Path $_ -PathType Container})] 
    [Alias("FullName")]
  [string]$Source,

 [Parameter(Mandatory=$true, ValuefrompipelineByPropertyName=$true)]
    [ValidateScript({Test-Path $_ -PathType Container})] 
  [Alias("FullName")]
    [string]$Destination
}

Don’t make you Parameter names all start with the same letter (Glenn Sizemore)

It makes tab completion harder than it needs to be!

How to correctly use Pipeline input for your functions (Boe Prox)

If you have a parameter that accepts pipeline input then you need to have the work performed on the pipeline input in a process block, otherwise it will only execute on the last object piped in. For example, take this function:

Function Test-Something { 
 [cmdletbinding()] 
 Param ( 
  [parameter(ValueFromPipeLine=$True)] 
  [string[]]$Computername
 ) 
 ForEach ($Computer in $Computername) { 
  $Computer
 }
}

If the you try the following:

1,2,3,4,5 | Test-Something

You will only get the following back

5

If you add a process block, like so:

Function Test-Something { 
 [cmdletbinding()] 
 Param ( 
  [parameter(ValueFromPipeLine=$True)] 
  [string[]]$Computername
 ) 
 Process {
  ForEach ($Computer in $Computername) { 
   $Computer
  }
 }
}

And rerun the command you will get each item output instead of the last item in the pipeline.

So over all use the Begin{} to initialize anything that you need to run only once prior to the Process{} block (e.g. make a SQL connection). The Process{} block will then execute for each piped in object. Finally the End{} block will execute any one time work that needs to be done after working with the piped in results (e.g. closing a SQL connection)

About mell9185

IT proffesional. Tech, video game, anime, and punk aficionado.
This entry was posted in PowerShell, Scripting Games, Uncategorized. Bookmark the permalink.

Leave a Reply