Skip to content

Shell Basics

A shell is both an interactive command interpreter and a scripting language interpreter. When you open a terminal emulator, it spawns a shell process — bash``zshOr dash. When you run a script with ./script.shThe shebang line determines which interpreter processes The file.

AspectInteractiveNon-Interactive
Startup files.bashrc``.profile``/etc/bash.bashrcOnly BASH_ENV variable if set
PromptPS1``PS2 displayedNo prompt
Job controlEnabled (set -m)Disabled by default
Line editingReadline active (readline library)Not active
AliasesExpandedExpanded (in bash) or not (POSIX mode)
Exit on errorDoes not exit on errorSame unless set -e

The shell”s startup sequence differs depending on whether it is a login shell or a non-login Shell:

  • Login shell: Sourced on first login (SSH, su -``login). Reads /etc/profileThen ~/.bash_profile or ~/.bash_login or ~/.profile (first one found).
  • Non-login interactive shell: Reads ~/.bashrc.
  • Non-login non-interactive shell: Inherits exported environment variables. Does not read startup files unless BASH_ENV points to one.
flowchart TD
    A[Shell Invoked] --> B{Login Shell?}
    B -->|Yes| C{Interactive?}
    B -->|No| D{Interactive?}
    C -->|Yes| E[/etc/profile]
    E --> F[~/.bash_profile]
    F --> G[~/.bashrc]
    C -->|No| H[BASH_ENV]
    D -->|Yes| I[~/.bashrc]
    D -->|No| J{BASH_ENV set?}
    J -->|Yes| K[Source BASH_ENV]
    J -->|No| L[No startup files]

In practice, most systems configure ~/.profile to source ~/.bashrcSo both login and non-login Interactive shells load the same configuration. However, scripts executed by cron or systemd do not Source ~/.bashrc — this is a frequent source of bugs.

POSIX specifies a shell standard (IEEE 1003.1, also known as the Single UNIX Specification). Bash is Largely POSIX-compliant but adds extensions. When writing portable scripts, target POSIX sh. When Writing for known-bash environments, use bash features deliberately.

FeaturePOSIX shBash
ArraysNoYes (arr=())
[[ ]]NoYes (preferred over [ ])
(( ))NoYes (arithmetic evaluation)
Process substitutionNoYes (<()``>())
Associative arraysNoYes (bash 4.0+)
[[ $a =~ $re ]]NoYes (regex matching)
${var:offset:length}NoYes (substring expansion)
&\>\&2 redirectionNoYes

On Debian and Ubuntu, /bin/sh is dash — a minimal POSIX shell that is significantly faster than Bash but lacks bash extensions. Scripts that use bash features must use #!/bin/bashNot #!/bin/sh.

Every command executed by the shell follows this general pattern:

command [options] [arguments] [--] [operands]

The shell performs the following steps before executing a command:

  1. Tokenization: Split the input line into words based on IFS (Internal Field Separator, default: space, tab, newline).
  2. Alias expansion: Replace aliases (only in interactive shells, and not for the first word in certain contexts).
  3. Brace expansion: Expand {a,b,c} patterns.
  4. Tilde expansion: Replace ~ with $HOME``~user with user’s home directory.
  5. Parameter expansion: Expand $VAR``${VAR}``${VAR:-default}Etc.
  6. Command substitution: Execute $(cmd) or `Cmd` and replace with stdout.
  7. Arithmetic expansion: Evaluate $((expression)).
  8. Word splitting: Split results on IFS (except in contexts where it is suppressed).
  9. Pathname expansion (globbing): Expand *``?``[...] patterns.
  10. Quote removal: Remove quoting characters that are not part of expansions.
  11. Redirection: Set up I/O redirections.
  12. Command execution: Execute the command using the resolved path.

Pipe buffer size: Linux pipes have a default buffer of 64 KiB (since kernel 2.6.11, configurable Via /proc/sys/fs/pipe-max-size). When the buffer is full, the writing process blocks until the Reader consumes data. For high-throughput pipelines, this can be a bottleneck.

Process substitution creates a temporary named pipe (FIFO) or /dev/fd/ entry:

Terminal window
# Compare output of two commands
diff <(command1) <(command2)
# Feed a command's output as a file argument
wc -l <(find /etc -name "*.conf")
# Tee output to multiple processes
command > >(process1) 2> >(process2)
Terminal window
# Open file descriptor 3 for reading
exec 3< /path/to/file
# Read from fd 3
read -r line <&3
# Open fd 4 for writing
exec 4> /path/to/output
# Write to fd 4
echo "data" >&4
# Close file descriptors
exec 3<&-
exec 4>&-
# Duplicate stderr to stdout, keeping stderr on fd 2
exec 2>&1

This is particularly useful in scripts where you need to maintain open file handles across multiple Operations:

#!/bin/bash
exec 3>&1 4>&2
exec > >(tee /var/log/script.log) 2>&1
# All output now goes to both terminal and log file
echo "This goes everywhere"
# Restore original file descriptors
exec 1>&3 2>&4
exec 3>&- 4>&-

The shell expands glob patterns into matching filenames before passing them to the command. This is Fundamentally different from regex — globbing operates on filenames in the filesystem, not on Arbitrary text.

PatternMeaningExampleMatches
*Any sequence of characters (except leading dot)*.logapp.log``error.log
?Exactly one characterfile?.txtfile1.txt``fileA.txt
[abc]One character from the set[abc].sha.sh``b.sh``c.sh
[a-z]One character from the range[a-m]*apple``banana``middle
[!abc]One character NOT in the set[!0-9]*file``name
**Recursive glob (bash 4.0+, globstar option)**/*.pyAll .py files recursively
Terminal window
# Enable recursive globbing
shopt -s globstar
# Delete all .tmp files recursively
rm -v **/*.tmp
# Match files starting with a dot
ls -la .*
# Case-insensitive matching (bash 4.0+)
shopt -s nocaseglob
ls *.TXT # matches file.txt, FILE.TXT, etc.