Shell Argument Syntax

There are a few, but extremely important concepts that we must keep in mind to make reasonable use of the command line and write shell scripts in general.

Whitespace and Quoting

In the shell, whitespace matters. It is used to break the input into tokens. See the spec and bash shell syntax documentation.

For the shell, the first word (a.k.a token) is the command, or the name of the program to run, and the remaining words are parameters to be passed to the program.

$ printf '%d\n' {1..3}
1
2
3

Here, printf is the first token (the name of the program to be run), %d\n is the second token (quotes are removed - unless escaped - before the argument is passed to the program, and {1..3} is the third token, except the shell (Bash in my case) performs brace expansion before passing the results as individual tokens to the printf program. When the shell finds the newline, it then executes the command line.

A Sad rm Incident

It is paramount that we prevent the shell from word splitting in certain (most) cases.

$ ls -1
message.txt
secret message.txt
secret.txt

Now we want to remove secret message.txt:

$ ls -1
message.txt
secret message.txt
secret.txt

$ rm -v secret message.txt
rm: cannot remove 'secret': No such file or directory
removed 'message.txt'

$ ls -1
'secret message.txt'
secret.txt

Oh shoot! Because we did not prevent the shell from breaking secret message.txt into individual tokens, what was passed to rm was not a single parameter, but two: secret and message.txt. rm was unable to remove a file named secret because no such file exists (we have secret.txt) but was able to remove message.txt because that was a file that really existed (but no longer). Unfortunately, we did not remove secret message.txt which is what we wanted, but accidentally removed secret.txt which we didn’t want to. Yes, that is a sad story…

We must prevent the shell from performing word splitting in cases like this (and many others). What we should have done is this:

$ rm -v 'secret message.txt'
removed 'secret message.txt'

$ ls -1
message.txt
secret.txt

Now we removed secret message.txt and not incidents took place.

End Of Options ‘--’

The end of options -- is used to indicate the end of options 🤣. It is documented under Utility Syntax Guidelines.

Guideline 10 says:

“The first -- argument that is not an option-argument should be accepted as a delimiter indicating the end of options. Any following arguments should be treated as operands, even if they begin with the ‘-’ character.”

It is useful when we want to tell a program something like “Look, from now on, these arguments are real files, directories, whatever. They are not options (command line flags) to the program.”

Note

The echo command treats -- as a normal string operand. See the echo spec.

Let’s see some use cases.

remove files starting with ‘-’

Sometimes, by accident or some other reason, we end up with files whose name start with one or more - (U+002D HYPHEN-MINUS character). If we try to remove (or rename, or some other operation) them, we run into problems.

shell
$ tree -CF .
.
├── --oops.txt
└── -w00t.txt

0 directories, 2 files

shell
$ rm -v -w00t.txt
rm: invalid option -- 'w'
Try 'rm ./-w00t.txt' to remove the file '-w00t.txt'.
Try 'rm --help' for more information.

shell
$ rm -v --oops.txt
rm: unrecognized option '--oops.txt'
Try 'rm ./--oops.txt' to remove the file '--oops.txt'.
Try 'rm --help' for more information.

“How embarrassing!”

—Master Yoda

But because we can use --, we have a way out!

$ rm -vi -- --oops.txt -w00t.txt
rm: remove regular empty file '--oops.txt'? yes
removed '--oops.txt'
rm: remove regular empty file '-w00t.txt'? yes
removed '-w00t.txt'

Another option is to use ./<name of the file> to force the shell to see that since we are using a path specifier (./), the thing must be a file:

$ tree -CF .
.
├── --oops.txt
└── -w00t.txt

0 directories, 2 files

$ rm -vi ./--oops.txt ./-w00t.txt
rm: remove regular empty file './--oops.txt'? y
removed './--oops.txt'
rm: remove regular empty file './-w00t.txt'? y
removed './-w00t.txt'

$ tree -CF .
.

0 directories, 0 files

(TO BE CONTINUED)