Skip to content

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
Understanding the difference between the different signals in a trap is crucial to building robust scripts that don't leave behind garbage, especially when working with background processes like your spinner.

#!/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 eval is 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:

  • $USER is 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 eval is involved

How eval makes the problem worse

eval "check_user=$USER"

Two things happen:

  • $USER expands before eval runs
  • eval executes the resulting string as code
  • This means an attacker can manipulate $USER so that eval executes something entirely different from what the developer intended.
  • eval allows 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

  • eval allows attackers to inject code into your script.
  • $USER is not a safe indicator of root privileges.
  • Combining eval with $USER is 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 -u always shows the real UID
  • whoami always shows the real username
  • eval cannot grant root privileges
  • eval cannot 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