Commands & Syntax
The Anatomy of a Command
Bash has a small grammar. Almost everything you do reduces to simple commands joined by operators. Once you can name the parts of a command, syntax errors stop looking random.
Simple Commands
A simple command is a sequence of words separated by blanks, terminated by a newline or a separator. The first word is the command; the rest are arguments. The shell expands the words, then asks the kernel to run the result.
ls -l /var/log # command=ls args=-l /var/log
echo "hi" "there" # command=echo args=hi there (quotes removed after expansion)
git commit -m "fix" # command=git args=commit -m fix
Word splitting is the subtle part: an unquoted variable may become several words, and those words become several arguments. Quoting is covered in its own lesson, but remember the rule now — quote unless you can explain why not to.
Arguments & Options
Conventionally, options start with a dash. Short options can be bundled (-la equals -l -a) and often take a value either separated or attached.
ls -la # -l long, -a all (bundled)
grep -n "TODO" f # -n adds line numbers
head -n 20 f # separated value
head -20 f # attached value (classic form)
-- # the double dash ends options; anything after is a filename
rm -- -weird.txt # delete a file literally named "-weird.txt"
Exit Status
Every command returns a status: 0 = success, non-zero = failure. $? holds the status of the last command. This is the backbone of all scripting logic.
ls /etc >/dev/null # discard output, keep the status
echo $? # 0
ls /nope 2>/dev/null
echo $? # 2 — a non-zero status means "handle the failure"
Where Commands Come From
Builtins vs External
Some words are handled by Bash itself; these are builtins. They must run in the shell process because they change the shell's own state — a child process could not change its parent.
cd /tmp # builtin: changes the shell's working directory
pwd # builtin: prints the shell's directory
export FOO=1 # builtin: puts FOO into the shell's environment
read -r line # builtin: reads from the shell's standard input
Anything not a builtin is an external command: a file on disk that Bash forks and executes. /bin/ls, /usr/bin/grep and your own scripts are all external.
PATH Lookup
When you type a bare word, Bash searches each directory listed in PATH, left to right, and runs the first match. That is why ./script.sh is needed for the current directory — it is usually not on PATH.
echo "$PATH" # /usr/local/bin:/usr/bin:/bin ...
which python3 # shows the first match on PATH
PATH="$HOME/bin:$PATH" # prepend a personal bin directory
type & command
type tells you how Bash would resolve a name; command bypasses functions and aliases to reach the real builtin or binary.
type cd # cd is a shell builtin
type ls # ls is /usr/bin/ls
type -a echo # every definition, in lookup order
command ls # run the real ls even if a function named ls exists
Command Separators
Semicolon & Newline
A newline ends a command. A semicolon does the same thing on one line. A backslash-newline joins lines.
echo one; echo two # two commands, one line
echo three + \
four # line continuation: one command "echo three + four"
&& and ||
&& runs the right side only if the left succeeded; || runs it only if the left failed. They are the idiomatic one-line if.
mkdir data && cd data # cd only if mkdir worked
ping -c1 host >/dev/null || echo "down"
test -f cfg || { echo "missing cfg"; exit 1; }
Grouping with ( ) and { }
Parentheses run commands in a subshell (a copy of the shell); braces group them in the current shell. The difference decides whether state survives.
( cd /tmp && pwd ) # subshell: the real shell stays put
{ cd /tmp; pwd; } # current shell: the directory change persists
{ and a semicolon (or newline) before }: { echo hi; }. Parentheses do not.