You write a script, type ./deploy.sh, and get “Permission denied”, “command not found” or bad interpreter. Each error has a simple cause once you know how Linux decides how to run a file.

The shebang line

The first line of a script tells the kernel which interpreter to use:

#!/bin/bash
echo "Hello from Bash"

When you execute the file, the kernel reads #! and the path after it, then runs /bin/bash ./script.sh. Other examples:

#!/bin/sh                    # POSIX shell (dash on Debian/Ubuntu)
#!/usr/bin/env bash          # bash, found via PATH
#!/usr/bin/env python3
#!/usr/bin/env node
#!/usr/bin/perl

Rules:

  • It must be the very first line, with #! as the first two bytes. There can be no blank line or byte-order mark before it.
  • Without a shebang, the file usually runs under /bin/sh, not Bash. Bash-only syntax ([[ ]], arrays) then fails in confusing ways.

#!/bin/bash or #!/usr/bin/env bash?

  • #!/bin/bash uses a fixed path. It’s predictable and works on virtually every Linux system.
  • #!/usr/bin/env bash finds bash in the PATH. It’s more portable, for example on macOS and BSD where a newer Bash lives elsewhere, and it respects virtualenvs and nvm for Python and Node.

Both are fine on Linux servers. For Python and Node, env is the usual choice. Passing extra arguments through env needs env -S (#!/usr/bin/env -S node --max-old-space-size=512).

sh is not bash

On Debian and Ubuntu, /bin/sh is dash, a minimal POSIX shell. A script with #!/bin/sh that uses Bash features gives errors like [[: not found or Syntax error: "(" unexpected. Either use #!/bin/bash or stick to POSIX syntax.

Make it executable

chmod +x deploy.sh
./deploy.sh

Without the execute bit you get Permission denied. See chmod. The file system also matters. On a partition mounted with noexec (often /tmp), even executable files can’t run directly.

Why ./ is needed

deploy.sh       # command not found
./deploy.sh     # works

The shell only looks up commands in the directories listed in $PATH. For safety, the current directory isn’t one of them. ./ gives an explicit path. To run a script by name from anywhere, put it in a directory on the PATH:

sudo install -m 755 deploy.sh /usr/local/bin/deploy
deploy

See the PATH variable.

Four ways to run a script

Command Shebang used? Needs +x? Runs in
./script.sh Yes Yes New process
bash script.sh No (bash forced) No New process
sh script.sh No (sh forced) No New process
source script.sh / . script.sh No No Current shell

source is different. The script runs inside your current shell, so variables, cd and functions it defines stay afterwards. That’s how ~/.bashrc, virtualenv activate scripts and .env loaders work. A script run with ./ can never change your shell’s directory or variables.

Common errors

/bin/bash^M: bad interpreter: No such file or directory — the file has Windows (CRLF) line endings. Fix it:

sed -i 's/\r$//' script.sh      # or: dos2unix script.sh

Configure your editor or Git (core.autocrlf=input) so it doesn’t come back. See cat -A to spot it.

Permission denied — no execute bit (chmod +x), a noexec mount, or no permission on the file or directory.

command not found — run it with ./ or a full path, or it isn’t in PATH.

bad interpreter: No such file or directory (without ^M) — the interpreter in the shebang doesn’t exist, for example #!/usr/bin/python on a system that only has python3.

Syntax error: "(" unexpected — Bash syntax running under sh. Check the shebang, and don’t start it with sh script.sh.

Works in the terminal, fails in cron — cron has a minimal PATH and environment. Use full paths or set PATH at the top of the script or crontab. See crontab.

Script hygiene

#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"     # work relative to the script's own directory
  • set -euo pipefail makes the script stop on errors. See Bash error handling.
  • cd "$(dirname "$0")" lets the script find files next to it, wherever it’s called from.
  • Name scripts without .sh when they’re installed as commands (/usr/local/bin/backup). The extension is irrelevant to Linux.

Scripts as systemd services or cron jobs

Use absolute paths everywhere and make sure the shebang is right. In a systemd unit, ExecStart=/usr/local/bin/backup executes the file directly, so it needs +x and a valid shebang.

Related: Bash scripting tutorial and script arguments.

  • Bash
  • Scripting
  • Linux