How to Run a Shell Script in Linux
The short answer:
chmod +x script.sh
./script.sh
Or, without changing permissions:
bash script.sh
The rest of this post explains what those two lines do, the differences between the ways to run a script, and the errors you will meet along the way.
A script that runs commands
A shell script is a text file with commands, run top to bottom:
#!/usr/bin/env bash
set -euo pipefail
src="${1:-/var/www/my-site}"
dest="/backup/$(basename "$src")-$(date +%F).tar.gz"
echo "Backing up $src to $dest"
tar -czf "$dest" -C "$(dirname "$src")" "$(basename "$src")"
ls -lh "$dest"
- The first line is the shebang. It tells the kernel which interpreter runs the file.
#!/usr/bin/env bashfindsbashinPATH;#!/bin/bashworks on every mainstream Linux distro too. It must be the very first line, with nothing before#!. set -euo pipefailstops the script at the first failing command (-e), treats unset variables as errors (-u), and makes a pipeline fail if any part of it fails (pipefail). It is not a complete safety net, since-eis ignored insideifconditions and a few other places, but it catches most mistakes.- Quote variables (
"$src"), or paths with spaces will break.
To chain commands on one line, && runs the next command only if the previous one succeeded, || only if it failed, and ; always:
cd /srv/my-app && git pull && docker compose up -d
Run it
1. Make it executable. chmod +x backup.sh adds execute permission for everyone; chmod u+x backup.sh only for the owner. See chmod in Linux for the details.
2. Run it with a path.
./backup.sh # from the current directory
~/scripts/backup.sh # from anywhere
/home/deploy/scripts/backup.sh # absolute path
The ./ is required because the current directory is not in PATH. That is deliberate: otherwise a file named ls dropped into a shared directory would run instead of the real ls. Typing just backup.sh gives command not found.
3. Or install it as a command. Put it in a directory that is in PATH:
mkdir -p ~/.local/bin
mv backup.sh ~/.local/bin/backup
backup /var/www/my-site
Ubuntu's default ~/.profile adds ~/.local/bin to PATH once the directory exists (log in again after creating it), and the RHEL-family ~/.bashrc adds it as well. Otherwise add export PATH="$HOME/.local/bin:$PATH" to ~/.bashrc. For all users, use /usr/local/bin with sudo.
./script.sh vs bash vs sh vs source
| Command | Needs +x | Interpreter | Runs in |
|---|---|---|---|
./script.sh | yes | from the shebang | a new process |
bash script.sh | no | bash | a new process |
sh script.sh | no | sh (dash on Debian and Ubuntu) | a new process |
source script.sh or . script.sh | no | your current shell | your current shell |
Two traps here:
- On Debian and Ubuntu,
/bin/shisdash, notbash. A bash script run withsh script.shfails on bash features such as[[ ]]or arrays, with errors like[[: not found. Run bash scripts withbashor via the shebang. On the RHEL familyshis bash, which hides the problem until the script reaches an Ubuntu host. sourceruns the file in your current shell. Variables,cdand exported settings stay after it finishes, which is the point for files likevenv/bin/activate. But anexitinside a sourced file closes your terminal session.
Pass arguments
./deploy.sh staging v1.4.2
Inside the script, $1 is staging, $2 is v1.4.2, $# is the number of arguments, "$@" is all of them and $0 is the script name:
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -lt 2 ]]; then
echo "Usage: $0 ENVIRONMENT VERSION" >&2
exit 1
fi
case "$1" in
staging|production) echo "Deploying $2 to $1" ;;
*) echo "Unknown environment: $1" >&2; exit 1 ;;
esac
After a script finishes, echo $? shows its exit code. 0 means success, anything else is a failure.
Run other files and programs
- Installed programs run by name,
nginx -torpython3.command -v nameshows which file that is. - A downloaded binary needs
chmod +x my-tooland then./my-tool. - A Python script runs with
python3 script.py, or directly with#!/usr/bin/env python3as the first line pluschmod +x. - An AppImage runs like a binary after
chmod +x. Many still need the FUSE 2 library (libfuse2t64on Ubuntu 24.04). .deband.rpmfiles are not run, they are installed:sudo apt install ./package.deborsudo dnf install ./package.rpm.
As root, in the background, on a schedule
sudo ./backup.sh # as root
sudo -u deploy ./deploy.sh staging v1.4.2 # as another user
./long-job.sh & # background; jobs, fg, bg
nohup ./long-job.sh > job.log 2>&1 & # keeps running after logout
sudo backup without a path may fail with command not found even though it works for your user. sudo uses its own secure_path, which does not include ~/.local/bin. Use the full path.
For long jobs over SSH, tmux is more convenient than nohup: detach with Ctrl+B then D, and reattach later with tmux attach. For recurring jobs, use cron (crontab -e) or a systemd timer. Cron runs with a minimal PATH, so use absolute paths for the script and for the commands inside it:
0 3 * * * /home/deploy/.local/bin/backup >> /home/deploy/backup.log 2>&1
Common errors
Permission denied: the file is not executable. Runchmod +x. If it already is, the filesystem may be mountednoexec, which is common for/tmpon hardened servers; move the script or runbash script.sh.command not found: you forgot./, or the directory is not inPATH./bin/bash^M: bad interpreter: No such file or directory: the file has Windows line endings. Fix withsed -i 's/\r$//' script.shordos2unix script.sh.cannot execute binary file: Exec format error: a binary built for another CPU architecture. Comparefile my-toolwithuname -m.syntax error near unexpected tokenor[[: not found: a bash script run bysh.
To see what a script is doing, run it with bash -x script.sh, which prints every command before it runs. shellcheck script.sh (package shellcheck) finds quoting bugs and other mistakes before you run anything.
Scripts from the internet
curl ... | bash executes code you have not read, with your privileges. Download it, read it, then run it:
curl -fsSLo install.sh https://example.com/install.sh
less install.sh
bash install.sh
Quick reference
chmod +x script.sh # make executable
./script.sh # run from the current directory
bash script.sh # run without execute permission
source env.sh # run in the current shell
./script.sh arg1 arg2 # with arguments ($1, $2, "$@")
echo $? # exit code of the last command
bash -x script.sh # trace every command
sed -i 's/\r$//' script.sh # fix Windows line endings
nohup ./script.sh > out.log 2>&1 & # keep running after logout
mv script.sh ~/.local/bin/name # run it by name from anywhere
