# Your first shell script

> Write a shell script from nothing: what a script is, the first line, making it run, running it, a variable, a loop, and what to do when it goes wrong.

Source: https://wolos.org/manual/first-shell-script

## What it is

A script is a text file with commands in it, one under the other. You write the commands once. From then on, one short word runs all of them, in order, the same way every time.

A shell script is written in the language you already type in the terminal. Everything [What a terminal is](/manual/terminal) and [zsh basics](/manual/zsh-basics) taught works inside one: `ls`, `cd`, `echo`, paths, quotes. This page writes the scripts in bash, the shell language of most scripts you will find on the web; it reads almost the same as zsh.

You need nothing you do not already have: Text Editor to write the file, and a terminal to run it.

## Why it is worth it

Anything you type more than twice is worth a script. A script does not forget a step, does not mistype a folder name at the end of a long day, and does the job the same way next month. It is also easy to share: send the file, and someone else does exactly what you did.

WolOS treats a script with care. It runs as you, with your permissions, never as the system's own account. Before you run a script you can ask WolOS to read it and point out mistakes, without running a single line.

## How

### 1. Make a file

Open **Text Editor** (press Super+Space and type "text"), and type these four lines exactly:

```bash
#!/usr/bin/env bash
# My first script: says hello.
echo "Hello!"
echo "Today is $(date +%A)."
```

Save it in your home folder with the name `hello`. A script needs no ending on its name; the first line says what it is.

What each line does:

| Line | Means |
|---|---|
| `#!/usr/bin/env bash` | The first line, called the shebang. It says: "read this file with bash". It must be the very first line, with nothing before `#!`. |
| `# My first script: says hello.` | A comment. Every line that starts with `#` is for people; the computer skips it. Say what the script does, so you still know in a year. |
| `echo "Hello!"` | Prints Hello! in the terminal. |
| `echo "Today is $(date +%A)."` | `$(…)` runs a command and puts its answer into the text. `date +%A` answers with the name of the day. |

### 2. Make it run

Open a terminal (Super+Return). Your home folder is where the terminal starts, so the file is right there. Type:

```
chmod +x hello
```

and press Return. `chmod +x` gives the file permission to run, the way a program has it. It prints nothing; silence means it worked. You do this once per script.

> **Why this step:** a text file you download or write is never allowed to run on its own. You decide which files are programs. That keeps a file that only looks like a document from starting anything.

### 3. Run it

```
./hello
```

You see:

```
Hello!
Today is Wednesday.
```

`./` means "in the folder I am in". The terminal runs only programs it knows by name, so a script of your own needs that short path in front of it.

You can also run it with WolOS's own command:

```
wol script run hello
```

That works even before `chmod +x`. And to have the script read for mistakes without running any of it:

```
wol script check hello
```

When everything is right you see one line, `hello: bash script, parses`. *Parses* means every line made sense. With the Developer module installed, the line also says that a style checker found nothing to improve.

### 4. A variable

A variable is a name that holds a value. Make a new file, `greet`:

```bash
#!/usr/bin/env bash
# Says hello to someone.
name="Alice"
echo "Hello, $name!"
```

`name="Alice"` puts the word Alice in the variable `name`. `$name` takes it out again. Run `chmod +x greet`, then `./greet`, and you see `Hello, Alice!`.

> **Careful:** No spaces around `=`. `name = "Alice"` is read as a command called `name`, and you see `name: command not found`.

A script becomes more useful when you tell it the value when you run it. Change the third line to:

```bash
name="$1"
```

`$1` is the first argument you give the script. Now:

```
./greet Alice
./greet "Alice Liddell"
```

print `Hello, Alice!` and `Hello, Alice Liddell!`. The quote marks around Alice Liddell make the two words one argument.

> **Tip:** Always write a variable inside double quotes, as `"$name"` and `"$1"`. Without them, a value with a space in it, such as *Summer Trip.jpg*, falls apart into two words, and the command looks for two files that do not exist.

### 5. A loop

A loop runs the same lines once for each item in a list. This script lists the photos in a folder of Pictures and counts them. Save it as `photos`:

```bash
#!/usr/bin/env bash
# Lists the photos in a folder of Pictures and counts them.
# How: ./photos Holiday
folder="$HOME/Pictures/$1"
if [ ! -d "$folder" ]; then
  echo "There is no folder called $1 in Pictures."
  exit 1
fi
count=0
for photo in "$folder"/*; do
  echo "Found: $(basename "$photo")"
  count=$((count + 1))
done
echo "$count photos in $1."
```

Run `chmod +x photos`, then `./photos Holiday`:

```
Found: Summer Trip.jpg
Found: beach.jpg
Found: cake.JPG
3 photos in Holiday.
```

Read it from the top:

| Lines | Mean |
|---|---|
| `folder="$HOME/Pictures/$1"` | `$HOME` is your home folder. With `Holiday`, the variable holds the path of Pictures/Holiday. |
| `if [ ! -d "$folder" ]; then` … `fi` | If there is no such folder (`!` is *not*, `-d` is *is a folder*), say so and stop. `fi` closes the `if`. |
| `exit 1` | Stops the script and reports that it failed (below). |
| `count=0` | A variable to count with. |
| `for photo in "$folder"/*; do` … `done` | The loop. `*` stands for every file in the folder; for each one, `photo` holds its path and the lines between `do` and `done` run. |
| `basename "$photo"` | The name of the file without the folders in front of it. |
| `count=$((count + 1))` | `$((…))` does arithmetic: one more than before. |

The name *Summer Trip.jpg* has a space and still comes through whole, because every variable is in double quotes.

### 6. Did it work? The exit status

Every command ends with an exit status: a number, 0 when it worked, anything else when it did not. The prompt arrow turns red after a command that did not end with 0, and `echo $?` prints the number of the last one:

```
./photos Holidya
There is no folder called Holidya in Pictures.
echo $?
1
```

`exit 1` in the script is how `photos` says "this did not work". Use 1 for a failure and let the script end normally when everything went well; the end of a script is a 0.

By itself, bash carries on after a command fails. To make a script stop at the first command that fails, put this line right under the first one:

```bash
set -e
```

## A worked example

Alice wants a copy of her Documents folder every Friday, kept in a folder named after the day. She writes `backup`:

```bash
#!/usr/bin/env bash
# Copies Documents into a folder in Backups named after today.
# How: ./backup
set -e
today=$(date +%Y-%m-%d)
mkdir -p "$HOME/Backups/$today"
cp -r "$HOME/Documents" "$HOME/Backups/$today/"
echo "Saved a copy in Backups/$today."
```

`date +%Y-%m-%d` answers with the date as 2026-11-04. `mkdir -p` makes the folder, and the folders above it if they are missing. `cp -r` copies a folder with everything in it. `set -e` makes sure the last line is printed only when the copy really happened.

She runs `wol script check backup` (it says `backup: bash script, parses`), then `chmod +x backup` and `./backup`, and sees `Saved a copy in Backups/2026-11-04.` In Files, Backups now holds a folder for today with Documents inside. Every Friday she presses Super+Return, types `./backup` and presses Return.

## If something goes wrong

The terminal tells you in one line what it did not like. The line often starts with the script's name and the number of the line in the file.

| What you see | What it means and what to do |
|---|---|
| `zsh: permission denied: ./hello` | The file may not run yet. Type `chmod +x hello` once, then run it again. |
| `zsh: command not found: hello` | You left out `./` in front of the name. Type `./hello`. |
| `zsh: no such file or directory: ./hello` | No file has that name in the folder you are in. `ls` shows what is there; capitals matter. |
| `line 2: name: command not found` | Spaces around `=`. Write `name="Alice"`, with no spaces. |
| `line 3: ech: command not found` | A mistyped command on that line. Fix the spelling, save, and run it again. Without `set -e` the script carries on after this. |
| `unexpected EOF while looking for matching` and a quote mark | A quote mark is opened and never closed. `wol script check` names the line; count the quote marks on it. |
| `Hello, !` | The script needs an argument and got none. Add it after the name: `./greet Alice`. |
| `Found: *` and `1 photos` | The folder is empty or its name is wrong, so `*` matched nothing. The check at the top of `photos` catches a wrong name. |
| `/usr/bin/env: 'bash\r': No such file or directory` | The file was saved on Windows, with line endings bash does not read. Open it in Text Editor and save it again. |
| The script runs for ever | Press **Ctrl+C** to stop it, then look at the loop: something in it never ends. |

The next step is [Your first Ruby script](/manual/first-ruby-script): the same ladder, with a language that keeps a name with a space in it whole without any quote marks. [Scripts and shells](/manual/scripts-and-shells) shows how to run a script on another computer.
