File types · Explainer

The shell script (.sh, .bash)

A shell script is a plain-text file of commands that a Unix shell such as bash runs one after another, as if you'd typed them into a terminal. This guide explains what's inside one, how to run it on each system, the errors worth knowing, and has free sample shell scripts to download for testing.

At a glance

Full name
Shell script (for the Bourne shell family: sh, bash, dash, zsh, ksh)
Extensions
.sh, .bash; often none at all, since Unix doesn't need one
MIME type
None registered; application/x-sh and text/x-shellscript are common
Kind
Plain text (commands)
Standard
POSIX Shell Command Language (IEEE Std 1003.1, latest edition 2024); bash and zsh add their own features
First released
The Bourne shell in Version 7 Unix (Bell Labs, 1979); bash in 1989, for the GNU Project
Current bash
Bash 5.3, maintained by Chet Ramey
Runs with
sh or bash on Linux and macOS; WSL or Git Bash on Windows
Line endings
LF only: Windows CRLF line endings break scripts
Magic number
#! (23 21) at the very start: the operating system reads it to choose the interpreter

The basics

What is an SH file?

A .sh file holds commands for a Unix shell: the program behind a Linux or macOS terminal that reads commands and runs them. Saving commands in a file turns a sequence you'd type by hand into a program you can rerun, share and schedule. Installers, build and deployment scripts, CI jobs, Docker container start-up scripts and nightly maintenance jobs are very often shell scripts.

Most of a shell script is calls to other programs (cp, grep, git, curl), glued together with the shell's own language: variables, if tests, loops, functions and pipes (|) that feed one program's output into the next.

  • The extension is a convention. Linux and macOS decide how to run a file from its execute permission and its #! line, not its name. Many scripts have no extension at all.
  • "Shell" means a family. sh is the standard POSIX shell; bash, zsh and ksh are supersets with extra features, and dash and BusyBox ash are small, fast POSIX shells. A script written for bash may not run in plain sh.
  • Windows has its own script formats: batch files (.bat, .cmd) and PowerShell (.ps1). It doesn't run .sh files without a Unix-like environment such as WSL.

Anatomy

What's inside a shell script

A shell script is a list of commands, one per line, plus the shell's own variables, tests, loops and functions to tie them together. Here is a short bash script that copies notes into a dated backup folder, part by part.

backup-notes.sh
#!/usr/bin/env bash
# backup-notes.sh: copy Markdown notes into a dated folder
set -euo pipefail

SOURCE_DIR="${1:-$HOME/notes}"
TARGET_DIR="$HOME/backups/$(date +%Y-%m-%d)"

log() {
  printf '[%s] %s\n' "$(date +%H:%M:%S)" "$*" >&2
}

if [[ ! -d "$SOURCE_DIR" ]]; then
  log "No folder at $SOURCE_DIR"
  exit 1
fi

mkdir -p "$TARGET_DIR"
count=0
for file in "$SOURCE_DIR"/*.md; do
  [[ -e "$file" ]] || continue
  cp -- "$file" "$TARGET_DIR/"
  count=$((count + 1))
done

log "Copied $count file(s) to $TARGET_DIR"
  1. 1#!/usr/bin/env bash

    The shebang

    When you run ./backup-notes.sh, the operating system reads these first bytes and starts bash with the script. It must be the very first line, with nothing before #!, not even a byte order mark.

  2. 2# backup-notes.sh: …

    A comment

    Everything after # is ignored. A comment also swallows an invisible \r at the end of a line, which is why CRLF damage shows up on commands and not on comments.

  3. 3set -euo pipefail

    Strict mode

    Stop at the first failing command, treat unset variables as errors, and count failures anywhere in a pipeline. Bash and zsh support all three; pipefail fails in some /bin/sh shells.

  4. 4SOURCE_DIR="${1:-$HOME/notes}"

    Variables and arguments

    $1 is the first argument given to the script; ${1:-…} supplies a default when there isn't one. No spaces around =: A = 1 would run a command called A.

  5. 5$(date +%Y-%m-%d)

    Command substitution

    Runs a command and inserts its output, here a date such as 2026-10-27.

  6. 6log() { … >&2 }

    A function

    Called like a command: log "message". "$*" joins its arguments, and >&2 sends the output to standard error, keeping messages apart from data.

  7. 7[[ ! -d "$SOURCE_DIR" ]]

    A test

    -d asks whether a folder exists. [[ … ]] is bash; portable sh scripts use [ … ], the test command.

  8. 8exit 1

    The exit status

    Ends the script and tells whoever ran it that something failed. 0 means success. A script's status is otherwise that of its last command.

  9. 9mkdir -p "$TARGET_DIR"

    Quoting

    Double quotes keep a value in one piece. Without them, a folder called My Notes would split into two arguments. Quote every variable unless you have a reason not to.

  10. 10[[ -e "$file" ]] || continue

    An empty glob guard

    If no .md files exist, the pattern *.md is left as literal text. This line skips that case instead of trying to copy a file called *.md.

  11. 11cp -- "$file" …

    End of options

    -- tells cp that what follows are file names, so a file named -r.md can't be mistaken for an option.

Under the hood

./script.sh vs bash script.sh vs source

There are four common ways to run a script, and they don't all use the same shell:

CommandNeeds chmod +xWhich shell runs itWhere it runs
./script.shYesThe one named in the shebangA new process
bash script.shNobash; the shebang is ignoredA new process
sh script.shNoWhatever /bin/sh is (dash on Debian and Ubuntu); the shebang is ignoredA new process
source script.sh or . script.shNoYour current shellYour current shell: it can change your folder and variables, and exit closes your session

With ./script.sh, the operating system's kernel reads the first two bytes. If they are #!, it runs the program named on that line and passes it the script's path. The shell then reads the script a command at a time and runs each as it goes. Unlike Python or Ruby, it doesn't check the whole file first: a syntax error on line 50 appears only after lines 1 to 49 have run.

Don't edit a script while it runs

Because bash reads the file as it goes, saving changes to a script that is still running can make it continue from the wrong place in the new version. Long-running scripts are safer edited as a copy.

Shebangs

#!/bin/sh vs #!/usr/bin/env bash, and POSIX sh vs bash

  • #!/bin/sh promises a portable POSIX script. On Debian and Ubuntu /bin/sh is dash, on Alpine Linux it's BusyBox ash, elsewhere often bash in POSIX mode. Use only POSIX features and test with dash.
  • #!/bin/bash uses the bash at that fixed path. On macOS that's bash 3.2, the last version released under the GPLv2 licence, which Apple has kept; zsh has been the default login shell since macOS 10.15.
  • #!/usr/bin/env bash uses the first bash on the PATH, such as a newer one from Homebrew, and works on systems where bash lives in /usr/local/bin (FreeBSD, for example).
  • One argument only. Linux passes everything after the interpreter as a single argument, so #!/usr/bin/env bash -e fails with "use -[v]S to pass options in shebang lines". Put set -e in the script instead.
Common bash features and their POSIX sh equivalents
bashPOSIX shNote
[[ $a == b* ]]case $a in b*) … ;; esacdash says [[: not found
names=(ada bob)set -- ada bobArrays don't exist in sh: Syntax error: "(" unexpected
function name { … }name() { … }Both work in bash
local x=1Prefixed global names, such as _x=1Not in POSIX, though dash and ash support local
echo -e "a\tb"printf 'a\tb\n'echo options differ between shells; printf doesn't
set -o pipefailCheck each command's statusSome /bin/sh shells reject it: dash on Ubuntu 24.04 says Illegal option -o pipefail

The sample-posix.sh file below is written to POSIX rules and runs in dash, bash, zsh's sh mode, ksh and BusyBox ash; sample-script.sh uses bash features on purpose.

Line endings

CRLF line endings and the "bad interpreter: /bin/bash^M" error

Windows ends lines with two characters, carriage return and line feed (CRLF, \r\n); Linux and macOS use only LF. Shells treat the \r as part of the line. A script saved on Windows, or checked out by Git with core.autocrlf on, fails in ways that look unrelated:

What you seeWhat happened
bash: ./script.sh: /bin/bash^M: bad interpreter: No such file or directoryThe shebang names a program called /bin/bash\r, which doesn't exist. ^M is how terminals show \r
./script.sh: cannot execute: required file not foundThe same problem, as reported by recent bash versions
/usr/bin/env: 'bash\r': No such file or directoryThe same problem with an env shebang
$'\r': command not foundAn empty line, which is now a line holding just \r
syntax error near unexpected token $'do\r'do, then and fi followed by \r aren't keywords any more
!ello, Ada instead of Hello, Ada!A variable picked up the \r, which moves the cursor back to the start of the line
terminal
# Check: "with CRLF line terminators" means Windows line endings
file script.sh

# Fix, any of these:
dos2unix script.sh
sed -i 's/\r$//' script.sh          # GNU sed (Linux, Git Bash, WSL)
sed -i '' 's/\r$//' script.sh       # BSD sed (macOS)

To stop it coming back, set your editor to LF for shell scripts, and add a .gitattributes rule so Git never converts them:

.gitattributes
# .gitattributes: always check out shell scripts with LF line endings,
# even on Windows with core.autocrlf switched on
*.sh   text eol=lf
*.bash text eol=lf

A UTF-8 byte order mark at the start of the file breaks the shebang the same way: the file no longer starts with #!, so the shell tries to run the #! line itself as a command. sample-crlf.sh below reproduces the CRLF problem safely.

How to

How to run, edit and check a shell script

LinuxmacOSWindows
Shell availablebash, nearly always; /bin/sh may be dashzsh (default), bash 3.2, shNone by default: install WSL, or Git for Windows (Git Bash)
Run a scriptbash script.shbash script.sh or zsh script.shwsl bash script.sh, or bash script.sh in Git Bash
Run it as a programchmod +x script.sh, then ./script.shchmod +x script.sh, then ./script.shInside WSL or Git Bash, as on Linux
Double-clickSome file managers ask whether to run itOpens in an editor; a .command file runs in TerminalNothing by default; Git for Windows can link .sh files to Git Bash
  • chmod +x once. Execute permission is part of the file's metadata. Files saved from a browser or an email don't have it, which is why a downloaded script gives Permission denied until you run chmod +x.
  • Why ./? The shell looks for commands only in the folders on your PATH, which normally doesn't include the current folder. ./script.sh says "this folder".
  • WSL runs a real Linux distribution on Windows: wsl --install in an administrator terminal sets it up. Git Bash is lighter, with bash and the common Unix tools, and is enough for most scripts.
  • Read or edit it: any plain-text editor. VS Code (with the ShellCheck extension) highlights problems as you type; on a server, nano and vim are usually available. Make sure the editor saves with LF line endings.
  • Check the syntax without running it: bash -n script.sh parses the whole file and reports syntax errors. It can't catch mistakes that only show up when a command runs.
  • Lint it: shellcheck script.sh, or paste it into shellcheck.net.
  • Trace it: bash -x script.sh prints each command, with variables filled in, before running it. Careful: that includes any passwords the script handles.
  • Format it: shfmt indents and formats shell scripts consistently.

Writing it safely

set -euo pipefail, quoting and other habits

  • set -e stops the script when a command fails, instead of carrying on with half-finished work. It doesn't apply to commands in an if test or before && and ||, and local x=$(cmd) hides cmd's failure, so it's a safety net, not a guarantee.
  • set -u turns a misspelt or missing variable into an error (unbound variable) rather than an empty string. Use ${1:-} for arguments that may be absent.
  • set -o pipefail makes cmd | sort fail when cmd fails. By default only the last command in a pipeline counts.
  • Quote variables: "$file", and "$@" to pass on all arguments intact. Unquoted, a value is split at spaces and its * and ? are expanded as file patterns.
  • Prefer printf to echo for anything but plain text: what echo does with -e and backslashes differs between shells.
  • Check with ShellCheck. It finds unquoted variables, bash features in sh scripts, CRLF line endings and many other bugs before you run anything.

Troubleshooting

Errors when running a shell script

Error messageUsual causeFix
Permission deniedThe file isn't executable, often after a download or unzipchmod +x script.sh, or run bash script.sh
script.sh: command not foundTyped script.sh without ./, so the shell searched the PATHRun ./script.sh
bad interpreter: … ^M, or $'\r': command not foundWindows (CRLF) line endingsdos2unix script.sh; see the section above
Syntax error: "(" unexpected / [[: not foundA bash script run by dash, through sh script.sh or a #!/bin/sh shebangRun it with bash, fix the shebang, or rewrite it to POSIX
set: Illegal option -o pipefailpipefail in a script that dash runsUse bash, or drop the option
unbound variableset -u and a variable that was never set, often a missing argumentPass the argument, or use ${1:-default}
No such file or directory, for a file that existsAn unquoted path with a space in it was split in twoQuote it: "$path"

Security

Is it safe to run a shell script? And curl | sh

Opening a .sh file in a text editor only shows its commands. Running it gives it everything your user account can do, and with sudo, everything the computer can do. There is no sandbox.

  • curl https://… | sh runs code you haven't seen. Many installers ask for this. If the download is cut off halfway, the shell runs the truncated part; and what the server sends to curl isn't necessarily what you saw when you opened the address in a browser. Safer: download the script, read it, then run it, and check the publisher's checksum if one is given.
  • Pasted commands are scripts too. Instructions to paste a command into Terminal to "fix" or "verify" something are a common way to get people to install malware.
  • Keep secrets out of scripts. Tokens written into a script end up in version control and in every copy. Read them from environment variables or a secrets manager, and keep set -x off around them.
  • Treat input as data. Never pass user input to eval. Quote every variable, and put -- before file names, so a value containing spaces, * or a leading - can't change what a command does.

An unquoted or unset variable in a command that deletes or overwrites files can hit far more than intended. set -u, quoting and ShellCheck exist largely because of mistakes like this.

Free downloads

Sample shell scripts to download

Free sample shell scripts for testing editors, syntax highlighters, ShellCheck, CI pipelines, upload filters and size limits. None of them change or delete files. All pass ShellCheck and run without errors, except sample-crlf.sh, which is broken on purpose.

sample-script.sh · 3.7 KBDownloadOpen
Loading…

What's inside sample-script.sh

The file is commented: open it in a text editor and each numbered comment marks one of these parts. Run it with bash sample-script.sh; it summarises a built-in sample log, or a log file you name.

  1. Comments 1 and 2: shebang and strict mode

    #!/usr/bin/env bash, written to work with the old bash 3.2 on macOS, then set -euo pipefail with an explanation of each option.

  2. Comments 3 and 4: settings and here-documents

    readonly values, ${0##*/} for the script's own name, and functions that print a usage message and a sample log with here-documents.

  3. Comment 5: options

    getopts parses -n NUMBER, -f, -h and -v, with error messages for a missing or non-numeric value.

  4. Comments 6 and 7: input and the read loop

    ${1:-} for an optional file argument, a readability check, then while IFS= read -r line with case patterns to count each level.

  5. Comments 8 and 9: output and arrays

    printf for aligned columns, quoting explained, and an array of error lines printed with a C-style for loop.

  6. Comment 10: exit status

    Exits with 0, or with 1 when -f is given and the log contains errors, so a CI job can stop on it.

All sample Shell script files

  • Commented bash script

    sample-script.sh · 3.7 KB (3,746 bytes)

    A log summariser with options, a here-document, a read loop, arrays and exit codes, with numbered comments explaining each part. Passes ShellCheck with no warnings.

    • Checking editors, highlighters and ShellCheck integrations
    • Teaching the parts of a bash script
  • Portable POSIX sh script

    sample-posix.sh · 1.8 KB (1,832 bytes)

    A Celsius to Fahrenheit table in POSIX sh, with integer-only arithmetic and a table of bash features and their POSIX equivalents in the comments. Runs in dash, bash, ksh and BusyBox ash.

    • Testing `/bin/sh` on minimal systems and containers
    • Comparing POSIX sh with bash
  • Windows (CRLF) line endings (deliberately broken)

    sample-crlf.sh · 1.1 KB (1,144 bytes)

    A short bash script saved with CRLF line endings. Clearly labelled; ./sample-crlf.sh fails with the bad interpreter error, and bash sample-crlf.sh shows $'\r': command not found and a variable damaged by \r.

    • Reproducing the ^M errors safely
    • Testing line-ending conversion in Git, editors and uploads
  • UTF-8 encoding test

    sample-utf8.sh · 1.2 KB (1,277 bytes)

    Greetings in 12 languages plus symbols and emoji, printing each string's length as the shell counts it and its size in bytes. The counts change with the shell and locale.

    • Checking terminal and locale settings
    • Spotting encoding bugs in editors and pipelines
  • 100 KB shell script

    sample-100kb.sh · 100 KB (102,400 bytes)

    A POSIX sh script with a 5,741-line here-document of made-up stock records and a loop that totals them, padded with a comment to exactly 102,400 bytes.

    • Upload and size-limit tests
    • Comparing shell speed on a long input

Ways to test with them

  • Line endings: pass sample-crlf.sh through Git, an editor or an upload, then run file sample-crlf.sh. If it no longer says CRLF, the tool converted it, and ./sample-crlf.sh starts working.
  • Which /bin/sh? sh sample-script.sh fails on Debian and Ubuntu, where sh is dash, while sh sample-posix.sh works everywhere.
  • Locale: run sample-utf8.sh with bash, then with LC_ALL=C bash sample-utf8.sh: the character counts become byte counts.
  • Upload filters: some email services and upload forms block .sh files. sample-100kb.sh is exactly 102,400 bytes, for testing a size limit at the boundary.

Free to use for testing, teaching and demos, commercially or not, with no attribution needed. A link back to this page is always appreciated.

Put it online

Have a shell script? Give it a link.

Drop a .sh file on 99helpers and it gets its own address in seconds. Opening the link shows the script as plain text in the browser, so people can read it before they run it; it is never executed on the server.

Questions

Shell script files, frequently asked

What is a .sh file?

A shell script: a plain-text file of commands for a Unix shell such as bash. Running it carries out the commands in order, as if you'd typed them into a terminal.

How do I run a .sh file?

In a terminal, run bash script.sh. To run it as ./script.sh, make it executable first with chmod +x script.sh. On Windows, use WSL or Git Bash.

What's the difference between sh and bash?

sh is the standard POSIX shell language; bash is a superset with arrays, [[ ]] tests and more. On Debian and Ubuntu, sh is dash, which rejects bash-only features, so sh script.sh can fail where bash script.sh works.

What does /bin/bash^M: bad interpreter mean?

The script has Windows (CRLF) line endings, so the shebang asks for a program called /bin/bash plus a carriage return. Convert the file to LF with dos2unix script.sh or your editor.

What does set -euo pipefail do?

It makes bash stop at the first failing command (-e), treat unset variables as errors (-u) and count failures anywhere in a pipeline (-o pipefail). It catches many mistakes, but not all.

Can I run a .sh file on Windows?

Not natively. Install WSL (wsl --install) for a full Linux environment, or Git for Windows for Git Bash. Windows's own script formats are .bat, .cmd and .ps1.

Is it safe to run curl … | sh?

It runs code you haven't read, with your permissions. Only do it for sources you trust; otherwise download the script, read it, and then run it.

Why do I get Permission denied?

The file isn't marked executable, which is common after downloading or unzipping it. Run chmod +x script.sh, or run it with bash script.sh.

Related formats

Other file types explained

Keep going

Related

Sources

Last reviewed 27 October 2026.