Shell Scripting: Concepts, Construction and Execution
Prerequisite Knowledge
This lecture builds on the following concepts from earlier lectures. If any feel unfamiliar, review the linked notes before proceeding.
Previously Covered in This Subject
- Shell as command interpreter, prompts and shell families — covered in Lecture 1 (Introduction to Systems Programming)
- File system navigation and basic commands — ls, cat, pwd, cd and absolute versus relative paths — covered in Lecture 4 (Linux Commands and File System Navigation)
- File permissions, rwx triplets and chmod in absolute and symbolic modes — covered in Lecture 5 (Linux File System — Inodes, File Types, Permissions and Links)
- Vi editor modes and file creation workflow with vi — covered in Lecture 6 (Linux Commands and the VI Editor) and Lecture 7 (The vi Editor — Modes, Navigation, Yank-Put, Recovery and Search)
- Redirection with > and >>, output capture and file updates — covered in Lecture 4 (Linux Commands and File System Navigation)
10.1 Shell Scripting Fundamentals: Purpose, Definition and Program versus Script
10.1.1 What a Shell Script Is and Why We Group Commands
Hook: Why type the same five commands every morning — who, ls -l, date, hostname, pwd — and risk a typo on step four that ruins your report? What if you could press play once and watch them run in perfect order, every time?
A shell script — a plain text file that stores a sequence of shell commands for the shell to read and execute in order — solves that repetition problem. A shell, the command interpreter that sits between you and the operating system kernel, is the program that reads what you type, parses it, locates the executable, runs it, and prints the result on your terminal.
The core idea is deceptively simple: execute a series of commands together, in one go, reproducibly. Instead of typing who then waiting, then ls, then date one by one at the prompt, you queue those lines in a file such as first.sh and ask the shell to run the file. The file becomes a saved workflow, a recipe card the shell follows line by line.
Intuition and analogy — the playlist model: Think of a shell script as a playlist for commands. Each command is a track. Interactive typing is like searching for and queuing one song at a time. A script is the saved playlist: you curate it once — who (track 1), ls (track 2), date (track 3) — then press play whenever you want that exact sequence. The mapping is explicit: adding a line to the file is like dragging a song into the playlist; running bash first.sh is pressing play; reordering lines changes the playback order. The analogy breaks where music is parallel — a playlist can shuffle, but a script executes strictly sequentially, and a failed track (a command that errors) does not stop the whole playlist unless you explicitly tell it to.
Formalize — definition and execution model: A shell script satisfies three properties:
- It is a regular text file (no special binary format), each line is a shell command or comment.
- The file is input data to a shell interpreter, not a standalone binary.
- Execution is sequential by default: the shell reads line , executes it, waits for its exit status, then reads line .
In symbols, if a script contains commands , execution is: where each arrow is the shell's read-execute-wait loop. No compilation translates the file first; interpretation happens line by line at run time.
This matters because manual repetition is fragile. You might mistype a flag (ls -l versus ls -I), forget a step in a deployment checklist, or run commands in the wrong order and clobber a file before backing it up. A script locks the order, flags, and arguments in place, making the workflow auditable and repeatable.
Worked example — first.sh groups who, ls, and date: The file first.sh as demonstrated contains:
who
ls
date
Run it explicitly:
bash first.sh
Output appears in three blocks, exactly as if you had typed them:
whoblock — e.g.,centos pts/0 2026-02-21 15:21 (10.0.2.15)andstudent tty1 2026-02-21 15:26, showing every logged-in user.lsblock — e.g.,first.sh one_st.sh shell_variables.sh, the directory listing at that moment.dateblock — e.g.,Mon Feb 21 15:38:19 IST 2026, the current time.
Sense-check: If you run who; ls; date manually on one line, you get the same three blocks. The script is just that one-liner saved to disk.Bold answer: the script produces the combined sequential output of all three commands with a single invocation.
Visual intuition: imagine a vertical flowchart with three boxes stacked — who at top, ls in middle, date at bottom — connected by downward arrows labeled "shell reads next line". The x-axis is time, the y-axis is command index. No branches, no loops yet. The shape is a straight pipeline. The takeaway is that control flows top-to-bottom with no jumps at this stage.
Assumptions and scope: This sequential model assumes the default shell behavior without traps, background &, pipes, or set -e. If a command fails (non-zero exit), the shell still continues to the next line unless you add error handling. The model applies to Bourne-family shells (sh, bash, ksh, zsh); csh uses a different script language. For now the script has no arguments and no variables — that generality comes later.
Pitfalls:
- Confusing "script is executable" with "script will run itself": A file with commands is inert data until a shell reads it. Saving
lsin a file does nothing until you invokebash fileorchmod +xand execute it. - Forgetting the file is read top-to-bottom: Adding a
cd /tmpin line 2 changes wherelsin line 3 looks. Order matters, unlike a bag of independent commands. - Assuming scripts are second-class: Beginners think scripts only chain fixed commands. They also branch, loop, and compute, as this lecture will show.
10.1.2 How a Shell Executes Commands from a File
When you type interactively, the shell loop is: read one line from the terminal → parse → execute → wait → prompt again. When you use a script, the loop is identical except the source of lines is the file, not the keyboard. The phrase used in class to cement this is: the shell reads and executes commands contained in a file.
Unpack that phrasing. The script does not "run" by magic. A shell process — identified by its PID, visible via echo \$\$ — opens the file as input, reads the first line into memory, forks and execs the command if it is external, waits for the exit status, then reads the second line. The file provides the characters; the shell provides the engine. This is why the same file can be run by different shells with different results: bash first.sh feeds the file to bash, sh first.sh feeds it to sh. The kernel's role is minimal at this stage — it just starts the interpreter you named.
A useful mental trace: bash first.sh → kernel starts bash → bash opens first.sh for reading → while not EOF: read line, expand variables, handle meta characters, execute, store \$?. Interactive bash does the same but with stdin tied to your keyboard.
Everyday analogy — teleprompter versus ad-lib: Interactive typing is ad-libbing — you decide the next sentence live. A script is a teleprompter — the lines are pre-written, the speaker (shell) just reads them in order. Both use the same voice, but the teleprompter guarantees you never skip line two.
10.1.3 Script versus Program: Interpretation, Constructs and Modularization
Why call this a script and not a program? In everyday talk you call Java or C code a program. The distinction here is not about importance but about how the code is handled and how heavyweight the language feels.
A program in a language like Java or C is typically seen as a full-featured construct with rich data structures, extensive libraries, and a distinct build step that translates source into machine code or bytecode before any execution happens. A script in the shell context is a list of commands that is interpreted — the interpreter reads the file and acts on it directly, line by line, without a separate compilation step. Bourne's classic line captures it: the shell is a programming language in which each statement runs a command.
Comparison table — script versus program as framed in this lecture:
| Dimension | Script (shell) | Program (C / Java) |
|---|---|---|
| Translation | Interpreted line by line, no build | Compiled to machine code / bytecode first |
| Primary data | Text, file names, exit statuses | Rich types, objects, arrays |
| Modularity | Functions, sourced files, separate script files | Functions, classes, libraries, packages |
| Startup cost | Low — write and run bash file.sh |
Higher — compile, link, or javac + java |
| Best fit | Glue, automation, file and process workflows | Heavy computation, large systems |
When to pick which: if the task is chaining system utilities, checking file states, or looping over file lists, a shell script wins on speed of authoring. If the task needs complex data structures or high performance, a compiled program wins. The professor stresses that many real jobs are glue — and for glue, interpretation is a feature, not a limitation.
Scope — what "interpreted" does not mean: Interpreted does not mean logic-free. It means no separate cc or javac step. The shell still parses, expands variables, handles quoting, and decides branches before executing each line. The T1 textbook notes that the language feels "strange" because it must serve both interactive and programming roles — that history explains some quirks, but not a lack of power.
Q: What do we use this shell script for — what is its fundamental purpose? A: One early answer was "handling system administration." That is true in a broad sense but too abstract for the demo, where the script only prints who is logged in, lists files, and prints the date with no admin privilege. The more precise, accepted answer is: to execute a series of commands at a time, grouping them into a file so the shell can run them together. System administration — backups, log rotation, health checks, deployment — then becomes a family of use cases for that core capability, not the definition itself. Several students asked this in slightly different words; the canonical confusion was vague purpose versus concrete mechanism.
Q: Why is it called a script rather than a program? Do we not use logic here? A: The name reflects interpretation rather than full compilation, and the lighter, glue-like weight of the construct, even though modularization with functions and sourced files is possible. The correction emphasized in class is that logic is present — conditionals such as if, loops, variables, arithmetic with expr and \$(( )), and functions will be covered — so a script is not just a dumb list of external commands. The misconception that "scripts have no logic" was explicitly rejected.
Pitfall — equating "script = no logic": If you treat the shell as only command chaining, you will reach for Python too early for simple file tests or miss that if [ \$# -ne 2 ] is genuine logic. Conversely, do not expect shell logic to look like Java — there are no int x = 5; declarations, and truth is encoded as exit status 0, not a boolean literal.
Recap and bridge: A shell script is a plain-text queue of commands that the shell — the command interpreter — reads and executes sequentially, like a playlist on repeat. It is interpreted, not compiled, yet it supports full programming constructs and modularization. The payoff is reproducible workflows instead of fragile retyping. Next, we turn that definition into an actual file on disk: creating one_st.sh with vi and watching bash one_st.sh replay the queue. Exam note: Be ready for a one-sentence definition question: "What is a shell script and what is its fundamental purpose?" The expected answer is the grouping-for-sequential-execution phrasing above, not the vague "system administration" label.
Real-world and domain connection: Shell scripts are the workhorses for repeatable operational tasks on Unix and Linux — nightly tar backups, grep ERROR app.log | wc -l health checks, rsync deployments, and CI runners that invoke bash build.sh — because they remove manual repetition where a GUI would require clicks. In systems programming broadly, they sit alongside C and assembly as the glue layer: C builds the tools, shell scripts orchestrate the tools into pipelines.
10.2 Creating a Shell Script File
10.2.1 Grouping Commands with an Editor
Hook: You already know how to run ls, date, and who one at a time. How do you turn that knowledge into a reusable tool you can hand to a teammate?
Creating a script is literally grouping a list of commands you would otherwise type, one per line, in a text file. You open a text editor, enter commands, save, and run. The editor used in the live demo is vi — the visual editor present on virtually every Unix system — and the steps narrated are: vi one_st.sh to open (or create) the file, i to enter insert mode, type commands, Esc then :wq to write and quit. Those vi moves are worth memorizing because on a minimal server vi may be the only editor available.
The filename in the demo evolves from first.sh to one_st.sh simply because first.sh already existed — the second name was "one more start" file. The suffix .sh is a human convention signaling "shell script"; the kernel and shell do not require it. myscript, one_st, or snapshot would run identically if invoked correctly. The convention helps humans and tools like ls *.sh to locate scripts quickly.
Formalize — file creation contract:
- First line optionally declares interpreter (shebang, covered next).
- One command per line (or
;separated — but one per line is clearer). - Save as plain text with Unix line endings (
\n). - No compilation; the file is ready to be read by
bash.
A minimal grouping shown:
ls
ls -l
date
who
hostname
Each line is a command you know interactively: ls lists names, ls -l adds permissions, sizes, and timestamps, date prints time, who prints logged-in users, hostname prints the machine name. Any flag you use interactively — ls -a, date "+%Y-%m-%d" — works identically inside the file because the same shell parses it.
Intuition — recipe card: Think of the script file as a recipe card taped to the kitchen wall. Each line is a step: "chop onions", "heat pan", "add onions". Interactive typing is cooking from memory — you might forget the heat step. The card guarantees the order and exact measurements every time. Where it breaks: a recipe card can have annotations like "optional", but a script line without a comment # is always executed; "optional" must be encoded as if logic later.
Scope and assumptions: This one-command-per-line grouping assumes no quoting, no variables, and no dependencies between commands yet. It works for any commands that are independent or where earlier commands set up state for later ones (e.g., cd affects subsequent ls). It assumes the editor saved with correct permissions — the file will have rw-rw-r-- after creation, not yet executable, which is why bash file is needed before chmod.
10.2.2 Demonstration with ls, date, who and hostname
Worked example — one_st.sh with bash one_st.sh: File one_st.sh contains:
ls
ls -l
date
who
hostname
Run explicitly:
bash one_st.sh
Observed output (illustrative, hostnames and dates vary):
first.sh one_st.sh
total 12
-rw-rw-r-- 1 centos centos 45 Feb 21 15:00 first.sh
-rw-rw-r-- 1 centos centos 52 Feb 21 15:10 one_st.sh
Mon Feb 21 15:38:19 IST 2026
centos pts/0 2026-02-21 15:21 (10.0.2.15)
server01.example.com
Walk the execution:
- Shell reads
ls→ forksls→ prints short listing. - Reads
ls -l→ prints long listing with permissions and sizes. - Reads
date→ prints current date. - Reads
who→ enumerates logged-in users, including the CentOS user who ran the script. - Reads
hostname→ prints machine name.
Key observation: Nothing special was needed in the file to get sequential execution — one command per line is sufficient, the shell handles the sequence. Adding flags like -l works exactly as interactively. Bold answer: bash one_st.sh reproduces the concatenated output of all five commands in file order.
Sense-check: If you delete hostname and re-run, that last line of output disappears, confirming each line contributes independently.
A common early question is whether this simple grouping is the "right" way. The answer in class is: yes, it will run, but it is not yet robust or portable. Without a shebang, direct ./one_st.sh may invoke the wrong shell; without chmod +x, it is not executable; without quoting and variables, it cannot adapt to arguments. The refinements that follow — shebang, permissions, wildcards, meta characters, variables, and conditionals — turn a raw command list into a reliable script that survives being moved to another machine or being run by cron.
Visual: picture a stack of dominoes labeled ls → ls -l → date → who → hostname. Pushing the first domino (bash one_st.sh) tips them in order, each fall is a command's output. No domino branches — that visual will change once if is introduced.
Pitfalls:
- Forgetting the editor mode: In
vi, typing commands withoutiappends tovicommands, not the file. Beginners save an empty file and wonder whybash fileproduces nothing. - Relying on
.shfor execution: Naming a filescript.shdoes not make it executable; permissions do.ls -lshowing-rw-means you must still usebash script.shorchmod +x. - Hidden characters: Copy-pasting from a word processor can inject Windows
\r\nline endings, causingbash: \$'\r': command not found. Use a plain text editor andcat -A fileto check.
Recap and bridge: Creating a script is saving the commands you already type into a plain-text file with vi, one per line — .sh is convention, not magic. Running bash one_st.sh replays them sequentially, as shown with ls, date, who, and hostname. That file runs but is not yet a well-formed, directly executable script. Next we add the shebang line and the two execution methods that make ./script.sh work predictably. Exam note: Be ready to list the vi steps and to explain why bash one_st.sh works even though one_st.sh alone says "Permission denied" before chmod.
Real-world: This grouping is how operators build daily snapshot scripts — uptime; df -h; free -m; who saved as morning_check.sh — so the morning health check is one command, not five, and can be mailed or logged automatically.
10.3 The Shebang Line and Two Execution Methods
10.3.1 Purpose and Syntax of the Shebang
Hook: You saved first.sh and typed ./first.sh — permission denied, or worse, it ran with the wrong shell and your [[ ]] test failed. How does the system know which interpreter should read your file when you do not name one?
The shebang — the first line of a well-formed script, written as #!/bin/bash — answers that question. The characters are hash # followed by bang ! (so hash-bang), then the absolute path to an interpreter. In #!/bin/bash, /bin/bash points to the Bash interpreter binary.
Formalize — anatomy and kernel role: The plain-language audit kept from class is: hash, bang, slash, bin, slash, bash — the shell which will be used to execute this file. The reconstructed form is: where # is the comment character in shell language, ! signals interpreter selection to the kernel's execve loader, and /bin/bash is the interpreter pathname.
Mechanically: when a file is marked executable and invoked as ./first.sh, the kernel reads the first two bytes. If they are #!, the kernel does not try to execute the file as machine code; instead it launches the program named after #! and passes the script file as its argument. Conceptually: The shebang is therefore an instruction to the operating system about which shell needs to be used to execute the commands in this file. It must be the very first line, starting at column one, with no blank line or space before #!, otherwise the kernel will not recognize it.
Intuition — shipping label: Think of a script as a parcel and the shebang as the shipping label that says "open with Bash". If you hand the parcel to a specific courier (bash first.sh), the label is irrelevant — you already chose the courier. If you drop it in the mail slot (./first.sh), the sorting office (kernel) reads the label to decide which courier to call. The mapping is explicit: #! equals "label present", path equals "courier address", rest of file equals "parcel contents". The analogy breaks where a parcel can be opened by anyone; a script without execute permission cannot be "mailed" directly at all.
Common variants you will see: #!/bin/sh (POSIX Bourne shell, often a symlink to bash or dash), #!/usr/bin/env bash (search PATH for bash, more portable), #!/bin/python3 for Python scripts. For this lecture the standard is #!/bin/bash.
10.3.2 Explicit Invocation versus Executable Permission
There are two distinct ways to run the same file, and the role of the shebang differs between them.
Two methods compared:
Method 1 — explicit interpreter (shebang ignored):
bash first.sh
sh first.sh
Here you type the interpreter name before the file name — you say explicitly which shell to use. Whatever shebang is inside first.sh is ignored because you already named bash or sh on the command line. You can write #!/bin/sh inside the file and still run it with bash by typing bash first.sh — the command line wins. This method needs no execute permission; the file is just data read by bash.
Method 2 — direct execution (shebang required):
chmod +x first.sh
./first.sh
You give the file execute permission and then name the file itself. The permission view before the change shows rw-rw-r-- (read/write for user, group, others). After chmod +x first.sh the listing shows -rwxrwxr-x with x flags, and the file name often changes color in the terminal to green to signal executable. Now ./first.sh asks the kernel to execute the file; the kernel reads the shebang to decide the interpreter. That is the purpose of the shebang — to tell the system what to use when you do not name the interpreter.
A subtle but exam-tested point: explicit invocation works even if the file is not executable and even if the shebang line is missing or wrong. Direct execution fails without x permission (Permission denied) and falls back to a default shell if the shebang is missing.
10.3.3 Making a Script Executable with chmod and Direct Execution
The command chmod +x first.sh modifies the file mode. chmod is change mode, +x adds execute permission. Without a qualifier, +x applies to user, group, and others — after the command you see rwx style flags for all three categories. With ls -l you move from -rw-rw-r-- to -rwxrwxr-x.
The focused question about user-only permission draws out the precise syntax: to add execute permission only for the user, write chmod u+x first.sh, where u restricts the change to the user class. The confirmation in class is: that is very right, u+x is the form for user-only. Other forms for completeness: g+x for group only, o+x for others only, a+x equals bare +x (all), chmod 755 file sets rwxr-xr-x numerically.
Once executable, the file runs by naming it: ./first.sh when in the current directory (the ./ avoids PATH search), or just first.sh if its directory is already on \$PATH or copied to ~/bin. You still see the same combined output — who, ls, date, hostname — but now dispatch is via the shebang rather than your explicit bash.
Worked example — chmod +x versus chmod u+x versus bash first.sh:
Setup file first.sh:
#!/bin/bash
who
ls
date
hostname
ls -l first.sh→-rw-rw-r-- 1 centos centos 42 Feb 21 10:00 first.sh(nox)./first.sh→bash: ./first.sh: Permission denied— not yet executable, kernel refuses direct exec.bash first.sh→ succeeds, printswho/ls/date/hostname— explicit interpreter needs nox.chmod +x first.sh→ls -l→-rwxrwxr-x ... first.sh(green),xfor u,g,o../first.sh→ succeeds via#!/bin/bash, same output as step 3.chmod 644 first.sh; chmod u+x first.sh→ls -l→-rwxr--r--— only user hasx. Owner can./first.sh; another user cannot unless granted.
Sense-check: chmod u+x and chmod +x both enable the owner, but u+x leaves group/others non-executable — the minimal permission for a personal script. Bold takeaway: +x is for sharing, u+x is for private execute.
Assumptions and scope: The chmod +x discussion assumes a Unix permission model on a local filesystem (ext4, etc.). On noexec mounts, FAT USB sticks, or Windows shares, the x flag may be ignored. Also, the shebang path must be absolute and must exist; #!/bin/bash fails on systems where Bash is at /usr/local/bin/bash — there #!/usr/bin/env bash is safer.
10.3.4 When the Shebang Matters and When It Does Not
If you always run scripts as bash script.sh, the shebang carries no operational weight — it is a comment that the invoked bash skips. If you run scripts as ./script.sh, via cron, via find -exec, or as a login script, the shebang is essential because no human is typing the interpreter name.
The discussion revisits this by removing the shebang line and running with bash — it still works, because the interpreter is explicit. Then the shebang is restored to #!/bin/bash or changed to #!/bin/sh, the file is made executable again, and direct execution is shown to follow whichever interpreter the shebang names. Switching to #!/bin/sh on a Bash-specific script that uses [[ or (( )) will then fail under dash-based sh — a classic portability bug.
Q: If I add execute permission to a shell file but the file has no shebang, what will the system do? Will it error or get confused? A: It will not error. It will fall back to the default shell. The environment variable that holds the default is shown with echo \$SHELL, which in the demo prints /bin/bash. That value is the interpreter that will be used when no shebang tells the system otherwise. On modern Linux the fallback is often /bin/sh via the kernel, but in this lecture environment the effective default is the value of \$SHELL.
Q: To give execute permission only to the user, what should I type? A: chmod u+x first.sh where u is user and +x adds execute. Using bare +x adds the flag for user, group, and others together. Several students asked variants of this; the canonical confusion was chmod +x versus chmod u+x scope.
Pitfalls:
- Blank line before shebang: A leading newline makes the kernel miss
#!, so./script.shruns with the default shell and Bash-specific syntax fails mysteriously. - Windows line endings: Saving with
\r\nmakes the kernel look for/bin/bash\r, which does not exist →bad interpreter: No such file or directory. - Forgetting
./: Typingfirst.shwithout./searches\$PATH, not the current directory, and reportscommand not foundeven thoughlsshows the file. - Assuming shebang guarantees Bash: On some systems
/bin/shisdash, notbash. Testing withbash script.shhides the bug; testing with./script.shunder#!/bin/shreveals it.
Visual: draw two flowcharts side by side. Left: bash first.sh → arrow labeled "you chose bash" bypasses shebang → bash reads file. Right: ./first.sh → kernel checks first bytes → branch: #! present → launch that interpreter; absent → launch \$SHELL (/bin/bash here). The takeaway is the decision diamond is the presence of x permission plus #!.
Recap and bridge: The shebang #!/bin/bash is a kernel-level shipping label, essential for direct ./script.sh execution after chmod +x (or u+x for user-only), and ignored when you explicitly run bash script.sh. Without it, direct execution falls back to \$SHELL. Mastering the two methods removes "Permission denied" and "bad interpreter" errors. Next we zoom out to the family of shells — sh, bash, csh, ksh, zsh — and why Bash is the standard for predictable construct handling. Exam note: Expect to explain both methods, write the exact chmod variants, state when the shebang is required versus ignored, and predict the fallback when the shebang is absent — cite echo \$SHELL yielding /bin/bash in this setup.
Real-world: Teams standardize on #!/bin/bash (or #!/usr/bin/env bash) and rely on direct execution so that cron jobs, systemd units, and CI pipelines do not need to remember which interpreter to name — the file itself declares its runtime.
10.4 Shell Variants and the Default Interpreter
10.4.1 Families of Shells: sh, bash, csh, ksh and zsh
Hook: If Bash is "the shell", why do manuals list sh, csh, ksh, and zsh as if they are different programming languages?
Bash is not the only shell. The session enumerates several distinct programs, each providing a prompt and a scripting language:
sh— the original Bourne shell (Stephen Bourne, 1979), the POSIX baseline. On many Linux systems/bin/shis a symlink tobashordash, but the language is the minimal portable subset.bash— Bourne Again Shell, GNU's superset ofshwith arrays,[[ ]],(( )), and brace expansion. This is the standard for this lecture and for most Linux distributions.csh— C shell (Bill Joy), with C-like syntax (set,foreach,if ( )). Loved interactively, awkward for scripting.ksh— Korn shell, a Bourne-compatible shell with C-shell-like interactive features; common on older commercial Unix.zsh— Z shell, a modernksh-compatible shell with advanced completion and floating-point arithmetic.
In the demo environment, bash, sh, and csh were present, while ksh and zsh were noted as not installed in that particular CentOS setup — ksh --version or zsh --version would report "command not found" until installed. The point is that each name denotes a separate binary at a distinct path (/bin/bash, /bin/sh, /bin/csh), not just a mode flag.
Intuition — dialects of one language family: Think of shells as dialects of English — American, British, Australian — mutually intelligible for simple sentences but diverging on idioms. ls works everywhere (like "hello"), but if [[ \$x == 5 ]] is American bash idiom that confuses a British sh speaker. The mapping: vocabulary equals built-ins and syntax; accent equals prompt and completion; dictionary equals manual page man bash versus man csh. The analogy breaks where dialects are not just accents — csh scripting syntax is fundamentally different, more like Dutch than a dialect.
10.4.2 Construct Compatibility Across Shells
Because each shell is a different program, the way they handle programming constructs varies — sometimes subtly, sometimes incompatibly.
Examples that bite in practice:
bashsupports[[ \$str == pattern ]],(( i++ )),function foo(), and arraysarr=(a b c).sh(asdash) does not — it needs[ "\$str" = pattern ],i=\$(expr \$i + 1), and no arrays.cshusesset var = value(spaces required),if ( \$x == 1 ) then, andforeach. Abashscript withvar=valuefails undercsh.kshandzshadd floating-point\$(( ))and associative arrays thatbashonly partially matches.
You can test this yourself by running the same file with different interpreters: bash first.sh versus sh first.sh versus csh first.sh. If the file uses only ls, date, and who, all three produce the same output. If it uses if [[ \$x -eq 5 ]] or (( )), sh and csh will error. For day-to-day work the advice in class is that bash is the standard for executing shell scripts precisely to keep construct handling predictable. Knowledge of the variation matters when you move a script from Linux (bash) to a BSD system where /bin/sh is ash or to a machine where ksh is default.
Assumptions and scope: The claim "bash is standard" assumes a Linux environment (Ubuntu, CentOS, Fedora) where /bin/bash exists and is recent (3.x+). On minimal containers (alpine) the only shell may be ash as /bin/sh; on macOS since Catalina the default interactive shell is zsh. Portable scripts that must run everywhere restrict themselves to POSIX sh features or declare #!/usr/bin/env bash and document the dependency.
10.4.3 The Default Shell and the SHELL Environment Variable
An environment variable — a named value that the shell maintains for the session, for the environment in which commands run — controls defaults. Several appear in the demo, always by convention in capitals to distinguish them from your own lowercase variables.
\$SHELL holds the default interpreter pathname. Running echo \$SHELL prints /bin/bash in this setup. When a script lacks a shebang and is executed directly (after chmod +x), the system consults this default — the conceptual equation is simple: if shebang present, use shebang path; if absent, use the value of \$SHELL (or /bin/sh on kernel fallback, lecture states \$SHELL).
Other environment variables shown as system variables include:
| Variable | Meaning | Typical value in demo |
|---|---|---|
\$PATH |
Colon-separated directories to search for commands | /usr/local/bin:/usr/bin:/bin:/home/centos/bin |
\$HOME |
Home directory path | /home/centos |
\$PWD |
Present working directory | /home/centos/shell_scripts |
\$BASH |
Executable path of current Bash | /bin/bash |
\$BASH_VERSION |
Release number of Bash | 4.2.46(2)-release |
\$OSTYPE |
Operating system type | linux-gnu |
\$LOGNAME / \$USER |
Login name | centos |
You inspect them with echo \$PATH, echo \$SHELL, etc., where the dollar retrieves the stored value. Try echo \$HOME versus echo HOME — without \$ you print the literal word.
Visual: imagine a table of Post-it notes on the wall of your session. Each note is labeled in capitals (SHELL, PATH) with a value. Child processes inherit copies of those notes (exported variables), but scribbling on a child's copy does not change the parent's wall — that is why export matters, previewed for later lectures.
Pitfalls:
- Assuming
\$SHELLequals the current shell:\$SHELLis your login default, not necessarily the shell you are currently running.echo \$0orps -p \$\$shows the actual current shell, which could bedasheven if\$SHELLis/bin/bash. - Hard-coding
/bin/bashon non-Linux: Scripts with#!/bin/bashfail on systems where Bash lives at/usr/local/bin/bash. Using#!/usr/bin/env bashsearches\$PATHand is more portable. - Case sensitivity:
echo \$pathprints nothing —PATHis capitalP,shellis not a defined variable.
Recap and bridge: Many shells exist — sh, bash, csh, ksh, zsh — and they are separate programs with incompatible construct syntax. bash is the predictable standard for this course. The environment variable \$SHELL (here /bin/bash) names the fallback interpreter when no shebang is present, alongside \$PATH, \$HOME, \$PWD, and others in capitals. Pinning the shebang and testing with the declared shell removes a whole class of "works on my machine" failures. Next we leave setup behind and meet the shell's first kind of magic: wildcards that expand before a command even runs. Exam note: Be ready to list the five shells and to state what echo \$SHELL prints in the demo, and to explain why the same script can behave differently under bash versus sh or csh.
Real-world: Portability bugs often trace back to an incorrect shebang or an assumption about which shell is default on a Docker image or a client's server. Explicitly writing #!/bin/bash and testing with shellcheck -s bash or with both bash and sh catches those mismatches early.
10.5 Wildcards and Pattern Matching
10.5.1 The Star, Question Mark and Bracket Wildcards
Hook: You have 200 files — report_01.txt through report_200.txt — and you need only those starting with report_1. Do you really want to type each name?
The shell interprets certain characters as patterns before it even runs the command — these are wildcards (also called globs). Expansion happens in the shell, not in ls or rm; the command sees the already-expanded list.
Formalize — three basic wildcards:
Star * — matches any number of characters, including zero. The pattern ls * lists everything in the current directory (the shell replaces * with all names). The pattern ls if*.* lists files whose name starts with if and has a dot followed by any extension — for example, if_dir.sh and if_odd.sh would both match, while my_if.txt would not because it does not start with if.
Question mark ? — matches exactly one single character, no more, no less. The pattern ls ? lists only single-character names. More focused, ls [EI]?? would match three-character names starting with E or I. A pattern like ls f_?.sh matches f_a.sh and f_b.sh but not f_ab.sh (needs exactly one char where ? sits) and not f_.sh (needs one).
Bracket list [ijk] — matches a single character that is any one of the characters inside the brackets. For example, ls [EI]* lists files whose first character is either E or I (upper case matters — e* is different). Inside brackets you enumerate the allowed set; [abc] means a or b or c at that one position.
Intuition — star is a blank check, question is a single slot, brackets are multiple choice: Think of * as a blank check — you can write any length, including leaving it blank. Think of ? as a single-slot token in a board game that must be filled by exactly one letter — no skipping, no overstuffing. Think of [EI] as a multiple-choice question with two answers at that position. The mapping is explicit: position in the pattern maps to position in the filename; each ? or [...] consumes exactly one, * consumes zero to many. Where it breaks: * in regex also means repetition, but in shell wildcards * alone means "any characters", not "repeat previous".
10.5.2 Negation, Ranges and Combined Patterns
Formalize — advanced bracket forms:
Negation [!...] — a bracket list that starts with ! (or ^ in some textbooks, but ! is shown in the demo) — matches a single character that is not in the listed set. ls [!EI]* lists files that do not start with E or I. The mental shortcut is: bracket with exclamation means all characters except those listed. So [!a]* matches b.txt and 1.txt but not a.txt.
Range a-z or A-Z or 0-9 inside brackets — shorthand for a contiguous set. [A-Z]* matches files starting with any capital letter (26 options). [0-9]* matches those starting with a digit. [x-z]* matches x*, y*, or z* at that position. Under the hood, a-z expands to the collation sequence defined by LC_COLLATE — usually ASCII order.
Combination merges these ideas positionally. The walk-through combines E or I as the first character with x or f as the next: ls [EI][xf]* — first character is E or I, second character is x or f, then * (anything), then implicitly any extension. If the directory contains Ex_file.sh, If_test.sh, Ef_data, Exx notes would match patterns accordingly, while Ax_file.sh fails at position one.
The demo confirms: ls [EI]* shows Ex* and If* families; ls [EI][xf]* narrows to only those whose second letter is x or f; ls [!EI]* flips the set and shows all files that do not start with E or I — the complement.
A note on dotfiles: by default wildcards do not match a leading . (hidden files). ls * will not show .bashrc. Use ls .* or ls -a to see dotfiles. This is part of the pattern matching rules table referenced in T1 Chapter 5.
Worked example — predicting listings: Suppose a directory contains: Ex1.sh, Ef_notes, If_old.sh, If_x, Anna.sh, E1.sh, *hidden* (not relevant), and .config.
Execute patterns and predict:
ls *→ all except.config→Anna.sh E1.sh Ef_notes Ex1.sh If_old.sh If_xls *.sh→Anna.sh E1.sh Ex1.sh If_old.shls [EI]*→ starts withEorI→E1.sh Ef_notes Ex1.sh If_old.sh If_x(all caps)ls [EI][xf]*→ second charxorf→Ef_notes Ex1.shplusIf*variants where second char isforx— soIf_old.sh(fat position 2) qualifies,If_xdoes too. Result:Ef_notes Ex1.sh If_old.sh If_x.ls [!EI]*→ notE/I→Anna.shls ?→ single-char names → none here.ls [x-z]*→ files startingx,y,zlower case → none (case matters,Ex1.shis capitalE).
Bold check: ? consumes exactly one, * consumes zero or more, brackets consume one restricted choice — combine positions left to right.
Visual: imagine a row of four slots for a filename pattern [EI][xf]*.*. Label slots 1,2,3,4. Slot 1 shows two cards E/I; slot 2 shows x/f; slot 3 shows a stretchy * accordion; slot 4 shows dot plus *. Any filename must place one card per fixed slot, the * stretches to fit the remainder. The takeaway is that pattern matching is positional replacement.
10.5.3 Practice Patterns and Expected Listings
The session points to worksheets to practice: try *, *.sh, ?.sh, ??*, *.*, ??_*.sh, and bracket forms, and compare the listings against ls without a pattern. The key intuition repeated is that ? is strict about occupying one slot while * is greedy about occupying any number of slots, and brackets narrow the choice at a single slot. That contrast — * is any length including zero, ? is exactly one, [ ] is exactly one from a set — is the one sentence to carry into practice.
Try this self-test with the file list above: ls ???.sh matches three-character names plus .sh → E1.sh would not match (only 2 before dot), Ex1.sh would? Ex1 is three before dot, so yes. ls ??_*.sh matches two chars, underscore, anything, .sh — designed for names like if_x.
Assumptions and scope: Wildcard expansion happens before command execution, even for echo. echo * prints the file list, not a literal *, unless you quote it. Null matches behave differently by shell option: by default ls no_such_* prints ls: cannot access if no file matches; with shopt -s nullglob in Bash, the pattern expands to nothing. This detail explains why ls errors differ from silent emptiness.
Pitfalls:
- Using
rmwith*carelessly:rm *.logdeletes all logs, butrm * .log(space) deletes everything plus.log— a catastrophic typo. Alwayslsthe pattern first. - Forgetting
*can be zero characters:ls if*.*matches fileif.sh? No, because it needs dot plus something after dot — butls if*does matchif.shwith zero chars afterif. - Case confusion:
[A-Z]*on a case-sensitive filesystem does not matchanna.sh(lowercase). Students often expect case insensitivity. - Quoting kills expansion:
ls "[EI]*"searches for a file literally named[EI]*, not the pattern. Use quotes only when you want literal, not pattern.
Recap and bridge: Wildcards * (any length), ? (exactly one), and [ ] (one of a set, with [! ] for negation and a-z for ranges) are expanded by the shell before the command runs, position by position. Combined patterns like [EI][xf]* narrow by successive positions. Mastering that positional reading lets you predict ls output without running it. Next we meet the meta characters that are not about names at all — redirection, pipes, and command composition that connect programs together. Exam note: A common question gives a directory listing and a pattern and asks you to predict the output. Practice bracket, negation, and range forms until you can narrow by one character position at a time, and remember that ? occupies exactly one slot.
Real-world: Wildcards power quick file selection in administration — ls *.log, rm temp_??.txt, cp data_[0-9]* /backup, cat access_[0-9][0-9].log | grep ERROR — and small mistakes in the pattern can select the wrong files, so precise understanding of * versus ? versus [ ] prevents accidental deletion or missed backups.
10.6 Meta Characters and Command Composition
10.6.1 Redirection, Append, Input Redirection and Pipes
Hook: ls prints to your screen, but what if you want that listing inside a file for tomorrow, or want the output of rpm -qa sorted without a temporary file?
A meta character — a character the shell treats specially, not as a literal letter — controls where input comes from and where output goes. The I/O meta characters are the glue for data plumbing.
Formalize — I/O meta characters:
Output redirection > — write the stdout of a command into a file instead of the terminal. Form: Example: ls > ls.out runs ls but the shell opens ls.out for writing, connects ls stdout to it, and ls output flows into the file; you see nothing on screen. Running cat ls.out then shows what was saved. If ls.out already existed, > truncates it first — previous content is lost before ls even starts. This is why sort file > file destroys file.
Append redirection >> — append stdout to a file, keeping existing content. ls >> ls.out opens ls.out in append mode and adds the new listing to the end. Switching from > to >> is the difference between overwrite and accumulate.
Input redirection < — take stdin from a file instead of the keyboard. Form: The file provides what the command reads. Example: wc < ls.out counts lines/words/bytes in ls.out without cat.
Pipe | — connect the stdout of one program as stdin to another. Form: Meaning: run command1, feed its output directly into command2 without any intermediate file. The verbal description kept alongside is: pipe is to connect the output of one program as input to the other program. The kernel creates an anonymous buffer; both programs run concurrently, data streams in real time.
Here string <<< — take input from a literal string on the command line. Form: command <<< "string" feeds the string as stdin. Example: bc <<< "3+5" would compute 8 if using bc.
Intuition — plumbing: Think of > as a faucet into a bucket (file) — you turn the stream away from the sink (screen) into the bucket; >> is adding to a bucket that already has water. Think of < as pouring from a bucket into a machine instead of hand-feeding it. Think of | as connecting two machines with a hose — the output hose of machine one screws directly into the input port of machine two, no bucket needed. The mapping: terminal is sink, file is bucket, pipe is hose. The analogy breaks where a pipe is concurrent — both machines run at once, unlike sequential bucket transfers.
Worked example — ls > ls.out, cat ls.out, append, and pipes: Start with directory containing a.txt, b.sh.
ls > ls.out→ no screen output.ls.outnow contains:
a.txt
b.sh
ls.out
(note ls.out itself appears because ls saw it after creation — order depends on filesystem).
cat ls.out→ displays the three lines above, confirming redirection saved correctly.
ls -l >> ls.out→ appends long listing tols.out.cat ls.outnow shows the original three lines plus a block like-rw-rw-r-- 1 centos ... a.txtetc.
ls > ls.outagain → overwrites —ls.outreverts to just the shortlslisting, proving>truncates.
- Pipe alternative without file:
rpm -qa | sort | more—rpm -qalists packages unordered,sortorders them alphabetically as data arrives,morepages the output. Norpm.listfile needed. Similarly,ls | grep "\.sh"filters the listing for shell scripts.
Bold takeaway: > creates/truncates, >> appends, < feeds file to command, | streams one command into the next concurrently.
10.6.2 Command Terminators and Sequential Execution
Formalize — terminators:
Semicolon ; — a command terminator that separates two commands on one line. Form: runs P_1 then P_2 sequentially, regardless of success. The exit status of the sequence is that of P_2. The demo shows echo hello ; cat ls.out — first prints hello, then prints the file content — both outputs appear one after the other. The file rewrite demo ls > ls.out ; cat ls.out shows overwrite then display as a single typed line.
Contrast with newline: newline also terminates, but ; lets you write two logical lines physically on one line for compactness, common in if one-liners or while loops.
A point of precision: ; is unconditional, while && and || (next) are conditional.
10.6.3 Logical Connectors and Short-Circuit Behavior
Formalize — conditional execution:
And && — executes P_2 only if P_1 succeeds (exit status zero). The phrase in class is: P_1 will execute, if P_1 is successful only then P_2 will execute. Example: mkdir newdir && cd newdir — cd only if mkdir succeeded.
Or || — executes P_2 only if P_1 fails (non-zero). Example: test -f file || echo "missing" — message only if file absent.
The convention behind succeed versus fail is: This is the opposite of C where 0 is false, so it is worth memorizing for shell work. grep pattern file && echo found exploits this: grep returns 0 on match (true), non-zero on no match.
Visual: imagine traffic lights. ; is a roundabout — you always go to the next road. && is a green-only gate — you proceed only if the light was green (status zero); || is a red-only detour — you divert only if the light was red (non-zero).
10.6.4 Quoting, Escaping and Comments
Formalize — quoting and escaping:
Escape \ — a backslash before a meta character removes its special meaning and restores literal. Since * means any characters and ? means single character, \* is a literal star and \? is a literal question mark. This becomes critical for arithmetic where * should be multiplication, not wildcard: expr 5 \* 3 gives (shown fully in 10.10). Similarly, echo Where are you going\? forces a literal ? when context would glob.
Single quotes '...' — everything inside is taken literally, character for character. No variable expansion, no wildcard expansion. ' \$HOME ' prints literally \$HOME.
Double quotes "..." — preserve literal meaning but allow variable and command substitution. "\$HOME" expands to /home/centos while "*" stays as * in some contexts but permits \$ and backticks. The demo points to later examples where quoting decides whether a pattern is expanded or kept as text.
Hash # at the start of a line — marks a comment. The shell ignores the rest of the line. A comment is not executed but counts as documentation. The shebang #!/bin/bash is the one exception where # followed by ! at the very first column is not a plain comment but an interpreter directive for the kernel.
Assumptions and scope:
- Redirections
>and>>affect stdout (file descriptor 1) by default. To redirect stderr, use2>— e.g.,find / -name "*.sh" 2> errors.log.&>or> file 2>&1captures both. - Pipes connect stdout of left to stdin of right; stderr is not piped unless merged.
Worked example — escaping and quoting decide expansion: With files a.txt, b.txt:
echo *→a.txt b.txt ls.out— star expanded.echo "*"→*— quotes protect, literal star.echo \*→*— backslash protects one char.expr 5 * 3→expr: syntax error— shell expanded*to file list beforeexprsaw it.expr 5 \* 3→15— escaped star reachesexpras multiplication.
Sense-check: If a multiplication star produces a file list or syntax error, you forgot the backslash or quotes.
Pitfalls:
- Overwriting with
>before reading:sort file > fileemptiesfilebeforesortreads it, yielding empty output. Usesort file > tmp && mv tmp fileoroverwritepattern fromT1. - Confusing
;with&&:cd /nonexistent ; rm *runsrm *even thoughcdfailed, deleting files in the wrong directory. Usecd /nonexistent && rm *to guard. - Forgetting quotes around variables:
rm "\$filename"protects names with spaces;rm \$filenamesplitsMy File.txtinto two names. - Treating
#as comment everywhere:echo "a # b"prints#literally inside quotes;#starts a comment only when unquoted and at line start or after a command separator.
Recap and bridge: Meta characters > (create/truncate), >> (append), < (feed file), | (hose between programs), ; (always next), && (next only if success), || (next only if failure), \ (literalize one char), ' ' and " " (protect many), and # (comment, except #!) compose commands into workflows where exit status zero means true. Mastering them lets you build pipelines like cat data.csv | sort | uniq > clean.txt without temp files. Next we turn from composition to data: variables and the positional parameters \$0 through \$9 that carry arguments into a script. Exam note: Be ready to distinguish >, >>, <, |, ;, &&, || on sight, to fix an expr 5 * 3 missing escape, and to explain why ; versus && changes safety after a cd failure.
Real-world: Redirection and pipes are the glue for log handling and data pipelines — grep ERROR app.log > errors.txt, cat data.csv | sort | uniq > clean.txt, make 2> build.err | tee build.log — and correct escaping of * and ? prevents the shell from expanding a multiplication sign into a file list.
10.7 Variables, Command-Line Arguments and Quoting Rules
10.7.1 Positional Parameters from \$0 to \$9
Hook: You wrote bash backup.sh and it always backs up the same file. How do you make one script handle backup.sh report.txt today and backup.sh data.csv tomorrow without editing the file?
A command-line argument — a word you add after a command when you run it, so the command can vary its behavior — is that answer. For shell scripts, those arguments are exposed inside the script as positional parameters, the exclusive special variables \$0 through \$9.
Formalize — mapping:
\$0is the command or script name itself — the file you executed, as typed (may include path./orbashprefix).\$1is the first parameter after the name,\$2the second, and so on through\$9.
The verbal audit kept from class is: various arguments and internally those arguments will be mapped to these exclusive special variables \$0 to \$9, and whatever arguments are passed will be replaced in the shell file with those special variables.
Illustrated:
./script.sh alpha beta gamma
# \$0 = ./script.sh
# \$1 = alpha
# \$2 = beta
# \$3 = gamma
# \$# later = 3 (count, covered in 10.8)
Arguments beyond nine require braced form \${10}, \${11} — \$10 would be interpreted as \$1 followed by 0. The core idea for this lecture is that the first nine slots are available as \$1 through \$9 plus \$0 for the name, and the shell replaces each \$1 textual occurrence with the corresponding argument word at run time.
Intuition — numbered mailboxes: Think of the command line as a row of numbered mailboxes outside the script's house. Mailbox 0 always holds the house's own nameplate. Mailboxes 1, 2, 3 hold each word the caller slipped in. Inside the script, writing \$1 is like opening mailbox 1 and reading the slip. The mapping is explicit: position on command line → mailbox number → variable expansion. Where it breaks: mailboxes only go to 9 by single digit; beyond that you need a combination lock \${10}.
10.7.2 Creating Variables and Accessing Values with \$ and \${}
You can create variables inside a script at will — no declaration, no int keyword, no import. The creation rule is simple and strict: with no spaces around =. Examples shown are number=10 (numeric string), vec=bus (word), and str=V (single character). Assigning a null value as vec= is allowed and means empty — the variable exists but holds the null string.
To use the value stored in a variable, you prefix the name with \$. The form \$var expands to the value of var, while \${var} does the same with braces that delimit the name explicitly. Braces disambiguate when the variable name abuts other characters: \${var}x is the value of var plus x, while \$varx would look up a different variable varx.
Formalize — creation versus access:
Creation (store): name=value — no \$, no spaces. Access (retrieve): \$name or \${name} — dollar means "value of".
The demo makes the distinction tangible with read:
read name # name is the container (no \$)
echo Hello \$name # \$name is the content of that container
In read name, name is the destination variable as a name. In echo \$name, the \$ signals you want the value present in that container. Whatever word the user typed — Tixna, Rajesh — gets assigned to name, and \$name retrieves it for display. The same pattern holds for created variables: number=10 creates, echo \$number retrieves 10.
Worked example — creation, access, and braced form: Script vars.sh:
#!/bin/bash
number=10
vec=bus
str=V
echo \$number \$vec \$str
echo \${number}ish
vec=
echo empty is [\$vec]
value1=10
value2=\$value1
echo value2 is \$value2
Execution bash vars.sh →
- Line
number=10stores10→echo \$numberprints10. vec=busstoresbus→echo \$vecprintsbus.str=VstoresV→ printsV→ combined10 bus V.\${number}ishexpands to10ish— braces keepnumberseparate fromish. Without braces,\$numberishwould look up non-existentnumberish.vec=clearsvec→[\$vec]prints[](empty inside brackets).value2=\$value1copies value10→value2 is 10. If you wrotevalue2=value1(no\$), you would get literalvalue1.
Bold check: \$ means value, no \$ means name; braces are the word boundary guard.
Visual: draw two boxes — a closed box labeled name (the variable name) and an open box showing its contents Tixna labeled \$name. read points to the closed box (fill it), echo points to the open box (read it).
10.7.3 Assignment Rules and Spacing Requirements
Two spacing rules are repeated as nitty-gritty that varies by shell and by command and therefore must be remembered for bash:
Rule 1 — assignment, no spaces: number=10 is correct; with spaces the shell parses number as a command name, = as an argument, and 10 as another argument — yielding number: command not found. No spaces around = is the law for assignment.
Rule 2 — expr operators, spaces required: For the expr command (detailed in 10.10), you must put spaces around operators. expr 5 + 3 evaluates to 8. expr 5+3 without spaces does not evaluate and simply echoes the text 5+3 back (or treats it as a single string). The same sensitivity applies to %, * (escaped), and parentheses.
| Context | Spacing | Why |
|---|---|---|
var=value |
No spaces around = |
Shell parses = as part of assignment token |
expr 5 + 3 |
Spaces around + |
expr needs separate arguments for operator and operands |
[ \$res -eq 0 ] |
Spaces after [, before ], and around operator |
[ is a command, needs argument boundaries |
The overall pattern for this section is: dollar for access, no spaces for assignment, spaces for arithmetic operators — three mnemonics that prevent 80% of beginner syntax errors.
Pitfalls:
- Adding spaces around
=:number = 10fails with "command not found" because the shell thinks you are running a program namednumber. - Omitting spaces for
expr:expr 5+3quietly returns5+3as a string, not8, masking the bug. - Forgetting
\$on access but using it on assignment:\$number=10(with\$) tries to run the value as a command. Assignment never uses\$on the left. - Case mixing:
\$Numberversus\$numberare different variables —number=10thenecho \$Numberprints empty.
Q: What are command-line parameters? A: They are the arguments passed to a command after its name. In a shell script those arguments are not accessed by raw text but through the special positional variables \$0 through \$9, where \$0 is the script name and \$1 onward are the successive words typed after it. The lecture promises to show how to fetch their values in the special-variables demo that follows (section 10.8), where shell_variables.sh 1 2 hello 5 will make the mapping concrete.
Scope — what quoting does here: Inside double quotes "\$1" the value is preserved as one word even if it contains spaces; without quotes, \$1 with a b splits. This preview matters for robust scripts but the core rule here is positional mapping.
Recap and bridge: Command-line words become \$0 (name) through \$9 (positional parameters), accessed with \$ and disambiguated with \${ }. You create your own variables as name=value with no spaces, and read them as \$name. The spacing duality — no spaces for =, spaces for expr operators — is the syntactic hinge for the next two sections. Next we complete the special-variable family: \$*, \$@, \$#, \$\$, \$? and the worked demo that makes empty versus populated runs visible. Exam note: Be ready to map a concrete line like bash script.sh alpha beta to \$0, \$1, \$2, \$#, \$*, to explain why number = 10 fails while expr 5+3 fails differently, and to write \${10} for the tenth argument.
Real-world: Argument-driven tooling depends on this mapping — backup.sh \$1 where \$1 is a filename, deploy.sh \$1 \$2 where \$1 is environment and \$2 is version — with if [ \$# -ne 2 ] guarding usage, as you will see in 10.8.
10.8 Special Shell Variables Exclusive to Scripts
10.8.1 PID of the Current Shell with \$\$, Process Listing
Hook: Two users run the same script at the same time and both write to /tmp/report.txt — whose output survives? How can each run get its own private filename without collision?
Special variables are names the shell reserves for script-level bookkeeping — exclusive to scripts and the shell itself, not for you to define.
The first one examined is \$\$, which expands to the process identifier (PID) of the current shell — the numeric ID the operating system assigns to the running shell process. Every running shell, every command, and every script has a distinct PID visible via ps.
Formalize — PID: The shell replaces \$\$ with that number before executing the command, so echo \$\$ prints such as 22233 or 6167.
The live check in class is: run ps (or ps -f) and see that the bash process has a PID like 22233, then run echo \$\$ and see the same 22233 printed. Both views report the same running shell, confirming that \$\$ is the PID of the current shell. If you start a subshell ( echo \$\$ ), the number changes — it is the subshell's PID.
The verbal description preserved is: dollar dollar is the PID of the current shell, everything that runs is a process, and \$\$ is a special variable used to identify that PID. This PID trick is the classic way to make temporary files unique: /tmp/myprog.\$\$ creates a name like /tmp/myprog.22233 that will not collide with another user's /tmp/myprog.22234.
Intuition — factory badge number: Think of PID as a factory badge number clipped to each worker (process). No two workers on the floor wear the same number at the same time. \$\$ is looking down at your own badge. The mapping: process equals worker, PID equals badge number, \$\$ equals self-badge read. Where it breaks: badge numbers are recycled after a worker leaves, so not globally unique over time — but they are unique at any instant, enough for temp files.
10.8.2 Filename, Parameters, All-Parameters and Count
Formalize — the positional family and its aggregates:
\$0— the file name or command name, as noted (not counted in\$#).\$1,\$2, ...\$9— first, second, etc. parameters (positional).\$*and\$@— both represent the whole string of all parameters, seen as an array of command-line arguments. In the demo phrasing: dollar star and dollar at represent the whole string of all parameters. The difference surfaces only when quoted:"\$*"expands to one word"\$1 \$2 ...","\$@"expands to"\$1" "\$2" ...as separate words — the famousT1/Bourne quoting distinction. For simple echo both print1 2 hello 5.\$#— the total number of parameters passed (the count, not the values), where#mnemonic is count: the number of words after the script name. If you ran./script.sh 1 2 hello 5, then\$#is4.\$?— the exit status of the last executed command,0for success, non-zero for failure.\$\$— as above, the PID.
The file used to make this tangible is shell_variables.sh (the name varies between shell variables.sh and shell_variables.sh in the narration) whose body is structurally:
#!/bin/bash
echo PID is \$\$
echo File is \$0
echo First is \$1
echo Second is \$2
echo All is \$*
echo Also is \$@
echo Count is \$#
echo Status is \$?
The shebang at its top is adjusted to #!/bin/bash before execution, and chmod u+x shell_variables.sh makes it directly executable.
Visual: imagine a dashboard with gauges. Gauge \$# shows a single number (count). Gauge \$*/\$@ shows a long tape with all words. Small dials \$1, \$2 show individual words. Dial \$\$ shows your badge number, and warning light \$? glows green for 0 or red for non-zero after each command.
10.8.3 Exit Status and Worked Demonstration
Formalize — exit status: Every command returns an integer status to the shell. By convention: The shell stores that status in \$? immediately after the command; any next command overwrites it, so you must capture \$? right away if you need it.
Worked example — shell_variables.sh empty versus four arguments: After chmod u+x shell_variables.sh:
Run 1 — no parameters:
./shell_variables.sh
Output (PIDs vary):
PID is 6167
File is ./shell_variables.sh
First is
Second is
All is
Also is
Count is 0
Status is 0
Walk: \$\$ prints 6167 (this run's PID, noted as 6167 or 6165 in alternative narration — both are correct examples of a PID). \$0 is shell_variables.sh (or ./shell_variables.sh as typed). \$1 empty because no first word. \$2 empty. \$*/\$@ empty. \$# = 0 for zero words. \$? = 0 because the script reached the end successfully.
Run 2 — four parameters:
./shell_variables.sh 1 2 hello 5
Output:
PID is 6172
File is ./shell_variables.sh
First is 1
Second is 2
All is 1 2 hello 5
Also is 1 2 hello 5
Count is 4
Status is 0
Walk: the four words 1, 2, hello, 5 separated by spaces fill \$1 = 1, \$2 = 2, \$3 = hello, \$4 = 5 (though the demo only echoes \$1 and \$2). \$*/\$@ = 1 2 hello 5 as the whole set. \$# = 4. \$? = 0 again for clean exit. A failing command like grep nonsense file would set \$? to non-zero on the next line.
This pairing — empty run versus populated run — makes the bookkeeping visible: \$0 never changes with arguments, \$# counts words, \$*/\$@ collect them, and \$1/\$2 pick by position. Bold takeaway: count with \$#, select with \$1/\$2, dump all with \$*/\$@, name with \$0, success with \$?, self with \$\$.
Sense-check: If you add a fifth word world, \$# becomes 5 and \$* gains world at the end — confirming count is not fixed.
Assumptions and scope: The table above assumes Bourne-family shells (bash, sh). In csh, \$*, \$# work differently. Also, beyond nine words you need \${10}; \$10 is not the tenth word but \$1 plus 0.
Pitfalls:
- Reading
\$?too late:grep pattern file; echo hi; echo \$?prints the status ofecho hi(always0), notgrep. Capture immediately:grep pattern file; status=\$?. - Confusing
\$*quoted versus unquoted:for i in \$*splitsa bwith spaces into two iterations;for i in "\$*"keeps it as one word — use"\$@"when you want to preserve each argument as a separate word, asT1demonstrates. - Forgetting
\$0is not counted:./script.sh a bhas\$# = 2, not3;\$0is the name, not a counted argument. - Assuming
\$\$is constant: Each new shell gets a new PID; runningbash script.shversus./script.shyields different\$\$values across invocations.
Recap and bridge: \$\$ is your PID-badge for unique temps; \$0 is the script name; \$1..\$9 select positions; \$*/\$@ gather all words; \$# counts them; \$? reports success as 0. The empty versus four-argument demo proves that \$# tracks the count and \$* mirrors the words. Next we add the human factor: interactive scripts that prompt with echo, read with read, and decide with if. Exam note: Be ready to map a concrete line like bash script.sh alpha beta to \$0 = script.sh, \$1 = alpha, \$2 = beta, \$# = 2, \$* = alpha beta, and to predict the output of a script that echoes each, including the PID variation.
Real-world: \$# is the guard for argument validation (if [ \$# -ne 2 ]; then echo "usage: \$0 file1 file2"; exit 1; fi), \$? drives error handling after a critical cp or grep, and \$\$ seeds temporary file names (/tmp/build.\$\$) or lock files so parallel CI jobs do not clash.
10.9 Interactive Scripts and Real-World Use
10.9.1 Making Scripts Interactive with echo, printf and read
Hook: A script that always backs up report.txt is useful once. A script that asks "Which file should I back up today?" is useful every day. How do you make a script ask?
A script can ask the user for input at run time and then act on that input — this is an interactive script. Up to now arguments arrived via \$1 on the command line; interaction brings data via the keyboard during execution.
Formalize — prompt and read:
echo(orprintf) — displays a prompt string on stdout.readfollowed by a variable name — pauses, reads a line from stdin up to Enter, stips the trailing newline, and stores the line (without newline) into the named variable. No prior declaration is needed — the variable comes into existence on first use.
Skeleton shown in class:
#!/bin/bash
# this is a comment line
echo What is your name\?
read name
echo Hello \$name
Stepwise: echo prints the question; read name waits; user types Tixna + Enter → shell assigns name=Tixna; next line echo Hello \$name expands \$name to Tixna and prints Hello Tixna.
The alternative printf is noted as also usable in place of echo — printf "What is your name? " gives finer control over newlines and formatting (e.g., no automatic newline without \n), but the prompt-and-read pattern stays the same. In T3 Chapter 14, read is shown with read -p "prompt" var as a compact form that both prompts and reads in one command, but the two-line echo/read form is the pedagogical base here.
The # line at the top inside the skeleton: # this is a comment line is ignored by the shell except for #! at line one — it documents intent for humans.
Intuition — interview versus questionnaire: Think of echo as the interviewer asking a question and read as handing the interviewee a labelled blank card (name) to write their answer. The card starts blank, the interviewee writes Tixna on it, then you read the card aloud with echo \$name. The mapping: prompt equals question, variable name equals blank card label, \$name equals reading the card. Where it breaks: a human interview allows multi-word answers naturally; read splits on IFS by default, so read first last with Jon Jake Jones puts Jon in first and Jake Jones in last — a nuance shown in R2 examples.
10.9.2 Escaping the Question Mark Prompt
Formalize — why \?: The prompt line is echo What is your name\? with a backslash before ?. The question is why that backslash is required.
The answer ties back to wildcards from 10.5: ? on its own means match a single character. If you write echo What is your name? without escaping, the shell performs pathname expansion before echo sees its arguments. If a one-character file like a exists, name? could expand to namea or be treated as a pattern, depending on shell options and context. To make the shell treat ? as a literal question-mark character — not as a single-character wildcard — you escape its special meaning: \? signals do not consider it as representing a single character, use it as a question mark itself. So echo What is your name\? reliably prints the literal ? as punctuation regardless of files present.
This is the same escaping principle as \* for multiplication in expr — star is wildcard, question is wildcard, backslash literalizes one character. Inside double quotes echo "What is your name?" the ? is already protected, so \? is not strictly needed there, but the bare-word form without quotes needs it.
Visual: picture the shell's preprocessing pipeline: raw line echo What is your name? → expansion phase where ? tries to match files → echo sees expanded words. Inserting \ short-circuits expansion: echo What is your name\? → escape phase strips \ and protects ? → echo sees literal ?.
10.9.3 Worked Interactive Example with Name Input
Worked example — interactive.sh with Tixna and Rajesh:
File interactive.sh:
#!/bin/bash
# this is a comment line
echo What is your name\?
read name
echo Hello \$name
echo Happy programming
Run sequence as demonstrated:
Attempt 1 — missing path: Typing interactive.sh without ./ or without execute permission yields bash: interactive.sh: command not found or Permission denied because the shell searches \$PATH, not the current directory, and the file may lack x. This is expected setup friction.
Attempt 2 — explicit interpreter: Typing bash interactive.sh succeeds because interpreter is explicit, no x needed.
Prompt display: The shell prints
What is your name?
with a literal ? — proof that escaping worked — and waits with a blinking cursor.
User response 1: Typing Tixna + Enter → variable name becomes Tixna → next line prints:
Hello Tixna
Happy programming
because \$name expanded to Tixna.
User response 2 — different data, same script: Re-running bash interactive.sh and typing Rajesh + Enter prints:
What is your name?
Hello Rajesh
Happy programming
showing that the variable takes whatever value is read and the later echo reflects it without editing the script.
Teaching emphasis distilled: The contrast between the two lines is read name mentions the variable as a destination (the box), echo \$name accesses the value via \$ (the content). One names the box, the other opens it.
Sense-check: If you type no name and just hit Enter, name becomes empty and Hello prints with a trailing space — the script does not crash, it just echoes empty.
Bold answer: The same three-line script personalizes output for any name supplied at run time.
Q: What is the purpose of the backslash before the question mark in the prompt? A: The question mark is a wildcard that matches a single character. The backslash escapes that built-in meaning so the character is taken literally as a question mark in the printed prompt. The phrasing in class is: if you want to escape its literal meaning — not representing a single character but used as question mark only — you provide the escape with the backward slash, which says do not consider it as a wildcard. Several students tripped on this because ? looks like plain punctuation.
Q: Why does the next line use \$name after read name had just name? A: In read name, name is the variable name as a container to store input into. In echo \$name, the dollar-braced form signals you want the value that is present in that container. Whatever word was typed gets assigned to name, and \$name retrieves it. The confusion is natural because both lines mention name, but one is write, one is read.
Pitfalls:
- Forgetting
\$on display:echo Hello nameprints literalname, not the typed value. The dollar is the read operator. - Using
\$onread:read \$namewould read into the variable whose name is the value ofname— a subtle indirection bug. - Spaces in input:
readwith one variable captures the whole line (including spaces) as one value, butread a bsplits onIFS. TypingMary Jonesintoread namestoresMary Jonesintact; typing it intoread first lastsplits as described.
10.9.4 Industry Application: Menu-Driven Test Harness for Banking
Beyond toy prompts, the same interactive pattern scales to operational work at industrial scale.
A recent industry project described involves core banking migration from a manual, paper-driven process to a core banking system (CBS). Within the accounts domain alone there were 23 test scripts, each checking a different functionality — account creation, balance enquiry, interest posting, statement generation, etc. Running them manually required remembering each script's name and repeatedly typing bash /path/to/test_account_create.sh, bash /path/to/test_interest.sh, with long paths and risk of typing the wrong test before a release.
The solution was a single interactive shell script — a harness or menu-driven driver — that ran in a loop:
#!/bin/bash
while true
do
echo "==== Accounts Test Menu ===="
echo "1) Create account"
echo "2) Balance enquiry"
echo "..."
echo "23) Statement generation"
echo "0) Exit"
echo "Enter choice: "
read choice
case \$choice in
1) bash tests/test_create.sh ;;
2) bash tests/test_balance.sh ;;
# ...
0) exit 0 ;;
*) echo "Invalid choice" ;;
esac
done
The harness displayed a menu mapping numbers to functionalities, read a number from the operator, case dispatched the corresponding functional script, showed results, and looped back for the next choice. Instead of typing a long command for each test, the operator typed 7 and the harness dispatched the right test and returned. Because the choice was read with read, the harness remained interactive; because dispatch used positional mapping via case or \$choice, adding a new test meant adding a new menu entry — no harness rewrite.
The narration stresses how this made life easy after a couple of days of struggle with manual typing and remembering paths — a few hours to write the harness saved hours every test cycle, and the same operator could run the full regression without being a shell expert.
Assumptions and scope: The harness assumes each test script is independently runnable and that its exit status \$? indicates pass/fail. It does not parallelize tests; a failing test blocks the menu until the operator presses a key. For unattended CI, the interactive read would be replaced by command-line arguments or a config file.
Recap and bridge: echo prompts, read name stores, \$name retrieves — escaping \? protects the question mark from wildcard expansion — and the interactive.sh demo with Tixna then Rajesh proves the variable captures whatever is typed. That same read-and-dispatch pattern scales to a 23-option banking test harness that replaced manual bash path typing with a numbered menu loop. Next we add computation: integer arithmetic with legacy expr and the modern \$(( )) form, where the same escaping of * and parentheses matters. Exam note: Be ready to write the three-line interactive skeleton, to explain why \? is needed versus "?", and to contrast read name (container) versus echo \$name (value). The banking harness is a likely "describe a real-world use" short answer.
Real-world: Similar menu-driven harnesses appear wherever many related jobs coexist — regression suites, deployment steps (1) deploy to staging 2) deploy to prod), backup options, or device flashing where a number chosen at run time selects among scripts — anywhere a keyboard choice should select a workflow without retyping paths.
10.10 Arithmetic Evaluation with expr and Modern Alternatives
10.10.1 Basic expr Usage and Operator Spacing
Hook: You can store 10 in a variable, but how do you compute 10 + 3 without opening a calculator — and why does expr 5+3 stare back at you with 5+3 instead of 8?
Shell arithmetic for integers is historically done with the expr command — the external expression evaluator from Bourne's original toolkit. On the command line you can type:
expr 3 + 5
and see 8 printed, because . The same holds for subtraction, division, and remainder. The set of operators shown in class and in T3 Table 11-1 and R2 Table 8.12 includes + add, - subtract, * multiply, / divide, % remainder, plus string and comparison operators including pattern match and substring.
Formalize — expr invocation and spacing law:
Each token must be a separate argument to the expr program. The shell splits on whitespace before expr sees anything, so spaces are the argument separators.
The spacing rule for expr is the opposite of assignment and is strict:
Assignment: number=10 — no spaces around =. expr operators: spaces required around +, -, *, /, %, \(, \).
expr 5 + 3 with spaces evaluates to 8 because expr receives three arguments: 5, +, 3. expr 5+3 without spaces gives expr a single argument 5+3 which is not an expression — it treats it as a string and simply echoes it. The same sensitivity applies to % and * and parentheses. This is the duality previewed in 10.7: assignment forbids spaces, expr demands them.
The arithmetic family: In integer arithmetic, / is integer division (truncated toward zero), % is remainder. So with remainder , not 3.33. This integer-only nature is a deliberate Bourne limitation; floating point needs bc or awk, as T3 Chapter 11 notes.
Intuition — assembly line with bins: Think of expr as a factory worker who needs parts in separate bins. You must place 5 in bin one, + in bin two, 3 in bin three. If you glue 5+3 into one bin, the worker says "I do not recognize this part" and hands it back. Spaces are the dividers between bins. The analogy breaks where modern \$(( )) does not need this binning — it parses the expression itself.
10.10.2 Escaping the Star and Handling Modulus
Two operators need special attention because they collide with shell wildcards and job control.
Star * — wildcard collision: * is also the wildcard for any characters (10.5). Typing expr 5 * 3 without escape causes the shell to expand * into matching filenames before expr even sees it. If the current directory contains a.txt, the command becomes expr 5 a.txt 3, yielding a syntax error, even though you expect . To get literal multiplication you must escape the star — protect it from pathname expansion — with a backslash: expr 5 \* 3 where the backslash removes the wildcard meaning and leaves a star for expr. Then the output is 15, and the class repeats the confirmation: five star three, answer will be 15.
Modulus % — remainder: The modulus form uses % for remainder (called mod in narration). The example expr 5 % 3 is described as one three is three, remainder is two: because . On some keyboards the percent sign is harder to read aloud, so narration says "mod", but the typed form is %. No escape needed for % in most shells, unlike *.
Parentheses ( ) — subshell collision: Parentheses are also meta characters for command grouping (subshell) — e.g., ( ls; date ). Inside an expr expression they must be escaped as \( and \) (or quoted) so the shell does not try to start a subshell. Example later: expr \( \$b + \$c \).
Worked micro-examples — spacing and escaping:
expr 3 + 5→8because .expr 5 + 3→8with spaces;expr 5+3→5+3literal.expr 5 * 3→expr: syntax error(star expanded to files).expr 5 \* 3→15with escaped star; class confirms five star three is 15.expr 5 % 3→2because .expr 11 % 3→2(another demo: ).expr 1+4→1+4(no spaces, string).expr 5 + 9 / 3→8— division first (9/3=3, then 5+3=8), showing precedence; withexpr 5 \* 4escaped →20.
Sense-check: If you see a literal expression echoed back or "syntax error" where you expected a number, check two things: are there spaces around the operator, and is * or ( escaped?
10.10.3 Using expr Inside Scripts with Backticks
Inside a script you typically want to capture the numeric result into a variable for later echo or if, not just print it to the terminal. The legacy way shown uses command substitution with backticks — the character that sits with tilde ~ on the US keyboard, not a straight single quote '.
Formalize — capture pattern:
where the backticks (or modern \$(...)) mean evaluate this command and substitute its output into the assignment. The template in class is:
sum=`expr \$x + \$y`
with x and y as variables holding numbers read from the user. The shell first expands \$x and \$y to their values (e.g., 10 and 3), then runs expr 10 + 3 and captures 13, assigning to sum.
Variants shown and warned about:
- `
sum=expr \$x + \$y` — legacy backtick form used in lecture. - `
sum=\$(expr \$x + \$y)— modern\$( )` form, functionally identical but nestable and clearer. - `
val=\$(( x + 1 ))— even more modern arithmetic expansion, noexpr` needed.
The caution in class distinguishes the backtick ` ` from a single quote ' — they sit near each other on the keyboard but have different meanings. Using the wrong quote (sum='expr \$x + \$y') assigns the literal string expr \$x + \$y to sum`, not the numeric result.
The script variables involved — x, y, sum, diff, mul, div, rem — are all user-named; when their values are later printed the form is echo \$sum etc., with the dollar to retrieve the value, per 10.7 rules.
10.10.4 Worked Arithmetic Script with Two Numbers
Worked example — expr.sh with 10 and 3 (five operations):
Script expr.sh (shebang noted as optional when run via bash expr.sh):
echo Enter first number
read x
echo Enter second number
read y
sum=`expr \$x + \$y`
diff=`expr \$x - \$y`
mul=`expr \$x \* \$y`
div=`expr \$x / \$y`
rem=`expr \$x % \$y`
echo Sum is \$sum
echo Difference is \$diff
echo Multiplication is \$mul
echo Division is \$div
echo Modulus is \$rem
Each line uses spaces around the operator and \* for multiplication. Note the spaces: expr \$x + \$y not expr \$x+\$y.
Execution with 10 and 3:
Input: 10 for x, 3 for y.
Outputs, line by line:
sumis → script printsSum is 13.differenceis →Difference is 7.multiplicationis →Multiplication is 30(requires\*).divisionis integer division →Division is 3— quotient part only, truncated, not3.333. This confirmsexpris integer-only.modulus(remainder, variable namedremorreminderin narration) is →Modulus is 1because .
Repetition in class: sum is 13, difference is 7, multiplication 30, division 3, remainder 1 — memorize this 10/3 quartet.
Sense-check: Division 3 plus remainder 1 reconstructs original: , confirming integer division semantics.
Alternative path — division check: Try 10 and 4 yourself: 10 + 4 = 14, 10 - 4 = 6, 10 * 4 = 40, 10 / 4 = 2, 10 % 4 = 2 → holds.
Assumptions and scope: expr handles signed integers within the system's word size. Very large numbers may overflow. Division by zero yields expr: division by zero and non-zero exit. Floating point like 10 / 3 = 3.333 is out of scope for expr; use bc or awk or zsh for that, as T3 demonstrates with scale=4.
10.10.5 Increment Variations and the \$(( )) Form
Incrementing a value — adding one — is shown in two styles that are equivalent in bash but belong to different eras.
Two increment idioms, same result:
Legacy expr with backticks:
x=10
val=`expr \$x + 1`
Steps: x=10 stores 10; expr \$x + 1 becomes expr 10 + 1 → 11; backticks capture 11 into val.
Modern arithmetic expansion with double parentheses: The same increment becomes:
val=\$(( x + 1 ))
or with explicit dereference val=\$(( \$x + 1 )). Both work because \$(( )) evaluates the arithmetic itself, no external expr process. Inside \$(( )), x can be written as x or \$x — the shell fetches the value automatically.
The demo creates expr_increment.sh, adds a comment # Following can be used instead of expr, and shows both lines producing the same incremented value (ten becomes eleven, narrated as "love" due to speech-to-text but meaning eleven). Running bash increment prints the incremented value twice, confirming both styles agree:
11
11
The advice in class and in T3 is that both are valid in bash, and you can use the double-parenthesis form for any arithmetic — addition, subtraction, multiplication, division, modulus — with the same integer semantics as expr but more readably and without backticks or escaping * (inside \$(( )), * is not a wildcard). Examples:
Visual: contrast two pipelines. Legacy: x → \$x → expr (fork external) → capture → val with a process fork dot. Modern: x → \$(( )) (shell internal, no fork) → val as a direct arrow — faster and not needing IFS tricks.
10.10.6 String Length and Complex Expressions
String length with expr length: expr can measure text length with the length keyword. The snippet shown:
#!/bin/bash
echo Enter the string
read str
a=`expr length "\$str"`
echo \$a
read str collects a word (or quoted phrase) into str. expr length "\$str" returns the number of characters. The quotes around "\$str" are essential: without them, a value with a space splits into two arguments.
Complex expression with precedence and escaped parentheses: The compound form is built as:
a=10
b=10
c=10
d=10
res=`expr \$a \* \( \$b + \$c \) / \$d`
Because parentheses ( ) are also meta characters for subshell grouping, they must be escaped as \( and \) inside the expr line, and * must be escaped as \*. Quoted fully, the line is:
Math as described is: with BODMAS/BODMAS precedence: brackets first, then multiplication and division left to right.
Worked examples — length quirk and BODMAS:
Length — System versus System Programming:
- Input
System→expr length "System"countsS(1) y(2) s(3) t(4) e(5) m(6)→6. Class notes: three plus three is six, used as an aside to note the length ofSystemis six. - Input
System Programmingwith a space but without quotes (expr length \$strwherestr=System Programming) → shell splits into two wordsSystemandProgramming→exprseeslength System Programmingas three arguments where it expects two →expr: syntax error. Fix:expr length "\$str"keeps it as one argumentSystem Programming→ length18including the space. - Alternative modern:
\${#str}in Bash gives length withoutexpr.
Complex — a=10, b=10, c=10, d=10: Substituting: Running bash expr_complex.sh prints 20. The class walks the precedence: brackets first, then multiplication and division, then addition and subtraction.
Try variation a=5, b=3, c=7, d=2: , , .
Sense-check for precedence: Without parentheses, expr \$a \* \$b + \$c / \$d would be , not 20 — parentheses change the answer dramatically, proving their effect.
Pitfalls and scope:
- Missing spaces:
expr \$a+\$bechoes literal with plus, not sum. - Forgetting escapes:
expr \$a * \( \$b + \$c \)fails because unescaped*globs and unescaped(starts subshell. - Integer truncation surprises:
expr 5 / 2yields2, not2.5. Students expecting decimals must be told to usebcor\$(( ))with awareness that Bash still truncates —echo "scale=2; 5/2" | bcgives2.50. - Modern preference: New code prefers
\$(( ))because it needs no escapes and no fork, yet exam questions may still requireexprwith correct\*and\( and \).
Recap and bridge: expr is the legacy integer calculator that demands spaces around operators, \* for multiply, and \( and \) for grouping, captured via ` expr ... ; the modern \$(( )) form does the same arithmetic internally without escapes or forking and is the preferred style. The 10-and-3 demo (13,7,30,3,1) and the 10-10-10-10 BODMAS demo (20) prove integer division and precedence, while expr length shows string measurement and its quoting pitfall. Next we step back from arithmetic to the broader variable landscape: system variables in capitals versus your own names and the rules that keep them distinct. **Exam note:** Expect to write an expr line with correct spaces and escapes — especially \* and — to predict expr 5 % 3 and the 10/3 suite, and to distinguish when backticks are required versus when \$(( )) is acceptable. Also be ready for expr length System is 6 and why System Programming` without quotes errors.
Real-world: expr is legacy but still appears in old scripts and exam papers; new code prefers \$(( )) for readability and performance, yet both respect integer arithmetic, so divide-by-zero and floating-point expectations must be handled separately — bc or awk for decimals, (( )) for in-shell integer loops.
10.11 User-Defined and System Variables
10.11.1 System Variables in Capitals: PATH, SHELL, HOME and Others
Hook: Why does echo \$HOME print your home path while echo \$home prints nothing, even though both look like the same word?
A system variable (often called an environment variable or shell variable) is a name the shell and the system maintain for the session, not a name you invent inside your own script. By convention its name is all capitals — that visual cue distinguishes institutional state from your temporary script state.
Formalize — system variables as session state: These variables are set before your script runs, inherited by child processes, and changed only by the system or explicit export. You read them; you rarely assign them without export, and you never need to declare them.
Variables displayed by echoing them in the demo:
\$PATH— the colon-separated list of directories where the shell searches for commands: e.g.,/usr/local/bin:/usr/bin:/bin:/home/centos/bin. If you typels, the shell scans each directory in\$PATHin order until it finds an executablels.\$SHELL— the pathname of the default shell, shown as/bin/bashin this environment, the fallback when no shebang is present.\$HOME— the home directory path, e.g.,/home/centos, wherecdwithout arguments takes you and~expands to.\$PWD— present working directory, the directory you are in: e.g.,/home/centos/shell_scripts, as inpwd.\$BASH— the shell name again (points to same binary as\$SHELLin this setup,/bin/bash).\$BASH_VERSION— shown as4.2.46(2)-releaseor4.2in short form, the release number of Bash present.\$OSTYPE— the operating system type, shown aslinux-gnu(orlinuxshort), useful forcase \$OSTYPE in linux*).\$LOGNAME(or\$USER) — login name, shown ascentos, the name you logged in with.
You inspect them with echo \$PATH, echo \$SHELL, etc., where the dollar retrieves the value. Without \$, you print the literal name. Try echo LOGNAME versus echo \$LOGNAME — the first prints the word, the second prints centos.
The env command lists only exported environment variables; set lists all, as R2 Chapter 8 notes.
Intuition — office nameplates in capitals: Think of system variables as permanent nameplates on office doors — PATH, HOME, SHELL — engraved in capitals, present for every employee (process). Your user variables are sticky notes you add inside your own office (name, x, vec) in lowercase, tossed when you leave (script ends). Capitals mean institutional, lowercase means personal.
10.11.2 User-Defined Variables and Naming Rules
A user-defined variable is a name you invent inside your own script to hold a value you need — name, x, y, vec, str, sum, count, etc. No declare needed in simple cases; first assignment creates it. The value is always a string; numeric interpretation happens only when you apply arithmetic.
Formalize — naming and assignment contract:
- The name must start with an underscore
_or a letter (upper or lower, but lower is conventional for user variables). It cannot start with a digit. Sonumber=10is fine,_count=2is fine,10something=5is not right — "command not found" because the shell reads it as a command name starting with a digit. - After the first character, letters, digits, and underscores are allowed. So
var2,my_var,X1are legal;my-varis not (dash is an operator),my varis not (space splits). - The assignment must have no spaces around
=:vec=busis correct,vec = busornumber = 10with spaces is incorrect and will be misread as a command. This duplicates the rule from 10.7 but is worth reinforcing because it is the single most common syntax error. - The value can be a number, a word, or a quoted phrase.
vec=busstoresbus,msg="hello world"needs quotes to keep the space inside one value,count=10stores the string10. - Values are retrieved with
\$nameor\${name}. The braces\${ }are the explicit delimiter when concatenating, as in\${vec}s→bussversus\$vecslooking up a different variable.
The demo file that illustrates pure assignment rules shows several lines and asks which are correct: number=10 is fine, number = 10 is incorrect, number= 10 is incorrect, vec=bus is fine. This is a typical predict-the-output or fix-the-syntax exam item.
Worked example — legal versus illegal names:
number=10 # legal, lowercase start
_Number=5 # legal, underscore start
var2=hello # legal, digit after first char
2var=hello # illegal — starts with digit → bash: 2var=hello: command not found
my var=hello # illegal — space splits → bash: my: command not found
my-var=hello # illegal — dash parsed as minus
vec=bus # legal, as in lecture
vec =bus # illegal — space before =
vec= bus # illegal — space after =
echo \$vec # prints bus
echo \${vec}ish # prints busish
Sense-check: If a variable assignment suddenly says "command not found", look first at the character before = — is it a space, a dash, or a leading digit?
10.11.3 Null Variables and Advice on Avoiding Confusing Names
A variable may be set to empty (null) as vec= with nothing after = — this creates vec with an empty value (zero-length string), which is allowed and sometimes used to clear a value before a loop or to reset a flag. if [ -z "\$vec" ] then tests for that emptiness.
Assumptions and scope: Null is not unset. vec= is set but empty; unset vec removes the name entirely. Under set -u (nounset), referencing an unset variable errors, while referencing a null variable does not. This distinction matters for strict scripts.
Because ? and * are wildcards, and #, \$, ! have special meanings (length, PID, status, history), it is advised not to use those characters in variable names — they are likely to be misinterpreted as patterns or expansions rather than name characters. Keeping names to letters, digits, and underscores ([a-zA-Z_][a-zA-Z0-9_]*) keeps parsing predictable and avoids needing escapes.
The conceptual contrast that closes the section is simple and exam-ready: capitals for the system (PATH, HOME, SHELL), your own choice in lower case for user variables (number, vec), and strict spacing discipline for assignment (= with no spaces) versus arithmetic spaces.
Pitfalls:
- Using capitals for user variables and shadowing system state:
PATH=myfileoverwrites the search path and suddenlylsis "command not found" — a devastating bug. Keep user variables lowercase. - Spaces around
=:name = Tixnafails, butname= Tixnawith a space after=setsnameto empty and tries to runTixnaas a command. - Forgetting quotes with spaces in value:
msg=hello worldassignsmsg=helloand tries to runworldas a command;msg="hello world"assigns the whole phrase. - Starting with a digit:
1st=hellois illegal; usefirst=helloor_1st=hello.
Recap and bridge: System variables in capitals (PATH, SHELL, HOME, PWD, BASH_VERSION, OSTYPE, LOGNAME) are shared session state; user variables in lower case (number, vec, str) are your script's private sticky notes with strict rules: start with letter or _, never digit, use only letters/digits/underscore, assign with no spaces around =, and retrieve with \$ — with null vec= allowed. Respecting that naming divide prevents shadowing and parse errors. Next we add decision power: if, test, [ ], and the file and string tests that let a script choose a branch. Exam note: Be ready to list three system variables and their values in the demo, to spot which assignment lines are syntactically correct (number=10 versus number = 10), and to state why PATH=myfile is dangerous.
Real-world: In deployment scripts, echo \$HOME finds the user's home for config writes, \$PWD logs where the job ran, and if [ -z "\$1" ] guards missing arguments using the null-variable pattern — all built on the capital-versus-lowercase contract.
10.12 Conditional Logic: if, test, String, Logical and File Tests
10.12.1 Exit Status Convention: Zero is True
Hook: In C, 0 is false. In shell, 0 is true. How can both be right — and which one will bite you on an exam?
In shell decision making, truth is encoded as an exit status number returned by every command, not as a boolean literal like true/false. The rule is the opposite of C:
Formalize — exit status as truth:
So 0 means success or true, any positive or negative non-zero number means false. When you compare 1 with 1 via test 1 -eq 1, the test command succeeds and returns 0; the shell then treats that 0 as true and takes the then branch. Anything apart from that — any other value, 1, 2, 127 (command not found) — is false and takes the else branch.
This inversion is explicitly flagged in class as something you have to understand: in C, if (0) is false; in shell, if [ 0 -eq 0 ] is true because the status is 0. The mnemonic is: zero problems means true — "no error" equals "yes".
Every command returns a status, even if you do not check it. grep is a classic: 0 if pattern found, 1 if not found, 2 if error in pattern or filenames. test and [ are unusual because their sole purpose is to return a status, producing no output.
Intuition — traffic ticket: Think of exit status as a traffic ticket. 0 means no violation — you passed, go through the green light (true branch). Non-zero means you were ticketed — the number is the fine code, and you are stopped (false). In C, the number is the value being tested; in shell, the number reports whether the last command succeeded.
Visual: imagine a number line with 0 highlighted green in the center labeled "true / go", and all other integers red on both sides labeled "false / stop". The takeaway is that only one value is green.
10.12.2 Syntax of if-then-else-fi
The if statement is the decision construct that branches on that exit status. Its skeleton is:
if condition
then
commands
fi
if, then, and fi are keywords and must be present. fi is if spelled backwards and closes the block — a Bourne heritage trick to avoid needing braces. With an else branch:
if condition
then
commands_when_true
else
commands_when_false
fi
The condition slot is itself a command whose exit status if inspects. Most often that command is test or its bracket shorthand [ ... ], both covered next. You can also put any command there: if grep -q pattern file branches on whether grep found the pattern.
The keywords and the closing fi are the scaffolding; the condition slot determines which branch runs. Newlines before then and else matter — then and else are recognized only after a newline or a semicolon. So if [ \$x -eq 0 ]; then echo yes; fi is valid with semicolons, but if [ \$x -eq 0 ] then without a separator is a syntax error.
Scope: if does not itself evaluate arithmetic; if [ \$x -eq 5 ] works because [ calls test. if [[ \$x == 5 ]] is a Bash extension with different rules (pattern matching), not covered here — stick to single [ ] for portable exam answers.
10.12.3 Worked Example: Odd or Even
Worked example — if_odd.sh with remainder test (13 then 12):
Core logic as taught (final version uses \$(( )), alternative uses expr and backticks):
x=13
res=\$(( x % 2 ))
if [ \$res -eq 0 ]
then
echo Even
else
echo Odd
fi
Steps, unpacked:
- Assign
x=13. - Compute remainder of division by 2: . In shell:
res=\$(( x % 2 ))or `res=expr \$x % 2`. - Test
\$res -eq 0inside brackets: does remainder equal zero? - If test succeeds (exit
0→ true), number is even; otherwise odd.
The nuance stressed in class is spacing inside brackets: you must have a space after [ and before ], and spaces around the operator:
[ \$res -eq 0 ] with spaces is required because [ is actually a command name (a file at /bin/[ and a shell built-in) that expects ] as its last argument. Without spaces, the shell sees [\$res as one word, not the command [. The mnemonic given is: have the value, space, operator, space, value, space before closing bracket.
Demo runs confirm:
- With
x=13, . Test[ 1 -eq 0 ]fails (status1→ false) →elsebranch → script prints Odd. - Change to
x=12, . Test[ 0 -eq 0 ]succeeds (status0→ true) →thenbranch → Even.
Running bash if_odd.sh executes whichever x value is saved. To make it interactive, replace x=13 with:
echo Enter number
read x
res=\$(( x % 2 ))
if [ \$res -eq 0 ]; then echo Even; else echo Odd; fi
Then typing 13 yields Odd, 12 yields Even, proving the branch responds to input.
Bold synthesis: odd is remainder 1, even is remainder 0, tested with [ \$res -eq 0 ] where spaces and -eq are mandatory.
Sense-check: Every even number ends in 0,2,4,6,8 — all give remainder 0 when divided by 2; every odd gives 1. The modulo test is exhaustive.
10.12.4 Numeric Comparisons with test and Bracket Form
For numeric comparison, the operators are dash-letter forms, not C symbols:
Numeric test / [ ] operators:
-eqequal (equals)-nenot equal-ltless than-leless than or equal-gtgreater than-gegreater than or equal
Mnemonic: -e for equal, -n for not, -l for less, -g for greater, plus t/e for than/equal.
Two equivalent ways to write the same test:
if test 5 -eq 6
and
if [ 5 -eq 6 ]
In the second form the brackets are syntactic shorthand for test, which is why spaces are mandatory around [ and ] — [ needs to see 5, -eq, 6, and ] as four separate arguments. The lecture contrasts C-style ==, !=, <, <= with these dash-letter shell forms and warns to use the shell forms when inside test or [ ]. Writing if [ 5 == 6 ] in single brackets is a string test, not numeric, and may give wrong answers for numbers like 010.
Numeric prediction drill:
| Test | Numeric meaning | Result (true=0) |
|---|---|---|
[ 5 -eq 5 ] |
true | |
[ 5 -ne 6 ] |
true | |
[ 3 -lt 5 ] |
true | |
[ 5 -le 5 ] |
true | |
[ 7 -gt 3 ] |
true | |
[ 7 -ge 10 ] |
false |
Bold pattern: every shell numeric operator starts with - and is two letters, unlike C's ==/!=.
10.12.5 String Comparisons and Empty Checks
For string comparison, simpler symbols and dedicated flags are used:
String operators inside [ ]:
string1 = string2— strings equal (note single=, not==;==inside[[ ]]is Bash-only).string1 != string2— strings not equal.-n string— true if string is non-empty (not null) — mnemonic:-nis not null.-z string— true if string is zero length (empty) — mnemonic:-zis zero length.
Both -n and -z test whether a string has content, and they are the shell's way to guard missing arguments.
The demonstration builds a tiny file to show these:
Set str=V then if [ -z "\$str" ] to check empty — with str=V (length 1), -z is false, so it prints the "not empty" branch (the else). Changing to str="" (or str= null) makes -z true.
Equality with two variables: setting str1=V and testing if [ "\$str1" = "\$str" ] compares the two variables; with both as V they are equal, so the script prints Strings are equal. Changing the test to != would flip to "not equal". The quotes "\$str" guard against empty values where [ \$str = V ] with empty \$str would become [ = V ] — a syntax error — while [ "\$str" = V ] becomes [ "" = V ] safely.
String prediction drill with str=V:
str=V
if [ -z "\$str" ]; then echo empty; else echo not_empty; fi # not_empty
if [ -n "\$str" ]; then echo not_empty; else echo empty; fi # not_empty
# Equality
str1=V
if [ "\$str1" = "\$str" ]; then echo Strings are equal; else echo not equal; fi # Strings are equal
if [ "\$str1" != "\$str" ]; then echo not equal; else echo equal; fi # equal
# Null guard
empty=""
if [ -z "\$empty" ]; then echo zero_length; fi # zero_length
Bold distinction: numeric uses -eq, string uses =; -z is empty, -n is not empty.
Sense-check: Every non-empty string satisfies -n and fails -z; every empty satisfies -z alone.
10.12.6 Logical Operators: Not, And, Or
Formalize — combining conditions inside test / [ ]:
!— logical NOT, a single exclamation!before a condition negates it.if [ ! -f file ]is true whenfileis not a regular file. The!must be a separate argument with spaces:[ ! -z "\$str" ].-a— logical AND between two expressions:expr1 -a expr2is true only when both parts are true. Inside[ ]:if [ -f "\$1" -a -r "\$1" ]checks regular file and readable. In modern Bash&&outside[ ]is preferred:[ -f "\$1" ] && [ -r "\$1" ].-o— logical OR:expr1 -o expr2is true when at least one part is true. Example:if [ "\$choice" = "y" -o "\$choice" = "Y" ].
These are used inside test / [ ] to chain checks when one condition alone is not enough. Precedence: ! highest, then -a, then -o, but parentheses \( and \) can group (escaped as in expr).
A note from R2 and T3: [ cond1 -a cond2 ] is legacy; modern style writes [ cond1 ] && [ cond2 ] or [[ cond1 && cond2 ]] for clarity and to avoid precedence surprises, but the exam expects the legacy -a/-o forms as taught.
Logical drill:
x=5
if [ \$x -gt 3 -a \$x -lt 10 ]; then echo "3 < x < 10"; fi # prints, both true
if [ \$x -eq 5 -o \$x -eq 7 ]; then echo "x is 5 or 7"; fi # prints
if [ ! \$x -eq 0 ]; then echo "x not zero"; fi # prints, x=5
Bold rule: -a needs both true, -o needs one true, ! flips.
10.12.7 File Property Tests and Worked Directory/Executable Checks
The shell can test properties of files via unary flags — a powerful alternative to ls that lets a script decide before it acts. Each flag is a single dash plus a letter, where case matters.
File test flags (inside [ ]):
-b— block special file (e.g.,/dev/sda)-c— character special file (e.g.,/dev/tty)-d— directory-f— regular file (not directory, not special)-r— readable file (permissionrfor you)-w— writable file (w)-x— executable file (x)-s— file has non-zero size (not empty)-e— file exists (regardless of type)-T— text file (capitalT; note it is not lower case)-B— binary file (capitalB)-L— symbolic link (capitalL, also-h)
Capitalization matters: lower case b and c are block and character special; capital T, B, L are text, binary, and symlink. Confusing case tests the wrong property. For portability, stick to POSIX lower-case flags (-d, -f, -r, -w, -x, -s, -e) which appear on every system; -T/-B are less portable.
Each test returns true (0) if the property holds, else false. Example: [ -d /home ] returns 0 because /home is a directory; [ -f /home ] returns non-zero.
Worked directory check — if_dir.sh with positional \$1:
File if_dir.sh:
filename=\$1
if [ -d "\$filename" ]
then
echo directory
fi
Here \$1 is the first command-line argument (from 10.7), assigned to filename. The test -d checks whether that pathname is a directory. Quotes "\$filename" protect against spaces and empty.
Runs illustrate positional behavior:
./if_dir.shwith no argument:\$1is empty,\$filenameis"".[ -d "" ]tests empty string — on some shells it errors, but in this demo the empty case is noted as reportingdirectorybecause the test falls back to the current directory (narration: "it is taking the current directory"). Pedagogically, this shows why you must guard missing arguments withif [ -z "\$1" ]../if_dir.sh .with dot (current directory):[ -d "." ]true → printsdirectory—.is a directory../if_dir.sh ..with dot-dot (parent directory): printsdirectory—..is a directory../if_dir.sh ...with three dots: prints nothing —...is not a valid directory in this layout (no such entry)../if_dir.sh first.shwherefirst.shis a regular file:[ -d "first.sh" ]false → prints nothing — it is not a directory.- Absolute paths
./if_dir.sh /homeand/home/centosand/home/centos/shell_scriptseach printdirectorywhen the path exists as a directory on this CentOS host.
Worked executable check — swapping -d to -x:
Changing the flag:
if [ -x "\$filename" ]
then
echo executable
fi
Testing with expr.sh (a non-executable file with 644) prints nothing; after chmod +x expr.sh, testing again prints executable. In the demo the echoed word "directory" remained due to a leftover echo directory string, but the logic is that -x maps to executable status. The point is that swapping the single letter flag changes the property under test without changing the surrounding if-then-fi scaffolding.
Prediction drill:
| Command | Test | Output | ||
|---|---|---|---|---|
./if_dir.sh /etc |
-d /etc |
directory |
||
./if_dir.sh /etc/passwd |
-d /etc/passwd |
(empty, it is -f not -d) |
||
[ -f /etc/passwd ] && echo file |
-f regular file |
file |
||
[ -x /bin/ls ] && echo executable |
-x executable |
executable |
||
| `[ -s empty.txt ] | echo empty` | -s non-zero size, file empty |
empty |
|
[ -e /nonexistent ] && echo exists |
-e exists |
(empty) |
Bold takeaway: -d for directory, -f for regular file, -x for executable, and \$1 carries the path to test.
Sense-check: Every directory satisfies -e and -d; a regular file satisfies -e and -f but not -d; a symlink satisfies -L and often -e.
Q: In the file test example, what does \$1 represent? A: It specifies the first command-line argument. In filename=\$1, whatever word is typed first after the script name is assigned to filename. The subsequent [ -d "\$filename" ] then checks whether that argument names a directory. If you run ./script.sh /home, \$1 is /home and the test examines /home. Several students asked this variant; the canonical confusion was thinking \$1 is a fixed file name rather than the positional slot for whatever the caller supplies.
Pitfalls and mandatory spacing:
- Bracket spacing:
[ -d "\$filename" ]needs space after[and before], and spaces around operators. Writing["\$filename" -d]or[-d "\$filename"]fails parsing because[is a command, not punctuation. - Forgetting quotes:
[ -d \$filename ]withfilename="My Docs"splits into two words, giving[: too many arguments. Always quote"\$filename". - Confusing numeric vs string operators: Use
-eqfor numbers,=for strings.[ "\$count" = 5 ]may work for5but fails for arithmetic logic;[ \$count -eq 5 ]is numeric. - Exit status inversion:
[ -d /home ]returning0is true — newcomers expecting1for true get the branch backwards. - Assuming
.is the argument when none given: If the user forgets an argument,\$1is empty, not.— guard withif [ \$# -eq 0 ]; then echo "usage: \$0 dir"; exit 1; fi.
Assumptions and scope: The table of flags assumes a Unix filesystem with standard permissions. On FAT or network mounts, -x may be meaningless. -T/-B heuristics may not exist on minimal sh. For portable exam scripts, prefer -d/-f/-r/-w/-x/-s/-e.
Recap and bridge: Truth is exit status 0; if condition; then ... else ... fi branches on that status, with condition usually test or [ ] where numeric tests use -eq family, string tests use =, !=, -n, -z, logical combos use !, -a, -o, and file tests use -d/-f/-r/-w/-x/-s/-e (plus capitals T/B/L). The if_odd.sh remainder demo (13→Odd,12→Even) and the if_dir.sh /home versus first.sh contrast prove the pattern: compute remainder with \$(( x % 2 )) or expr, then test [ \$res -eq 0 ] with mandatory spaces, and guard files with [ -d "\$1" ]. Exam note: Be ready to write the full if-then-else-fi skeleton, to fix missing bracket spaces, to choose -eq versus = correctly, to combine with -a/-o/!, and to predict if_dir.sh output for ., .., ..., /home, and a regular file — quoting matters throughout.
Real-world: File tests guard every production script before touching data — if [ ! -f "\$1" ]; then echo "file not found: \$1" >&2; exit 1; fi or if [ -x "\$prog" ]; then "\$prog"; fi — and logical connectors let you combine guards: [ -f "\$1" -a -r "\$1" ] checks it is a regular file and readable before you try to cat it, while mkdir tmp || exit bails if creation fails.
Exam Guidance Summary
- Conduct — zero tolerance for copying: A serious warning was given that direct copying in the mid-session exams was detected, including wrong answers copied ditto with identical quotes and formatting. If copying recurs in the comprehensive exam, the stated consequence is a direct zero — even for a right answer — and a possible report to the WLP division. The message is that learning, not reproduction, is what is assessed; the risk of an identical wrong answer is high because it is proof of copying.
- Class context: 24 to 27 students are enrolled, but attendance in live sessions is lower; the work and practice are expected from those who attend regularly, and the detailed examples are the exam source.
- What the exam will lean on — shell scripting as a very important topic: Expect questions that check precise understanding of:
- The purpose of a script (grouping commands for the shell to read and execute sequentially, not the vague "system administration" label)
- The shebang line
#!/bin/bash— when it is required (direct./script.sh) versus ignored (explicitbash script.sh), and the fallbackecho \$SHELLyielding/bin/bashwhen shebang is absent - Executable permission with
chmod +x(user, group, others) versuschmod u+x(user only) and thels -lrwxview - Wildcard patterns:
*(any number including zero),?(exactly one),[ ](one of set),[! ](negation), ranges[A-Z]/[0-9]/[a-z], and combined positional patterns like[EI][xf]* - Meta characters and their distinct roles:
>(overwrite),>>(append),<(input),|(pipe),;(always next),&&(next only if success),||(next only if failure),\(escape),' '/" "(quoting),#(comment, except#!) - Positional parameters
\$0through\$9plus\$*,\$@,\$#(count),\$\$(PID),\$?(exit status where0is true) - Interactive
readand the container versus value distinction:read nameversusecho \$name, and escaping\?for a literal question mark - Spacing rules: no spaces around
=for assignment (number=10) versus spaces required aroundexproperators (expr 5 + 3) - Escaping
\*and\( and \)forexprmultiplication and grouping, and the difference between backticks ``and single quotes'` - String length with
expr length "\$str"(e.g.,Systemlength6, and whySystem Programmingwithout quotes errors) - Complex
exprwith BODMAS:a * (b + c) / dwitha=b=c=d=10yielding20 - The full
if-then-else-fistructure including numeric tests-eq/-ne/-lt/-le/-gt/-ge, string tests=/!=/-n/-z, logical!/-a/-o, and file flags-d/-f/-r/-w/-x/-s/-e(plus-T/-B/-Lwith case sensitivity) - The bracket spacing mandate:
[ \$res -eq 0 ]with a space after[, before], and around the operator —[ \$res -eq 0]or[\$res -eq 0 ]fails - The contrast between
testand[ ](brackets are shorthand fortest, hence the spacing), and that exit status0is true, non-zero is false, opposite to C
- Practice advice: A worksheet on wildcards (
*,?, bracket lists, negation, ranges) was assigned — try each pattern against a sample directory and compare listings. All examples shown in class are candidates for replication questions where you must write the script, show the command line, and state the output:shell_variables.shempty versus1 2 hello 5,interactive.shwithTixnaversusRajesh,expr.shwith10and3yielding13, 7, 30, 3, 1,expr_complex.shyielding20,if_odd.shwith13→Oddthen12→Even, string equality withstr=V, andif_dir.shwith.,..,...,/home, andfirst.sh.
- Preparation for next session: The next meeting will continue shell scripting with the remaining constructs not yet covered (loops, functions, and more). An assignment and Quiz 2 will be announced before the following week with dates for execution. Studying the current set thoroughly — especially the spacing and quoting traps — before that continuation will keep the sequence intact and prevent compounding gaps.
Key Industry Applications
- Repeatable system administration and machine snapshots: Grouping
who,ls -l,date,hostname,uptime,df -h,free -mand similar probes into one script such asmorning_check.shlets operators snapshot a machine state on demand or via a scheduler (cronorsystemdtimer). Instead of retyping five commands each morning and risking a forgotten flag, they runbash morning_check.sh > snapshot.\$(date +%Y%m%d).logand archive the result. This is the direct production use of the grouping idea from 10.1–10.2.
- Menu-driven test harness for large migration (banking core-system example): The industry project described had 23 test scripts in the accounts domain alone. Rather than remembering 23 paths and typing
bash /long/path/test_create.sheach time, a single interactive loop displayed a numbered menu,readthe operator's numeric choice, dispatched the matching test script viacase, showed output, and looped back. This pattern generalizes to any regression suite, deployment matrix, or device-flashing workflow — anywhere a number or keyword chosen at run time selects among many scripts without retyping paths. Adding test 24 is one new menu line.
- Log and data pipelines using redirection and pipes:
ls > filefor fresh captures,ls >> filefor accumulation across runs,2> errors.logfor error capture, andcat access.log | grep ERROR | sort | uniq -c | sort -n > summary.txtstyle pipelines that connect the output of one program to the input of the next. In production, this isgrep ERROR app.log > errors.txtfor triage,cat data.csv | sort | uniq > clean.txtfor deduplication, andmake 2> build.err | tee build.logfor dual logging — all built on>,>>,|from 10.6.
- Argument-driven tooling using positional parameters: Scripts that take a filename or a threshold as
\$1and validate with\$#— the classic guardif [ \$# -ne 2 ]; then echo "usage: \$0 input output"; exit 1; fi— then branch and track success with\$?. Examples:backup.sh \$1for the file to back up,deploy.sh \$1 \$2for environment and version,process.sh data.csv 100where\$2is a threshold, all leveraging\$0..\$9,\$*/\$@, and\$#from 10.7–10.8.
- File-state guarding in unattended automation: Using
[ -d path ]to check a directory exists beforecd,[ -f file ]to confirm a regular file before parsing,[ -x prog ]to verify executability before running,[ -r file ]and[ -s file ]to check readability and non-emptiness,[ -e file ]for existence regardless of type, and combinations with-a/-osuch as[ -f "\$1" -a -r "\$1" ]to decide whether to create, process, or skip a file in nightly jobs that run without human supervision — the direct use of the file tests from 10.12.
- Remote connection and orchestration scripts: Mentioned as a context where learners already use a shell script to
sshinto a remote machine,scpa file, orrsynca directory. The same primitives extend outward: a local script canssh user@host "bash -s" < local_script.shto run a script remotely, or loop over a list of hosts andscpa config — illustrating that shell scripting reaches from local file handling to networked operation without leaving the language introduced in this lecture.
SP Lecture 10 notes · Shell Scripting: Concepts, Construction and Execution
Sections Breakdown
Defines shell script as queued commands for sequential interpretation, contrasted with compiled programs, with playlist analogy and execution model.
Groups interactive commands into a vi-edited plain-text file; .sh is convention and bash file replays them sequentially.
Shebang #!/bin/bash as kernel interpreter directive; explicit bash file vs chmod +x and direct execution, with u+x scope and fallback to $SHELL.
Families sh/bash/csh/ksh/zsh as distinct programs; construct incompatibility and $SHELL default (/bin/bash) with environment variables.
Star any-length, question single-char, bracket list/negation/range; positional expansion before command with combined patterns like [EI][xf]*.
Redirections >, >>, <, pipe |, terminator ;, connectors && || with exit-status true=0, and quoting/escaping for literal meta.
Positional $0-$9, creation as var=value no spaces, access via $ and ${}, expr needs spaces -- with read container vs value intuition.
PID $$, filename $0, positional $1-$9, aggregates $*/$@, count $#, status $? with empty vs four-arg runs of shell_variables.sh.
echo/printf prompt, read name storage, $name retrieval, \? escaping, Tixna/Rajesh demo and 23-test banking menu harness.
expr integer arithmetic with space/escape rules, backtick capture, 10/3 suite (13,7,30,3,1), $(( )) modern form, length and BODMAS a*(b+c)/d=20 demo.
System capitals PATH/SHELL/HOME etc. versus user lowercase; naming must start letter/_, no spaces around =, null vec= allowed.
Zero-true exit status, if-then-else-fi with test/[ ], numeric -eq family vs string =/-n/-z, logical ! -a -o, file flags -d/-f/-x etc., odd/even 13/12 and if_dir.sh demos.
Conduct warning, exam focus on shebang/chmod/wildcards/meta/positional/arithmetic/if, practice replications, next session preview.
Six real-world patterns: snapshots, menu harness, pipelines, arg tooling, file guarding, remote orchestration.
Exam Revision Notes
Below is the distilled, exam-ready core. Every entry comes from the full explanation above. Use this section for rapid review; return to the main notes when a point needs more context.
Shell Scripting Fundamentals: Purpose, Definition and Program versus Script
Must-know: Shell script groups commands for the shell to read and execute sequentially; interpreted not compiled, yet has logic and modularization.
⚠️ Top pitfall: Thinking scripts have no logic or that the file runs by itself without a shell.
Self-check: What distinguishes execution of bash first.sh versus ad-lib typing?
Connects to: 10.2, 10.3
Creating a Shell Script File
Must-know: Create file in vi, one command per line, run via bash file; .sh suffix is human-readable hint only.
⚠️ Top pitfall: Thinking .sh makes file executable; permissions do.
Self-check: Why does bash one_st.sh work but ./one_st.sh fail before chmod?
Connects to: 10.1, 10.3
The Shebang Line and Two Execution Methods
Must-know: Two methods: bash file ignores shebang, ./file requires chmod +x and shebang; u+x for user only; missing shebang falls back to $SHELL.
⚠️ Top pitfall: Blank line before #! or Windows line endings breaking interpreter, forgetting ./
Self-check: What changes between chmod +x and chmod u+x in ls -l?
Connects to: 10.2, 10.4
Shell Variants and the Default Interpreter
Must-know: Five shells and bash as standard; $SHELL is fallback when shebang missing; constructs differ across shells.
⚠️ Top pitfall: Assuming $SHELL equals current shell; hard-coding /bin/bash on non-Linux.
Self-check: What prints for echo $SHELL in the demo and why does it matter?
Connects to: 10.3, 10.5
Wildcards and Pattern Matching
Must-know: * any including zero, ? exactly one, [EI] one of set, [!EI] not set, a-z range; expansion before command.
⚠️ Top pitfall: Rm * typo, case sensitivity, quoting kills expansion, thinking * matches dotfiles.
Self-check: What matches [EI][xf]* given files Ex1.sh, If_old.sh, Anna.sh?
Connects to: 10.6
Meta Characters and Command Composition
Must-know: > create/truncate, >> append, < input, | pipe concurently; && on success, || on failure; true is 0; \ escapes *.
⚠️ Top pitfall: sort file > file truncation, ; vs && after cd, forgetting quotes around variables.
Self-check: Fix expr 5 * 3 syntax error and explain difference between ; and &&.
Connects to: 10.5, 10.10
Variables, Command-Line Arguments and Quoting Rules
Must-know: $0 script name, $1..$9 args; var=value no spaces; $var value; expr needs spaces around operator.
⚠️ Top pitfall: Spaces around = fails; expr without spaces echoes string; $number=10 wrong side.
Self-check: Map ./script.sh alpha beta to $0 $1 $2 and explain number = 10 error.
Connects to: 10.8, 10.10
Special Shell Variables Exclusive to Scripts
Must-know: $$ PID, $0 name, $1/$2 select, @ all, # count, ? status 0 true; empty vs 1 2 hello 5 demo.
⚠️ Top pitfall: Reading * vs 0 counted in $#.
Self-check: Map ./shell_variables.sh 1 2 hello 5 to each special variable.
Connects to: 10.7, 10.9
Interactive Scripts and Real-World Use
Must-know: echo prompt, read name stores, echo name retrieves; \? escapes wildcard; read vs distinction.
⚠️ Top pitfall: echo Hello name literal, read $name indirection bug, forgetting \? without quotes.
Self-check: Explain read name vs echo $name and purpose of \? in What is your name\?
Connects to: 10.7, 10.10
Arithmetic Evaluation with expr and Modern Alternatives
Must-know: expr needs spaces, \* for multiply, for grouping; 10 3 gives 13/7/30/3/1 integer; $(( )) needs no escapes; length 6 for System.
⚠️ Top pitfall: Missing spaces gives string, unescaped * globs, integer division truncation, length without quotes errors on spaces.
Self-check: Compute expr 10 * 10 with correct escape and a*(b+c)/d for 10 10 10 10.
Connects to: 10.6, 10.7, 10.12
User-Defined and System Variables
Must-know: Capitals for system, lowercase for user; start letter/_, no digit; no spaces around =; null allowed.
⚠️ Top pitfall: PATH=myfile overwrites search path; spaces around =; starting with digit; msg=hello world without quotes.
Self-check: Which is legal: number=10, number =10, 2var=hi, _x=5?
Connects to: 10.7, 10.8, 10.12
Conditional Logic: if, test, String, Logical and File Tests
Must-know: true is 0; if then else fi with [ ] needs spaces; -eq numeric vs = string; -n/-z empty; -d/-f/-x file; if_odd 13 Odd 12 Even; $1 is first arg.
⚠️ Top pitfall: Missing spaces inside [ ], using == in [ ], forgetting quotes around $1, confusing 0 true with C.
Self-check: Fix [ $res -eq 0 ] spacing and predict ./if_dir.sh . vs ... vs /home.
Connects to: 10.7, 10.8, 10.10
Exam Guidance Summary
Must-know: Shell scripting heavily examinable; know purpose, shebang, chmod, patterns, meta, positional, expr spacing, if structure.
⚠️ Top pitfall: Copying triggers zero; missing bracket spaces; confusing -eq vs =.
Self-check: List five exam topics from the lean list.
Connects to: None — standalone
Key Industry Applications
Must-know: Each application maps to lecture primitives: grouping, read, redirection, positional, file tests.
⚠️ Top pitfall:
Self-check: Map banking harness to read/case/positional.
Connects to: None — standalone
Was this lecture useful?
BitsNotes AI Assistant
Subject Notes AssistantConfigure AI Chat
Choose how to access the chatbotSigned in as
Powered by BitsNotes — 20 messages per day. No API key needed. Want unlimited access? Use "Bring Your Own Key" mode.
Sign in to use AI Chat
Get 20 free AI messages per day to ask questions about your lecture notes. Sign in with Google or GitHub — it takes 5 seconds.
Sign In to BitsNotesSwitch to "Bring Your Own Key" tab above for unlimited access with any OpenAI-compatible provider.