Sign in
← Terminal: bash scripts
4 min · easy · Lesson 2 of 5

Script with arguments

Accept an argument in a script and write it to a file.

A script that always does the same thing is rarely useful. Usually you need to pass it something from outside: a file name, an environment (staging or prod), a server address, a version. That's what command-line arguments are for. You write them after the script's name, separated by spaces, and the script receives them in special variables $1, $2, and so on.

Where to practice

The examples below work in an ubuntu:24.04 container. You can start the same one yourself:

docker run --rm -it ubuntu:24.04 bash

On Linux, macOS and in WSL2 the commands also work in your own terminal. There are no editors in the container, so we create files through cat and a heredoc: everything between the line with <<'EOF' and the EOF line goes into the file. The quotes around EOF keep the shell from substituting $1 and other variables while the file is being created.

Positional parameters

Create a script that shows what it got:

cat > /tmp/args.sh <<'EOF'
#!/bin/bash
echo "script: $0"
echo "arguments: $#"
echo "first: $1"
echo "second: $2"
echo "all: $@"
EOF
chmod +x /tmp/args.sh

Run it with two arguments:

/tmp/args.sh web 8080
script: /tmp/args.sh
arguments: 2
first: web
second: 8080
all: web 8080

What's what here:

An argument that wasn't passed isn't an error, it's an empty string. Run it without arguments:

/tmp/args.sh
script: /tmp/args.sh
arguments: 0
first: 
second: 
all: 

The script ran as if nothing happened, just with empty values. That's why scripts break silently: an empty $1 ends up in a path or a file, and nobody sees the error.

Starting from the tenth, the number is written in curly braces: ${10}. The shell reads $10 as $1 followed by the character 0. With the arguments a b c d e f g h i j, the line echo "$10 ${10}" prints a0 j.

Quotes

The shell splits a string into arguments by spaces. To pass a value with a space as a single argument, wrap it in quotes:

/tmp/args.sh "hello world" x
script: /tmp/args.sh
arguments: 2
first: hello world
second: x
all: hello world x

Inside the script, the same rule applies a second time. If you write $1 without quotes, the shell splits the value by spaces again and expands * into a list of files. Example: a script writes $1 to a file three different ways, and we run it in a directory that has f1.txt and f2.txt, with the argument 'build 42' (two spaces) and then '*.txt'.

command in the scriptbuild 42*.txt
printf '%s' $1build42f1.txtf2.txt
echo $1build 42 and \nf1.txt f2.txt and \n
printf '%s' "$1"build 42*.txt

The rule is simple: always put "$1", "$2", "$@" in double quotes, unless there's a specific reason not to.

The same goes for $@. "$@" passes the arguments as they are, $@ without quotes splits them again. Here's that loop in a script run with the arguments "a b" and c:

for a in "$@"; do echo "[$a]"; done
[a b]
[c]

Without quotes, the same loop would print three lines: [a], [b], [c].

Newline: echo and printf

echo adds a newline \n at the end. You won't see it on screen, but it's there in a file, and a program that reads the whole file gets the value with an extra character. printf '%s' "$1" prints the string as is, with no \n. You can see every byte of a file with od -c:

printf '%s' "build  42" > /tmp/out
od -c /tmp/out
0000000   b   u   i   l   d           4   2
0000011

With echo "build 42" > /tmp/out, a \n would show up at the end, and the size would be 10 bytes instead of 9 (od prints the offset in octal, so 9 there looks like 0000011).

There's also echo -n, but it trips up on values that look like flags: a value of -n would be treated as a flag and print nothing. printf '%s' doesn't have this problem.

Checking the number of arguments

Since an empty argument isn't an error, you have to check it yourself. Good practice: at the start of the script, compare $# to the number you need, and if there aren't enough arguments, print a hint and exit with an error:

cat > /tmp/usage.sh <<'EOF'
#!/bin/bash
[ $# -lt 1 ] && { echo "usage: $0 <arg>" >&2; exit 1; }
echo "ok: $1"
EOF
bash /tmp/usage.sh; echo "rc=$?"
usage: /tmp/usage.sh <arg>
rc=1

There are three details here:

The check should come before any work with files, or the script might damage something first.

Sometimes an argument is optional and has a default value. That's what ${1:-value} is for:

cat > /tmp/def.sh <<'EOF'
#!/bin/bash
ENV=${1:-staging}
echo "env=$ENV"
EOF
bash /tmp/def.sh; bash /tmp/def.sh prod
env=staging
env=prod

How to run a script

/tmp/args.sh runs the file directly: it needs permission to run (chmod +x) and a first line #!/bin/bash, which tells the system which shell to run it with. bash /tmp/args.sh runs it through bash explicitly, so execute permission isn't needed then. sh /tmp/args.sh runs it in sh: on Ubuntu that's dash, not bash. This lesson's examples work there too, but, for example, [[ ... ]] from bash isn't something dash knows.

Common mistakes

What you'll need for the tasks

Practice in your browser

Linux runs right here. Solve the task and we'll save your progress automatically.

01

The script accepts an argument

Create the script /root/echo.sh, which takes one argument and writes it (just it, with no trailing newline) to the file /tmp/last-arg. For example, ./echo.sh hello → /tmp/last-arg will contain hello.

{ i }

Go through it with an AI agent

The agent explains anything unclear in plain words, gives you 2 labs for this lesson and drills you on interview questions. $49 one-time, lifetime access.

Get access for $49

Tell the agent: use MCP izi, give me lesson terminal-scripts-02-arg-script