<!-- category: hidden-gems -->
In-Memory Filehandles: open to a Scalar
Open a filehandle to a Perl scalar instead of a file:That backslash-scalar (my $buffer = ''; open my $fh, '>', \$buffer or die $!; print $fh "line one\n"; print $fh "line two\n"; close $fh; say $buffer; # line one # line two
\$buffer) is the key. Instead of a filename, you pass a reference to a scalar variable. Perl opens a filehandle that reads from or writes to that variable. Every print appends to the string. Every <$fh> reads from it. seek and tell work. The scalar is your file.
No temp files. No disk I/O. No cleanup. Just a string pretending to be a file, and doing a convincing job of it.
Part 1: WRITING TO STRINGS
The most common use: capture output in a variable.Output:my $report = ''; open my $fh, '>', \$report or die "Cannot open string: $!\n"; printf $fh "%-20s %8s %8s\n", "SERVER", "CPU", "MEMORY"; printf $fh "%-20s %7d%% %7d%%\n", "web01", 45, 78; printf $fh "%-20s %7d%% %7d%%\n", "web02", 62, 55; printf $fh "%-20s %7d%% %7d%%\n", "db01", 12, 91; close $fh; # $report now contains the formatted table print $report;
Why not just concatenate strings? BecauseSERVER CPU MEMORY web01 45% 78% web02 62% 55% db01 12% 91%
printf formatting is cleaner than string interpolation for tabular data. And because some code expects a filehandle. In-memory filehandles let you use print and printf to build strings without changing the API.
Part 2: READING FROM STRINGS
Open for reading with<:
Output:my $csv_data = <<'END'; name,age,city Alice,30,Toronto Bob,25,Montreal Carol,35,Vancouver END open my $fh, '<', \$csv_data or die $!; my $header = <$fh>; # read first line chomp $header; my @columns = split m~,~, $header; while (<$fh>) { chomp; my %row; @row{@columns} = split m~,~; say "$row{name} lives in $row{city}"; } close $fh;
The string is read line by line, exactly like a file.Alice lives in Toronto Bob lives in Montreal Carol lives in Vancouver
$. increments. EOF is detected when the string is exhausted. Everything you'd do with a file works on a string.
Part 3: SEEKING WITHIN STRINGS
Yes,seek and tell work on in-memory filehandles:
You can jump forward, backward, to the beginning, to the end. The scalar is random-access, just like a file on disk. This makes in-memory filehandles useful for parsers that need to backtrack.use Fcntl qw(:seek); my $data = "AAAA\nBBBB\nCCCC\nDDDD\n"; open my $fh, '<', \$data or die $!; my $first = <$fh>; # "AAAA\n" my $pos = tell($fh); # byte offset after first line say "Position after first line: $pos"; # 5 my $second = <$fh>; # "BBBB\n" say "Second line: $second"; seek($fh, $pos, SEEK_SET); # go back to start of second line my $reread = <$fh>; # "BBBB\n" again say "Re-read: $reread"; seek($fh, 0, SEEK_SET); # back to the very beginning my $from_top = <$fh>; # "AAAA\n" say "From top: $from_top";
Part 4: CAPTURING OUTPUT
Here's the killer use case. You have a function that prints to STDOUT. You want its output in a variable. Redirect STDOUT to a string:Thesub noisy_function { print "Starting process...\n"; print "Step 1: Loading data\n"; print "Step 2: Crunching numbers\n"; print "Result: 42\n"; } # Capture everything it prints my $captured; { open my $fh, '>', \$captured or die $!; local *STDOUT = $fh; noisy_function(); } # STDOUT is restored here (local undoes at end of block) say "I captured:\n$captured";
local *STDOUT = $fh temporarily replaces STDOUT with your in-memory filehandle. Everything printed inside that block goes to $captured. When the block ends, local restores the original STDOUT. Clean. Reversible. No global damage.
This is invaluable for testing. You can capture what a function prints, then assert on the content:
# test that the function outputs what we expect { my $output; open my $fh, '>', \$output or die $!; local *STDOUT = $fh; generate_report(@test_data); close $fh; die "Missing header!" unless $output =~ m~^SERVER~m; die "Missing web01!" unless $output =~ m~web01~; say "All checks passed."; }
Part 5: TESTING I/O CODE
You wrote a function that reads from a filehandle. You want to test it without creating files:Output:sub parse_config { my ($fh) = @_; my %config; while (<$fh>) { chomp; next if m~^\s*#~; next if m~^\s*$~; if (m~^(\w+)\s*=\s*(.+)$~) { $config{$1} = $2; } } return %config; } # Test it without a file my $test_input = <<'END'; # database config host = localhost port = 5432 name = mydb # optional timeout = 30 END open my $fh, '<', \$test_input or die $!; my %cfg = parse_config($fh); close $fh; say "$_: $cfg{$_}" for sort keys %cfg;
The function doesn't know or care that it's reading from a string. It gets a filehandle, it reads lines, it parses. In production, pass a real file. In tests, pass a string. The function stays the same.host: localhost name: mydb port: 5432 timeout: 30
This is dependency injection at its simplest. No mocking frameworks. No test doubles. Just a scalar reference pretending to be a file.
Part 6: BUILDING STRINGS WITH PRINT
Sometimesprint and printf are more natural than string concatenation, especially for complex formatting:
Output:my $html = ''; open my $fh, '>', \$html or die $!; print $fh "<table>\n"; print $fh " <tr><th>Name</th><th>Score</th></tr>\n"; my %scores = (Alice => 95, Bob => 87, Carol => 92); for my $name (sort keys %scores) { printf $fh " <tr><td>%s</td><td>%d</td></tr>\n", $name, $scores{$name}; } print $fh "</table>\n"; close $fh; print $html;
Compare to the concatenation version:<table> <tr><th>Name</th><th>Score</th></tr> <tr><td>Alice</td><td>95</td></tr> <tr><td>Bob</td><td>87</td></tr> <tr><td>Carol</td><td>92</td></tr> </table>
Themy $html = "<table>\n"; $html .= " <tr><th>Name</th><th>Score</th></tr>\n"; for my $name (sort keys %scores) { $html .= sprintf(" <tr><td>%s</td><td>%d</td></tr>\n", $name, $scores{$name}); } $html .= "</table>\n";
print-to-string version is arguably cleaner. No .= operators. No sprintf (just printf directly). And if you're generating a lot of output, the in-memory filehandle handles buffering for you.
Part 7: PERLIO LAYERS
In-memory filehandles support PerlIO layers, just like regular filehandles:The# UTF-8 encoding my $utf8_text = ''; open my $fh, '>:encoding(UTF-8)', \$utf8_text or die $!; print $fh "Hello, \x{263A}!\n"; # smiley face close $fh; # Binary mode my $binary = ''; open my $fh2, '>:raw', \$binary or die $!; print $fh2 pack('N', 0xDEADBEEF); close $fh2; say length($binary); # 4 bytes
:encoding(UTF-8) layer ensures proper encoding when writing Unicode characters. The :raw layer strips all text transformations for binary data.
This is useful when you're building content in memory that will eventually be written to a file with a specific encoding. Build it with the right layer from the start, and the encoding is handled consistently.
Part 8: MODULES THAT EXPECT FILEHANDLES
Some modules only accept filehandles. They were written before everyone agreed that strings are valid input. In-memory filehandles bridge the gap:The function doesn't care where the data comes from. File on disk, HTTP response body in a variable, hardcoded test string. An in-memory filehandle makes them all look the same.use JSON; # Suppose you have a function that reads JSON from a filehandle sub load_json { my ($fh) = @_; local $/; my $text = <$fh>; return decode_json($text); } # In production: open a real file # open my $fh, '<', 'config.json' or die $!; # In tests: use a string my $json_str = '{"host": "localhost", "port": 8080, "debug": true}'; open my $fh, '<', \$json_str or die $!; my $config = load_json($fh); say "Host: $config->{host}"; say "Port: $config->{port}";
CSV parsers, XML readers, log processors, anything that takes a filehandle. Feed it a string reference and it works.
Part 9: IO::STRING COMPARISON
Before Perl 5.8, in-memory filehandles didn't exist as a core feature. People usedIO::String from CPAN:
use IO::String; my $buffer = ''; my $fh = IO::String->new($buffer); print $fh "hello\n"; print $fh "world\n"; say $buffer;
IO::String still works and some legacy code uses it. But since Perl 5.8 (released in 2002), the core open my $fh, '>', \$scalar syntax does everything IO::String does, without any CPAN dependency.
If you seeFEATURE open \$scalar IO::String ------- ------------- ---------- Core Perl yes (5.8+) no (CPAN) Read mode yes yes Write mode yes yes Append mode yes yes seek/tell yes yes PerlIO layers yes no Speed native wrapper overhead 2026 recommendation use this legacy only
IO::String in old code, you can safely replace it with open my $fh, '>', \$var. Same behavior, no dependency, slightly faster.
Part 10: APPEND MODE AND READ-WRITE
In-memory filehandles support append mode and read-write mode:Read-write mode:# Append mode: adds to existing content my $log = "existing content\n"; open my $fh, '>>', \$log or die $!; print $fh "appended line\n"; close $fh; say $log; # existing content # appended line
Theuse Fcntl qw(:seek); my $data = "AAABBBCCC"; open my $fh, '+<', \$data or die $!; # Read first 3 chars read($fh, my $chunk, 3); say "Read: $chunk"; # AAA # Overwrite next 3 chars print $fh "XXX"; # Check the result say "Data is now: $data"; # AAAXXXCCC close $fh;
+< mode opens for both reading and writing. You can read some bytes, then overwrite others, just like with a real file. The scalar grows or shrinks as needed.
This is useful for building binary protocols in memory, or for any situation where you need random-access read-write on a buffer.
In-memory filehandles are one of those features that seems niche until you need it. Then you need it everywhere. Testing code that prints. Capturing output from functions you can't modify. Feeding strings to modules that only speak filehandle. Building formatted content withopen $fh, '>', \$str write (truncate) open $fh, '>>', \$str append open $fh, '<', \$str read only open $fh, '+<', \$str read + write open $fh, '+>', \$str read + write (truncate first) +----------+ | $scalar | <- your "file" lives here +----------+ ^ | | v +----------+ | $fh | <- filehandle points to the scalar +----------+ ^ | | v print, read, seek, tell, eof, close ... all work as expected .--. |o_o | "A string is just a file |:_/ | that never touches the disk." // \ \ (| | ) /'\_ _/`\ \___)=(___/
printf instead of concatenation.
The syntax is simple: open $fh, '>', \$scalar. The backslash-reference is the signal. Everything after that is just normal I/O. Perl doesn't care whether the bytes come from a disk platter or a scalar variable. A filehandle is a filehandle.
Use real files for real files. Use strings for everything else.
perl.gg