How to Pass Arguments to Terminal Commands in Bash / Linux

Share
In this article, we'll learn how to pass arguments to a terminal command in Bash, the default shell in many Linux distributions. Specifically, we'll learn about passing positional arguments, passing arguments containing space characters, escaping reserved characters like double quotes, and a bit about how flags, standard input and output streams, piping, variable substitution, and globbing works in Bash.

Positional Arguments Syntax

The basic syntax for passing arguments in the terminal is to simply separate them by spaces. This is true not only on Bash, but in other Linux's shells, and even shells of other operating systems, including BSD, Windows, and macOS. This is just the obvious, most common sense way to do it.

The Zeroth Argument

Let's start from the beginning with the simplest example:

date

Above we have a terminal command with one argument. The argument is date. This may sound weird, but we say that date, the name of the command itself, is the zeroth argument.

The way Bash interprets this zeroth argument is rather complicated and not the focus of this article. See [How the Name of the Terminal Command Works in Bash / Linux] for details.

What's important to know is that if we execute this date command, the output will be a date like "Thu Mar 6 11:41:28 AM -03 2025." This contains a lot of information, including an abbreviation for the weekday, the month, the day of the month, the hour, minute, and second, the timezone, and the year.

We can modify the behavior of the program by supplying to it additional arguments. What arguments are supported by the program and what they mean exactly depend on the program, but the method to supply them from the command-line is always the same.

Separating Arguments by Spaces in the Terminal

Bash uses spaces to separate arguments. This means that the first argument that we pass to a command must come after a space after the name of the command. For example:

date +%R

If we do this, the output of date should be something like "11:41", in other words, just the hour and minute, instead of the entire date. The reason why this happens is because of how date was programmed, it has nothing to do with the terminal in general.

What is responsibility of the shell is merely splitting the arguments from the text code you type in the command-line into a list of arguments that starts at zero. All Bash does is take the above and turn it into this:

  1. date
  2. +%R

And that's what the program gets. A list of two arguments, the zeroth argument being date, and the first argument being +%R.

This means that the program doesn't actually know exactly what we typed in the terminal, because what reaches the program's algorithm is a already processed list.

For example, it doesn't matter for Bash how many spaces we type between the arguments. This means if we type:

date                  +%R

The list of arguments is going to be the same:

  1. date
  2. +%R

You can try this yourself with the echo command. The echo simply outputs every argument you pass to it, separated by spaces. (below, $ represents the shell prompt)

$ echo         hello             world
hello world

Above, the output of echo is just "hello world," because it's simply not aware of how many spaces we gave the Bash. It assumes one space, as that's a reasonable assumption to make.

  1. echo
  2. hello
  3. world

There are other cases where a program appears to know what we typed with spaces, but it's actually just combining the arguments it got with spaces by itself.

From the Program's Side

I think it's used to take a look at what happens from the program's perspective to have a better understanding of this. Below is a basic program's source code in the C programming language.

#include <stdio.h>

int main(int argc, char *argv[]) {
    printf("Hello world!");
    return 0;
}

Not every program is made in C, but in practice the world runs on C, so that's a good way to visualize it.

The main entry point of a C program is a function called main. Its signature typically takes two arguments, argc is an integer that tells the program how many arguments it got, and argv and is essentially a list of pieces of text. The reason why it has these two arguments is that in C the simplest list structure (called an array) doesn't know how many items it contains, so that needs to be a separate variable passed to the function.

For echo hello world, for example, argc would have the value 3, because we have 3 arguments, then we would be able to get the text echo using the C code argv[0] because it's the zeroth argument. Then argv[1] is hello, and argv[2] is world. If we do argv[3] the program crashes.

The reason it crashes is because this source code will compile to a program that tries to access a memory address that it hasn't reserved. argv is a pointer to the start of a space in memory reserved for the program that, in this case, would contain 3 items. If we try to access more than 3 items, we'll be beyond the reserved boundary. When the program runs the operating system won't allow this access to happen. That's why we need argc. Some programs allow you to keep passing as many arguments you want, so without argc the program would have no way of knowing when to stop looking for more arguments.

This is how it looks from the program's perspective. Just two arguments in a function that tell the programs how many arguments they got from the command-line. There are tools that allow programs to do complicated things with this list, but fundamentally, for all programs, it's just a list of pieces of text and nothing more complicated than that.

Order of Positional Arguments

In some programs, the order of the positional arguments is important, meaning that the first argument means one thing, and the second argument means a completely different thing.

For example, the cp command copies a file, and the mv commands moves (renames) a file. In both cases, the first positional argument is the filepath or filename of the file to copy (or move), while the second positional argument is the target filepath.

cp source.txt source-backup.txt
mv improtant.txt important.txt

Above, the cp command is being used to create a backup copy of a file, while the mv command is being used to fix a typo in a file's name.

Passing Arguments that Contain Spaces in the Terminal

If a spaces are used to separate arguments, how, then, do we pass an argument that is supposed to contain spaces? For example, if we want to pass a filepath or filename to a program, and our file has a space in its name, what do we do then?

In this case, we must surround the argument by double quotes ("). For example:

$ echo "Hello    world!"
Hello    world!

Above, echo has outputs "Hello world!" with four space characters between the two words because this time it's interpreted by Bash as being a single argument with spaces inside.

  1. echo
  2. Hello world!

Bash uses the zeroth argument to figure out what command to execute. The way it works is a bit complicated.

Single quotes (') also work.

$ echo 'Hello    world!'
Hello    world!

Unexpectedly, the quotes don't need to be at the start of the argument.

$ echo Hello's    world!'
Hellos    world!

Above, it looks like we have an apostrophe after Hello, but because the apostrophe and the single quote are the same character in a computer, Bash will interpret it as a single quote, so it will treat the spaces differently until it finds the matching closing quote at the end.

By the way, if you type just one apostrophe, the command won't work because the shell expects the closing apostrophe. You'll see a shell prompt (>) appear on the next line as it waits for you to complete the argument.

$ echo Hello's    world!
>

Passing Arguments that Contain Quotes in the Terminal

To pass an argument that contains a double quote or single quote character in Bash, we need to escape it using the backslash character (\). The backslash can also be used to escape other reserved characters in Bash.

For example, if we typed this:

$ echo John said "Hello!"
John said Hello!

We don't get any double quotes in the output because the list of arguments parsed by Bash was:

  1. echo
  2. John
  3. said
  4. Hello!

This may seem strange, but when we use double quotes to surround an argument, even if that argument doesn't contain any spaces in it the double quotes will be treated specially and as such won't become part of the argument list.

In order to make this work, we put a backslash before the special character:

$ echo John said \"Hello!\"
John said "Hello!"

This time, the shell won't interpret the quotes as special characters, but as literals, meaning it will literally put the quotes in the list.

  1. echo
  2. John
  3. said
  4. "Hello!"

Standard Input Stream

Positional arguments aren't the only type of input that a program can receive. Another type of input is the standard input stream, also called STDIN. The STDIN is a way to pass arbitrary data to a program, even if that data doesn't exist in a file.

For example, instead of telling a program to open an image file, it could be that we already have the bytes of the image file in memory, but not saved to a file, so there needs to be a way to simply pass those bytes without having to save it to a file first. This method is called the standard input stream.

Piping

The typical way to use the standard input stream is by piping the standard output stream (STDOUT) of a different program into it. To pipe the streams we use a pipe character (|). What this will do is send the output of one program as the input of another program.

The easiest way to understand this is with grep. The program grep can search its standard input and output lines that match a search string. For example, if we do this:

ls / | grep i

We're executing the command ls /, then piping the output of this command to grep i.

The command ls / will list all files in /, while grep i will show all lines in its input that contain the text "i". This means you should see on the terminal lines for the directories bin and lib with their i's highlighted, and directories that don't contain with i, like /etc, won't appear.

The usefulness of grep is that ANYTHING that displays text on the terminal can be grepped, because the text that shows in the terminal after you execute a command is a combination of the standard output stream and the standard error stream (STDERR).

Input from a file

Bash has a syntax for sending a file as standard input of a program, although this is often not necessary since most programs have a way to specify a filepath. To do this, we use < at the end of the command before the filepath.

grep i < my-file.txt

For the record, it's also possible to save the output of a program to a file using >. This means we can do this:

ls / > my-file.txt
grep i < my-file.txt

And that's the same thing as ls / | grep i, except it creates a file called my-file.txt as well.

Shell Syntax Ends Here

What we've learned above is pretty much all the syntax that the shell is responsible for, except for the substitutions it can make. In order words, all the shell does is separate text by spaces and turn it into a list. That's it.

We'll see below that there are many conventions implemented by terminal-based programs. These are ways to treat positional arguments that many basic programs share in common. However, it's important to keep in mind that these conventions aren't enforced by the shell. It's up to the developer of the program to make them work.

Optional Arguments

By convention, optional arguments in a terminal program start with two dashes (--). An algorithm can easily determine which programs are positional and which ones are optional thanks to this prefix.

For example, consider the follow command:

ls /etc

Above, we have the terminal command ls that lists all files in a given directory. We can pass one positional argument to it, which is the filepath of the directory whose files we want to list.

Now, consider the following command:

ls --all /etc

In this case, we're passing the optional argument --all, which makes ls list hidden files (dotfiles), because by default it wouldn't.

Order of Optional Arguments

How does the ls program know that --all is supposed to be an optional argument and not the filepath of a directory? What would its algorithm look like?

As we already know, the job of the shell is simply to turn this text code into a list of arguments, so what ls has access to is just this:

  1. ls
  2. --all
  3. /etc

There is nothing in this list that tells us which arguments are optional and which ones are not.

What the algorithm does is essentially this: it goes through all items in this list, and if there is an argument that starts with --, it removes the argument and changes a setting in the program. Then it executes the processed list.

By doing that, the program will remove --all from the list, and then the list becomes:

  1. ls
  2. /etc

With --all as a modifier.

The reason why this is important to understand is that due to this commonly used algorithm, optional arguments can be placed on ANY position of the list. For example, if we execute:

ls /etc --all

Then the list of arguments that ls gets is going to be different:

  1. ls
  2. /etc
  3. --all

But because it processes the list in two passes, first removing the arguments prefixed with --, and then interpreting the remaining arguments, we effectively have the same end result:

  1. ls
  2. /etc

With --all as a modifier.

After all optional arguments are removed, ls can simply assume whatever is the first argument in the final list is going to be the filepath.

Note: this behavior depends on how the processing of arguments is implemented in the program. Although this algorithm makes sense in most cases, there are some rather unique programs in the world that behave differently depending on the order of optional arguments.

Passing Multiple Optional Arguments to a Terminal Command

Passing multiple optional arguments works as you would expect: the order (generally) doesn't matter, but they must be separated by spaces, and all of them must be prefixed with two dashes (--).

ls --all --reverse /etc
ls --reverse --all /etc
ls --all /etc --reverse
ls /etc --all --reverse

Any of the variations above will list the files in reverse order.

Flags

Another convention found in Linux terminal programs is the use of flags. Flags are passed as arguments that start with a single dash (-). Every character after the dash is a flag. Flags are abbreviations of optional arguments.

For example, consider the following command:

ls -a /etc

In this case, we have the flag a. This flag is an abbreviation for --all.

Warning: what a flag is an abbreviation of depends entirely on the program. For example, -q is an abbreviation for --hide-control-chars in ls. While it makes sense that the abbreviation is going to be the first letter of the option, that's not always the case, so don't rely on this assumption. Check the documentation before start typing random flags.

Warning: assume the flag is case-sensitive. For example, on ls, -a (lower case) means --all, but -A (upper case) means --almost-all. In general, a program will have lower case flags by default, and only use upper case flags when it runs out of letters or if the upper case flag has the opposite effect of the lower case flag (e.g. lower case for "hide" something and upper case for "show" something).

Passing Multiple Flags to a Terminal Command

To pass multiple flags to a terminal command, we can either pass multiple arguments, each prefixed by a single dash (-), or a single argument with multiple flags. For example:

ls -a -r /etc
ls -ar /etc

Both variations above have the same effect.

Essentially, what the algorithm does is simply check if an argument starts with a single dash, and then, for each character after the dash, it checks what is it an abbreviation for. In this case, a is an abbreviation for --all and r an abbreviation for --reverse.

Personally, I recommend against using flags in script files if you can avoid them, because it's just hard to tell what each flag means.

Escaping Dashes in Terminal Commands

What happens if we want to pass a positional argument to a terminal command that starts with a dash? For example, the cat program outputs the content of the file located by the filepath passed as its first argument. It supports several optional arguments and flags, such as -n. What if we have a file whose filename is -n? We can't do this:

cat -n

Because that won't work. So how do we do it?

Now, first of all. Why does your filename start wit a dash? That's just a very weird way to name things. But sometimes it happens. We can get around this limitation by using ./ before the filename. ./ is another way to refer to the current working directory, so if we have a file named -n in the current working directory, then ./-n refers to it.

cat ./-n

Observe that this time the argument doesn't start with -, so it doesn't get processed specially by the program as a flag.

Substitution

Not everything you type in the terminal gets passed to the programs as-is. Besides handling quotes and escaping quotes, Bash is also able to perform certain kinds of substitutions on the text you type in the command line that gets processed before the list of arguments is passed to the terminal command. Let's take a lot at what they are.

Variables

Variables can replace one or more arguments in the terminal when they're substituted using a dollar sign ($) before their name. For example:

$ FOO="echo Hello" BAR=world!
$ $FOO $BAR
Hello world!

Above, Bash turned the text code $FOO $BAR into echo Hello world!, and that was the command that it processed into a list.

This syntax allows you to use environment variables. For example, the $USER variable contains your username, which should be the same value as executing the command whoami. Observe the difference:

$ whoami
john
$ echo $USER
john
$ sudo echo $USER
john
$ sudo whoami
root

Observe above than when using sudo, the output of sudo whoami is root, because whoami is run as the root user. However, when we execute sudo echo $USER, the output is instead john, a human user's username.

The reason this happens is that Bash is substituting $USER by the value of the variable before sudo is executed, so our argument list does NOT look like this:

  1. sudo
  2. echo
  3. $USER (wrong!)

It looks like THIS:

  1. sudo
  2. echo
  3. john

The sudo command never gets $USER as an argument, so by the time echo is executed as root, $USER has already been evaluated as john.

One way this is used is to change ownership of files. For example:

sudo chown $USER some-file.txt

If john runs this command, it will call chown (change ownership) as root, but $USER will be evaluated as john. This makes this command change a file's ownership to whoever types the command in the terminal. This is useful when telling someone on the Internet to change the ownership of a files to make it theirs, for example, since you don't know what their username would be on the system, but their username will be whatever is contained in the $USER variable.

Note: there are many other ways to do variable substitution in Bash, but I won't explain them in this article.

~

The tilde (~) is substituted by the user's home directory (i.e. by $HOME). For example, if we do this:

ls ~/

The argument list will be:

  1. ls
  2. /home/john/

In other words, ls never sees the tilde we typed.

Naturally this works with sudo as well. When we sudo anything ~, root will do something with our home directory, not with root's home directory.

./

For the record, ./ isn't substituted like ~/. This means if we type:

ls ./

The argument list is actually going to be, literally:

  1. ls
  2. ./

And the program itself has to resolve ./ into the correct filepath using the current working directory environment variable ($PWD).

Fortunately, most programs won't actually need to do this themselves because there are countless utilities you can use that handle filepaths for you. But it's worth keeping in mind that the shell doesn't do anything in this case.

*

This is a fun one: Bash turns a star * into a variable number of arguments containing the filenames of all files in the current working directory except dotfiles. The order of the files is arbitrary. This is also called a glob pattern. For example, let's say that we have 4 files in the current working directory:

  1. foo.txt
  2. bar.png
  3. baz.jpg
  4. .fish

If we use this command:

echo *

The argument list that Bash will construct may be something like this:

  1. echo
  2. foo.txt
  3. bar.png
  4. baz.jpg

Observe how .fish isn't in this list because it's a dotfile (it starts with a dot), so Bash just kind of ignores it, I guess.

In other words, since echo just outputs all arguments we pass to it, this is what will happen:

$ echo *
foo.txt bar.png baz.jpg

At first glance, this looks like a completely useless feature, since terminal programs like mv and cp require the arguments to be in a specific order, there is no way we can use this asterisk that just places the filenames in random order. However, there are some commands in Linux that work very well with files in arbitrary order.

A good example is rm, the command that DELETES your files.

Danger: do not try to use the rm command from the terminal, as you may end up deleting your files by accident.

The rm command will delete all files that you pass to it as positional arguments, e.g. rm foo bar will remove both foo and bar. Likewise, rm * will remove all files in the current working directory.

Fun fact: rm normally can't delete directories, unless you pass the --recursive optional argument, or the -r flag, and it asks for confirmation, unless you pass the --force argument or -f. The command rm -rf is notorious for having the ability to just delete all your files, and this has happened many times in the past, including due to a space.

A red line numbered 351, with a minus meaning something was removed. It reads rm -rf /usr (a space character is a darker shade of red) /lib/nvidia-current/xorg/xorg.
The highlighted space between /usr and /lib was a tiny mistake that destroyed some people's entire systems. Fortunately, they only had to reinstall Linux, and shouldn't have lost personal files in this case. See [It Happened: Bumblebee does the rm -rf /usr on Linux].

Glob patterns are text patterns, and they can be used to filter all files the end in a specific file extension or that start in some way. For example, *.jpg will select only files that have the .jpg extension.

Although mv and cp normally are used with two positional arguments, you can actually use globbing with them. To do this, simply make the last argument a directory. For example:

mkdir my-jpegs
mv *.jpg my-jpegs

The command above will move all files with .jpg extension to a folder called my-jpegs which we created with mkdir.

We can also do the reverse. Let's say that we have a dozen different subfolders full of jpegs, and we want to move all of them to the parent folder, but doing this one by one in a file manager takes too long. It takes a single command:

mv */*.jpg ./

To elaborate, the argument list created by Bash for this command will be something like this:

  1. mv
  2. folder-1/file-1.jpg
  3. folder-1/file-2.jpg
  4. folder-2/file-1.jpg
  5. ./

The mv command will see that it has received more than 2 positional arguments, so it will assume the last positional argument is going to be the target, and anything before the last argument are the files to move. This is a very logical and very simple algorithm. That's why this sort of thing is able to work.

Other Peculiar Syntax

+

Some programs appear to use a + syntax for its arguments, e.g. chmod +rwx. This is completely up to the program and doesn't mean anything special.

Key-value arguments

Some programs support key-value arguments, e.g. --foo=bar. This is completely up to the program and doesn't mean anything special in Bash.

It's worth noting, however, that in this case you probably need to type the whole thing as a single argument, without spaces around the equals sign, i.e. --foo=bar is 1 argument, while -foo = bar is 3 arguments. The program may be able to parse this either way, but this behavior really depends on the program.

Note that you can surround this by quotes in Bash, e.g. "--foo=bar baz", and Bash will remove the spaces and turn it into a single argument as we've already seen, and this argument will start with two dashes (--) for the program even though it starts with a double quotes character when we type it.

Repeating Arguments

In general, programs do not treat repeating arguments specially. This means two things:

rm foo foo will try to remove foo twice. If it exists, it will succeed the first time, and fail on the second.

ls --all -all means the same thing as ls --all since the algorithm only checks if --all is present in the argument list, it doesn't do anything special if the optional argument appears multiple times.

However, once again, how the program parses its argument list is entirely upon the program. Some programs can have strange ways to process the arguments which hopefully will be explained in their manuals.

Written by Noel Santos.

About the Author

I'm a self-taught Brazilian programmer graduated in IT from a FATEC. In a world of increasingly complex and essential computers, I decided to use my technical expertise in hardware, desktop applications, and web technologies to create an informative resource to make PC's easier to understand.

View Comments