perl.gg / hidden-gems

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

The -s Shebang Switch for Instant CLI Arguments

2026-04-09

You need a quick script that takes a flag. Maybe --verbose. Maybe --output=file.txt. The responsible way is Getopt::Long. The fast way is -s.
#!/usr/bin/perl -s use strict; use warnings; our ($verbose, $output); print "Verbose mode ON\n" if $verbose; print "Output: $output\n" if $output;
Run it:
$ ./script.pl -verbose -output=report.txt Verbose mode ON Output: report.txt
No use Getopt::Long. No GetOptions() call. No option spec. No hash. No error handling boilerplate. The -s switch tells Perl to scan @ARGV for switches, strip them out, and create package variables with matching names. That is the entire argument parsing step, done before your code even starts.

Part 1: HOW IT WORKS

The -s flag goes on the shebang line:
#!/usr/bin/perl -s
When Perl sees -s, it processes @ARGV before executing your script. For each argument that starts with -, Perl:
  1. Strips the leading -
  2. Creates a package variable with that name
  3. Sets it to 1 (boolean flag) or the value after =
  4. Removes the argument from @ARGV

Processing stops at the first argument that does not start with -, or at --.

ARGUMENT VARIABLE CREATED VALUE -------------- ----------------- -------- -verbose $verbose 1 -debug $debug 1 -output=foo $output "foo" -count=42 $count "42" -x $x 1
After processing, @ARGV contains only the remaining non-switch arguments. Your script sees a clean argument list.

Part 2: BOOLEAN FLAGS

The simplest use. A flag is either present or absent:
#!/usr/bin/perl -s use strict; use warnings; use feature 'say'; our ($verbose, $dry_run, $force); say "Verbose mode" if $verbose; say "Dry run mode" if $dry_run; say "Force mode" if $force; # remaining args are files or whatever say "Files: @ARGV" if @ARGV;
$ ./script.pl -verbose -force file1.txt file2.txt Verbose mode Force mode Files: file1.txt file2.txt
The variables are either 1 (flag present) or undef (flag absent). With our declarations and strict mode, you get clean boolean behavior with no warnings.

Part 3: STRING AND NUMERIC VALUES

Use = to pass a value:
#!/usr/bin/perl -s use strict; use warnings; use feature 'say'; our ($name, $count, $sep); $name //= "world"; $count //= 1; $sep //= ", "; for (1 .. $count) { say "Hello${sep}${name}!"; }
$ ./script.pl -name=Perl -count=3 Hello, Perl! Hello, Perl! Hello, Perl! $ ./script.pl -name=Dave -sep=" dear " Hello dear Dave! $ ./script.pl Hello, world!
The //= operator provides defaults. If the flag is not passed, the variable is undef, and the default kicks in. Clean fallback behavior.

Note that values are always strings. If you need a number, Perl's automatic string-to-number conversion handles it. -count=3 gives you the string "3", but 1 .. $count works fine because Perl numifies it.

Part 4: THE OUR DECLARATION

With use strict, you must declare the variables with our:
our ($verbose, $output, $count);
Without our, strict mode complains about undeclared globals. Without strict mode, the variables are created as package globals automatically. But you should always use strict. So always use our.

You can declare them individually or in a list:

our $verbose; our $output; our $count; # or all at once our ($verbose, $output, $count);
These are package variables, not lexical (my) variables. They live in the current package's symbol table. For a standalone script in main::, this is fine. In a module, be careful. You probably do not want CLI switches polluting a module's namespace.

Part 5: QUICK AND DIRTY SCRIPTS

This is where -s shines. Scripts you write in 30 seconds that need one or two flags:

Grep with optional line numbers:

#!/usr/bin/perl -s use strict; use warnings; our ($n, $i); my $pattern = shift or die "Usage: $0 [-n] [-i] pattern files...\n"; my $flags = $i ? 'i' : ''; my $re = qr~(?$flags)$pattern~; while (<>) { if (m~$re~) { print $n ? "$ARGV:$.: $_" : $_; } }
$ ./mygrep.pl -n -i error /var/log/*.log
Quick file renamer:
#!/usr/bin/perl -s use strict; use warnings; use feature 'say'; use File::Copy 'move'; our ($dry, $from, $to); die "Usage: $0 -from=PATTERN -to=REPLACEMENT files...\n" unless $from && $to; for my $file (@ARGV) { (my $new = $file) =~ s~$from~$to~; if ($new ne $file) { if ($dry) { say "Would rename: $file -> $new"; } else { move($file, $new) or warn "Failed: $file: $!\n"; say "Renamed: $file -> $new"; } } }
$ ./rename.pl -dry -from='\.txt$' -to='.md' *.txt Would rename: notes.txt -> notes.md Would rename: todo.txt -> todo.md
These scripts take 60 seconds to write. Adding Getopt::Long would triple the code for no practical benefit.

Part 6: HOW @ARGV IS MODIFIED

The -s processing removes switches from @ARGV, leaving only positional arguments:
#!/usr/bin/perl -s use strict; use warnings; use feature 'say'; our ($verbose, $output); say "Flags:"; say " verbose = " . ($verbose // 'undef'); say " output = " . ($output // 'undef'); say "Remaining ARGV:"; say " $_" for @ARGV;
$ ./script.pl -verbose -output=log.txt file1 file2 file3 Flags: verbose = 1 output = log.txt Remaining ARGV: file1 file2 file3
The -- double-dash stops switch processing:
$ ./script.pl -verbose -- -not-a-flag file1 Flags: verbose = 1 Remaining ARGV: -not-a-flag file1
Everything after -- stays in @ARGV as-is, even if it starts with -. This is the standard Unix convention.

Part 7: LIMITATIONS

Time for honesty. The -s switch has real limitations:

No long options. Only single-dash flags. No --verbose. If you pass --verbose, Perl creates a variable called $verbose (stripping both dashes), but users expect --long and -short to be different, and -s does not make that distinction.

No validation. Typo a flag name? Perl happily creates a variable you never check. -verbos does not trigger an error. It just creates $verbos and nobody notices.

No type checking. Everything is a string. -count=banana is perfectly valid. You get "banana" in $count and a runtime explosion when you try to use it as a number.

No bundling. -abc creates a variable $abc, not three variables $a, $b, $c. No way to combine single-character flags.

No negation. No --no-verbose or -noverbose. You have to handle that yourself.

No help generation. Getopt::Long with Pod::Usage gives you --help for free. With -s, you write your own usage message.

LIMITATION WORKAROUND --------------------- ---------------------------------- No validation Check defined() and die with usage No type checking Validate manually after parsing No long options Accept it or switch to Getopt No bundling Accept it No help generation Write a usage() sub

Part 8: WHEN TO USE -S VS GETOPT::LONG

The decision is simple:
USE -s WHEN: * Script is < 50 lines * 1-4 simple flags * Personal use or team tooling * Throwaway / prototyping * You want zero boilerplate USE Getopt::Long WHEN: * Script is a real tool others will use * Complex option types (arrays, hashes) * You need --help auto-generation * Long and short option forms (--verbose / -v) * Required options with validation * You care about typo detection
There is a middle ground. Start with -s for the prototype. If the script survives long enough to need proper argument handling, refactor to Getopt::Long. The variable names usually stay the same. The switch is mechanical.
# -s version our ($verbose, $output); # Getopt::Long version use Getopt::Long; my ($verbose, $output); GetOptions( 'verbose' => \$verbose, 'output=s' => \$output, ) or die "Bad options\n";
The bodies of the scripts are identical. Only the argument parsing preamble changes.

Part 9: COMBINING WITH OTHER SHEBANG FLAGS

You can combine -s with other Perl flags on the shebang line:
#!/usr/bin/perl -sw
That gives you -s (switch parsing) and -w (warnings). You can also combine with -l (auto-chomp and auto-newline):
#!/usr/bin/perl -sl our $upper; while (<>) { print $upper ? uc : $_; }
$ echo -e "hello\nworld" | ./script.pl -upper HELLO WORLD
Common combinations:
FLAGS MEANING -------- ------------------------------------ -s Switch parsing only -sw Switches + warnings -sl Switches + auto line handling -swl Switches + warnings + line handling -sn Switches + implicit while(<>) loop
Be careful with -sn and -sp. The -n and -p flags wrap your code in a while (<>) loop, which changes how the script behaves. The -s processing still happens first, before the loop starts.

Part 10: THE PHILOSOPHY OF QUICK

Perl was designed for getting things done. Larry Wall called it the "duct tape of the Internet." The -s switch is duct tape for argument parsing.

It is not robust. It is not safe. It does not generate help text or validate input. But it does one thing brilliantly: it gets arguments into your script with zero ceremony.

When you are writing a script at 11 PM to fix a problem right now, you do not need a 15-line Getopt::Long preamble. You need -s and two our declarations and the problem solved before midnight.

.--. |o_o | "Life's too short for |:_/ | GetOptions()." // \ \ (| | ) /'\_ _/`\ \___)=(___/
The -s switch has been in Perl since version 1. It is older than most Perl programmers. It is not deprecated. It is not going away. It sits there on your shebang line, ready to parse your arguments in zero lines of code.

For quick scripts, disposable tools, and late-night fixes, it is perfect. For anything else, use Getopt::Long. Knowing when to use which is the mark of a pragmatic Perl programmer.

Keep -s in your back pocket. You will reach for it more often than you think.

perl.gg