Write Perl That Survives Beyond Linux
You will finish with a small Perl program that handles paths and text deliberately, records the platform it is running on, and avoids the assumptions that usually make a script Linux-only. The examples follow perlport as installed with Perl 5.38.2 on this machine. Allow about 20 minutes, plus time to run the checks on any additional operating systems you support.
The route
Jump straight to the step you need, or tick off Done means at the end.
You need Perl 5.38 or a compatible Perl installation and a shell. No elevated privileges are required. Work in a temporary directory or a copy of your project: the examples create and remove only files below the directory you choose.
1. Decide what portability means for this script
Portability is a scope decision, not a promise that every operating system behaves identically. If a program is explicitly a Linux service helper, using Linux tools can be reasonable. If it is a reusable command-line utility, define the supported systems before you write platform-specific code. Supporting Linux and Windows needs different decisions from supporting every Perl port.
Check the interpreter and its operating-system identifier:
$ perl -v | sed -n '1,4p'
This is perl 5, version 38, subversion 2 (v5.38.2) built for x86_64-linux-gnu-thread-multi
$ perl -MConfig -e 'printf "perl=%s os=%s arch=%s\n", $^V, $^O, $Config{archname}'
perl=v5.38.2 os=linux arch=x86_64-linux-gnu-thread-multi
$^O is useful for selecting a known platform branch. %Config contains more build information, but perlport warns that it was created at build time and can be wrong if a Perl installation is moved. Do not treat it as a live hardware probe.
2. Build paths with File::Spec
Do not concatenate paths with a literal slash, and do not assume that a path accepted by Perl is also the native syntax required by another program. File::Spec supplies platform-aware operations. This example creates a path from components and prints the result without touching the filesystem:
$ perl -MFile::Spec::Functions=catfile,updir -e '
my $file = catfile(updir(), "reports", "daily.txt");
print "$file\n";
'
../reports/daily.txt
On Windows, the same components can produce a native path using backslashes. Keep user-supplied locations or configuration values as inputs, rather than hardcoding a directory such as /tmp. The manual specifically warns that even familiar Unix files such as /etc/passwd and /etc/resolv.conf are not universal, and that a text file may not end with a newline.
Checkpoint: your path-building code should contain no hand-written separator between variable path components. If an external utility needs a native path, construct it with File::Spec at that boundary.
3. Separate text files from binary data
Perl's logical newline is platform-dependent. Unix normally uses LF, DOS-like text I/O can translate LF and CRLF, and other ports use different representations. For ordinary text, read records and use chomp to remove the logical record ending. Do not use a hardcoded \n as a network protocol delimiter unless the protocol defines that byte.
use strict;
use warnings;
open my $input, '<', $ARGV[0] or die "open $ARGV[0]: $!\n";
while (my $line = <$input>) {
chomp $line;
print "<$line>\n";
}
close $input or die "close $ARGV[0]: $!\n";
For a binary file, open it in binary mode before reading or writing. Do not reuse text-file assumptions for image data, archives, or a custom binary format. Raw integers also have byte-order and width differences between machines. When a binary format is yours to design, text is often simpler. When it is not, use documented pack and unpack formats, such as network-order n and N, or explicit little- and big-endian modifiers where the Perl version supports them.
For an Internet protocol, use the protocol's required CRLF or another specified sequence. The Socket module exports portable constants for this purpose. A socket's local newline is not a substitute for the protocol's wire format.
4. Avoid making a shell part of the API
External commands differ in name, location, arguments and output. Some platforms have no conventional command line, and a command name is not always the same thing as the executable's filename or suffix. A pipe to /usr/lib/sendmail may be acceptable in a known Unix deployment, but it is not a portable mail interface.
Prefer a Perl module with a stable interface. If an external process is genuinely part of the contract, document it and check failures immediately. Do not interpolate untrusted input into a shell command. A list-form call avoids shell parsing in the normal Unix case:
my @command = ('/path/to/tool', '--input', $input_path);
system @command;
if ($? == -1) {
die "could not start tool: $!\n";
}
die "tool failed: " . ($? >> 8) . "\n" if $? >> 8;
This still depends on the named program and its arguments. The list form is not a licence to assume that every operating system implements process creation in the same way. Treat it as a deliberately platform-specific boundary and test that boundary on every supported platform.
5. Treat environment and errors as optional data
Do not assume that a variable exists in %ENV, that its spelling is case-sensitive, or that a particular current directory or glob syntax is available. Use opendir, readdir and closedir when you need directory contents rather than relying on filename globbing.
opendir my $dir, $directory or die "opendir $directory: $!\n";
while (my $name = readdir $dir) {
next if $name eq '.' || $name eq '..';
print "$name\n";
}
closedir $dir or die "closedir $directory: $!\n";
Inspect $! immediately after the failed system call. Its text can be translated, and its numeric value is not a portable message format. Where a POSIX-style environment is justified, Errno provides symbolic names such as ENOENT; otherwise report the operation, path and original error without matching an English sentence.
6. Keep platform-specific code narrow
Sometimes a platform branch is the honest solution. Put it in one small function, use explicit positive matches, and make the final branch a real fallback rather than treating every non-Windows system as Linux:
use strict;
use warnings;
sub platform_name {
return 'windows' if $^O eq 'MSWin32';
return 'linux' if $^O eq 'linux';
return 'other';
}
print platform_name(), "\n";
Keep file handling and most business logic outside this branch. Tests must also be portable: avoid assuming a path separator, a fixed error string, a particular executable, or a specific filesystem permission model. On systems with mandatory file locking, close a file before renaming or deleting it. Do not test permissions and then assume the later operation is safe: permissions can change between the check and the operation.
7. Run a portability checkpoint
Run the script with warnings enabled, then test it on each supported platform or in a continuous-integration job for that platform:
$ perl -c path/to/script.pl
path/to/script.pl syntax OK
$ perl -Mwarnings -Mstrict path/to/script.pl /path/to/sample.txt
For each platform, verify normal text input, an empty file, a filename containing spaces, a missing input, and any external command failure. If the program handles sockets or binary files, add fixtures that assert the specified wire bytes or file format rather than comparing a host-native representation.
There is no need to make every program portable. The useful result is a clear boundary: either document that the tool is Linux-specific, or replace accidental Linux assumptions with portable Perl and tested adapters.
Done means
- The supported operating systems are stated, rather than implied.
- Paths are assembled with
File::Spec, and site-specific files are configurable. - Text, binary data and network delimiters use separate handling.
- External processes are explicit boundaries, with arguments and failures checked.
- Missing environment variables, translated errors and filesystem differences do not become hidden assumptions.
- Platform branches are narrow, and the same test cases have run on every supported system.