- Applies to
- GNU Bash 3.2 to 5.3 · GNU coreutils 9.x
- Platform
- Linux, macOS
- Updated
- 19 Aug 2026
- Examples
- 57
Bash error messages are short, and they name the failure rather than the cause.
bash: /usr/bin/rm: Argument list too long says the command could not start. It does not
say that the shell expanded * into more arguments than the kernel accepts, or that the way
out is find . -maxdepth 1 -type f -delete.
Every message below was reproduced on GNU Bash 3.2 through 5.3 with GNU coreutils on Linux, and on the stock BSD tools on macOS. Where the two disagree, both strings are shown.
Running a command
Files and directories
Syntax and scripting
bash: syntax error near unexpected tokensyntax error: unexpected end of filebad substitutionambiguous redirectsyntax error: invalid arithmetic operatordivision by 0unary operator expectedBad file descriptor
Network, SSH and X11
Resource limits and crashes
command not found
$> foobar
bash: foobar: command not found
$> echo $?
127Bash searched every directory in
$PATH
and found nothing called foobar. Exit status 127 always means this. In order of how
often each one turns out to be the cause:
A typo in the command name. Check with
type -a foobar, orcompgen -c | grep foobarto list what is actually available.The package is not installed. On Debian and Ubuntu, the
command-not-foundhandler will usually suggest which package provides it.The command lives in
/sbinor/usr/sbin, which are not in a regular user’s$PATH. This one is confusing because the binary is right there on disk:session $> echo $PATH /usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games $> useradd bash: useradd: command not found $> /usr/sbin/useradd --help | head -1 Usage: useradd [options] LOGINCall it by full path, or with
sudo, which uses a different$PATH.
A script that exists but has no execute permission gives
Permission denied and exit status 126, not this error.
Permission denied
$> cat /etc/shadow
cat: /etc/shadow: Permission deniedThe kernel refused the operation with EACCES. That one errno covers several different
situations, which is why the message reads the same whether the problem is the file’s mode,
your group membership, or the filesystem it sits on.
The everyday case is a file you do not own, as above. Two others are worth checking when the permissions look correct.
A script without the execute bit. Exit status 126 distinguishes this from
command not found:
$> chmod 644 script.sh
$> ./script.sh
bash: ./script.sh: Permission denied
$> echo $?
126
$> chmod +x script.shA filesystem mounted noexec. The script is mode 755 and still refuses to run. This
catches people on /tmp, on /home under hardened configurations, and on removable media:
$> ls -l /mnt/usb/script.sh
-rwxr-xr-x 1 nicolas nicolas 29 Aug 19 09:14 /mnt/usb/script.sh
$> /mnt/usb/script.sh
bash: /mnt/usb/script.sh: Permission denied
$> mount | grep /mnt/usb
tmpfs on /mnt/usb type tmpfs (rw,relatime,noexec)Run it through the interpreter instead, with bash /mnt/usb/script.sh, or remount without
noexec.
On macOS, Operation not permitted in place of Permission denied usually points at System
Integrity Protection or a missing Full Disk Access grant for your terminal rather than at a
file mode.
No such file or directory
$> cat test
cat: test: No such file or directoryENOENT: nothing exists at that path. Almost always a typo, a relative path resolved from a
directory you did not expect, or a file that was never created. pwd and ls -l settle it.
The case worth knowing is when the file is plainly there and Bash still says this. The
missing file is then the interpreter, not the script. A script saved with Windows CRLF
line endings has a shebang that reads /bin/bash followed by a carriage return, and no such
interpreter exists:
$> ls -l script.sh
-rwxr-xr-x 1 nicolas nicolas 29 Aug 19 09:20 script.sh
$> ./script.sh
bash: ./script.sh: /bin/bash^M: bad interpreter: No such file or directoryBash 5.2 reworded that message and no longer names the interpreter, which makes it harder to recognize:
$> ./script.sh
bash: ./script.sh: cannot execute: required file not foundConfirm it with file script.sh, which reports with CRLF line terminators, or with
cat -A script.sh, which shows ^M$ at the end of every line. Fix it with
dos2unix script.sh or sed -i 's/\r$//' script.sh, and stop it recurring with a
.gitattributes entry. The same characters cause
syntax error: unexpected end of file when they land elsewhere in
the file, and there is a longer write-up in
removing ^M in imported Windows files.
An interpreter that genuinely does not exist gives the same shape of message, and there the message is telling the truth:
$> head -1 script.py
#!/usr/bin/python
$> ./script.py
bash: ./script.py: /usr/bin/python: bad interpreter: No such file or directoryPrefer #!/usr/bin/env python3 so the lookup goes through $PATH.
Argument list too long
$> ls *
bash: /usr/bin/ls: Argument list too long
$> rm *
bash: /usr/bin/rm: Argument list too longBash names the resolved binary, so the message contains /usr/bin/ls or /bin/rm rather
than the short command name you typed.
The shell expands the
wildcard first, then hands the whole result
to execve(), which refuses with E2BIG. The limit is on the total byte size of the
argument and environment buffer, not on the number of files, which is why no fixed file count
predicts it. getconf ARG_MAX reports the buffer size, and the effective ceiling is lower
still because the environment shares it:
$> getconf ARG_MAX
2097152There is an in-depth explanation of the real limit if you want the details.
The fix is to stop passing the file list through the shell. find never builds a single
oversized argument list:
$> find . -maxdepth 1 -type f -delete
$> find . -maxdepth 1 -type f -print0 | xargs -0 rmUse -print0 with xargs -0. Without it, a filename containing a space or a newline is
split into several arguments and the wrong files get removed. If the whole directory is
disposable, rm -r dir && mkdir dir beats both.
The same limit is why counting entries with ls | wc -l breaks on large directories; there is
a
safe way to count files with find and wc.
To reproduce it, build a directory with enough entries:
$> mkdir test
$> n=$(( $(getconf ARG_MAX) / 10 ))
$> for ((i=0; i<n; i++)); do : > "test/$i"; done
$> ls test/*
bash: /usr/bin/ls: Argument list too longA brace range will not work here. Brace expansion runs before parameter and command
substitution, so {0..$((...))} is never treated as a range: the loop body runs exactly once
and creates a single file with a very strange name. See
seq and brace expansion for why.
Not a directory
$> touch test
$> cd test
bash: cd: test: Not a directoryENOTDIR: the path exists, but a component of it is a regular file where a directory was
required. cd into a file is the obvious case; the same error appears for ls a/b/c when
b is a file, and for a path that runs through a symlink pointing at a file.
Zsh, the default login shell on macOS since Catalina, words it differently. Both strings describe the same condition:
% cd test
cd: not a directory: testFile exists
$> mkdir test
$> mkdir test
mkdir: cannot create directory 'test': File existsEEXIST. mkdir -p test succeeds quietly when the directory is already there, which is what
you want in a script.
macOS ships BSD mkdir, which is terser:
$> mkdir test
mkdir: test: File existsGNU mkdir quotes the name with directional quotes in a UTF-8 locale and with plain
apostrophes under LC_ALL=C, so an exact-string search may need both forms.
invalid option
$> ls -3
ls: invalid option -- '3'
Try 'ls --help' for more information.The option is not one the command accepts. Check --help for the tool you are actually
running, which may not be the one you think: type -a ls reports whether ls is an alias, a
function, or a binary.
BSD tools on macOS use a different format and print a usage line instead of a hint:
$> ls -3
ls: invalid option -- 3
usage: ls [-@ABCFGHILOPRSTUWXabcdefghiklmnopqrstuvwxy1%,] [--color=when] [-D format] [file ...]When the argument starting with a dash is meant as a filename or a pattern rather than an
option, put -- in front of it so the command stops parsing options: grep -- -v file.
No space left on device
$> cp bigfile /mnt/data/
cp: error writing '/mnt/data/bigfile': No space left on deviceENOSPC. df -h normally shows the offending filesystem at 100%, and
du -xh --max-depth=1 / narrows down what filled it.
When df -h shows plenty of free space and writes still fail, the filesystem has run out of
inodes rather than blocks. Every file consumes one regardless of its size, so a directory
full of tiny files can exhaust them while the disk looks nearly empty:
$> touch /mnt/data/f12
touch: cannot touch '/mnt/data/f12': No space left on device
$> df -h /mnt/data | tail -1
tmpfs 1.0M 12K 1012K 2% /mnt/data
$> df -i /mnt/data | tail -1
tmpfs 12 12 0 100% /mnt/datadf -i is the check. A third cause, when both df -h and df -i look healthy, is a deleted
file still held open by a running process, so its space is not reclaimed until the process
exits. lsof +L1 lists them.
bash: syntax error near unexpected token
Bash could not parse the line, and it names the token it choked on.
An incomplete redirect. The redirect has no target, so the newline arrives where a filename should be:
$> echo "some test" >
bash: syntax error near unexpected token `newline'Unquoted parentheses. Parentheses are shell syntax, so they need quoting to survive as an argument:
$> echo this (fail)
bash: syntax error near unexpected token `('
$> echo 'this (works)'
this (works)The same token error shows up for an unmatched }, for ;; outside a case, and for a
then or do that follows a condition without a separating ; or newline. When Bash names
a token that looks perfectly valid, check whether the file has CRLF line endings, which
attach an invisible carriage return to the last token on every line.
bash -n script.sh parses a script without running it, which is the safe way to find these.
syntax error: unexpected end of file
$> ./test.sh
./test.sh: line 8: syntax error: unexpected end of fileBash reached the end of the file with a compound command still open: a quote that was never
closed, an if without its fi, a { without its }, a heredoc whose terminator never
arrived. The line number is where Bash ran out of file, not where the construct opened,
so it is rarely the line with the bug. The seven-line script below reports line 8.
The common cause is a missing terminator you can find by reading the file. The one you cannot
see is a Windows CRLF line ending on the closing keyword: fi followed by a carriage return
is not the fi keyword, so the if is still open at the end of the file.
#!/bin/bash
# script: test.sh
if [ $# -eq 0 ]
then
echo "The variable is zero"
fi^M$> ./test.sh
./test.sh: line 8: syntax error: unexpected end of filecat -A test.sh shows the ^M$ endings, and dos2unix test.sh removes them.
A single-line
function whose body is not terminated with a semicolon
produces the same error, because the } is read as an argument to exit rather than as the
closing brace:
#!/bin/bash
# script: test.sh
myfunc () { echo "$@"; exit 1 }
if [ $# -eq 0 ]
then
echo "The variable is zero"
fi$> ./test.sh
./test.sh: line 10: syntax error: unexpected end of fileWriting it as myfunc () { echo "$@"; exit 1; } fixes it. Note the semicolon after exit 1.
bad substitution
$> bash -c '${x }'
bash: ${x }: bad substitutionBash found something inside ${...} that is not valid parameter expansion syntax.
The most frequent cause has nothing to do with the syntax being wrong. It is a Bash script
run by a different shell. sh on Debian and Ubuntu is dash, which does not implement
Bash’s pattern substitution, case modification or indirection, so perfectly valid Bash gets
rejected:
$> sh -c 'x=abc; echo ${x//a/b}'
sh: 1: Bad substitutionNote the different wording and the line number. dash writes sh: 1: Bad substitution; Bash
writes bash: ${x//a/b}: bad substitution. Run the script with bash script.sh, set the
shebang to #!/usr/bin/env bash, and stop invoking it as sh script.sh. This is also why
the error turns up in CI pipelines and Jenkins sh steps that run Bash-only syntax.
The remaining causes are genuine syntax problems:
- Stray whitespace inside the braces:
bash -c '${x }' - A mistyped operator, such as a semicolon where
${x:-}needs a colon:bash -c '${x;-}' - An expansion from a newer Bash than the one running it.
${x@Q}needs Bash 4.4, so it fails on the Bash 3.2 that macOS still ships as/bin/bash:
bash-3.2$ echo ${x@Q}
bash: ${x@Q}: bad substitution👉 ${parameter@operator}
The expansion is either a transformation of the value of parameter or information about parameter itself, depending on the value of operator. Each operator is a single letter:
Q The expansion is a string that is the value of parameter quoted in a format that can be reused as input.
Check which shell is running with echo $BASH_VERSION before assuming the syntax is wrong.
ambiguous redirect
$> echo "a" > $file_name
bash: $file_name: ambiguous redirectBash raises this when the target of a redirect expands to something other than exactly one word. There are two ways to get there, and quoting fixes both.
The variable is unset or empty, so the redirect has no target at all, as above.
The variable holds a value with whitespace, so the redirect gets several targets and cannot choose between them:
$> f="a b"
$> echo x > $f
bash: $f: ambiguous redirectQuoting the expansion resolves both. The unset case then fails at the point that actually matters, with a message that names the real problem:
$> echo "a" > "${file_name}"
bash: : No such file or directoryset -u catches it earlier still, before the redirect is attempted:
$> set -u
$> echo "a" > $file_name
bash: file_name: unbound variablesyntax error: invalid arithmetic operator
$> echo $((5.3 + 1))
bash: 5.3 + 1: syntax error: invalid arithmetic operator (error token is ".3 + 1")Bash arithmetic is integer only. The . is not an operator it knows, so it stops there and
reports the rest of the expression as the offending token.
Bash 5.3 added a prefix to the same message, so both forms are in circulation:
$> echo $((5.3 + 1))
bash: 5.3 + 1: arithmetic syntax error: invalid arithmetic operator (error token is ".3 + 1")For floating point, use bc or awk. There is more on this in
math and arithmetic calculation in Bash.
The same message appears for a variable holding a non-numeric value, because the expansion happens before the arithmetic is parsed.
division by 0
$> echo $((5 / 0))
bash: 5 / 0: division by 0 (error token is "0")The divisor evaluated to zero. In a script this is usually a variable that was never set, or
that came back empty from a command, because an empty string evaluates to 0 inside
arithmetic expansion.
Guard the divisor before dividing:
$> [ "${n:-0}" -ne 0 ] && echo $((5 / n))unary operator expected
$> unset v
$> if [ $v = x ]; then echo yes; fi
bash: [: =: unary operator expectedAn unquoted empty variable disappears entirely before [ runs, so the test sees [ = x ] and
finds an operator where it expected a value. The companion error appears when the variable
holds more than one word:
$> v="a b"
$> if [ $v = x ]; then echo yes; fi
bash: [: too many argumentsBoth are the same bug, and quoting fixes both:
$> if [ "$v" = x ]; then echo yes; fiBetter still, use [[ ... ]], which does not word-split its operands; see
the Bash if statement for the comparison. shellcheck flags every
unquoted expansion inside [ ].
Bad file descriptor
$> read -u 3 var
bash: read: 3: invalid file descriptor: Bad file descriptorEBADF: the program used a descriptor number that is not open. Descriptors 0, 1 and 2
are always there; anything above that has to be opened first:
$> exec 3< /etc/hostname
$> read -u 3 var
$> echo "$var"
myhost
$> exec 3<&-In a script, the usual cause is a descriptor opened inside a subshell or one side of a
pipeline, which does not survive into the parent shell, or one closed earlier with
exec 3<&- and used again afterwards.
Connection refused
Nothing is listening on that port. The connection is rejected immediately, and that is the
useful part: a refused connection comes back at once, while a firewall dropping packets
gives Connection timed out after a wait. The two point at different problems.
$> curl http://localhost:123
curl: (7) Failed to connect to localhost port 123 after 0 ms: Could not connect to server
$> ssh -p 2222 localhost
ssh: connect to host localhost port 2222: Connection refused
$> wget localhost:123
--2026-08-19 07:17:18-- http://localhost:123/
Resolving localhost (localhost)... 127.0.0.1, ::1
Connecting to localhost (localhost)|127.0.0.1|:123... failed: Connection refused.
Connecting to localhost (localhost)|::1|:123... failed: Connection refused.Check that the service is running with systemctl status, that it is listening where you
think with ss -tlnp or sudo lsof -iTCP -sTCP:LISTEN -n -P, and that it is not bound to
127.0.0.1 only while you connect from another host. The wget output above tries both
127.0.0.1 and ::1, which is worth remembering when a service listens on IPv4 but the name
resolves to IPv6 first.
No route to host means the packet never reached the machine, and Connection timed out
usually means a firewall is dropping rather than rejecting.
Permission denied (publickey)
$> ssh user@example.com
user@example.com: Permission denied (publickey).The server accepted the connection and rejected every key the client offered. Start with
ssh -v, which shows what was tried:
$> ssh -v user@example.com
debug1: Authentications that can continue: publickey
debug1: Offering public key: /home/nicolas/.ssh/id_ed25519 ED25519 SHA256:pfUz...
debug1: Authentications that can continue: publickey
user@example.com: Permission denied (publickey).In order of how often each is the cause:
The public key is not in the server’s
~/.ssh/authorized_keys. Copy it withssh-copy-id user@example.com.The permissions on the server side are too open. Worth knowing because the error is byte-for-byte identical to the first case while the key is installed and correct:
sshdsilently ignoresauthorized_keyswhen the file or its directory is group- or world-writable.session $> chmod 700 ~/.ssh $> chmod 600 ~/.ssh/authorized_keysThe server log,
/var/log/auth.logorjournalctl -u ssh, records the real reason when the client cannot see it.The client offered a key the server will not accept. OpenSSH 8.8 disabled RSA SHA-1 signatures by default, so an older
ssh-rsakey stops working after a server upgrade.ssh -vreportsno mutual signature supportedin that case. Generating an Ed25519 key withssh-keygen -t ed25519is the durable fix; addingPubkeyAcceptedAlgorithms +ssh-rsato~/.ssh/configis the stopgap.The wrong key is being offered, because the agent has several loaded. Force one with
ssh -i ~/.ssh/id_ed25519 -o IdentitiesOnly=yes user@example.com.
Error: cannot open display
Two different messages come out of this situation, and which one you get says where the problem is.
No $DISPLAY at all. The program has nowhere to draw. This is what a plain SSH session
gives you, because sshd does not set DISPLAY unless X11 forwarding was negotiated:
$> ssh user@example.com firefox
Error: no DISPLAY environment variable specified$DISPLAY is set but unreachable. The X server is not running, is not accepting the
connection, or the forwarding channel has gone away:
$> firefox
Error: cannot open display: :0For a remote GUI, connect with ssh -X, and add ForwardX11 yes to ~/.ssh/config to make
it the default. The server needs X11Forwarding yes in /etc/ssh/sshd_config and the
xauth binary installed, or the connection reports:
X11 forwarding request failed on channel 0Once forwarding works, echo $DISPLAY on the remote side shows something like
localhost:10.0. A cannot open display: localhost:10.0 after that means the channel was set
up and then broke, which is usually an xauth or session-timeout problem rather than a
configuration one.
ssh -Y is often suggested when -X misbehaves. It works because it drops the X11 security
extension restrictions, which also gives the remote host full access to your local X server,
including other windows and keystrokes. Prefer -X unless you control the remote machine.
On macOS all of this needs XQuartz installed; the system has shipped without an X server for a long time.
bash: fork: retry: Resource temporarily unavailable
$> ./spawn-many.sh
bash: fork: retry: Resource temporarily unavailable
bash: fork: retry: Resource temporarily unavailableEAGAIN: the kernel refused to create a new process. Bash retries a few times before giving
up, which is why the message repeats. A limit has been hit rather than memory exhausted:
- The per-user process limit.
ulimit -ushows it,/etc/security/limits.confsets it. A script that forks in a loop reaches it quickly. - A container’s
pidscgroup limit, which is whatdocker run --pids-limitsets. - System-wide,
cat /proc/sys/kernel/threads-maxandsysctl kernel.pid_max.
ps -eLf | wc -l counts threads, and ps -u "$USER" | wc -l counts your own processes.
The same errno on a file operation gives a different message and a different limit, ulimit -n
rather than -u:
$> exec {fd}< /etc/hostname
bash: redirection error: cannot duplicate fd: Too many open filesKilled
$> ./big-job
Killed
$> echo $?
137One bare word, and a status code you have to know to look for. Exit status 137 is 128 + 9,
meaning the process was terminated by SIGKILL, and the usual sender is the kernel’s
out-of-memory killer. Bash reports it as a job notification, so in a script the fuller form
appears:
bash: line 3: 6 Killed python3 -c ...The shell will not say why. The kernel will: dmesg -T | grep -i -e oom -e killed or
journalctl -k records which process was chosen and how much memory it was holding. Under
systemd, systemd-cgtop shows which unit is consuming what.
Once the OOM killer is confirmed, the options are to use less memory, give the machine more,
or make the process a less attractive target through /proc/<pid>/oom_score_adj. In a
container this is the memory limit doing its job, and docker inspect reports
"OOMKilled": true.
SIGKILL sent by a person or a script produces exactly the same Killed and the same 137,
so read the kernel log before assuming memory was the problem.
Segmentation fault
$> ./crash
Segmentation fault (core dumped)
$> echo $?
139The program touched memory it was not allowed to touch and the kernel killed it with
SIGSEGV. Exit status 139 is 128 + 11. The (core dumped) half appears only when core
dumps are enabled; without them the message is a bare Segmentation fault.
This is a bug in the program rather than in your command line, so the useful move is to capture the dump instead of changing the invocation:
$> ulimit -c unlimited
$> ./crash
$> coredumpctl gdbdmesg -T | tail records the faulting address and the mapped region. For a program you build
yourself, rebuilding with -g -fsanitize=address will usually name the line directly.
When something that used to work starts doing this after an upgrade, a stale shared library is
a common cause; ldd ./crash shows what it links against.
Two habits catch most of the syntax half of this list before it reaches a terminal. Run
bash -n script.sh to parse a script without executing it, which finds every unclosed quote,
if and heredoc above. Then run shellcheck over anything you intend to keep, which catches
the unquoted expansions behind
ambiguous redirect and
unary operator expected before they misfire. Both are covered in
5 simple steps on how to debug a Bash shell script, along with set -x
for the failures that only show up at runtime.
