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

Network, SSH and X11

Resource limits and crashes

command not found

session
$> foobar
bash: foobar: command not found
$> echo $?
127

Bash 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:

  1. A typo in the command name. Check with type -a foobar, or compgen -c | grep foobar to list what is actually available.

  2. The package is not installed. On Debian and Ubuntu, the command-not-found handler will usually suggest which package provides it.

  3. The command lives in /sbin or /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] LOGIN

    Call 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

session
$> cat /etc/shadow
cat: /etc/shadow: Permission denied

The 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:

session
$> chmod 644 script.sh
$> ./script.sh
bash: ./script.sh: Permission denied
$> echo $?
126
$> chmod +x script.sh

A 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:

session
$> 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

session
$> cat test
cat: test: No such file or directory

ENOENT: 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:

session
$> 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 directory

Bash 5.2 reworded that message and no longer names the interpreter, which makes it harder to recognize:

session
$> ./script.sh
bash: ./script.sh: cannot execute: required file not found

Confirm 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:

session
$> head -1 script.py
#!/usr/bin/python
$> ./script.py
bash: ./script.py: /usr/bin/python: bad interpreter: No such file or directory

Prefer #!/usr/bin/env python3 so the lookup goes through $PATH.

Argument list too long

session
$> ls *
bash: /usr/bin/ls: Argument list too long
$> rm *
bash: /usr/bin/rm: Argument list too long

Bash 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:

session
$> getconf ARG_MAX
2097152

There 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:

session
$> find . -maxdepth 1 -type f -delete
$> find . -maxdepth 1 -type f -print0 | xargs -0 rm

Use -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:

session
$> 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 long

A 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

session
$> touch test
$> cd test
bash: cd: test: Not a directory

ENOTDIR: 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:

bash
% cd test
cd: not a directory: test

File exists

session
$> mkdir test
$> mkdir test
mkdir: cannot create directory 'test': File exists

EEXIST. 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:

session
$> mkdir test
mkdir: test: File exists

GNU 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

session
$> 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:

session
$> 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

session
$> cp bigfile /mnt/data/
cp: error writing '/mnt/data/bigfile': No space left on device

ENOSPC. 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:

session
$> 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/data

df -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:

session
$> 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:

session
$> 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

session
$> ./test.sh
./test.sh: line 8: syntax error: unexpected end of file

Bash 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.

bash
#!/bin/bash
# script: test.sh

if [ $# -eq 0 ]
then
    echo "The variable is zero"
fi^M
session
$> ./test.sh
./test.sh: line 8: syntax error: unexpected end of file

cat -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:

bash
#!/bin/bash
# script: test.sh

myfunc () { echo "$@"; exit 1 }

if [ $# -eq 0 ]
then
    echo "The variable is zero"
fi
session
$> ./test.sh
./test.sh: line 10: syntax error: unexpected end of file

Writing it as myfunc () { echo "$@"; exit 1; } fixes it. Note the semicolon after exit 1.

bad substitution

session
$> bash -c '${x }'
bash: ${x }: bad substitution

Bash 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:

session
$> sh -c 'x=abc; echo ${x//a/b}'
sh: 1: Bad substitution

Note 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:

session
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.

GNU Bash - Shell Parameter Expansion

Check which shell is running with echo $BASH_VERSION before assuming the syntax is wrong.

ambiguous redirect

session
$> echo "a" > $file_name
bash: $file_name: ambiguous redirect

Bash 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:

session
$> f="a b"
$> echo x > $f
bash: $f: ambiguous redirect

Quoting the expansion resolves both. The unset case then fails at the point that actually matters, with a message that names the real problem:

session
$> echo "a" > "${file_name}"
bash: : No such file or directory

set -u catches it earlier still, before the redirect is attempted:

session
$> set -u
$> echo "a" > $file_name
bash: file_name: unbound variable

syntax error: invalid arithmetic operator

session
$> 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:

session
$> 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

session
$> 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:

session
$> [ "${n:-0}" -ne 0 ] && echo $((5 / n))

unary operator expected

session
$> unset v
$> if [ $v = x ]; then echo yes; fi
bash: [: =: unary operator expected

An 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:

session
$> v="a b"
$> if [ $v = x ]; then echo yes; fi
bash: [: too many arguments

Both are the same bug, and quoting fixes both:

session
$> if [ "$v" = x ]; then echo yes; fi

Better 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

session
$> read -u 3 var
bash: read: 3: invalid file descriptor: Bad file descriptor

EBADF: 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:

session
$> 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.

session
$> 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)

session
$> 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:

session
$> 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:

  1. The public key is not in the server’s ~/.ssh/authorized_keys. Copy it with ssh-copy-id user@example.com.

  2. 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: sshd silently ignores authorized_keys when the file or its directory is group- or world-writable.

    session
    $> chmod 700 ~/.ssh
    $> chmod 600 ~/.ssh/authorized_keys

    The server log, /var/log/auth.log or journalctl -u ssh, records the real reason when the client cannot see it.

  3. The client offered a key the server will not accept. OpenSSH 8.8 disabled RSA SHA-1 signatures by default, so an older ssh-rsa key stops working after a server upgrade. ssh -v reports no mutual signature supported in that case. Generating an Ed25519 key with ssh-keygen -t ed25519 is the durable fix; adding PubkeyAcceptedAlgorithms +ssh-rsa to ~/.ssh/config is the stopgap.

  4. 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:

session
$> 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:

session
$> firefox
Error: cannot open display: :0

For 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:

bash
X11 forwarding request failed on channel 0

Once 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

session
$> ./spawn-many.sh
bash: fork: retry: Resource temporarily unavailable
bash: fork: retry: Resource temporarily unavailable

EAGAIN: 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:

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:

session
$> exec {fd}< /etc/hostname
bash: redirection error: cannot duplicate fd: Too many open files

Killed

session
$> ./big-job
Killed
$> echo $?
137

One 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
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

session
$> ./crash
Segmentation fault (core dumped)
$> echo $?
139

The 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:

session
$> ulimit -c unlimited
$> ./crash
$> coredumpctl gdb

dmesg -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.