perl.gg / hidden-gems

<!-- category: hidden-gems -->

$\ and $, - Invisible Modifiers That Change print

2026-05-01

You are reading someone else's code. Every print statement outputs a newline at the end. But none of them say \n. There is no say. No explicit newline anywhere.

Somewhere, hidden at the top of the file, there is this:

$\ = "\n";
And now every print in the entire program silently appends a newline. You just stared at the output for twenty minutes wondering where the newlines were coming from. Two characters in a dark corner of the file changed the behavior of a builtin.

Welcome to Perl's output separator variables. They are powerful, invisible, and absolutely maddening when you do not know they exist.

Part 1: $\ THE OUTPUT RECORD SEPARATOR

The special variable $\ (also known as $OUTPUT_RECORD_SEPARATOR if you use English) is appended to the end of every print statement. By default, it is undef, which means print appends nothing.
$\ = "\n"; print "hello"; # outputs: hello\n print "world"; # outputs: world\n print "done"; # outputs: done\n
You just turned print into say. Every single print in your program now adds a newline, whether you asked for one or not.

Set it to something else and things get interesting:

$\ = "\n---\n"; print "section one"; print "section two";
section one --- section two ---
Every print gets a section divider appended. No changes to any print statement. The separator is invisible at the call site.

Part 2: $, THE OUTPUT FIELD SEPARATOR

The special variable $, (also known as $OUTPUT_FIELD_SEPARATOR) is inserted between the arguments of print. By default, it is undef, so arguments are concatenated with nothing between them.
print "a", "b", "c"; # outputs: abc
Set $, and you get separation:
$, = " | "; print "a", "b", "c"; # outputs: a | b | c
Every comma-separated argument to print now gets " | " inserted between them. Print just became a join-and-output function.
$, = ","; print "name", "age", "city"; # outputs: name,age,city
Instant CSV. Sort of. (Real CSV needs quoting. But for quick output, this is faster than building strings.)

Part 3: COMBINING BOTH

Use them together for formatted output:
$, = "\t"; # tab between fields $\ = "\n"; # newline after each print print "Name", "Age", "City"; print "Alice", 30, "Toronto"; print "Bob", 25, "Montreal";
Name Age City Alice 30 Toronto Bob 25 Montreal
Tab-separated output with automatic newlines. Every print is now a row emitter. Clean. Concise. And completely invisible to anyone reading just the print statements.

This pattern is popular in one-liners:

$ perl -lane '$, = "\t"; $\ = "\n"; print @F[0,2,4]' data.txt
Extract columns 0, 2, and 4 from whitespace-delimited data, output as tab-separated. The $, and $\ variables do all the formatting.

Part 4: TURNING PRINT INTO SAY

The most common use of $\ is the newline trick:
$\ = "\n";
This predates say by decades. Before Perl 5.10 added say in 2007, this was how you avoided typing \n at the end of every print statement. Old Perl code is full of it.
# Pre-5.10 code $\ = "\n"; print "Starting process"; print "Step 1 complete"; print "Step 2 complete"; print "Done";
Modern code should just use say. It does the same thing without the invisible global state. But understanding $\ matters because you will encounter it in legacy code, and because it can do things say cannot. Like appending something other than a newline.
$\ = " [END]\n"; print "First record"; print "Second record";
First record [END] Second record [END]
say always appends \n. $\ appends whatever you want.

Part 5: THE CSV TRICK

Quick and dirty CSV output using $,:
#!/usr/bin/env perl use strict; use warnings; $, = ","; $\ = "\n"; print "id", "name", "score"; my @students = ( [1, "Alice", 95], [2, "Bob", 87], [3, "Carol", 92], ); for my $s (@students) { print @$s; }
id,name,score 1,Alice,95 2,Bob,87 3,Carol,92
Each print @$s automatically comma-separates the array elements and appends a newline. No join, no string building, no explicit formatting. The variables handle it.

Is this real CSV? No. Real CSV needs to handle commas inside fields, quoting, and escaping. Use Text::CSV for that. But for quick data dumps where you control the content, this is fast and clean.

Part 6: SCOPING WITH LOCAL

Here is the critical safety tip. $\ and $, are global variables. Setting them affects every print in your program. Every module you call. Every library function that uses print. Everything.
$, = "|"; $\ = "\n"; print "a", "b"; # a|b\n (expected) some_library_function(); # surprise! its print statements # now use | separators too
The fix is local:
{ local $, = "|"; local $\ = "\n"; print "a", "b"; # a|b\n } # $, and $\ are restored to their previous values here print "a", "b"; # ab (no separator, no newline)
The local keyword dynamically scopes the variable. Inside the block, the new value is in effect. Outside, the old value is restored. Even if the block calls other functions, those functions see the localized value. But when the block ends, everything goes back to normal.

Always use local with these variables. Setting them globally is asking for bizarre bugs in any non-trivial program.

Part 7: THE DEBUGGING NIGHTMARE

Someone sets $\ at the top of a module. You use that module. Now your print statements append things you did not ask for.
package WeirdModule; $\ = "!!!\n"; # global side effect # ... rest of module ... 1;
use WeirdModule; print "hello"; # outputs: hello!!!\n
You did not set $\. You did not ask for exclamation marks. But there they are, because a module you imported set a global variable as a side effect. This is why responsible Perl code localizes these variables and why code reviews should flag any bare $\ = ... outside a local block.

Debugging this is miserable. The symptom (weird output) is far from the cause (a global variable set in another file). If you ever see mysterious extra characters in your output, check $\ first:

# diagnostic use Data::Dumper; say Dumper($\); # see what $\ is set to say Dumper($,); # and $,

Part 8: USE IN ONE-LINERS

These variables truly shine in command-line one-liners where brevity is everything:
# Print every line with a line number $ perl -ne '$\ = "\n"; print "$. $_"' file.txt # Join fields with pipes $ perl -ane '$, = " | "; $\ = "\n"; print @F' data.txt # TSV to CSV conversion $ perl -F'\t' -ane '$, = ","; $\ = "\n"; print @F' input.tsv > output.csv
In a one-liner, you do not care about global state. The program starts, runs, and dies. No modules are affected. No long-running process suffers side effects. The global nature of $, and $\ is a feature here, not a bug.

The -l flag is related. It sets $\ to the input record separator (usually \n) automatically:

$ perl -lne 'print "PREFIX: $_"' file.txt
The -l flag is doing $\ = "\n" behind the scenes. It also chomps each input line. A tiny flag doing two invisible things.

Part 9: THE ENGLISH MODULE NAMES

If $\ and $, look like line noise to you, the English module provides readable names:
use English qw(-no_match_vars); $OUTPUT_RECORD_SEPARATOR = "\n"; $OUTPUT_FIELD_SEPARATOR = ", "; print "one", "two", "three"; # one, two, three\n
Same variables, human-readable names. The -no_match_vars flag is important because use English without it enables $PREMATCH, $MATCH, and $POSTMATCH, which have a global performance penalty. More on that in another post.

The English names are great for documentation and learning. In production code, most Perl programmers use the short names because they are faster to type and universally recognized (among Perl programmers, anyway).

Part 10: WHEN TO USE THEM

.--. |o_o | "print does what I tell it. |:_/ | Even the things I told it // \ \ three hundred lines ago." (| | ) /'\_ _/`\ \___)=(___/
$\ and $, are power tools. They let you change the behavior of print globally, which means you can format output without touching any print statement. One assignment at the top and every print in the program obeys.

That power is also the danger. Global state is global state. One careless assignment and every module, library, and function that uses print is affected. Always scope with local. Always.

For one-liners, use them freely. They save keystrokes and make complex text transformations concise.

For scripts, use them with local in a limited scope. Or just use say and join explicitly. The explicitness of say join(",", @fields) beats the cleverness of setting $, and hoping nobody else touches it.

For modules, never set them without localizing. A module that sets global output separators is a module that gets uninstalled.

Know they exist. Know what they do. Know how to spot them in someone else's code. That is the real value. Not using them yourself, but recognizing them when they are silently changing your output from somewhere you did not expect.

perl.gg