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 $PS1 so the operator knows the shell is ready

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

\u

Username

\h

Hostname (short, up to the first .)

\H

Hostname (full)

\w

Working directory (~ for $HOME)

\W

Basename of the working directory only

\$

# when root, $ otherwise

\d

Date as Weekday Month Day

\D{fmt}

Date / time formatted with strftime (e.g. \D{%F %T})

\t

Time in 24-hour HH:MM:SS

\T

Time in 12-hour HH:MM:SS

\@

Time in 12-hour am/pm

\A

Time in 24-hour HH:MM

\s

Shell name (basename of $0)

\v

Bash version (e.g. 5.2)

\V

Bash version + patch level

\j

Number of jobs the shell is currently managing

\l

Basename of the shell’s terminal device

\!

History number of this command

\#

Command number of this command (current shell session)

\a

Bell (ASCII 07)

\e

Escape (ASCII 033), starts an ANSI sequence

\n

Newline

\r

Carriage return

\nnn

Character with octal value nnn

\\

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

CMD

Command with no flags or arguments

CMD -f

Short flag (single letter)

CMD --flag

Long flag

CMD -abc

Stacked short flags (same as -a -b -c)

CMD --flag=value

Long flag with a value

CMD -- arg

-- ends flags; everything after is an argument

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: $VAR, $(...), \ still active

\X

Escape one character

$(cmd)

Command substitution; inserts cmd’s stdout

${VAR}

Brace-quote a variable name

*

Match any number of characters in a filename

?

Match exactly one character

[abc]

Match any one of a, b, c

{a,b,c}

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

history

List the current shell’s history

!!

Re-run the previous command

!N

Re-run command number N from history

!cmd

Re-run the last command starting with cmd

!$

Last argument of the previous command

Ctrl-R

Reverse-incremental search through history

Ctrl-P / Ctrl-N

Previous / next command

$HISTSIZE / $HISTFILE

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

cmd &

Start cmd in the background

Ctrl-Z

Suspend the foreground job

jobs

List jobs in the current shell

fg %N

Bring job N to the foreground

bg %N

Resume job N in the background

kill %N

Send SIGTERM to job N

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

NAME=value

Shell variable; visible only in this shell

export NAME=value

Environment variable; inherited by children

echo $NAME / ${NAME}

Read the value

unset NAME

Remove the variable

printenv

List all environment variables

set

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

local NAME=value (inside a function)

Function

The function (and anything it calls, until local is shadowed)

NAME=value (top level or function without local)

Shell

This shell only; children see nothing

export NAME=value / declare -x NAME=value

Process (environment)

This shell and every child process it spawns

NAME=value cmd

Process, scoped to one command

cmd only; gone the moment cmd exits

        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

0

Success

1

Generic failure

2

Usage / syntax error

126

Found but not executable

127

Command not found

128 + N

Killed by signal N (e.g. 130 = SIGINT / Ctrl-C)

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?”

man CMD / CMD --help

“Just show me an example.”

tldr CMD

“What does this command do?”

whatis CMD

“What command does X?”

apropos KEYWORD (alias for man -k)

“Where is this installed?”

which CMD / whereis CMD

“Is this a builtin or binary?”

type CMD

“Help on a Bash builtin?”

help BUILTIN

“Full GNU manual?”

info CMD

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 (ls, cd, grep)

2

System calls (read, open, write)

3

Library functions (printf, malloc)

4

Special files and devices (/dev/null, /dev/tty)

5

File formats and configuration (passwd, crontab)

6

Games

7

Miscellaneous (signal, ascii, regex)

8

System administration (mount, iptables, sshd)

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

type CMD

Builtins, aliases, functions, AND $PATH

type -a CMD

Every match (a name can resolve multiple ways)

command -v CMD

POSIX-portable; safe in scripts (returns the path)

which CMD

$PATH only (skips builtins); less reliable

whereis CMD

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, especially QUOTING, EXPANSION, REDIRECTION, and JOB CONTROL sections).

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