Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

A Linux shell script is a plain-text file containing commands that a shell executes in sequence. This guide uses Bash for its examples: create a file with a Bash shebang, add commands, check its syntax, run it with bash or directly after granting execute permission, then extend it with variables, arguments, conditions, loops, and error handling.

What a shell script is

A shell is a command interpreter such as Bash, Dash, Zsh, or KornShell. A shell command is one instruction typed at a prompt. A shell script is a text file containing commands that run non-interactively. A Bash script is a shell script that depends on Bash features.

Bash can read commands from a file and execute them in a non-interactive shell. The GNU Bash manual describes this file-based execution model.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

What you need

  • A Linux terminal
  • Bash (check with bash --version)
  • A text editor such as nano
  • Optional: ShellCheck for static analysis

Create your first Bash script

  1. Open a terminal editor: nano hello.sh.
  2. Enter this content:
    #!/usr/bin/env bash
    
    printf 'Hello, Linux!n'
  3. Save with Ctrl+O, press Enter, then exit with Ctrl+X.

The #!/usr/bin/env bash line is the shebang. It tells the operating system which interpreter to use when the file is executed directly. The .sh suffix is only a naming convention; the shebang and file contents determine interpretation.

You can create the same file without an editor:

cat > hello.sh <<'EOF'
#!/usr/bin/env bash

printf 'Hello, Linux!n'
EOF

Commands such as cat, chmod, and ./hello.sh are entered at the terminal. The lines between EOF markers become the script.

Run the script

Invoke Bash explicitly

bash hello.sh

This works even when the file is not executable because Bash reads it as input.

Execute it directly

chmod u+x hello.sh
./hello.sh

chmod adds the execute bit. Equivalent common modes are:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
chmod +x hello.sh   # Add execute permission according to the umask
chmod 755 hello.sh  # Owner reads/writes/executes; others read/execute
chmod 700 hello.sh  # Only the owner reads/writes/executes

Avoid chmod 777 as a routine fix: it grants unnecessary access. Direct execution also requires a valid shebang, an available interpreter, and a filesystem that permits execution.

Use a path

Most shells do not search the current directory automatically. Therefore hello.sh may produce “command not found,” while ./hello.sh works. You can also use an absolute path such as /home/alex/scripts/hello.sh, or install a script in a directory listed in $PATH.

Bash or POSIX sh?

Use #!/usr/bin/env bash when you need Bash features. Use #!/bin/sh only when you intentionally write POSIX-compatible shell code. On some distributions, including Ubuntu, /bin/sh points to Dash rather than Bash; see Ubuntu’s Dash documentation.

Choose Features and trade-off
#!/usr/bin/env bash [[ ... ]], arrays, (( ... )), local, Bash parameter expansions, and pipefail; requires Bash.
#!/bin/sh POSIX syntax and utilities; generally more portable, but without Bash-only features.

ShellCheck’s SC2039 explains why Bash syntax in an sh script can fail on another machine. A shebang also helps ShellCheck infer the intended shell (SC2148).

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Organize a readable script

#!/usr/bin/env bash

# Describe the script's purpose

main() {
    printf 'Running the script...n'
}

main "$@"

Use comments for intent, variables for values, functions for reusable operations, a clear main path, and meaningful exit statuses. A function’s local variables are Bash-specific.

Variables, quoting, and command substitution

#!/usr/bin/env bash

name="Ada"
today="$(date +%F)"
printf 'Hello, %s! Today is %s.n' "$name" "$today"

Assignments have no spaces around =. Read values with $name or ${name}. The $(...) form captures command output and is preferred over legacy backticks.

Quote expansions used as command arguments. Unquoted text can undergo word splitting and wildcard expansion, as documented in ShellCheck SC2086:

# Unsafe when the name contains spaces or wildcard characters
rm $file

# Safer
rm -- "$file"

Do not quote syntax where intentional splitting is required. For a list of arguments, arrays are safer:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
options=(-j 5 -B)
make "${options[@]}" file

Pass arguments to a script

#!/usr/bin/env bash

printf 'Script name: %sn' "$0"
printf 'First argument: %sn' "$1"
printf 'Argument count: %sn' "$#"

for arg in "$@"; do
    printf 'Argument: %sn' "$arg"
done

Here, $0 is the invocation name or path, $1 and later variables are positional arguments, $# is the count, and "$@" preserves each argument as a separate item. Run it with ./greet.sh "Ada Lovelace". $? contains the previous command’s exit status.

Conditions and loops

Test files with Bash conditions

if [[ -f "$1" ]]; then
    printf '%s is a regular filen' "$1"
else
    printf 'File not found: %sn' "$1" >&2
    exit 1
fi

[[ ... ]] is Bash syntax. Common tests include -e (any directory entry), -f (regular file), -d (directory), -r (readable), -x (executable), and [[ "$a" == "$b" ]] (string comparison). POSIX code instead uses forms such as [ -f "$1" ].

Loop over files

for file in "$HOME"/*.log; do
    [[ -e "$file" ]] || continue
    printf 'Log: %sn' "$file"
done

Without the existence check, an unmatched glob can remain a literal pattern.

Use arithmetic in a while loop

count=1
while (( count <= 3 )); do
    printf 'Count: %sn' "$count"
    ((count++))
done

(( ... )) is Bash-specific.

Functions and validation

backup_file() {
    local source_file=$1
    local destination=$2
    cp -- "$source_file" "$destination"
}

backup_file "notes.txt" "notes.txt.bak"

Validate arguments before using them, and send usage or error messages to standard error:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
#!/usr/bin/env bash

if (($# != 1)); then
    printf 'Usage: %s FILEn' "$0" >&2
    exit 1
fi

file=$1
if [[ ! -f "$file" ]]; then
    printf 'Error: not a regular file: %sn' "$file" >&2
    exit 1
fi

printf 'Processing %sn' "$file"

Exit statuses and error handling

Every command returns a status: zero conventionally means success; a nonzero value indicates failure. Check operations whose failure matters:

if cp -- "$source" "$destination"; then
    printf 'Backup createdn'
else
    printf 'Backup failedn' >&2
    exit 1
fi

set -u treats unset variables as errors. Bash’s set -o pipefail makes a pipeline report failure when an earlier component fails. These options are shell-dependent; SC3040 documents portability concerns. set -e is context-dependent and does not mean every failure always terminates the script. Prefer explicit checks instead of treating set -euo pipefail as a universal safety switch.

Redirection and pipelines

command > output.txt       # Replace standard output
command >> output.txt      # Append output
command 2> errors.txt      # Redirect standard error
command >all.log 2>&1     # Send both streams to one file
command | grep pattern      # Pipe output

For POSIX sh, use command >log 2>&1; Bash-only command &> log is not portable (SC3020).

Check, debug, and test

  1. Check syntax without running: bash -n script.sh.
  2. Trace executed commands: bash -x script.sh.
  3. Run static analysis: shellcheck script.sh or shellcheck -s bash script.sh.

ShellCheck finds common mistakes and portability hazards; it cannot prove that your business logic is correct. Test missing arguments, missing or unreadable files, absent commands, empty directories, filenames beginning with -, spaces, tabs, newlines, wildcard characters, and execution from a different working directory:

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
./script.sh "file with spaces.txt"
./script.sh "*.txt"
./script.sh ""

A practical file-inspection script

#!/usr/bin/env bash

set -u
set -o pipefail

usage() {
    printf 'Usage: %s FILEn' "$0" >&2
}

if (($# != 1)); then
    usage
    exit 1
fi

file=$1

if [[ ! -f "$file" ]]; then
    printf 'Error: file does not exist or is not a regular file: %sn' "$file" >&2
    exit 1
fi

printf 'File: %sn' "$file"
printf 'Size: %s bytesn' "$(wc -c < "$file")"

Save it as inspect.sh, check it, then run:

bash -n inspect.sh
chmod u+x inspect.sh
./inspect.sh "notes with spaces.txt"

Paths and working directories

./script.sh does not mean “run relative to the script’s own directory”; it means relative to the terminal’s current directory. Cron, services, SSH sessions, and CI jobs may start elsewhere and have a different $PATH.

When a Bash script must locate files beside itself, construct its directory deliberately:

script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"

Use this Bash-specific technique only when needed, and use absolute or deliberately constructed paths for important files.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Common errors

“Permission denied”

Run chmod u+x script.sh. If bash script.sh works but ./script.sh does not, inspect the execute bit, shebang, and whether the filesystem is mounted with execution disabled.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

“Command not found”

The program may be missing, outside $PATH, misspelled, or being referenced from the wrong working directory. Diagnose with:

command -v program
printf '%sn' "$PATH"
pwd

“Bad interpreter: No such file or directory”

The shebang may name a nonexistent interpreter, or the file may contain Windows CRLF endings. Inspect with:

command -v bash
file script.sh
sed -n '1p' script.sh | cat -A

Where appropriate, remove carriage returns with sed -i 's/r$//' script.sh.

“Syntax error near unexpected token”

Typical causes are Bash syntax being run by sh, an unmatched quote or delimiter, or CRLF endings. Run bash -n script.sh and ensure the declared interpreter matches the syntax.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Security rules worth adopting

  • Quote variable expansions and use -- before user-controlled filenames where supported.
  • Never use eval with untrusted input or build shell commands by concatenating input.
  • Inspect scripts copied from the internet before running them, especially with sudo, rm, recursive operations, or ownership changes.
  • Use unpredictable temporary-file creation methods rather than predictable names.
  • Do not expose secrets in command-line arguments, logs, or bash -x traces.
  • Validate destructive targets and consider confirmation or a dry-run mode.

When shell is not the best tool

Shell excels at orchestrating existing command-line programs. Choose Python, Go, or another language for complex data structures, substantial JSON or CSV processing, sophisticated recovery logic, cross-platform applications, large parsers, networking, unit-test-heavy code, or performance-sensitive workloads.

FAQ

Do shell scripts need a .sh extension?

No. The extension helps humans recognize the file; permissions, the shebang, and the chosen interpreter control execution.

How can I check which Bash version is installed?

Run bash --version. The GNU manual’s current edition is Bash 5.3, updated May 18, 2025, but distributions may ship different versions.

How do I make a script available as a command?

Place an executable copy in a user-owned directory included in $PATH, then invoke it by name. Avoid relying on the current directory.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.