How Shell and Bash Work
A terminal window does not execute Linux commands by itself. It gives you a text interface, sends your input to a shell, and displays the output that comes back. The shell is the program that reads a command line, applies its own rules, and either runs a built-in operation or starts another program. On most Ubuntu Server systems, that shell is Bash.
This distinction explains many surprises beginners meet early: cd changes the current directory even though there is no ordinary /usr/bin/cd program; *.log is expanded before ls starts; a command can behave differently in an interactive SSH session than in a script. The examples assume Ubuntu Server LTS with Bash. In command examples, the leading $ prompt is not part of the command.
Terminal, Shell, Program, Kernel
These layers do different jobs:
- Terminal emulator: the window or remote session that handles keyboard input and screen output. GNOME Terminal, Windows Terminal, and an SSH client are common examples.
- Shell: the command interpreter. Bash, Zsh, Fish, and Dash are shells.
- Program: an executable file such as
/usr/bin/ls, or a shell built-in such ascd. - Kernel: the Linux core that manages processes, memory, filesystems, devices, and permissions.
The path from a typed command to real work looks like this:
user
-> terminal or SSH client
-> Bash
-> shell built-in or external program
-> Linux kernel system calls
-> files, devices, processes, network
For example:
Bash finds cat, prepares the argument /etc/hostname, starts a new process, and that process asks the kernel to read the file. By contrast:
cd is a Bash built-in because it changes the current working directory of the shell itself. An external child process could change only its own directory, then exit; it could not move the parent shell.
Finding the Current Shell
Do not rely only on the prompt. Prompts are configurable. Use commands that inspect the running process:
Typical output:
Read the lines carefully:
$SHELLusually shows the login shell configured for your account. It is not guaranteed to prove what is running now.$BASH_VERSIONis set when the current shell is Bash.$$is the current shell process ID, sops -p $$shows the actual process.- A command name that begins with
-, such as-bash, usually indicates a login shell.
The system's allowed login shells are commonly listed in:
That file is used by tools that need to decide which shells are valid for login accounts. It is not a complete inventory of every shell-like program on the disk.
Reading the Prompt
A common prompt looks like this:
It usually means:
admin: current username.web01: hostname.~/project: current working directory, with~expanded by the shell to the user's home directory.$: ordinary-user prompt by convention.
Root prompts often end in #, but this is only a convention. Verify privilege with:
If id -u prints 0, the process is running as root. That matters more than the prompt character.
Warning
Root is the superuser account with UID 0. The filesystem root directory / is also called "root", but it is a different concept. Avoid long-running root shells for routine work. Use sudo only for the specific command that requires administrative permission.
Built-ins, Aliases, Functions, and External Commands
The same command name can refer to different things. Bash decides what it means before anything runs.
Possible output:
cd is a shell builtin
echo is a shell builtin
pwd is a shell builtin
pwd is /usr/bin/pwd
ls is aliased to `ls --color=auto'
ls is /usr/bin/ls
This output shows why "the command" is not always just one file. Bash has built-ins, users can define aliases and functions, and external programs are found through PATH. When behavior differs between two servers, run type -a <name> before guessing.
Built-ins have Bash help:
External GNU commands usually have their own help and manual pages:
The next article, Command Syntax and Help Documentation, explains how to read those syntax forms.
What Bash Does Before a Program Starts
When you press Enter, Bash does not simply split the line on spaces and run it. A simplified order is:
- Parse the command line into words and operators.
- Expand aliases in suitable positions.
- Expand variables, command substitutions, arithmetic, and
~. - Apply word splitting where allowed.
- Expand wildcards such as
*.conf. - Set up redirection such as
>and<. - Run a built-in or start an external program.
- Store the exit status in
$?.
A safe way to see expansion is to use a lab directory:
Expected output:
Bash expanded the wildcard before echo received the arguments. If no file matches, Bash normally leaves the pattern unchanged unless options such as nullglob are enabled. Scripts should not assume that every wildcard has a match.
Word splitting, step 4 in the list above, is controlled by the IFS variable (Internal Field Separator). By default IFS is space, tab, and newline, which is why an unquoted $variable containing spaces breaks into several arguments. Scripts that process unusual input, such as CSV lines, sometimes set IFS temporarily to change that behavior:
line='admin:1001:/home/admin'
IFS=':' read -r username uid home <<< "$line"
printf 'user=%s uid=%s home=%s\n' "$username" "$uid" "$home"
Restore or scope IFS carefully; leaving a modified IFS active in an interactive shell or a longer script changes how every later unquoted expansion splits.
Quoting Changes Arguments
Consider a value with a space:
Output:
The quoted variable became one argument. The unquoted variable was split into two words. This is why "$variable" is the safe default when passing variable values to commands.
Single quotes and double quotes are different:
Double quotes still allow variable expansion. Single quotes preserve the text literally.
Command Substitution
Use $(...) to capture a command's standard output:
The captured output becomes data. Do not feed untrusted output into eval; that turns data back into shell code and can create command injection vulnerabilities.
Interview angle: why is eval dangerous?
eval takes its argument, joins it into a single string, and parses that string as a new Bash command line — a second, later parsing pass that ordinary command substitution does not get. If any part of that string came from outside your control, an attacker can end your intended command early and start their own.
The example below only proves the mechanism with a harmless echo; never test injection payloads with a real destructive command such as rm, even in a lab.
Bash does not run cat on a file literally named report.txt; echo INJECTED. It runs cat report.txt, then, because eval re-parses the whole string, it runs echo INJECTED as a second, separate command — proof that attacker-controlled data became attacker-controlled code. Without eval, the same unquoted value would still cause word splitting, but it could not inject a new command:
cat: report.txt;: No such file or directory
cat: echo: No such file or directory
cat: INJECTED: No such file or directory
That version fails safely — cat treats every word as a filename argument, none of which exist, and echo INJECTED never runs as a command. eval is what turns attacker-controlled data into attacker-controlled code. Avoid eval on any value that came from a user, a network request, a filename you did not create yourself, or a config file with looser write permissions than the script that reads it.
Interactive, Login, and Non-Interactive Shells
Bash startup behavior depends on how it was started:
- Interactive shell: reads commands from a user and shows a prompt.
- Login shell: starts a login session. Bash reads login startup files.
- Non-interactive shell: runs a script or a command passed with
bash -c.
Check the current shell:
case $- in
*i*) echo "interactive shell" ;;
*) echo "non-interactive shell" ;;
esac
shopt -q login_shell && echo "login shell" || echo "not a login shell"
On Debian and Ubuntu, an interactive non-login Bash normally reads /etc/bash.bashrc and ~/.bashrc. An interactive login Bash reads /etc/profile, then the first readable file among ~/.bash_profile, ~/.bash_login, and ~/.profile. Non-interactive Bash does not read ~/.bashrc by default.
This matters in production. SSH login, cron, systemd services, containers, and scripts may not load the same aliases and variables you use in your terminal. The article on Aliases and Environment Variables explains how to persist shell settings without assuming every process reads the same file.
Exit Status
Every command returns an exit status. Shells and scripts use it to decide what happened.
Typical output:
drwxr-xr-x 160 root root 12288 Jul 29 12:00 /etc
status=0
ls: cannot access '/path/that/does/not/exist': No such file or directory
status=2
The exact text and status can vary by command, but the convention is stable: 0 means success, and non-zero means some other condition. Read the command's manual page for the exact meanings.
$? changes after every command, so inspect or save it immediately:
Practical Scenario: Why Did ls Behave Differently?
Problem: On one server, ls shows colors and a long format when you type ll; on another server, ll is not found.
Check the command source:
Check whether the shell is interactive:
Bypass aliases and run the external program explicitly:
Conclusion: the difference is usually not the kernel or filesystem. It is shell configuration, often an alias loaded from ~/.bashrc on one host but not another.
Common Mistakes
- Treating the terminal and shell as the same thing. The terminal displays text; Bash interprets commands.
- Trusting
$SHELLas proof of the current shell. Use$BASH_VERSIONandps -p $$. - Forgetting that
cd,alias,export, andtypeare shell built-ins. - Running commands as root because the prompt looks confusing. Verify with
id. - Leaving variables unquoted. This changes argument boundaries and can trigger wildcard expansion.
- Expecting aliases from
~/.bashrcto work in cron, scripts, orsystemdservices.
Exercises
- Run
type -a cd pwd echo lsand identify which names are built-ins, aliases, and external programs on your system. - Create
~/lab/shell/demo one.txtand~/lab/shell/demo-two.txt. Useprintf '<%s>\n'to prove the difference between quoted and unquoted filenames. - Start a child Bash with
bash, compare$$,$PPID, andpwd, then return withexit. - In a new shell, test whether it is login and interactive. Do not change startup files yet.
Verification criterion: you should be able to explain why cd must be a built-in and why echo *.conf can print filenames that you did not type.
References
- Bash manual page: https://man7.org/linux/man-pages/man1/bash.1.html
- GNU Bash Reference Manual: https://www.gnu.org/software/bash/manual/bash.html
execve(2)Linux manual page: https://man7.org/linux/man-pages/man2/execve.2.htmlenviron(7)Linux manual page: https://man7.org/linux/man-pages/man7/environ.7.html- GNU Coreutils manual: https://www.gnu.org/software/coreutils/manual/coreutils.html