The reason is that while single quotes actually remove the meaning of special characters, this refers to variable expansion, globbing and word splitting - i.e. those characters that are special to the shell and that are interpreted by the shell before the result is passed to the program - and shell metacharacters (such as the |
).
The -
is not a "special character" in that sense. What makes it special is that it is a de facto standard way to indicate to a program that the string started by it is an "option" argument. In fact, many (if not most) programs in the Unix/Linux ecosystem rely on the external getopt()
function for that purpose1, which is part of the POSIX specification and provides a standardized way to handle command-line parameters, and the convention of interpreting parameters that start with -
as "options" is embedded there.
So, the single quotes ensure that the -|h4ker|-
is passed verbatim from the shell to the program (cat
in your case), but also removes the quotes in that process. Hence the program still thinks that since this parameter starts with a -
, it should be treated as an "option" argument, not an operand (like a file name to process).
This is the reason why many programs (again, all that rely on getopt()
) interpret the --
token on the command line as a special "end-of-options" indicator, so that they can safely be applied to files that start with -
.
Another possibility, which you already explored in your investigation, is to "protect" the filename by either stating it as an absolute filename ('/path/to/file/-|h4ker|-'
) or prepend the current directory ('./-|h4ker|-'
), because then, the argument will no longer start with the "ambiguous" -
. Note that quoting/escaping is still necessary in this example because the |
is a shell metacharacter.
A nice demonstration is trying to create and list a file named -l
:
~$ touch '-l'
touch: Invalid option -- l
~$ touch -- '-l'
~$ ls '-l'
< ... the entire directory content in long list format ... >
~$ ls -- '-l'
-l
~$ ls -l -- '-l'
-rw-r--r-- 1 user user 0 Feb 24 17:16 -l
1 For shell scripts, there is an equivalent getopts
builtin command
cat
doesn't even have any way to know that the quotes existed in the first place. Bothcat foo
andcat "foo"
(andcat 'foo'
, andcat \f\o\o
) turn intoexecve("/bin/cat", char[][]{"cat", "cat", "foo"}, ...)
-- what's handed off to the operating system when starting a program is not a single command line, but a list of individual arguments, each as its own NUL-terminated string. – Charles Duffy Feb 24 '21 at 23:21