Bash
An overview of Bash metacharacters, their functions, and how they influence parsing, expansion, and execution in Unix‑like environments
The following table provides an overview of meta‑characters commonly used in both the Bourne shell and the C shell. To improve completeness, several compound character sequences have also been included.
While the table offers a broad reference, it does not cover every nuance of shell behavior. Shell meta‑characters can interact in subtle and sometimes unexpected ways, which is important to keep in mind when analyzing or constructing command sequences in a security context.
Bash set Options
Bash’s set built‑in modifies shell behavior by enabling or disabling specific execution modes.
These flags are essential for debugging, error handling, safety, and strict scripting.
| Option | Meaning | Description | Example |
|---|---|---|---|
set -e | Exit on error | Exits immediately if any command returns a non‑zero status. | bash\nset -e\n |
set -u | Undefined variables | Treats unset variables as errors. | bash\nset -u\n |
set -x | Execution trace | Prints each command before executing it. | bash\nset -x\n |
set -v | Verbose mode | Prints shell input lines as they are read. | bash\nset -v\n |
set -o pipefail | Pipefail | Pipeline fails if any command fails. | bash\nset -o pipefail\n |
set -f | Disable globbing | Turns off wildcard expansion (*, ?). | bash\nset -f\n |
set -o noglob | Disable globbing | Turns off wildcard expansion (*, ?). | bash\nset -o noglob\n |
set -n | No‑exec | Parses commands but does not execute them. | bash\nset -n\n |
set -C | No clobber | Prevents > from overwriting files. | bash\nset -C\n |
set -E | ERR trap inheritance | ERR traps propagate into functions and subshells. | bash\nset -E\n |
set -T | Function tracing | DEBUG/RETURN traps inherited by functions. | bash\nset -T\n |
set -b | Notify jobs | Reports background job completion immediately. | bash\nset -b\n |
set -m | Job control | Enables job control in non‑interactive shells. | bash\nset -m\n |
set -H | History expansion | Enables ! history expansion. | bash\nset -H\n |
set -B | Brace expansion | Enables {a,b,c} expansion. | bash\nset -B\n |
set -o nounset | Same as -u | Error on undefined variables. | bash\nset -o nounset\n |
set -o errexit | Same as -e | Exit on error. | bash\nset -o errexit\n |
set -o xtrace | Same as -x | Execution tracing. | bash\nset -o xtrace\n |
set +e | Disable -e | Turns off exit‑on‑error. | bash\nset +e\n |
set +u | Disable -u | Allows undefined variables. | bash\nset +u\n |
set +x | Disable -x | Turns off tracing. | bash\nset +x\n |
set +o | Disable option | Generic disable syntax. | bash\nset +o pipefail\n |
Bash Redirection Operators
Bash uses redirection operators to control where input comes from and where output goes. These symbols allow you to route standard input, standard output, and error output to files, devices, or other commands.
| Syntax | Description | Example |
|---|---|---|
< | Redirect stdin (read input from a file) | sort < input.txt |
> or 1> | Redirect stdout (overwrite file) | echo ""hello"" > out.txt |
2> | Redirect stderr | ls missing 2> errors.log |
&> | Redirect stdout and stderr | command &> output.log |
>> or 1>> | Append stdout | echo ""line"" >> out.txt |
2>> | Append stderr | cmd 2>> errors.log |
&>> | Append stdout and stderr | cmd &>> combined.log |
1>&2 | Redirect stdout → stderr | echo ""err"" 1>&2 |
2>&1 | Redirect stderr → stdout | cmd 2>&1 |
Bash Special Characters
These characters define how Bash interprets commands, arguments, quoting, variables, job control, and file descriptors.
| Character | Where | Meaning | Example |
|---|---|---|---|
<RETURN> | bash,sh | Execute command | ls<RETURN> |
# | bash,sh | Start a comment | # this is a comment |
<SPACE> | bash,sh | Argument separator | echo hello world |
` | bash,sh | Command substitution | echo `date` |
"" | bash,sh | Weak quotes | echo ""$HOME"" |
' | bash,sh | Strong quotes | echo '$HOME' |
\ | bash,sh | Escape one character | echo \""quoted\"" |
$variable | bash,sh | Variable | echo $USER |
${variable} | bash,sh | Same as variable (safer) | echo ${USER} |
| | bash,sh | Pipe | ls | grep txt |
^ | bash,sh | Pipe (csh/tcsh style) | cmd1 ^ cmd2 |
& | bash,sh | Run in background | sleep 5 & |
? | bash,sh | Match one character | ls file?.txt |
* | bash,sh | Match any number of characters | ls *.log |
; | bash,sh | Command separator | cmd1; cmd2 |
;; | bash,sh | End of case branch | case x in y) ;; esac |
~ | bash,sh | Home directory | cd ~ |
~user | bash,sh | User’s home directory | cd ~root |
! | bash,sh | History expansion | !ls |
- | bash,sh | Start of option | ls -l |
$# | bash,sh | Number of script arguments | echo $# |
$* | bash,sh | All arguments (one string) | echo $* |
$@ | bash,sh | All arguments (preserves quoting) | echo ""$@"" |
$- | bash,sh | Shell flags | echo $- |
$? | bash,sh | Exit status of last command | echo $? |
$$ | bash,sh | PID of current shell | echo $$ |
$! | bash,sh | PID of last background job | echo $! |
&& | bash,sh | Logical AND | cmd1 && cmd2 |
|| | bash,sh | Logical OR | cmd1 || cmd2 |
. | bash,sh | Filename extension | file.txt |
. | bash,sh | Source a file | . script.sh |
: | bash,sh | No‑op command | : ${var:=default} |
: | bash,sh | PATH separator | /usr/bin:/bin |
: | bash,sh | Variable modifier | ${var:=default} |
[ ] | bash,sh | Character range | [a-z] |
[ ] | bash,sh | Test | [ -f file ] |
%job | bash,sh | Job number | kill %1 |
(cmd;cmd) | bash,sh | Subshell | (cd /tmp; ls) |
{ } | bash,sh | Brace expansion | {1..5} |
{cmd;cmd} | bash,sh | Group commands (same shell) | { echo hi; echo bye; } |
>ofile | bash,sh | Redirect stdout | echo hi >ofile |
>>ofile | bash,sh | Append stdout | echo hi >>ofile |
<ifile | bash,sh | Redirect stdin | cat <ifile |
<<word | bash,sh | Here‑doc (with substitution) | cat <<EOF |
<<\word | bash,sh | Here‑doc (no substitution) | cat <<\EOF |
<<-word | bash,sh | Here‑doc (strip tabs) | cat <<-EOF |
>>!file | bash,sh | Append, force overwrite | echo hi >>!file |
>!file | bash,sh | Overwrite, force | echo hi >!file |
>&file | bash,sh | Redirect stdout+stderr | cmd >&file |
<&digit | bash,sh | Redirect stdin from FD | cat <&3 |
<&- | bash,sh | Close stdin | <&- |
>&digit | bash,sh | Redirect stdout to FD | echo hi >&3 |
>&- | bash,sh | Close stdout | >&- |
digit1<&digit2 | bash,sh | Connect FD2 → FD1 (stdin) | 0<&1 |
digit<&- | bash,sh | Close FD | 3<&- |
digit2>&digit1 | bash,sh | Connect FD2 → FD1 (stdout) | 2>&1 |
digit>&- | bash,sh | Close FD | 3>&- |
A solid understanding of shell meta‑characters is essential when working in Unix‑like environments, particularly in security‑focused contexts. Throughout this material, we will also look at practical methods for verifying how the shell interprets these characters, enabling you to identify where unexpected behavior originates.
Most keyboards provide three different types of quotation marks. Two of them — the single quote (') and the double quote (") — are the same characters used for standard English punctuation, and both play important roles in controlling how the shell processes text. The third character, the backtick (`), is visually similar to the single quote, which often leads to confusion in shell scripts.
Despite the resemblance, the backtick does not serve as a quoting mechanism.
Instead, the backtick is used for command substitution. Any text enclosed within backticks is executed by the shell, and the resulting output is inserted directly into the surrounding command.
-
For example:
result=`command`
A precise understanding of how these characters behave — and how the shell interprets them under different conditions — is fundamental in offensive security work. Meta‑characters control parsing, expansion, redirection, and execution flow, and even small misunderstandings can lead to unexpected command behavior.
For an attacker, these mechanics are tools; for a defender, they are potential entry points.
Mastering these distinctions is essential when crafting payloads, bypassing filters, manipulating input vectors, or analyzing how a target system processes user‑supplied data.
Whether you are building reliable exploitation chains, testing for injection vulnerabilities, or tracing how a command is being parsed during debugging, knowing exactly how the shell treats each character is what allows you to predict — and control — the outcome.
Process Substitution <( ) och >( )
This is one of Bash's most powerful features, but almost no one documents it properly.
Example:
diff <(sort file1) <(sort file2)
It is important for:
- pipelines
- OSINT tools
- data comparisons
- advanced automation
Arrays & Associative Arrays
It is important because arrays solve:
- dynamic variables
- eval problems
- complex data handling
- output parsing
Example:
arr=(one two three)
declare -A map=([key]=value)
Here‑Strings `<<<``
Example:
grep foo <<< "bar foo baz"
POSIX vs Bash Differences
This is important for portability and security.
| Feature | POSIX | Bash |
|---|---|---|
| [[ ]] | no | yes |
| Arrays | no | yes |
| Brace expansion | no | yes |
| echo -e | no | undefined |
ShellCheck Best Practices
- Always quote variables
- Prefer
[[ ]]over[ ] - Avoid
echo -e - Use
printf - Avoid
eval
Debugging Techniques
- set -x
- PS4='+ \({BASH_SOURCE}:\): '}:${FUNCNAME[0]
- declare -p
- trap 'echo error at $LINENO' ERR
This is advanced but extremely useful.
Subshells vs Groups
You have the table, but an explanatory section would make it complete:
- `( )``-> subshell
- `{ }``-> same shell
It is important for:
- variable scope
- performance
- pipelines
Arithmetic & Integer Handling
(( x++ ))
let x+=1
x=$((x+1))
And the difference between:
- integer context
- string context
Signals & Traps
This is important for:
- cleanup
- security
- robust scripts
- debugging
A table of signals (INT, TERM, EXIT, HUP):
trap 'cleanup' EXIT
#!/usr/bin/env bash
# Create a cleanup function
cleanup() {
local exit_code=$? # Spara den sista exit-koden
# 1. Kill background processes (like your spinner)
[[ -n ${spinner_pid} ]] && kill "${spinner_pid}" 2>/dev/null
# 2. Delete temp files
rm -f "${tmp_batch}" "${count_tmp}"
# 3. Log exit (optional)
if [[ ${exit_code} -ne 0 ]]; then
printf "\n${RED}Skriptet avbröts oväntat (Code: %d)${RST}\n" "${exit_code}"
fi
exit "${exit_code}"
}
# Set your trap for all relevant signals
# We include EXIT so that it cleans up even if the script finishes
trap 'cleanup' INT TERM HUP EXIT
Why this is better than trap rm file EXIT:
Captures Exit Code: - By getting $? the first thing you do in the function, you know why the script died.
Portability: - EXIT works in almost all shells, but adding INT and TERM ensures that the cleanup happens immediately when the signal is received, instead of waiting for Bash to catch up to the exit stage.
Cleanliness: - You don't have to have long lines of commands inside the trap string itself.
An important detail: SIGKILL (kill -9) - Remember that SIGKILL (-9) cannot be caught. If someone runs kill -9 on your script, no trap in the world will be executed.
Therefore, you should always use TERM (the default) when you want to terminate something gracefully.
| Signal | Full Name | Description | Triggering when |
|---|---|---|---|
| EXIT | Pseudo-signal) | The most important one | The script terminates naturally, via exit or on a fatal error |
| INT | SIGINT | Interrupt | You press Ctrl+C in the terminal |
| TERM | SIGTERM | Termination | Someone is running kill <pid> (the default target for kill) |
| HUP | SIGHUP | Hangup | The terminal window closes or the SSH session is terminated |
| QUIT | SIGQUIT | Quit | You press `Ctrl+`` (often creates a core dump) |
| ERR | (Pseudo-signal) | Error | A command returns an exit code that is not 0 (often requires set -e) |
Exit Codes & Error Propagation
From file: /usr/include/sysexits.h
$?set -e pitfalls- pipelines
- PIPESTATUS
- || vs &&
This is important for robust scripts.
| Exit | Meaning | Example | Comments |
|---|---|---|---|
| 0 | EX_OK | true | Successful termination |
| 1 | Catchall for general errors let | "var1 = 1/0" | Miscellaneous errors, such as "divide by zero" and other impermissible operations |
| 2 | Misuse of shell builtins | empty_function() {} | Missing keyword or command, or permission problem (and diff return code on a failed binary file comparison) |
| 64 | EX_USAGE | ./script.sh –wrong-arg | Command line usage error (invalid arguments). |
| 65 | EX_DATAERR | grep "pattern" binary_file | Data format error (input data is malformed). |
| 66 | EX_NOINPUT | cat <(non_readable_pipe) | Cannot open input (file not found or unreadable). |
| 67 | EX_NOUSER | mail bad_user@localhost | Addressee unknown (user/alias doesn't exist). |
| 68 | EX_NOHOST | ssh non_existent_host | Host name unknown. |
| 69 | EX_UNAVAILABLE | ping -c 1 192.0.2.1 | Service unavailable or support program not found. |
| 70 | EX_SOFTWARE | awk 'BEGIN { exit 70 }' | nternal software error (bug in the code logic). |
| 71 | EX_OSERR | ulimit -u 0; ls | System error (cannot fork, out of memory). |
| 72 | EX_OSFILE | missing /etc/passwd | Critical OS file missing (system-level file). |
| 73 | EX_CANTCREAT | touch /root/file | Can't create output file (permissions/readonly FS). |
| 74 | EX_IOERR | read < /dev/bad_sector | Input/output error (hardware or low-level I/O). |
| 75 | EX_TEMPFAIL | sendmail -q | Temp failure; user is invited to retry later. |
| 76 | EX_PROTOCOL | invalid_handshake | Remote error in protocol (illegal/bad response). |
| 77 | EX_NOPERM | sudo -u user /root/bin | Permission denied (access rights issue). |
| 78 | EX_CONFIG | nginx -t (with error) | Configuration error (syntax error in config files). |
| 126 | Command invoked cannot execute | /dev/null | Permission problem or command is not an executable. |
| 127 | "command not found" | illegal_command | Possible problem with $PATH or a typo. |
| 128 | Invalid argument to exit | exit 3.14159 | Exit takes only integer args (0 - 255). |
| 128+n | Fatal error signal "n" | kill -9 $PPID of script | $? Returns 137 (128 + 9). |
| 130 | Script terminated by Control-C | Ctl-C | Terminated by Control-C (128 + 2). |
| 255* | Exit status out of range | exit -1 | Wraps around; only integers 0-255 are valid. |
Examples of Bourne shell filename expansions
| Pattern | Matches |
|---|---|
| * | Every file in the current directory |
| ? | Files consisting of one character |
| ?? | Files consisting of two characters |
| ??* | Files consisting of two or more characters |
| [abcdefg] | Files consisting of a single letter from a to g. |
| [gfedcba] | Same as above |
| [a-g] | Same as above |
| [a-cd-g] | Same as above |
| [a-zA-Z0-9] | Files that consist of a single letter or number |
| [!a-zA-Z0-9] | Files that consist of a single character not a letter or number |
| [a-zA-Z]* | Files that start with a letter |
| ?[a-zA-Z]* | Files whose second character matches a letter. |
| *[0-9] | Files that end with a number |
| ?[0-9] | Two character filename that end with a number |
| *.[0-9] | Files that end with a dot and a number |
English Terms
Basic Bash Symbols
Dessa är byggstenarna i nästan alla scripts.
| Symbol | Engelskt namn | Vad det betyder i Bash |
|---|---|---|
$ | Dollar sign | Variabelexpansion. Hämtar värdet av en variabel. |
${ } | Parameter expansion | Avancerad variabelhantering (trimma, default, substring). |
$( ) | Command substitution | Kör ett kommando och ersätter med dess output. |
` ` | Backticks | Äldre command substitution (ersätts av $( )). |
# | Comment | Kommentar, ignoreras av Bash. |
Test and Comparison Operators
Dessa används i if, while, [[ ]], [ ] och case.
| Symbol | Engelskt namn | Förklaring |
|---|---|---|
[ ] | Test brackets | Klassisk test‑syntax. |
[[ ]] | Extended test brackets | Säkrare och kraftfullare tester. |
-z | Zero length | Strängen är tom. |
-n | Non‑zero length | Strängen är inte tom. |
-eq | Equal | Numerisk jämförelse. |
-ne | Not equal | Numerisk jämförelse. |
-gt | Greater than | Numerisk jämförelse. |
-lt | Less than | Numerisk jämförelse. |
== | String equal | Strängjämförelse (endast i [[ ]]). |
!= | String not equal | Strängjämförelse. |
< | Less‑than (string) | Strängjämförelse. |
> | Greater‑than (string) | Strängjämförelse. |
Loops, Blocks, and Grouping
| Symbol | Engelskt namn | Förklaring |
|---|---|---|
{ } | Brace expansion | Skapar sekvenser: {1..10}. |
{ cmd; cmd; } | Command group | Kör kommandon i samma shell. |
( cmd ) | Subshell | Kör kommandon i ett nytt shell. |
(( )) | Arithmetic expansion | Aritmetik utan $. |
for | For loop | Itererar över lista. |
while | While loop | Kör så länge villkor är sant. |
until | Until loop | Kör tills villkor blir sant. |
Pipes, Redirection, and Background Jobs
| Symbol | Engelskt namn | Förklaring |
|---|---|---|
| | Pipe | Skickar stdout → stdin. |
> | Redirect | Skriver över fil. |
>> | Append redirect | Lägger till i fil. |
< | Input redirect | Läser från fil. |
2> | Redirect stderr | Skickar fel till fil. |
&> | Redirect all | Skickar stdout + stderr. |
& | Background operator | Kör kommando i bakgrunden. |
&& | Logical AND | Kör nästa om första lyckas. |
|| | Logical OR | Kör nästa om första misslyckas. |
Control Structures
| Symbol | Engelskt namn | Förklaring |
|---|---|---|
: | Null command | Gör ingenting, returnerar 0. |
~ | Tilde | Hemkatalog. |
* | Wildcard | Matchar allt. |
? | Single‑char wildcard | Matchar ett tecken. |
-- | End of options | Stoppar flaggtolkning. |
= | Assignment | Sätter variabel. |
! | Logical NOT | Inverterar villkor. |
Kontrollstrukturer
| Symbol | Engelskt namn | Förklaring |
|---|---|---|
if | If statement | Villkor. |
then | Then | Startar block. |
elif | Else‑if | Alternativt villkor. |
else | Else | Fallback. |
fi | End of if | Avslutar block. |
case | Case statement | Switch‑liknande logik. |
esac | End of case | Avslutar case. |
do | Do | Startar loop‑block. |
done | Done | Avslutar loop. |
Understanding eval in Bash
eval is one of the most misunderstood, yet most powerful — and dangerous — commands in Bash. It effectively gives the shell a second chance to interpret a string as code. This allows you to construct dynamic commands, but it also opens the door to command injection, unexpected expansions, and behaviors that would otherwise be impossible.
This section explains:
- why
evalis dangerous - when its use is legitimate
- the common pitfalls
- how to avoid unsafe patterns
Why eval can break root‑checks
Many beginner scripts attempt to check for root privileges like this:
if [ "$USER" = "root" ]; then
echo "Running as root"
fi
This is unsafe because:
$USERis just an environment variable- it can be modified by the user
- it does not reflect the real UID
- it should never be used to determine privilege level
- and the situation becomes even worse when
evalis involved
How eval makes the problem worse
eval "check_user=$USER"
Two things happen:
$USERexpands before eval runsevalexecutes the resulting string as code- This means an attacker can manipulate
$USERso thatevalexecutes something entirely different from what the developer intended. evalallows an attacker to inject code directly into the root‑check logic.
Safe, abstract example (non‑harmful)
Suppose a script contains:
eval "current_user=$USER"
A user can set:
export USER='root some_other_stuff'
Or even something that causes eval to execute additional commands.
Because eval executes the string as code, an attacker can:
- modify script logic
- make the script believe the user is root
- inject arbitrary commands
“If you use eval with untrusted data, you have already lost.”
Why this happens
1) $USER is not a trustworthy source
It is just an environment variable and can be set to anything.
2) eval performs double expansion
This allows an attacker to:
- break out of the intended string
- inject additional code
- modify variables
-
alter script logic
-
Root checks must be based on UID, not variables
The only safe way to check for root is:
if [ "$(id -u)" -eq 0 ]; then
This cannot be manipulated through environment variables.
This cannot be manipulated through environment variables.
if [ "$(id -u)" -eq 0 ]; then
echo "Running as root"
fi
Never use eval on data coming from:
- user input
- environment variables
- files
- command‑line arguments
- network data
Avoid eval entirely unless you fully understand the expansion order and trust the input.
Summary
evalallows attackers to inject code into your script.$USERis not a safe indicator of root privileges.- Combining eval with
$USERis a classic security trap. - It can make a script believe the user is root.
- The only safe root check is
id -u.
Can eval “fake” UID 0?
No - eval can never change the actual UID.
UID is stored in the kernel’s process table. The shell cannot modify it, and eval cannot modify it.
This means:
id -ualways shows the realUIDwhoamialways shows the real usernameevalcannot grant root privilegesevalcannot bypass kernel‑level checks
So the system remains safe.
But eval can fool a poorly written script into thinking the user is root.
How eval can fool a root‑check (without changing UID)
eval "current_user=$USER"
if [ "$current_user" = "root" ]; then
echo "You are root"
fi
A user can simply do:
export USER='root'
The script now believes the user is root, even though:
id -u
still returns a non‑root UID (e.g., 1000).
It becomes even worse if the script does something like:
eval "$USER_check"
An attacker can set:
export USER_check='current_user=root'
eval then executes attacker‑controlled code, altering the script’s logic.
Again: this does not change the real UID - it only manipulates the script’s behavior.
Why this happens
1) $USER is not a secure indicator
It is user‑controlled.
2) eval executes attacker‑controlled text as code
This allows:
- variable manipulation
- logic modification
- command injection
3) The script uses the wrong method for privilege checks
The only correct method is:
if [ "$(id -u)" -eq 0 ]; then
Correct approach
if [ "$(id -u)" -eq 0 ]; then
echo "Running as root"
fi
Never use eval on data from:
- environment variables
- user input
- arguments
- files
- network sources
Avoid eval unless absolutely necessary.
Summary
eval cannot change the real UID. eval cannot grant root privileges. eval can trick a poorly written script into believing the user is root. * This happens when scripts rely on $USER or other environment variables for privilege checks. * The only safe method is id -u.
Bash Commands
Set or unset values of shell options and positional parameters.
bash -c "help set"
Enable execution tracing for debugging
bash --debug
Start Bash with the built‑in debugger enabled
bash --debugger
Print translatable strings (gettext .po format) from Bash
bash --dump-po-strings
Print all static strings compiled into the Bash binary
bash --dump-strings
Display Bash help and usage information
bash --help
Specify an initialization file to read before executing commands
bash --init-file
Start Bash as a login shell
bash --login
Disable command‑line editing (no readline)
bash --noediting
Do not read the system‑wide profile scripts
bash --noprofile
Do not read the user’s ~/.bashrc file
bash --norc
Enable POSIX‑compliant behavior
bash --posix
Pretty‑print shell scripts after parsing
bash --pretty-print
Specify an alternative rc file instead of ~/.bashrc
bash --rcfile
Start Bash in restricted mode
bash --restricted
Print shell input lines as they are read
bash --verbose
Show version information and exit
bash --version
Resource(s)
- http://www.gnu.org/software/bash
- https://www.grymoire.com/Unix/Bourne.html
- https://tldp.org/LDP/abs/html/
- https://github.com/awesome-lists/awesome-bash
- https://web.archive.org/web/20230406195713/https://www.gnu.org/software/bash/
- https://lists.gnu.org/archive/html/bug-bash/2025-07/msg00005.html
- https://mywiki.wooledge.org/BashFAQ/061
- https://web.archive.org/web/20230328220001/https://wiki.bash-hackers.org/start
- https://tiswww.case.edu/php/chet/bash/bashtop.html
- https://google.github.io/styleguide/shellguide.html
- https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html
- https://www.topbug.net/blog/2017/07/31/inputrc-for-humans/
- https://www.gnu.org/savannah-checkouts/gnu/bash/manual/bash.html#Readline-Killing-Commands
- http://jpsdomain.org/linux/linux.html
- https://www.linuxdoc.org/LDP/nag2/index.html