Windows Shell to Perl Program

Have you ever whipped up a quick shell script that later you wished was a real program? We did an experimental workshop at MadMongers on Tuesday where we, as a group, roughed out a Perl program from a Windows batch file. The whole process worked out rather well and we plan to try it again with another topic, perhaps something like web scraping or doing something with a web service. Here are the brief notes (also available in MadMongers files):

We have a windows script that we want to convert to a real programming language for the following reasons:

* Cross platform availability
* Code readability
* Flexibility in parsing, templating, and storage of data
* To learn a real programming language

The existing script looks like this:

del c:\Logs\*.txt

dir d: | find “free” > c:\Logs\d.txt & for %G in (bak, trn) do forfiles -p “D:\Program Files\SQL Server\MSSQL\BACKUP” -m *.%G -s -c “cmd /c dir @file /n” | find “2013” > c:\Logs\%G.txt

cd C:\Logs & copy /b c:d.txt+c:bak.txt+c:trn.txt c:report.txt

c:\Tools\sendEmail -f -t -u “[clientName] SQL02 bak and TRN report” -m “attached” -a c:\Logs\report.txt -s -o tls=yes -xu rmccubbins.tester -xp oldPassword


The steps we need it to perform are:

1) Delete old report log files.
2) Find the free space on the database partition.
3) Gather a list of all the database backup files.
4) Delete the database backup files older than 3 days.
5) Generate a report that includes free space, a list of all the files, and the files that were deleted.
6) Store the report into the logs folder.
7) Email the report.


How could we solve all the criteria.

1) opendir and unlink; File::Find; File::Path::remove_tree()
2) Win32::DriveInfo; Filesys::DfPortable
3) opendir and readdir; File::Find; File::Find::Rule
4) File::Find::Rule::Age; stat()
5) Template Toolkit; Mustache; sprintf; Email::MIME::Kit
6) open()
7) Email::Sender; Net::SMTP; Email::Stuffer

Getting Set Up

grabbed notepad++
installed strawberry perl [latest x64]
used cpan to install cpanminus
cpan install App::cpanminus
searched for modules on
used cpanm to install modules

Solution We Workshoped

use 5.012;
use File::Path qw(remove_tree make_path);
use Filesys::DfPortable;
use File::Find::Rule;
use File::Find::Rule::Age;
use Email::Stuffer;

# globals
my $logs = ‘/path/to/logs’;
my $partition = ‘C:\’;
my $databases = ‘/path/to/databases’;


my $freespace = find_freespace($partition);
my @all_files = find_all_files($databases);
my @old_files = find_old_files($databases);
unlink @old_files;
my $report = generate_report($freespace, \@all_files, \@old_files);

# functions

sub write_report {
  my $report = shift;
  open my $file, “>”, $logs.’/report.’.time().’.txt’;
  print {$file} $report;
  close $file;

sub generate_report {
  my ($freespace, $all, $deleted) = @_;
  my $report = sprintf(‘Freespace:\t\t%s\n\n’, $freespace);
  foreach my $file (@{$all}) {
    $report .= $file .($file ~~ $deleted ? ’ *’ : ”).”\n”;
  $report .”\n\n* Deleted”;
  return $report;

sub email_report {
  my $report = shift;
  Email::Stuffer->from(‘’ )
    ->to (‘’ )
    ->bcc (‘’ )
    ->text_body($report )

sub find_old_files {
  my $path = shift;
  return File::Find::Rule->file()
    ->name( ‘*.trn’, ‘*.bak’ )
    ->age(older => ‘3D’)
    ->in( $path );

sub find_all_files {
  my $path = shift;
  return File::Find::Rule->file()
     ->name( ‘*.trn’, ‘*.bak’ )
     ->in( $path );

sub clear_old_logs {
   remove_tree($logs) and make_path($logs);

sub find_freespace {
   my $partition = shift;
   my $ref = dfportable($partition, 1024 * 1024);
   return $ref->{bfree};

NOTE: We just did this as an exercise. This code may or may not actually work.

If We Had More Time

Write tests.

Use an actual templating language for the report.

Add documentation (comments / POD).

Include File::stat information in the report so you could see file sizes and dates and such. Something like this perhaps:

use DateTime;
use File::stat;
my $stats = stat($path_to_file);
my $date = DateTime->from_epoch(epoch=>$stats->mtime, time_zone=>’America/Chicago’);

[From my blog.]


One should also mention the cons: a raw Windows script can be installed by just copying the script. A Perl script on Windows first requires to install Perl and its modules.

You want to take a look at Path::Tiny also. You could then write e.g.

sub write_report {
  path( $logs, sprintf 'report.%s.txt', time() )->spew( @_ );

It also has built-in methods for File::Path tasks.

In the same vein I recommend Path::Iterator::Rule rather than other file finders. Compared to File::Find::Rule the major advantage is that you can get an actual iterator. Here you get to avoid the File::Find::Rule::Age dependency by just calling ->modified(">3") (I think). (Its POD has some stuff on comparisons with other file finders.)

All in all you’d get to drop a good deal of the upfront boilerplate.

Leave a comment

About JT Smith

user-pic My little part in the greater Perl world.