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.
shis 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 plainsh. - Windows has its own script formats: batch files (
.bat,.cmd) and PowerShell (.ps1). It doesn't run.shfiles 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.
#!/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
#!/usr/bin/env bashThe shebang
When you run
./backup-notes.sh, the operating system reads these first bytes and startsbashwith the script. It must be the very first line, with nothing before#!, not even a byte order mark. - 2
# backup-notes.sh: …A comment
Everything after
#is ignored. A comment also swallows an invisible\rat the end of a line, which is why CRLF damage shows up on commands and not on comments. - 3
set -euo pipefailStrict 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;
pipefailfails in some/bin/shshells. - 4
SOURCE_DIR="${1:-$HOME/notes}"Variables and arguments
$1is the first argument given to the script;${1:-…}supplies a default when there isn't one. No spaces around=:A = 1would run a command calledA. - 5
$(date +%Y-%m-%d)Command substitution
Runs a command and inserts its output, here a date such as
2026-10-27. - 6
log() { … >&2 }A function
Called like a command:
log "message"."$*"joins its arguments, and>&2sends the output to standard error, keeping messages apart from data. - 7
[[ ! -d "$SOURCE_DIR" ]]A test
-dasks whether a folder exists.[[ … ]]is bash; portableshscripts use[ … ], thetestcommand. - 8
exit 1The exit status
Ends the script and tells whoever ran it that something failed.
0means success. A script's status is otherwise that of its last command. - 9
mkdir -p "$TARGET_DIR"Quoting
Double quotes keep a value in one piece. Without them, a folder called
My Noteswould split into two arguments. Quote every variable unless you have a reason not to. - 10
[[ -e "$file" ]] || continueAn empty glob guard
If no
.mdfiles exist, the pattern*.mdis left as literal text. This line skips that case instead of trying to copy a file called*.md. - 11
cp -- "$file" …End of options
--tellscpthat what follows are file names, so a file named-r.mdcan'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:
| Command | Needs chmod +x | Which shell runs it | Where it runs |
|---|---|---|---|
./script.sh | Yes | The one named in the shebang | A new process |
bash script.sh | No | bash; the shebang is ignored | A new process |
sh script.sh | No | Whatever /bin/sh is (dash on Debian and Ubuntu); the shebang is ignored | A new process |
source script.sh or . script.sh | No | Your current shell | Your 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/shpromises a portable POSIX script. On Debian and Ubuntu/bin/shis dash, on Alpine Linux it's BusyBox ash, elsewhere often bash in POSIX mode. Use only POSIX features and test with dash.#!/bin/bashuses 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 bashuses the firstbashon 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 -efails with "use -[v]S to pass options in shebang lines". Putset -ein the script instead.
| bash | POSIX sh | Note |
|---|---|---|
[[ $a == b* ]] | case $a in b*) … ;; esac | dash says [[: not found |
names=(ada bob) | set -- ada bob | Arrays don't exist in sh: Syntax error: "(" unexpected |
function name { … } | name() { … } | Both work in bash |
local x=1 | Prefixed global names, such as _x=1 | Not 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 pipefail | Check each command's status | Some /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 see | What happened |
|---|---|
bash: ./script.sh: /bin/bash^M: bad interpreter: No such file or directory | The 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 found | The same problem, as reported by recent bash versions |
/usr/bin/env: 'bash\r': No such file or directory | The same problem with an env shebang |
$'\r': command not found | An 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 |
# 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: always check out shell scripts with LF line endings,
# even on Windows with core.autocrlf switched on
*.sh text eol=lf
*.bash text eol=lfA 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
| Linux | macOS | Windows | |
|---|---|---|---|
| Shell available | bash, nearly always; /bin/sh may be dash | zsh (default), bash 3.2, sh | None by default: install WSL, or Git for Windows (Git Bash) |
| Run a script | bash script.sh | bash script.sh or zsh script.sh | wsl bash script.sh, or bash script.sh in Git Bash |
| Run it as a program | chmod +x script.sh, then ./script.sh | chmod +x script.sh, then ./script.sh | Inside WSL or Git Bash, as on Linux |
| Double-click | Some file managers ask whether to run it | Opens in an editor; a .command file runs in Terminal | Nothing by default; Git for Windows can link .sh files to Git Bash |
chmod +xonce. 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 givesPermission denieduntil you runchmod +x.- Why
./? The shell looks for commands only in the folders on yourPATH, which normally doesn't include the current folder../script.shsays "this folder". - WSL runs a real Linux distribution on Windows:
wsl --installin 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,
nanoandvimare usually available. Make sure the editor saves with LF line endings. - Check the syntax without running it:
bash -n script.shparses 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.shprints each command, with variables filled in, before running it. Careful: that includes any passwords the script handles. - Format it:
shfmtindents and formats shell scripts consistently.
Writing it safely
set -euo pipefail, quoting and other habits
set -estops the script when a command fails, instead of carrying on with half-finished work. It doesn't apply to commands in aniftest or before&&and||, andlocal x=$(cmd)hidescmd's failure, so it's a safety net, not a guarantee.set -uturns 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 pipefailmakescmd | sortfail whencmdfails. 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
printftoechofor anything but plain text: whatechodoes with-eand backslashes differs between shells. - Check with ShellCheck. It finds unquoted variables, bash features in
shscripts, CRLF line endings and many other bugs before you run anything.
Troubleshooting
Errors when running a shell script
| Error message | Usual cause | Fix |
|---|---|---|
| Permission denied | The file isn't executable, often after a download or unzip | chmod +x script.sh, or run bash script.sh |
| script.sh: command not found | Typed script.sh without ./, so the shell searched the PATH | Run ./script.sh |
| bad interpreter: … ^M, or $'\r': command not found | Windows (CRLF) line endings | dos2unix script.sh; see the section above |
| Syntax error: "(" unexpected / [[: not found | A bash script run by dash, through sh script.sh or a #!/bin/sh shebang | Run it with bash, fix the shebang, or rewrite it to POSIX |
| set: Illegal option -o pipefail | pipefail in a script that dash runs | Use bash, or drop the option |
| unbound variable | set -u and a variable that was never set, often a missing argument | Pass the argument, or use ${1:-default} |
| No such file or directory, for a file that exists | An unquoted path with a space in it was split in two | Quote 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://… | shruns 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 tocurlisn'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 -xoff 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.
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.
Comments 1 and 2: shebang and strict mode
#!/usr/bin/env bash, written to work with the old bash 3.2 on macOS, thenset -euo pipefailwith an explanation of each option.Comments 3 and 4: settings and here-documents
readonlyvalues,${0##*/}for the script's own name, and functions that print a usage message and a sample log with here-documents.Comment 5: options
getoptsparses-n NUMBER,-f,-hand-v, with error messages for a missing or non-numeric value.Comments 6 and 7: input and the read loop
${1:-}for an optional file argument, a readability check, thenwhile IFS= read -r linewithcasepatterns to count each level.Comments 8 and 9: output and arrays
printffor aligned columns, quoting explained, and an array of error lines printed with a C-styleforloop.Comment 10: exit status
Exits with
0, or with1when-fis 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.shfails with the bad interpreter error, andbash sample-crlf.shshows$'\r': command not foundand 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.shthrough Git, an editor or an upload, then runfile sample-crlf.sh. If it no longer says CRLF, the tool converted it, and./sample-crlf.shstarts working. - Which /bin/sh?
sh sample-script.shfails on Debian and Ubuntu, whereshis dash, whilesh sample-posix.shworks everywhere. - Locale: run
sample-utf8.shwith bash, then withLC_ALL=C bash sample-utf8.sh: the character counts become byte counts. - Upload filters: some email services and upload forms block
.shfiles.sample-100kb.shis 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
- POSIX.1-2024: Shell Command Language (The Open Group)
- GNU Bash
- Apple Support: Use zsh as the default shell on your Mac
- ShellCheck: finds bugs in shell scripts
- Microsoft Learn: How to install Linux on Windows with WSL
Last reviewed 27 October 2026.