What Does -- Mean in a Shell Command?
A command such as this can look deceptively simple:
du -sh -- * | sort -hr
Yet several different systems are involved in interpreting it.
Some characters are handled by the shell. Some arguments are passed through to du. Others belong to sort. The standalone -- is especially confusing because it looks like shell syntax, but usually it is not.
The central idea is:
--is normally an argument passed to a program, telling that program to stop interpreting later arguments as options.
It is fundamentally different from shell operators such as |.
The practical meaning of --
Consider a file whose name begins with a dash:
-important
Unix-like filesystems generally allow this. The problem is that command-line programs commonly interpret arguments beginning with - as options.
For example:
rm -important
The program may try to interpret the letters in -important as command-line flags rather than treating the entire string as a filename.
The conventional solution is:
rm -- -important
For programs that support the convention, the standalone -- means:
Stop parsing options. Treat everything after this as an operand, filename, path, or other positional argument.
Therefore:
rm program
-- end of options
-important filename
The important detail is that -- is not usually interpreted by the shell. The shell passes it to rm, and rm decides what it means.
One command line, multiple parsers
The easiest way to understand shell commands is to recognize that more than one parser is involved.
The shell interprets shell-language features such as:
| pipes
> output redirection
< input redirection
* wildcard expansion
$HOME variable expansion
"..." quoting
$(...) command substitution
After performing its parsing and expansions, the shell starts the requested program and gives it an array of arguments.
The program then interprets things such as:
-h
-v
-sh
--help
--version
--
This produces a layered process:
Typed command line
↓
The shell parses shell syntax
↓
The shell performs expansions
↓
The shell starts the program with argv
↓
The program parses its own options
The shell and the program are not interpreting the same things.
What happens to argv
In C, a program traditionally begins with something like:
int main(int argc, char **argv)
The operating system does not generally tell the program which arguments are flags and which are filenames. The program receives an array of strings.
For example:
du -sh -- Downloads Music
might result in an argument array conceptually equivalent to:
argv[0] = "du"
argv[1] = "-sh"
argv[2] = "--"
argv[3] = "Downloads"
argv[4] = "Music"
The du program, or a command-line parsing library used by it, then examines those strings.
Its logic is approximately:
Read the next argument.
If it is "--":
Stop parsing options.
Otherwise, if it begins with "-":
Interpret it as an option.
Otherwise:
Treat it as a path.
The kernel does not inherently assign this meaning to --. It is a widely followed command-line convention implemented by programs and argument-parsing libraries.
The same principle applies in other languages:
Python: sys.argv, argparse
Go: os.Args, flag
C#: string[] args, command-line parser libraries
Rust: std::env::args, clap
A program can support --, ignore it, reject it, or give it some other meaning. Most conventional Unix command-line tools support it, but it is not a universal law.
To see this boundary implemented directly, continue with How Go's Standard Library Implements --. That article follows os.Args into Go's flag package and shows where the parser recognizes the standalone --, stops interpreting flags, and leaves the remaining strings as positional arguments.
What the star means
Now consider:
du -sh -- *
The * is not interpreted by du.
It is a shell wildcard, also called a glob.
Suppose the current directory contains:
Downloads
Music
Pictures
-important
Before starting du, the shell expands * into the matching filenames. The command becomes conceptually equivalent to:
du -sh -- Downloads Music Pictures -important
Then du receives approximately:
argv[0] = "du"
argv[1] = "-sh"
argv[2] = "--"
argv[3] = "Downloads"
argv[4] = "Music"
argv[5] = "Pictures"
argv[6] = "-important"
The star itself normally does not appear in du's argument array. The shell replaces it with the matching names.
This is why * is placed after --.
The intention is not to apply -- to the wildcard itself. The intention is to place all filenames produced by the wildcard after the option terminator.
Without --, the shell might construct:
du -sh -important Downloads Music Pictures
Then du could mistake -important for one or more options.
With --, the shell constructs:
du -sh -- -important Downloads Music Pictures
Now du knows that all remaining arguments are paths, even if one begins with a dash.
The star does not launch the program repeatedly
This command:
du -sh *
does not normally launch one du process for each child.
It does not mean:
Run du for Downloads.
Run du for Music.
Run du for Pictures.
Instead, the shell expands the wildcard and launches one process:
du -sh Downloads Music Pictures
That single du process receives multiple path operands. It loops over them and prints one summary for each:
12G Downloads
40G Music
8G Pictures
This is different from:
du -sh
When no path is supplied, du defaults to the current directory. It behaves roughly like:
du -sh .
That produces one combined total:
60G .
The distinction is therefore:
du -sh .
Summarize the current directory as one operand.
du -sh *
Summarize each non-hidden child as a separate operand.
The filesystem traversal work may be broadly similar, but the reporting boundary is different. In one case, du prints one total for the whole directory. In the other, it prints one total for each child supplied as an argument.
How the pipe is different
Now return to the full command:
du -sh -- * | sort -hr
The pipe character belongs to the shell's grammar.
The shell sees the command as a pipeline containing two programs:
Left side: du -sh -- *
Right side: sort -hr
It creates an operating-system pipe and connects the processes:
du standard output
↓
pipe
↓
sort standard input
The du process does not normally receive | in its argument array.
The sort process does not normally receive | either.
The shell consumes the pipe symbol while constructing the pipeline.
Conceptually, the two programs receive arguments like these:
du:
["du", "-sh", "--", "Downloads", "Music", "Pictures"]
sort:
["sort", "-hr"]
The pipe is absent because it was shell syntax, not a program argument.
This is fundamentally different from --.
-- changes how one program interprets later arguments
| tells the shell to connect two processes
There is no transfer of control back to the shell when a program encounters --. The shell has already done its work of parsing the command line, expanding the wildcard, creating the pipe, and starting the processes.
The du program simply encounters the string -- while reading its own arguments and changes its own parsing behavior.
A step-by-step execution model
For this command:
du -sh -- * | sort -hr
a useful mental model is:
- The shell parses the command and recognizes the pipeline.
- The shell identifies
duas the left-side command. - The shell identifies
sortas the right-side command. - The shell expands
*into matching filenames. - The shell creates an operating-system pipe.
- The shell starts one
duprocess. - The shell starts one
sortprocess. duparses-shas options.dusees--and stops parsing options.dutreats the expanded names as paths.ducalculates and prints one summary for each path.- The output travels through the pipe.
sortreads those lines from standard input.sortorders them by human-readable numeric size in reverse order.
In plain English, the command means:
Take every non-hidden child in the current directory, calculate the disk usage of each one, and sort the resulting lines from largest to smallest.
Why -hr works in sort
The right side contains:
sort -hr
Those are options interpreted by sort.
The -h option tells sort to understand human-readable size suffixes such as:
4K
250M
12G
Without -h, a purely textual or ordinary numeric comparison could place human-readable values in an unintuitive order.
The -r option reverses the result, producing descending order.
Therefore:
-h compare human-readable quantities
-r reverse the order
Again, these options are not interpreted by the shell. They are passed to sort.
Wildcards are still expanded after --
Because -- is usually meaningful only to the target program, it does not disable shell processing.
For example:
du -sh -- *
does not tell the shell to stop interpreting syntax.
The shell still expands *.
Likewise:
printf '%s\n' -- "$HOME"
does not stop the shell from expanding $HOME. The shell expands the variable before printf begins.
This reinforces the layered model:
The shell handles shell expansion.
The program handles option parsing.
The standalone -- affects the second layer, not the first.
Quoting the wildcard changes everything
Compare:
du -sh -- *
with:
du -sh -- "*"
In the first command, the shell expands * into matching filenames.
In the second, the quotes prevent wildcard expansion. du receives one literal argument:
*
It then looks for a file or directory literally named *.
This again shows that globbing belongs to the shell. Quoting changes what the shell does before the program starts.
Hidden files are usually omitted
A bare * normally does not match names beginning with a dot.
For example, it may include:
Downloads
Music
Pictures
while omitting:
.cache
.config
.local
Therefore:
du -sh -- *
may not account for every child in the directory.
This is one reason the totals shown for the expanded children may not add up exactly to:
du -sh .
The latter includes hidden descendants because it traverses the current directory itself.
-- versus long options
The standalone argument:
--
is different from a long option such as:
--help
--version
--recursive
For example:
ls --help
passes one long option named help.
By contrast:
ls -- -strange-file
passes the standalone option terminator followed by a filename.
They share the double-dash spelling, but they serve different purposes:
--help a named long option
-- the conventional end-of-options marker
Not every program behaves identically
Although -- is widely supported, command-line parsing is ultimately controlled by each program.
Some programs:
- follow the convention exactly;
- use a standard library such as
getopt; - stop parsing options at the first positional argument;
- allow options and operands to be mixed;
- interpret
--specially in another way; - do not support it at all.
Shell built-ins can also have their own parsing rules. Documentation remains the final authority for a particular command.
Still, for mainstream Unix utilities, -- is a reliable and valuable defensive habit.
The deeper lesson
The most important lesson is not merely what -- means.
It is that a command line is interpreted in layers.
Characters do not have one universal meaning independent of context. Their meaning depends on which parser sees them.
In:
du -sh -- * | sort -hr
the pieces belong to different layers:
du program selected by the shell
-sh options parsed by du
-- argument parsed by du as the end of options
* wildcard expanded by the shell
| pipeline operator consumed by the shell
sort second program selected by the shell
-hr options parsed by sort
A useful summary is:
* is usually expanded by the shell
| is consumed by the shell
-- is usually passed to the program
-sh is parsed by the program
Once that boundary is clear, -- stops looking like a mysterious shell operator.
It is simply one string in argv—a string that many programs agree to interpret as:
Options end here.
Comments
No comments yet. Be the first!