The Terminal#
A terminal in Linux is a command line interface running a shell program that allows users to interact with the operating system by entering commands. It provides a way to execute commands, navigate files, and perform various tasks without a graphical user interface.
Bash is the default shell on Debian, Ubuntu, and Kali. Other
common shells (zsh, fish, dash) behave the same way
for the material in this section; differences only matter when
writing scripts.
REPL#
A shell is a read-eval-print loop, or REPL. For every line the operator types, the shell runs the same five steps, then returns to the start and waits for the next line:
Step |
What happens |
|---|---|
Prompt |
Print |
Read |
Read one line from stdin |
Execute |
Parse the line and run the command |
Write |
Send the command’s stdout / stderr to the terminal |
Capture |
Store the command’s exit code in |
sequenceDiagram
actor U as Operator
participant S as Shell
participant C as Command
loop every line
S->>U: Prompt ($PS1)
U->>S: Read (line on stdin)
S->>C: Execute (parse and run)
C->>U: Write (stdout / stderr)
C->>S: Capture (exit code → $?)
end
The rest of this page walks through each piece of that loop.
Prompt#
The prompt is the line of text the shell prints when it is ready for input. It identifies who is logged in, which host the shell is running on, where in the filesystem the shell currently sits, and whether the operator is running as root. A default Bash prompt looks like:
operator@system:~$
Anatomy of the prompt:
operator@system:~$
└───┬──┘ └──┬─┘ ┬┬
│ │ ││
│ │ │└── prompt symbol ($ for user, # for root)
│ │ └─── current directory (~ = $HOME)
│ └─────── hostname
└─────────────── username
Customise the prompt by setting the PS1 environment variable.
Bash recognises a set of backslash escapes that expand at prompt time:
Escape |
Expands to |
|---|---|
|
Username |
|
Hostname (short, up to the first |
|
Hostname (full) |
|
Working directory ( |
|
Basename of the working directory only |
|
|
|
Date as |
|
Date / time formatted with |
|
Time in 24-hour |
|
Time in 12-hour |
|
Time in 12-hour am/pm |
|
Time in 24-hour |
|
Shell name (basename of |
|
Bash version (e.g. |
|
Bash version + patch level |
|
Number of jobs the shell is currently managing |
|
Basename of the shell’s terminal device |
|
History number of this command |
|
Command number of this command (current shell session) |
|
Bell (ASCII 07) |
|
Escape (ASCII 033), starts an ANSI sequence |
|
Newline |
|
Carriage return |
|
Character with octal value |
|
A literal backslash |
|
Begin / end a run of non-printing characters; required around ANSI color codes so Bash counts the prompt width correctly |
Inspect the current format. $PS1 holds the literal escape
string the shell expands every time it prints a new prompt.
echo prints its arguments to stdout, separated by spaces:
operator@system:~$ echo $PS1
\[\e]0;\u@\h: \w\a\]${debian_chroot:+($debian_chroot)}\u@\h:\w\$
Set a new format. Assigning to PS1 takes effect on the next
prompt; export it (or put the line in ~/.bashrc) to make it
stick across new shells:
operator@system:~$ PS1='\$ '
$
Note
All code examples will drop the operator@system:~$ and will use $ as the prompt to save space.
Commands#
A command is the first word on a line. The shell looks it up
in this order: alias, function, builtin, then
executable on $PATH (the colon-separated list of
directories the shell searches for binaries). Everything after
the command name is either a flag (modifies behavior; starts
with - or --) or an argument (the thing the command
operates on).
ls -lah /etc
┬ └┬┘ └─┬┘
│ │ │
│ │ └── argument(s)
│ └─────── flag(s) / option(s)
└─────────── command
Form |
Meaning |
|---|---|
|
Command with no flags or arguments |
|
Short flag (single letter) |
|
Long flag |
|
Stacked short flags (same as |
|
Long flag with a value |
|
|
Example: ls lists directory contents and is the standard
example for flag forms.
Bare command, no flags or arguments:
$ ls
Documents Downloads notes.txt scripts
Short flag (single letter, -l for long format):
$ ls -l
drwxr-xr-x 2 operator operator 4096 May 1 09:12 Documents
-rw-r--r-- 1 operator operator 42 May 1 09:14 notes.txt
Long flag (whole word):
$ ls --all
. .. .bashrc Documents Downloads notes.txt scripts
Stacked short flags (same as -l -a):
$ ls -la
drwx------ 5 operator operator 4096 May 1 09:14 .
drwxr-xr-x 3 root root 4096 Apr 28 17:02 ..
-rw------- 1 operator operator 220 Apr 28 17:02 .bashrc
drwxr-xr-x 2 operator operator 4096 May 1 09:12 Documents
Long flag with a value:
$ ls --color=auto
Documents Downloads notes.txt scripts
-- ends flags; everything after it is treated as an argument:
$ ls -- -filename
ls: cannot access '-filename': No such file or directory
Subcommands#
Some commands group related actions under subcommands, a
second word that picks the operation. git commit,
apt install, and systemctl start are subcommands of their
parent commands. A subcommand is not a flag: git -v prints
git’s version; git log is a separate operation entirely. Each
subcommand usually takes its own flags and arguments.
git commit -m "msg"
└┬┘ └──┬─┘ ┬ └─┬─┘
│ │ │ │
│ │ │ └── argument to the subcommand (commit message)
│ │ └─────── flag of the subcommand (-m / --message)
│ └─────────── subcommand (commit)
└───────────────── command name (git)
Example: git is a subcommand-driven CLI; the second word picks
the operation.
Show the working-tree state:
$ git status
On branch main
nothing to commit, working tree clean
Stage a file (subcommand + argument):
$ git add file.txt
(no output; success is silent)
Record the staged changes (subcommand + flag + argument):
$ git commit -m "fix: typo"
[main 4e89a04] fix: typo
1 file changed, 1 insertion(+), 1 deletion(-)
Show recent commits, one per line (subcommand + flags):
$ git log --oneline -5
55c5c60 updates
4e89a04 updates
6d0e5ea updates
810c53f Initial commit
Quoting & Globs#
The shell does not pass the operator’s line to the command
verbatim. Before the command runs, the shell expands
variables ($VAR), runs command substitutions ($(cmd)),
and matches filename patterns (*, ?, [...]).
Quoting turns expansion off; globs are the patterns
themselves.
Form |
Effect |
|---|---|
|
Single quotes: nothing inside is expanded |
|
Double quotes: |
|
Escape one character |
|
Command substitution; inserts |
|
Brace-quote a variable name |
|
Match any number of characters in a filename |
|
Match exactly one character |
|
Match any one of |
|
Brace expansion; a literal list, not a glob |
Example: each form below shows one expansion or quoting rule.
Single quotes block all expansion; the literal $HOME is printed:
$ echo '$HOME'
$HOME
Double quotes still expand $VAR and $(...):
$ echo "$HOME"
/home/operator
Command substitution runs printenv (which prints one or all
environment variables) and inserts its stdout into the line:
$ echo "user is $(printenv USER)"
user is operator
Glob: * matches any number of characters in a filename:
$ ls *.log
auth.log kern.log syslog.log
? matches exactly one character:
$ ls report-?.txt
report-1.txt report-a.txt
Character class [0-9] matches one digit:
$ ls log-[0-9].txt
log-0.txt log-3.txt log-7.txt
Brace expansion is a literal list, not a glob; the files need not exist:
$ echo {dev,test,prod}.env
dev.env test.env prod.env
Always quote variables holding paths so spaces in the value do not split into separate arguments:
$ ls "$DIR"
(lists contents of whatever path $DIR holds, with spaces preserved)
History#
Bash remembers every command run in the current session and
writes them to ~/.bash_history when the shell exits. History
is both an audit log and a re-execution shortcut.
Form |
Effect |
|---|---|
|
List the current shell’s history |
|
Re-run the previous command |
|
Re-run command number |
|
Re-run the last command starting with |
|
Last argument of the previous command |
|
Reverse-incremental search through history |
|
Previous / next command |
|
Size limit / location of history file |
Example: a normal command, followed by the history shortcuts that recall pieces of it.
Run the original command:
$ ls /etc/nginx/
conf.d nginx.conf sites-available sites-enabled
!! expands to the previous command verbatim before running:
$ echo !!
echo ls /etc/nginx/
ls /etc/nginx/
!$ expands to just the last argument of the previous command:
$ echo !$
echo /etc/nginx/
/etc/nginx/
history lists every command run in the current session,
numbered:
$ history
40 ls /etc/nginx/
41 echo !!
42 echo !$
43 history
!N re-runs command number N from the history list:
$ !42
echo !$
/etc/nginx/
Job Control#
A job is a process or pipeline started from the shell. The shell can run a job in the foreground (the operator sees its output and cannot type anything else until it finishes), suspend it (pause without killing it), or run it in the background (it runs while the operator continues typing).
Form |
Effect |
|---|---|
|
Start |
|
Suspend the foreground job |
|
List jobs in the current shell |
|
Bring job |
|
Resume job |
|
Send SIGTERM to job |
Transitions between the three states:
sequenceDiagram
actor U as User
participant FG as Foreground
participant BG as Background
Note over FG: job running
U->>FG: Ctrl-Z
Note over FG: suspended
U->>BG: bg
Note over BG: job running
U->>FG: fg
Note over FG: job running
Example: sleep is the standard long-running command for
demonstrating job states.
Start sleep in the background; the shell prints the job
number and PID and returns to the prompt:
$ sleep 600 &
[1] 18432
jobs lists every job tracked by the current shell:
$ jobs
[1]+ Running sleep 600 &
Run a foreground job and press Ctrl-Z to suspend it:
$ sleep 60
^Z
[2]+ Stopped sleep 60
bg resumes the most recently suspended job in the background:
$ bg
[2]+ sleep 60 &
fg %N brings job N back to the foreground:
$ fg %1
sleep 600
kill %N sends SIGTERM to job N (signal 15 by default):
$ kill %1
[1]+ Terminated sleep 600
Variables#
The shell has two kinds of named values. Shell variables are
visible only to the current shell. Environment variables are
inherited by every process the shell starts. Both are set with
NAME=value; the difference is whether the operator
exports them.
Command |
Effect |
|---|---|
|
Shell variable; visible only in this shell |
|
Environment variable; inherited by children |
|
Read the value |
|
Remove the variable |
|
List all environment variables |
|
List all shell variables and functions |
Without export a child process never sees the variable. With
export the variable is inherited:
flowchart LR
subgraph WO["NAME=value"]
direction TB
SHO["shell<br/>NAME = value<br/><i>(shell variable only)</i>"]
CHO["child<br/>NAME unset"]
SHO -. "fork (env-only crosses)" .-> CHO
end
subgraph WE["export NAME=value"]
direction TB
SHE["shell<br/>NAME = value<br/><i>(in env block)</i>"]
CHE["child<br/>NAME = value"]
SHE == "fork (env inherited)" ==> CHE
end
WO ~~~ WE
classDef inherited stroke:#58a6ff,stroke-width:2px;
class CHE inherited
$PATH is the most important environment variable. It tells
the shell which directories to search when the operator types a
command name:
Set a shell variable; assignment is silent:
$ NAME=value
(no output; assignment is silent)
Read it back in this shell:
$ echo $NAME
value
Spawn a child shell with bash -c; the child does not inherit
shell variables, so $NAME is empty:
$ bash -c 'echo $NAME'
(empty line; child does not inherit shell variables)
export promotes a shell variable to an environment variable so
child processes inherit it:
$ export NAME=value
(no output)
The child shell now sees the value:
$ bash -c 'echo $NAME'
value
$PATH is the colon-separated list of directories the shell
searches for executables:
$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
Append a directory to $PATH; export makes the change
visible to anything launched from this shell:
$ export PATH=$PATH:/new/dir
(no output)
printenv reads one environment variable (or lists all of them
when called with no argument):
$ printenv USER
operator
Operators often store secrets (API keys, tokens, passwords) in environment variables so they are not hard-coded into source. Better still, fetch them from a secrets manager at runtime so they never sit on disk in plain text.
Scope#
Variables live in one of three concentric scopes, from
narrowest to widest. Function scope is the binding local
creates inside a shell function and is dropped when the function
returns. Shell scope is global to the current shell process;
every function and command run by the shell sees it, and it dies
when the shell exits. Process scope is the environment block
the shell hands to every child process at fork / exec;
only export-ed variables make it across that boundary.
Form |
Scope |
Visible to |
|---|---|---|
|
Function |
The function (and anything it calls, until |
|
Shell |
This shell only; children see nothing |
|
Process (environment) |
This shell and every child process it spawns |
|
Process, scoped to one command |
|
flowchart TB
subgraph PROC["Process scope (environment block, inherited by every child)"]
direction TB
subgraph SHELL["Shell scope (current bash process, global to this script)"]
direction TB
subgraph FUNC["Function scope (local NAME=value, popped on return)"]
FV["local NAME=value<br/>visible inside fn only"]
end
SV["NAME=value<br/>visible to whole shell, not exported"]
end
EV["export NAME=value<br/>crosses fork/exec to children"]
end
A plain assignment inside a function writes to the shell-global
binding, not a function-local one. local is what makes it
private:
$ name="caller"
$ set_global() { name="$1"; } # writes to outer name
$ set_private() { local name="$1"; } # writes to a private copy
$ set_global "analyst"; echo "$name"
$ set_private "ghost"; echo "$name"
analyst
analyst
Subshells (( ... ), pipelines, and command substitution
$( ... )) fork a child bash that inherits both shell and
environment scope, but nothing the child changes leaks back:
$ x=1
$ ( x=99; echo "in subshell: $x" )
$ echo "in parent: $x"
in subshell: 99
in parent: 1
The same effect explains why echo data | read v leaves $v
empty in bash: the right side of the pipe runs in a subshell, so
its assignment dies when the stage exits.
Exit Codes#
Every command returns an integer to the shell when it finishes.
0 means success; any non-zero value means something went
wrong. The exact meaning of a non-zero code is up to the program.
The shell stores the most recent exit code in $? and uses it
to decide whether && and || should fire. Scripts that say
set -e abort on the first non-zero exit, which is the safest
default once the operator starts writing automation.
Code |
Meaning |
|---|---|
|
Success |
|
Generic failure |
|
Usage / syntax error |
|
Found but not executable |
|
Command not found |
|
Killed by signal |
Example: $? holds the most recent exit code; the next four
forms read it directly or branch on it.
A successful command exits 0:
$ echo "hello"; echo "exit=$?"
hello
exit=0
A missing path makes ls exit non-zero (2 on GNU coreutils):
$ ls /no/such; echo "exit=$?"
ls: cannot access '/no/such': No such file or directory
exit=2
When the shell cannot find the command at all, it returns 127:
$ nosuchcmd; echo "exit=$?"
bash: nosuchcmd: command not found
exit=127
true always exits 0; && runs the right-hand side only
after a zero exit:
$ true && echo "ran"
ran
false always exits non-zero; || runs the right-hand side
only after a non-zero exit:
$ false || echo "fired"
fired
Documentation#
Linux commands ship with their own documentation. Several tools read it, each answering a slightly different question. Pick the right tool first:
Question |
Reach for |
|---|---|
“How do I use this command?” |
|
“Just show me an example.” |
|
“What does this command do?” |
|
“What command does X?” |
|
“Where is this installed?” |
|
“Is this a builtin or binary?” |
|
“Help on a Bash builtin?” |
|
“Full GNU manual?” |
|
The rest of this section walks each tool with worked examples; the question table above is the quick lookup.
Man Pages#
Man pages (man) are the standard reference. Each page covers
one command, sectioned by topic. The eight sections matter when a
name lives in more than one (passwd is both a command and a
file format); use man N CMD to disambiguate.
Section |
Topic |
|---|---|
1 |
User commands ( |
2 |
System calls ( |
3 |
Library functions ( |
4 |
Special files and devices ( |
5 |
File formats and configuration ( |
6 |
Games |
7 |
Miscellaneous ( |
8 |
System administration ( |
GNU’s info reader is hypertext documentation, used heavily by
GNU projects (gcc, make, coreutils); for those, the
info page is often more detailed than the man page. --help
prints a short usage summary. For Bash builtins like cd,
export, set, alias, and read, there is no man page,
so use the help builtin instead. tldr is a community
project with practical examples for the case where the man page
is correct but unfriendly. On Debian, Ubuntu, and Kali, every
package ships its README, examples, and changelog under
/usr/share/doc/PKG/, often more useful than the man page
when the operator needs a config template or a worked example.
Open the standard man page (press q to quit):
$ man ls
LS(1) User Commands LS(1)
NAME
ls - list directory contents
...
Disambiguate by section number; passwd is both a command and
a file format, and section 5 selects the file format:
$ man 5 passwd
PASSWD(5) File Formats and Conversions PASSWD(5)
NAME
passwd - the password file
...
man -f prints just the one-line summary (the same output as
whatis):
$ man -f ls
ls (1) - list directory contents
Open the full GNU info manual for ls:
$ info ls
File: coreutils.info, Node: ls invocation
10.1 'ls': List directory contents
...
Jump straight to a named node inside an info manual:
$ info coreutils 'ls invocation'
10.1 'ls': List directory contents
===================================
...
Print the binary’s built-in usage summary:
$ ls --help
Usage: ls [OPTION]... [FILE]...
...
help is the Bash builtin documentation tool; use it for
builtins that have no man page:
$ help cd
cd: cd [-L|[-P [-e]] [-@]] [dir]
Change the shell working directory.
...
help -m formats the output like a man page (NAME, SYNOPSIS,
DESCRIPTION):
$ help -m export
NAME
export - Set export attribute for shell variables.
SYNOPSIS
export [-fn] [name[=value] ...] or export -p
...
tldr shows a few practical examples instead of the full
manual:
$ tldr tar
tar
Archiving utility.
Create an archive from files:
tar cf target.tar file1 file2
...
Refresh the locally cached tldr pages:
$ tldr --update
Successfully updated local database
Browse the package’s bundled docs (README, examples, changelog):
$ ls /usr/share/doc/curl/
changelog.Debian.gz copyright NEWS.gz README.Debian
Finding by name#
When the operator does not know the command yet, search the
man-page summaries by keyword. apropos and man -k are the
same tool. whatis is the inverse: given a command, print just
its one-line description (the first line of the man page).
Search every man-page summary on the box for a keyword:
$ apropos compress
gzip (1) - compress or expand files
xz (1) - compress or decompress .xz and .lzma files
zstd (1) - zstd, zstdmt, unzstd, zstdcat - Compress or decompress .zst files
Quote multi-word phrases so they are treated as one pattern:
$ apropos 'directory listing'
ls (1) - list directory contents
tree (1) - list contents of directories in a tree-like format
man -k is the same tool as apropos:
$ man -k regex
regex (3) - POSIX regex functions
regex (7) - POSIX.2 regular expressions
whatis is the inverse of apropos: given a name, print
its one-line description.
$ whatis ls
ls (1) - list directory contents
Restrict the lookup to a single section:
$ whatis -s 1 ls
ls (1) - list directory contents
Locating commands#
Several tools answer “what runs when I type CMD?”. They differ in what they consider:
Command |
Sees |
|---|---|
|
Builtins, aliases, functions, AND |
|
Every match (a name can resolve multiple ways) |
|
POSIX-portable; safe in scripts (returns the path) |
|
|
|
Binary + source + man-page paths |
type shows whether a name is a builtin, alias, function, or
$PATH binary; the most informative of the four:
$ type cd
cd is a shell builtin
type -a lists every match across builtins and $PATH,
which matters when a name resolves more than one way:
$ type -a echo
echo is a shell builtin
echo is /usr/bin/echo
command -v is the POSIX-portable, script-safe presence check;
prints the path if found and exits non-zero if not:
$ command -v ls && echo "installed"
/usr/bin/ls
installed
which searches $PATH only and skips builtins; less
reliable than type or command -v but still common:
$ which ls
/usr/bin/ls
whereis prints the binary, source, and man-page paths for a
command in one go:
$ whereis ls
ls: /usr/bin/ls /usr/share/man/man1/ls.1.gz
Common Tasks#
Identify the current shell and login flavour (different shells read different RC files).
$ echo $0
$ ps -p $$
$ readlink -f /proc/$$/exe
$ shopt login_shell 2>/dev/null
Inspect the environment (what was inherited).
$ env | sort
$ echo "$PATH" | tr ':' '\n'
$ alias
$ type -a ls
Audit RC files for persistence (common quiet places to drop a payload).
$ ls -la ~/.bashrc ~/.bash_profile ~/.profile ~/.zshrc 2>/dev/null
$ ls -la /etc/profile /etc/profile.d/ /etc/bash.bashrc
$ grep -rEn 'curl|wget|nc|bash -i|/dev/tcp' ~/.bashrc ~/.profile /etc/profile.d/ 2>/dev/null
Read history (the operator before me typed something).
$ history | tail -50
$ ls -la ~/.bash_history ~/.zsh_history
$ HISTTIMEFORMAT='%F %T ' history | tail -50
Get an interactive shell when the spawn looks broken (TTY upgrades).
$ python3 -c 'import pty; pty.spawn("/bin/bash")'
$ script -q /dev/null
$ stty raw -echo; fg # in a backgrounded reverse shell
Stop history from leaking (when the operator is on a box they own and want to keep work private).
$ unset HISTFILE
$ export HISTSIZE=0
$ history -c
Search and reuse history (find a prior command fast).
$ history | grep -i ssh
$ Ctrl-r # incremental reverse search
$ !! # repeat last command
$ !$ # last argument of previous command
Run, background, foreground, detach (job control basics).
$ long-running-cmd &
$ jobs -l
$ fg %1; bg %1
$ disown -h %1
$ nohup ./script.sh >run.log 2>&1 &
Apply config without re-login (reload an RC file).
$ source ~/.bashrc
$ . /etc/profile
$ exec $SHELL -l # restart the login shell
Resolve a command (which binary will actually run).
$ command -v python3
$ type -a ls
$ which -a python3
$ hash -r # forget cached lookups
Persist a session across disconnects (multiplexers and detachable shells).
$ tmux new -s ops; tmux a -t ops
$ screen -S ops; screen -r ops
$ nohup command &
References#
man 1 bash(the standard Bash reference, especiallyQUOTING,EXPANSION,REDIRECTION, andJOB CONTROLsections).man 7 bash-builtins(summary of Bash builtins).man 1 man,man 1 info,man 1 tldr,man 1 whatis,man 1 apropos,man 1 help(manual and help lookup).man 1 which,man 1 whereis,man 1 type,man 1 command(resolve what runs when the operator types a name).man 1 echo,man 1 printenv,man 1 export,man 1 unset,man 1 set(environment manipulation).man 1 history,man 1 jobs,man 1 bg,man 1 fg,man 1 kill,man 1 sleep(history and job control).man 1 tmux,man 1 screen(session multiplexers).Standard I/O for standard I/O, redirects, and pipes that wire commands together.
Users for the identity layer the shell runs under.
Linux for the command quick-reference index.