Home / Alt manpages / perlport(1)

  • perlport(1)
  • User command
  • linux

Write Perl That Survives Beyond Linux

A script full of hardcoded slashes and English error strings works fine until someone runs it on Windows, which is the whole reason perlport exists. You will build a small Perl program that handles paths and text deliberately, records the platform it runs 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 extra operating systems you support.

You need Perl 5.38 or a compatible installation and a shell. No elevated privileges 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 entirely reasonable. If it is a reusable command-line utility, define the supported systems before you write a single line of platform-specific code. Supporting Linux and Windows is a different decision from supporting every Perl port that has ever existed.

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 what you want for selecting a known platform branch. %Config holds more build information, but perlport warns it was captured at build time and can be wrong if a Perl installation gets moved afterwards, so 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 a path accepted by Perl is also the native syntax another program requires. File::Spec supplies platform-aware path operations. This example builds 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 with backslashes instead. 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 even 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 right 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 between LF and CRLF, and other ports use different representations again. For ordinary text, read records and use chomp to strip the logical record ending. Never use a hardcoded \n as a network protocol delimiter unless the protocol actually 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 your Perl version supports them.

For an Internet protocol, use the protocol's required CRLF or whatever sequence it specifies. The Socket module exports portable constants for exactly this. 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 between platforms. Some have no conventional command line at all, 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 fine 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. Never 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 exact arguments. The list form is not a licence to assume every operating system implements process creation the same way. Treat it as a deliberately platform-specific boundary, and test that boundary on every platform you support.

5. Treat environment and errors as optional data

Do not assume a variable exists in %ENV, that its spelling is case-sensitive, or that a particular current directory or glob syntax is even available. Use opendir, readdir and closedir when you need directory contents, rather than leaning 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 gives you symbolic names such as ENOENT; otherwise report the operation, path and original error rather than pattern-matching an English sentence.

6. Keep platform-specific code narrow

Sometimes a platform branch really is the honest solution. Put it in one small function, use explicit positive matches, and make the final branch a real fallback rather than quietly 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 be portable too: 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 in the gap 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 against 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 the accidental Linux assumptions with portable Perl and tested adapters.

Done means

  • Scope stated: the supported operating systems are stated, not implied.
  • Paths assembled properly: paths use File::Spec, and site-specific files are configurable.
  • Data kept separate: text, binary data and network delimiters use separate handling.
  • Processes bounded: external processes are explicit boundaries, with arguments and failures checked.
  • Assumptions surfaced: missing environment variables, translated errors and filesystem differences are not hidden assumptions.
  • Branches tested everywhere: platform branches are narrow, and the same test cases have run on every supported system.