Back
Keentune
Bash & Shell curriculum 22 chapters
·
218 concepts
·
free
Everything the adaptive question bank can teach and test in Bash & Shell, from foundations through advanced practice. Work through it in order, or start practising and let the questions find your level.
Start practising Bash & Shell
New here? Read the Bash & Shell guide
A free 17-minute primer — the mental model, the mistakes beginners make, and what to practise first.
A. What the shell is, and how a script runs
•
the kernel reads only the first line; #!/usr/bin/env bash finds bash on PATH
•
running a script forks a new shell; source /. runs it in the current one
•
./script needs chmod +x ; bash script does not
•
# starts a comment only at the start of a word, never mid-word or inside quotes
•
the shell tokenizes the line into words first, then expands, then executes
•
different startup files run, which is why a cron script sees a different environment
•
type /command -V says whether a name is a keyword, alias, function, builtin or file
•
PATH is searched left to right; any name containing a slash skips the search entirely
•
bash caches resolved paths, so a moved binary needs hash -r
•
everything inside is literal, and a single quote can never appear inside
•
$ , backtick and \ still act, but word splitting and globbing do not
•
outside quotes \ escapes any character; inside double quotes only before $ , backtick, " , \ or newline
•
a bare $var is word-split then globbed; "$var" is the fix, and the single most common bug
•
quotes are stripped in the final expansion step, so the executed command never sees them
•
$'...' interprets \n , \t , \x41 ; plain quotes do not
•
quoting inside $(...) is parsed independently of the surrounding quotes
•
quoting a glob stops the shell expanding it so the command receives the pattern verbatim
•
a newline between quotes is kept, producing one word containing a newline
C. Simple commands, pipelines and lists
•
words plus redirections; after expansion the first remaining word is the command name
•
| connects stdout only; |& also routes stderr into the next stage
•
a pipeline reports the LAST stage's status, so a failing head goes unnoticed
•
PIPESTATUS holds every stage's status and is overwritten by the next command
•
each stage runs in its own subshell, so assignments made there are lost
•
&& runs on success, || on failure; equal precedence, left to right
•
& returns 0 immediately and puts the pid in $!
•
! cmd inverts the status and is a reserved word, so it needs a following space
•
time is shell syntax, which is why it can time a whole pipeline
D. The expansion order, and word splitting
•
brace, tilde, parameter/arithmetic/command substitution, word splitting, filename expansion, quote removal
•
an expansion's result is not rescanned for $ , so nested variables need ${!x} or eval
•
splitting applies only to UNQUOTED expansion results, using the characters in IFS
•
runs of IFS whitespace collapse and edge ones vanish; other IFS characters each delimit a field
•
IFS= turns word splitting off for that command
•
IFS=, read -ra parts changes IFS only for that command
•
{1..$n} fails because braces expand before parameters
•
{1..10..2} steps, {01..10} zero-pads, {a..e} walks characters
•
brace expansion invents words whether or not the files exist; a glob only matches
•
~ is HOME, ~user that user's home, and only unquoted at the start of a word
•
"$@" yields one word per parameter; "$*" joins them with IFS's first character
E. Parameter expansion operators
•
${var}x where $varx would name a different variable
•
${v:-word} substitutes when unset OR empty; ${v-word} only when unset
•
${v:=word} also assigns, and is illegal on a positional parameter
•
${v:?message} aborts a non-interactive shell with that message
•
${v:+word} substitutes only when v IS set, the idiom for an optional flag
•
${#v} is the character length of a string, not a byte count
•
${v:offset:length} , and a negative offset needs a space to avoid reading as :-
•
# trims the shortest matching prefix, ## the longest
•
% trims the shortest matching suffix, %% the longest
•
${p##*/} and ${p%/*} do it without forking, and how they differ on edge paths
•
${v/pat/rep} replaces the first match, ${v//pat/rep} every match
•
the pattern in # , % and / is a glob, not a regex
•
${v^^} uppercases and ${v,,} lowercases (bash 4+); the single form hits only the first character
•
${!name} expands the variable whose NAME is held in name
F. Parameters, variables and attributes
•
x=1 assigns; x = 1 tries to run a command called x
•
a variable is invisible to child processes until it is exported
•
VAR=1 cmd sets VAR for that one command's environment only
•
unset v removes it, v= leaves it set and empty, and ${v+x} tells them apart
•
$1 …$9 , and that ${10} needs braces to be the tenth
•
discards leading positional parameters and renumbers the rest
•
$# counts positional parameters and never includes $0
•
$? is the previous command's status and is destroyed by the very next command
•
$$ is the shell's pid, $! the last background pid, $BASHPID the real current one
•
declare -r readonly, -i integer, -x export, -a /-A array
•
declare -n ref=target makes assignments to ref write through to target
G. Command and process substitution
•
$(...) nests cleanly; backticks re-interpret backslashes at each level
•
command substitution deletes ALL trailing newlines from the output
•
$(cmd) unquoted is word-split and globbed, so it must be quoted
•
variables set inside $(...) do not survive it
•
the substitution's status becomes $? only when it is the entire command
•
<(cmd) expands to a /dev/fd path a command can open like a file
•
the outer command's status says nothing about the substituted command's
•
while read; do …; done < <(cmd) keeps assignments the pipeline form would lose
•
$((expr)) substitutes the value, and variables inside need no $
•
((expr)) evaluates for its exit status: a nonzero value means success
•
bash arithmetic has no floats; division truncates toward zero
•
% is the remainder of that truncating division, so it takes the sign of the LEFT operand: -7 % 3 is -1 , not 2
•
((i++)) returns 1 when i was 0, which kills a set -e script
•
i++ yields the old value, ++i the new one
•
a leading 0 means octal, 0x hex, base#digits anything else — so 08 is an error
•
an unset or empty variable evaluates to 0 rather than erroring
•
** , then * / % , then + - , then comparisons, then && || , then ?: , then assignment
•
an array subscript is evaluated as arithmetic, which makes an untrusted index a code-injection hole
I. Filename expansion and pattern matching
•
* matches any string including empty; it is not a quantifier on the previous character
•
? is exactly one character; [abc] , [a-z] and [!a] are character sets
•
[[:digit:]] , [[:alpha:]] , [[:space:]] inside a bracket expression
•
a leading . is never matched by a wildcard unless dotglob is set
•
a glob cannot cross a directory separator, so * stays within one level
•
shopt -s globstar makes ** descend into subdirectories
•
an unmatched pattern normally survives literally; these make it vanish or abort instead
•
?() , *() , +() , @() , !() add alternation and repetition after shopt -s extglob
•
matches come back sorted, and the collation order depends on the locale
•
the same syntax drives case , [[ == ]] and the # /% // parameter expansions
J. Redirection and file descriptors
•
0 stdin, 1 stdout, 2 stderr; an unnumbered redirection defaults to 0 for input and 1 for output
•
> truncates an existing file, >> appends; both create it
•
set -o noclobber makes > refuse to overwrite, and >| forces it anyway
•
>file 2>&1 sends both to the file; 2>&1 >file leaves stderr on the terminal
•
2>&1 copies where fd 1 points AT THAT MOMENT, not a permanent link
•
2>&- closes a descriptor so writes to it fail
•
&>file and &>>file are the bash spelling of >file 2>&1
•
a redirection written on a loop, group or function covers everything inside it
•
done > file opens the file once instead of truncating it every iteration
•
exec > file redirects the shell itself for the remainder of the script
•
exec 3<file , read -u 3 , then exec 3<&- to release it
•
the target file is created even when the command turns out not to exist
K. Here-documents and here-strings
•
<<EOF feeds the following lines to stdin until the delimiter appears alone on a line
•
<<'EOF' disables all expansion inside the body; unquoted expands $ and backticks
•
<<-EOF removes leading TABS only, so space-indented bodies still break
•
indentation or trailing whitespace on the delimiter line stops it terminating
•
<<<"$var" supplies one string as stdin and appends a newline
•
read -r -d '' v <<'EOF' works but returns nonzero, which trips set -e
L. Conditional expressions: test, [ and [[
•
[ is an ordinary command whose final argument must be ] , so every space is required
•
[[ ]] is parsed by the shell, so its operands are not word-split or globbed
•
an empty or spaced value turns [ $x = y ] into a syntax error
•
= is the portable spelling inside [ ; [[ ]] accepts == as well
•
in [[ $f == *.txt ]] the right side is a glob, and quoting it makes it literal
•
[[ $s =~ re ]] fills BASH_REMATCH , and quoting the regex makes it a literal string
•
-eq compares integers while < compares strings, so "10" sorts before "9"
•
-e exists, -f regular file, -d directory, -s non-empty, -L symlink, -r /-w /-x access
•
-z empty, -n non-empty, and the bare [ "$x" ] shorthand
•
-a /-o inside [ are deprecated and ambiguous; use && between commands or inside [[
•
(( x > 1 )) is the clearer numeric test and needs no $
M. Compound commands and control flow
•
if branches on an exit status; the condition is a command, not an expression
•
both take a command list as the condition, and until inverts the sense
•
iterates the words left after expansion, so an empty list runs the body zero times
•
for ((i=0; i<n; i++)) runs its three parts in arithmetic context
•
while IFS= read -r line — IFS stops trimming, -r stops backslash eating
•
read assigns a last line lacking a newline but still returns nonzero
•
glob patterns with | alternatives; ;; ends, ;& falls through, ;;& retests
•
the optional count escapes more than one enclosing loop
•
{ …; } groups in the current shell; ( … ) groups in a subshell
•
a cd , assignment or trap inside ( ) does not survive it
•
cmd | while read updates variables in a subshell; use < <(cmd) or lastpipe
N. Shell functions and scoping
•
name() { …; } is portable; the function keyword is a bash extension
•
the body needs a space after { and a ; or newline before }
•
a function gets its own $1 …$n and $# , but keeps the script's $0
•
return leaves the function with a status; exit terminates the whole shell
•
return takes 0–255; to return a value, print it and capture with $(...)
•
assignments inside a function leak to the whole script unless declared local
•
a callee can see and modify the caller's locals; bash has no lexical scope
•
local x=$(cmd) reports local 's status, hiding a failing command from set -e
•
FUNCNAME is the call stack as an array, and recursion is depth-limited
•
export -f passes a function to a child bash; other shells never see it
O. Arrays and associative arrays
•
a=(one two three) splits on whitespace; quoting keeps an element containing spaces whole
•
indices start at 0 and can have holes after an unset
•
"${a[@]}" gives one word per element; unquoted it re-splits every element
•
"${a[*]}" collapses the array into ONE word joined by IFS's first character
•
$a means ${a[0]} , silently dropping the rest
•
a+=(x) appends an element, while a=x overwrites element zero only
•
"${a[@]:1:2}" takes a range, and "${@:2}" drops the first argument
•
declare -A is mandatory (bash 4+); without it the keys collapse to index 0
•
hash keys come back in no defined order, so sort if order matters
•
"${!a[@]}" yields indices or keys, which is the only safe way to iterate a sparse or associative array
•
arrays cannot be placed in the environment, so a child process cannot inherit one
P. Exit status and error handling
•
0 succeeds; 126 is not-executable, 127 is not-found, 128+n means killed by signal n
•
set -euo pipefail combines errexit, nounset and pipeline-failure propagation, but each flag retains its documented exceptions
•
set -e exits when a simple command fails
•
a command inside if , while , && , || or after ! never triggers set -e
•
calling a function in a condition suspends -e for its whole body
•
only the last stage counts, so false | true succeeds without pipefail
•
makes a pipeline fail if any stage failed, and exposes SIGPIPE 141 from an early-closing reader
•
set -u makes expanding an unset variable fatal; ${v:-} is the deliberate opt-out
•
"${a[@]}" on an empty array aborted under -u before bash 4.4
•
set -x traces expanded commands, and PS4 can add the script name and line number
•
cmd || { msg; exit 1; } gives a real message where set -e gives silence
•
( exit 1 ) ends only the subshell, so the script keeps going
•
: and true are no-ops returning 0, and || true deliberately swallows a failure
•
trap 'code' SIGNAL stores the handler as a STRING that is re-parsed when the signal arrives
•
a trap on EXIT runs however the shell ends, which is the cleanup idiom
•
ERR fires on exactly the conditions that would make set -e exit
•
functions, subshells and substitutions do not inherit ERR/DEBUG/RETURN traps without set -E /set -T
•
a trap in a non-interactive shell runs after the current foreground command completes
•
trap '' SIG ignores the signal; trap - SIG restores the default action
•
SIGKILL and SIGSTOP can be neither trapped nor ignored
•
the shell reports 128 + the signal number, so Ctrl-C shows 130
•
the trap must be installed before the resource exists to be leaked, not after the work
R. Job control and processes
•
jobs lists them and %1 , %+ , %- and %name address them
•
resume a stopped job in the foreground or let it continue in the background
•
Ctrl-Z sends SIGTSTP, leaving the process stopped rather than terminated
•
wait pid returns that child's status; bare wait waits for all children
•
wait -n returns as soon as any one child finishes (bash 4.3+)
•
one removes the job from the shell's table, the other makes it ignore SIGHUP
•
monitor mode is off in non-interactive shells, so jobspecs and Ctrl-Z do not apply
•
kill -- -PGID reaches the children a plain kill leaves orphaned
•
starts a background command with two-way pipes exposed as a file-descriptor array
S. Shell options: set and shopt
•
set -o toggles the POSIX-style options, shopt -s/-u the bash-only ones
•
set -- a b c replaces the positional parameters, and -- stops flag parsing
•
set -f turns filename expansion off for the rest of the shell
•
set -a exports every variable assigned afterwards, the trick for loading an env file
•
a called script starts with its own options; only subshells inherit them
•
shopt -s nocasematch makes case and [[ == ]] case-insensitive
•
runs the last pipeline stage in the current shell, so its assignments survive (job control must be off)
•
set -o posix , or invoking bash as sh , changes several behaviors and disables extensions
T. Invocation, startup files and the environment
•
which of /etc/profile , ~/.bash_profile , ~/.bashrc and ~/.bash_logout run for each shell kind
•
a plain script sources no rc file at all unless BASH_ENV names one
•
ssh host cmd gets a non-interactive non-login shell, which is why PATH is different there
•
a child receives a copy, so nothing it does can change the parent's variables
•
export marks a variable for children; export -n removes the mark without unsetting it
•
prepend versus append, and that an empty PATH element silently means the current directory
•
BASH_VERSION, RANDOM, SECONDS, LINENO, PWD/OLDPWD, BASH_SOURCE
•
pushd /popd /dirs , and that a cd in a subshell never moves the parent
U. The builtins that carry scripts
•
read a b c splits the line on IFS and puts all remaining fields into the last variable
•
without -r , read consumes backslashes as escapes and mangles Windows paths
•
-a fills an array, -d changes the terminator, -n limits characters
•
echo 's handling of -n , -e and backslashes varies by shell; printf is defined
•
leftover arguments re-run the format string, which formats a whole list in one call
•
%q quotes a value so the shell can safely re-read it
•
eval triggers a whole extra expansion pass, which is exactly why it is an injection risk
•
exec cmd never returns; with no command it applies only the redirections
•
command x bypasses functions and aliases, builtin x forces the builtin over a function
•
parses clustered short options from an optstring, using OPTARG and OPTIND, and cannot do long options
•
mapfile -t arr < file reads lines into an array with no loop and no word splitting (bash 4+)
V. Portability: bash versus POSIX sh
•
[[ , arrays, local , += , <<< , $'...' and process substitution are all outside POSIX
•
Debian and Ubuntu link /bin/sh to dash, so a #!/bin/sh script must avoid extensions
•
flag and escape handling differ between shells and builtins, so scripts should use printf
•
only the name() { …; } form is POSIX
•
. is POSIX; source is a bash spelling of it
•
[ a == b ] is a bashism; = is the portable string comparison
•
associative arrays and mapfile need bash 4, wait -n and ${v@Q} need 4.3/4.4, and macOS ships 3.2
•
the static linter that catches the unquoted-variable, word-splitting and set -e classes automatically
Keentune is not affiliated with or endorsed by the organizations whose documentation informs these maps.
Start practising Bash & Shell
All about Bash & Shell practice
All exam, test, and product names and trademarks are the property of their respective owners and are used here for identification and reference only. Keentune is independent study practice — not affiliated with, authorized, or endorsed by any of these organizations.
© 2026 SportaApp LLC