Perl Scripts ·
Perl Files and Folders: Create, Zip, CSV and Downloads
By Manu Mathew · Last updated 4 October 2026
A lot of business Perl code moves files about: it creates a folder for this month’s exports, writes a CSV from a database query, zips it up, and offers it as a download. This page brings together several short notes I wrote between 2013 and 2014. I have rewritten them in one place, with tested examples and current advice, so you don’t have to piece the answer together from fragments.
Quick answer
Create folders with mkdir, or make_path from File::Path for nested folders, and test them with -d, -e and -f. Compress with IO::Compress::Gzip or IO::Compress::Zip. Write CSV with Text::CSV so quoting is handled for you, Excel files with Excel::Writer::XLSX, and send downloads with a Content-Disposition: attachment header.
I tested each example with Perl 5.40, using the module versions packaged in Debian 13.
How do I create a folder in Perl?
Use the built-in mkdir for one folder, and make_path from File::Path (a core module) when parent folders may be missing.
use strict;
use warnings;
use File::Path qw(make_path);
mkdir 'reports' or die "Cannot create reports: $!" unless -d 'reports';
# Create nested folders in one call (like mkdir -p)
make_path('exports/2026/10');
print "Folders ready\n";
Always check the result of mkdir: it returns false and sets $! if the folder already exists or you lack permission.
How do I check whether a folder or file exists?
-d is true for a folder, -f for a plain file, and -e for anything at that path.
use strict;
use warnings;
my $path = 'reports';
if (-d $path) {
print "$path is a folder\n";
}
elsif (-e $path) {
print "$path exists but is not a folder\n";
}
else {
print "$path does not exist\n";
}
print "notes.txt is a plain file\n" if -f 'notes.txt';
Testing first and then creating has a small race if two processes run at once, so for important folders it is simpler to call make_path and let it do nothing if the folder is already there.
How do I gzip or zip files in Perl?
IO::Compress::Gzip handles a single file and IO::Compress::Zip handles several. Both have shipped with Perl since 5.10.
use strict;
use warnings;
use IO::Compress::Gzip qw(gzip $GzipError);
gzip 'report.csv' => 'report.csv.gz'
or die "gzip failed: $GzipError\n";
print "Wrote report.csv.gz\n";
use strict;
use warnings;
use IO::Compress::Zip qw(zip $ZipError);
# Several files in one archive, stored without their folder paths
my @files = ('exports/2026/10/a.csv', 'exports/2026/10/b.csv');
zip \@files => 'exports.zip', FilterName => sub { s{.*/}{} }
or die "zip failed: $ZipError\n";
print "Wrote exports.zip\n";
The FilterName option strips the folder part of each name, so the archive holds a.csv and b.csv rather than the full paths. On the command line, zip -j exports.zip exports/2026/10/*.csv does the same job, but the Perl version avoids building a shell command from file names.
How do I export a database query to CSV?
Use Text::CSV with a placeholder query, and let the module quote commas, quotes and line breaks.
use strict;
use warnings;
use DBI;
use Text::CSV;
my $dbh = DBI->connect('dbi:SQLite:dbname=shop.db', '', '', { RaiseError => 1 });
my $sth = $dbh->prepare('SELECT id, name, notes FROM customers WHERE city = ?');
$sth->execute('Kozhikode');
my $csv = Text::CSV->new({ binary => 1, eol => "\n" });
open my $out, '>:encoding(UTF-8)', 'customers.csv' or die "customers.csv: $!";
$csv->print($out, $sth->{NAME}); # header row from the column names
while (my $row = $sth->fetchrow_arrayref) {
$csv->print($out, $row); # quotes commas, quotes and newlines for you
}
close $out;
print "Wrote customers.csv\n";
id,name,notes
1,Asha,"Prefers ""email"", not calls"
2,Ravi,"line1
line2"
The example uses SQLite so it runs anywhere; with MySQL or MariaDB only the DSN changes. Two habits from the older note are worth dropping. Building SQL from $q->param('table') invites SQL injection: use placeholders for values, and check table or column names against a fixed list. Hand-made quoting also tends to break on a value that contains a line break.
How do I create an Excel file in Perl?
Use Excel::Writer::XLSX, which writes the modern .xlsx format.
use strict;
use warnings;
use Excel::Writer::XLSX;
my $workbook = Excel::Writer::XLSX->new('report.xlsx') or die "Cannot create report.xlsx: $!";
my $worksheet = $workbook->add_worksheet('Summary');
my $bold = $workbook->add_format(bold => 1);
$worksheet->write_row(0, 0, ['Month', 'Orders'], $bold);
$worksheet->write_row(1, 0, ['August', 42]);
$worksheet->write_row(2, 0, ['September', 57]);
$workbook->close;
print "Wrote report.xlsx\n";
Spreadsheet::WriteExcel, which the older note used, writes the old binary .xls format and is no longer actively developed; its author points new work to Excel::Writer::XLSX, which has almost the same interface.
How do I send a file as a download from a Perl CGI script?
Print a header with the right content type and an attachment file name, then copy the file to STDOUT in binary mode.
#!/usr/bin/perl
use strict;
use warnings;
use CGI;
use File::Basename qw(basename);
my $q = CGI->new;
my $dir = '/srv/reports';
my $name = basename($q->param('file') // ''); # strip any path the visitor sends
if ($name !~ /\A[\w.-]+\.csv\z/ or !-f "$dir/$name") {
print $q->header(-status => '404 Not Found', -type => 'text/plain'), "No such report\n";
exit;
}
open my $in, '<:raw', "$dir/$name" or die "Cannot open $name: $!";
print $q->header(
-type => 'text/csv',
-charset => 'UTF-8',
-attachment => $name, # the name the browser saves it as
-Content_length => -s "$dir/$name",
);
binmode STDOUT;
while (read($in, my $buffer, 64 * 1024)) {
print $buffer;
}
close $in;
The header must be printed before any of the file. The script never trusts the requested name: basename drops any path, the pattern allows only plain .csv names, and the file must exist in the reports folder. A request for ../../etc/passwd gets a 404. If you generate a temporary file just for the download, delete it after sending, not before.
If you need help with an existing Perl codebase, see Perl development and maintenance.
Sources: perlfunc: mkdir; perlfunc: file test operators; File::Path; IO::Compress::Zip; Text::CSV; Excel::Writer::XLSX; CGI.pm: creating a standard HTTP header.