Skip to content

Chapter 8: Shell Scripting

Source slides: V06_shell_scripting.pdf. Exercise: E06_shell_scripting.pdf. Code: lecture_scripts/example01.sh..example18.sh, backup-data.sh, if_syntax, test01/. Exercise solutions: task1_solution.sh (prime), task2_solution.sh (extended backup-data with -s).


1. Chapter Overview

Shell scripts are executable text files that automate workflows: launch CFD jobs, sweep parameters, post-process logs, back up results, set up the environment. Bash, the default shell on Linux, has its own programming language with variables, control flow, arrays, functions, options parsing, and command substitution.

Why it matters in HPC/CFD: 90 % of the "glue code" between solvers, schedulers and post-processing is bash. A correct script saves hours; a buggy one wastes core-hours.

What the examiner asks (very high frequency):

  • "Write a script that …"
  • "Predict the output of this script."
  • "Explain ${1}, $#, $@, $?, $$."
  • "What is the difference between [ ], [[ ]], (( ))?"
  • "Convert this if to a case (and vice versa)."
  • "Explain the getopts block."
  • "Modify the backup script to add -s for sync."

What you must master for top grade:

  • The script header (#!/bin/bash), permission, and run methods.
  • All control structures: if/elif/else, case, for, while, until, select.
  • Variables, quoting, command substitution $(…), arithmetic (( … )).
  • Arrays.
  • Functions, return values, exit codes.
  • getopts for options parsing.
  • Reading user input with read.
  • The full backup-data.sh line-by-line.

2. Basics from Zero

A shell script is just a list of commands the shell would otherwise have run interactively, saved into a .sh file with a shebang on line 1:

#!/bin/bash
echo "Hello $USER"

To run it:

chmod +x hello.sh
./hello.sh
# or:
bash hello.sh

The shell already knows about variables, conditions, loops, functions, file tests, command substitution. You combine those with the existing tools (grep, awk, sed, find, chmod, rsync, …) to automate tasks.

বাংলায়: Shell script মানে এমন একটা টেক্সট ফাইল, যেখানে আপনি টার্মিনালে যা যা টাইপ করতেন সেগুলোই লাইন ধরে সাজিয়ে রাখা হয়েছে। প্রথম লাইনের shebang বলে দেয় কোন interpreter দিয়ে ফাইলটা চালাতে হবে, আর chmod +x দিয়ে চালানোর অনুমতি দিতে হয়। পরীক্ষায় প্রায়ই আসে script চালানোর দুটো উপায় — ./script.sh (executable bit লাগবে) আর bash script.sh (executable bit লাগবে না)।

Real-life analogy. A bash script is like a paper recipe. Each line is a step ("preheat oven", "grep ERROR run.log"). Variables = quantities. Functions = mini sub-recipes. Loops = "repeat for each carrot".

Real-life HPC use. The classic backup-data.sh from the lecture: it takes [SOURCE] and [DEST] directories, parses a -v (verbose) option, validates inputs, copies (or syncs in the extended version), and reports success.

What if you misunderstand? You write if [ $a==$b ] (no spaces) → always true. Or you forget to quote a variable that contains spaces → the test breaks with cryptic errors. The shell's behaviour is fragile in many specific ways; mastering it earns the grade.

বাংলায়: Bash-এর syntax খুবই খুঁতখুঁতে — স্পেস এদিক-ওদিক হলেই অর্থ বদলে যায়। যেমন VAR=5 ঠিক, কিন্তু VAR = 5 লিখলে bash ভাবে VAR নামের একটা command চালাতে বলা হয়েছে। আবার [ $a==$b ] লিখলে test-টা সবসময় true হয়, কারণ পুরোটাকে একটাই non-empty string হিসেবে দেখা হয়। পরীক্ষায় এই ভুলগুলো খুঁজে বের করতে বলা প্রায় নিশ্চিত।


3. Hard English Made Easy

Hard Term Simple English বাংলা Example
Shebang #! line specifying interpreter কোন ইন্টারপ্রিটার চালানো হবে #!/bin/bash
Positional parameter Argument passed to script কমান্ডে দেওয়া আর্গুমেন্ট ${1}, ${2}
Special variable Predefined shell var পূর্বনির্ধারিত ভেরিয়েবল $#, $@, $?
Command substitution Use a command's output as text কমান্ডের আউটপুটকে টেক্সট হিসেবে $(date)
Arithmetic expansion Math inside (( )) শেলে গণনা (( a+1 ))
Conditional expression Test [ ] / [[ ]] শর্ত পরীক্ষা [ -f f ]
Exit status 0 success, ≠0 failure প্রস্থান কোড $?
Function Reusable block পুনঃব্যবহারযোগ্য ব্লক myfn(){…}
Local variable Lives in a function only শুধু ফাংশনের ভেতরে local x
Subshell Child shell পুত্র শেল ( cd dir; cmd )
Source / dot Run in current shell বর্তমান শেলে চালানো source f
Pipe Output → input chain আউটপুট পরের কমান্ডে যাওয়া a \| b
Stdin/Stdout/Stderr 0/1/2 streams তিনটি স্ট্যান্ডার্ড স্ট্রিম 2>&1
getopts Builtin for short options ছোট অপশন পার্সার while getopts
Trap Catch signals সিগন্যাল ধরা trap 'echo bye' EXIT
Quoting Single vs double quotes উদ্ধৃতি চিহ্ন ব্যবহার "$x" vs '$x'
Glob Filename pattern ফাইল প্যাটার্ন *.sh
Heredoc Inline multi-line input একাধিক লাইন ইনপুট cat <<EOF
Process ID Number of running process প্রসেস আইডি $$

4. Deep Theory Explanation

4.1 Script anatomy

#!/bin/bash                       # 1. shebang
# Description: …                  # 2. comments
set -e                            # 3. fail on error
SOURCE="${1:-./default}"          # 4. defaulted positional
[[ -d $SOURCE ]] || { echo "missing"; exit 1; }   # 5. guard

main() {                          # 6. function
    for f in "$SOURCE"/*; do
        process "$f"
    done
}

process() {
    echo "Doing $1"
}

main "$@"                         # 7. forward all args to main
exit 0

4.2 Variables and special variables

NAME="alice"          # assignment, no spaces around =
echo "$NAME"          # use → "alice"
echo '${NAME}'        # single quotes don't expand → ${NAME}
echo "${NAME}"        # double quotes do
echo "${NAME}File"    # the braces protect the boundary
Var Meaning
${1}…${9} Positional
${10+} requires braces
$0 Script name
$# Number of arguments
$@ All args (preserves quoting)
$* All args as a single string
$? Last exit status
$$ PID of script
$! PID of last background
$_ Last arg of previous command
$IFS Internal field separator

বাংলায়: Special variable গুলো হলো shell-এর নিজে থেকে রাখা তথ্য: $# মানে কয়টা argument এসেছে, $@ মানে সবগুলো argument আলাদা আলাদা ভাবে, $? মানে আগের command সফল হয়েছিল কি না (0 মানে সফল), আর $$ হলো script-এর নিজের process ID। পরীক্ষায় "Explain $#, $@, $?" প্রায় প্রতি বছরই আসে — এই চারটা মুখস্থ রাখুন।

4.3 Quoting rules

Form Behaviour
'…' Literal — no expansion
"…" Variable, command-substitution, arithmetic expand; globs do not
\$x Escape $
\$IFS" Reset IFS

Always quote variables that may contain spaces or be empty: if [ -z "${VAR}" ].

বাংলায়: Single quote মানে ভেতরের সবকিছু হুবহু literal — $NAME লেখা থাকলে $NAME-ই ছাপা হবে। Double quote-এর ভেতরে variable আর command substitution কাজ করে, কিন্তু শব্দ ভাগ (word splitting) হয় না — তাই স্পেসওয়ালা path নিরাপদ থাকে। নিয়ম একটাই: variable ব্যবহারের সময় সবসময় double quote দিন, যেমন "$VAR" — মান খালি বা স্পেসওয়ালা হলেও script ভাঙবে না।

4.4 Conditional expressions

Three test forms:

Form Type Notes
[ EXPR ] POSIX test needs spaces around [ and ]; quote vars
[[ EXPR ]] Bash safer; supports &&, ||, regex =~, glob ==
(( EXPR )) Arithmetic C-like math

File tests:

Test True when
-f f regular file exists
-d d directory exists
-e p path exists
-r f readable
-w f writable
-x f executable
-s f non-empty
-L f symlink
f1 -nt f2 newer than
f1 -ot f2 older

String tests:

Test True when
-z s empty
-n s non-empty
s1 = s2 equal
s1 != s2 unequal
s1 < s2 lex. less (in [[ ]])
s =~ regex regex match (in [[ ]])

Numeric tests (in [ ]):

-eq -ne -lt -le -gt -ge. In (( )) use C operators == != < <= > >=.

বাংলায়: তিন রকম test-এর কাজ আলাদা: [ ] হলো পুরনো POSIX test, [[ ]] হলো bash-এর নিরাপদ সংস্করণ (regex আর && সাপোর্ট করে), আর (( )) শুধু গণিতের জন্য। সবচেয়ে বড় ফাঁদ: সংখ্যা তুলনায় -eq, -lt ব্যবহার করতে হবে; = বা < দিলে string হিসেবে তুলনা হয়, ফলে "10" কে "9"-এর চেয়ে ছোট ধরা হয়। পরীক্ষায় এই তিন ফর্মের পার্থক্য জিজ্ঞেস করা প্রায় নিশ্চিত।

4.5 if / elif / else

if [ ${1} -lt 2006 ]; then
    echo "above 18"
elif [ ${1} -eq 2006 ]; then
    echo "exactly 18"
else
    echo "below 18"
fi

(from example06.sh)

4.6 case

case "$choice" in
    1) echo "Service started" ;;
    2) echo "Service stopped" ;;
    3) ls ;;
    *) echo "invalid" ;;
esac

(from example08.sh)

Globs allowed: [Yy][Ee][Ss]), alternation start|begin).

4.7 Loops

while:

count=1
while [ ${count} -le 5 ]; do
  echo "Count = ${count}"
  ((count++))
done

for (C-style):

for ((i=0; i<5; i++)); do
  echo "${i}: Hello World"
done

for (list):

ARRAY=(23 12 45 67)
for i in "${ARRAY[@]}"; do
  echo "$i"
done

until:

count=1
until [ ${count} -ge 5 ]; do
  echo "Count = ${count}"
  ((count++))
done

(from example10–13.sh)

break / continue to skip iterations.

বাংলায়: কোন loop কখন ব্যবহার করবেন সেটাই আসল প্রশ্ন: নির্দিষ্ট সংখ্যকবার চালাতে for, শর্ত সত্য থাকা পর্যন্ত while, আর শর্ত সত্য হওয়া মাত্র থামতে until। until আসলে while-এর উল্টো — একই কাজ শর্তে ! বসিয়ে while দিয়েও করা যায়। break মানে loop থেকে পুরোপুরি বেরিয়ে যাওয়া, আর continue মানে শুধু এই iteration বাদ দিয়ে পরেরটায় যাওয়া — দুটো গুলিয়ে ফেললে output prediction প্রশ্নে নম্বর যায়।

4.8 Arrays

ARRAY=(23 45 56 76)
echo ${ARRAY[0]}        # first
echo ${ARRAY[@]}        # all
echo ${#ARRAY[@]}       # length
ARRAY+=(99)             # append
unset 'ARRAY[1]'        # remove element

(example09.sh)

Associative array (Bash 4+): declare -A AA; AA[name]="alice".

বাংলায়: Array-র চারটা রূপ মুখস্থ রাখুন: ARRAY[0] দিয়ে প্রথম element, "${ARRAY[@]}" দিয়ে সবগুলো আলাদা আলাদা শব্দ হিসেবে, ${#ARRAY[@]} দিয়ে দৈর্ঘ্য, আর += দিয়ে শেষে যোগ। Loop-এ সবসময় "${ARRAY[@]}" (double quote সহ) লিখুন — নাহলে স্পেসওয়ালা element ভেঙে টুকরো হয়ে যাবে। পরীক্ষায় প্রায়ই array-র output predict করতে দেয়।

4.9 Reading user input

read -p "Enter a number: " NUMBER
read -s PASSWORD            # silent
read -t 10 ANS              # timeout
read -a ARR <<< "a b c"     # into array

4.10 Command substitution & arithmetic

LINE=$(sed -n '2p' ./test01/file1)
NOW=$(date +%F)
SUM=$(( 3 + 4 ))
((count++))
result=$((a*b))

(from example17.sh)

4.11 Functions

is_even() {
  if (($1 % 2 == 0)); then return 1; else return 0; fi
}
is_even 4
echo $?    # 1

return only carries an exit status (0–255). To "return a value", echo and capture: r=$(myfn 4).

local var to keep variables function-scoped (highly recommended).

বাংলায়: Bash function-এর return কিন্তু C-এর মতো মান ফেরত দেয় না — শুধু 0 থেকে 255-এর একটা exit status দেয়, যেটা $? দিয়ে পড়তে হয়। আসল মান ফেরত দিতে হলে function-এর ভেতরে echo করে বাইরে command substitution দিয়ে ধরতে হয়, যেমন r=$(myfn 4)। আর local না লিখলে function-এর ভেতরের variable পুরো script-এ ছড়িয়ে পড়ে — পরীক্ষায় এই দুটো পার্থক্যই বারবার আসে।

4.12 getopts (single-letter options)

OPTSTRING=":a:b:v"
while getopts "$OPTSTRING" opt; do
  case "$opt" in
    a) echo "-a $OPTARG" ;;
    b) echo "-b $OPTARG" ;;
    v) VERBOSE=1 ;;
    :) echo "-$OPTARG needs an argument"; exit 1 ;;
    ?) echo "invalid -$OPTARG"; exit 1 ;;
  esac
done
shift $((OPTIND-1))

":" at start of OPTSTRING enables silent error handling. Letter followed by : means it requires an argument.

(from example14.shexample16.sh)

How the arguments flow through OPTIND/OPTARG for ./script.sh -v -s out.dat data1 with OPTSTRING=":vs:":

Command line:   ./script.sh   -v    -s    out.dat    data1
                               │     │       │          │
                              $1    $2      $3         $4        OPTIND starts at 1
                               │     │       │
        ┌──────────────────────┘     │       └────────────┐
        ▼                            ▼                    ▼
┌──────────────────────────────────────────────────────────────────┐
│ while getopts ":vs:" opt; do ... done                            │
│                                                                  │
│ pass 1: reads $1 = "-v"  ─► opt='v', OPTARG empty,  OPTIND ─► 2  │
│ pass 2: reads $2 = "-s"  ─► opt='s', needs an argument:          │
│         consumes $3 = "out.dat" ─► OPTARG="out.dat", OPTIND ─► 4 │
│ pass 3: $4 = "data1" has no leading "-" ─► getopts returns false │
│         loop ends, OPTIND stays 4                                │
└──────────────────────────────────────────────────────────────────┘
shift $((OPTIND-1))      # shift 3  ─► drops  -v  -s  out.dat
Now $1 = "data1"         # the first non-option argument

বাংলায়: getopts প্রতিবার একটা করে option পড়ে: option-এর অক্ষরটা opt-এ যায়, আর সেই option-এর argument (যদি লাগে) OPTARG-এ যায়। OPTSTRING-এ অক্ষরের পরে : মানে ওই option argument নেয়, আর শুরুতে : মানে error নিজে handle করবেন। Loop শেষে shift $((OPTIND-1)) দিতেই হবে — না দিলে $1 তখনও -v-ই থেকে যায়, আসল argument নয়। পরীক্ষায় এই shift ভুলে যাওয়াটাই সবচেয়ে common ভুল।

4.13 Backup script (backup-data.sh) – walkthrough

#!/bin/bash
OPTSTRING=":v"
VERBOSE=0

USAGE() {
    echo "Error : ${1}"
    echo "Usage: ./backup-data [OPTIONS] [SOURCE] [DEST]"
    echo "-v   Verbose flag"
    return 0
}

while getopts ${OPTSTRING} opt; do
    case ${opt} in
        v) VERBOSE=1 ;;
        ?) USAGE "Invalid option"; exit 1 ;;
    esac
done

shift $(( OPTIND - 1 ))
SOURCE=${1}
DEST=${2}

if [ -d "${SOURCE}" ] && [ -d "${DEST}" ]; then
    if [ ${VERBOSE} -eq 1 ]; then
        echo "Taking backup ..."
        cp -vr ${SOURCE} ${DEST}
        if [ ${?} -eq 1 ]; then
            echo "Something went wrong"; exit 1
        else
            echo "Done"; exit 0
        fi
    else
        cp -r ${SOURCE} ${DEST}
        [ ${?} -ne 0 ] && exit 1 || exit 0
    fi
else
    echo "Invalid directories"; exit 1
fi

Steps:

  1. Header & vars.
  2. USAGE function helps print the help line.
  3. getopts parses -v.
  4. shift consumes parsed options so ${1} is now SOURCE.
  5. Validate both dirs exist.
  6. Branch on verbose to cp -vr or cp -r.
  7. Check $? exit status of cp.
  8. Return 0 on success.

বাংলায়: backup-data.sh হলো এই chapter-এর সবকিছুর সমন্বয়: getopts দিয়ে -v পড়া, shift দিয়ে option সরানো, -d test দিয়ে directory আছে কি না দেখা, তারপর cp চালিয়ে $? দিয়ে সফলতা যাচাই। পরীক্ষায় হয় পুরো script-টা line ধরে ব্যাখ্যা করতে বলে, নয়তো -s (rsync) option যোগ করতে বলে — দুটোই আগে থেকে হাতে লিখে প্র্যাকটিস করুন।

4.14 Lecture diagram explanations

  • if-then-elif-else-fi flowchart: condition diamond ↦ branch arrow ↦ block; describe in answers as "C-like control flow".
  • getopts loop: "while there are options, look up letter in case, set vars; after the loop, shift away the consumed options".
  • Stream redirection diagram: stdin/stdout/stderr arrows around the script box — same as Ch. 7.

Full execution flowchart for a script combining if/elif/else and a loop:

#!/bin/bash
N=${1}
if [ "${N}" -lt 0 ]; then
    echo "negative"; exit 1
elif [ "${N}" -eq 0 ]; then
    echo "zero"; exit 0
else
    i=1
    while [ ${i} -le ${N} ]; do
        echo "i = ${i}"
        (( i++ ))
    done
fi
echo "done"
            ┌─────────────────────┐
            │ START: N = $1       │
            └──────────┬──────────┘
            ┌─────────────────────┐  yes   ┌──────────────────┐
            │      N < 0 ?        ├───────►│ echo "negative"  │──► exit 1
            └──────────┬──────────┘        └──────────────────┘
                       │ no
            ┌─────────────────────┐  yes   ┌──────────────────┐
            │      N == 0 ?       ├───────►│ echo "zero"      │──► exit 0
            └──────────┬──────────┘        └──────────────────┘
                       │ no  (else branch)
            ┌─────────────────────┐
            │       i = 1         │
            └──────────┬──────────┘
            ┌─────────────────────┐   no    ┌──────────────┐
      ┌────►│     i <= N ?        ├────────►│ echo "done"  │──► exit 0
      │     └──────────┬──────────┘         └──────────────┘
      │                │ yes
      │                ▼
      │     ┌─────────────────────┐
      │     │ echo "i = $i"       │
      │     │ (( i++ ))           │
      │     └──────────┬──────────┘
      └────────────────┘   back to the loop test

Subshell vs source — where do your variables go?

./script.sh   (or: bash script.sh)        source script.sh   (or: . script.sh)
──────────────────────────────────        ──────────────────────────────────
┌────────────────────────────┐            ┌────────────────────────────┐
│ parent shell   (PID 4000)  │            │ current shell  (PID 4000)  │
│ $X is unset                │            │                            │
│        │ fork + exec       │            │ script lines run IN THIS   │
│        ▼                   │            │ SAME process, line by line │
│  ┌───────────────────────┐ │            │                            │
│  │ child shell (PID 4123)│ │            │ X=5     ─► survives        │
│  │ X=5     set HERE      │ │            │ cd /tmp ─► pwd CHANGES     │
│  │ cd /tmp only here     │ │            │                            │
│  │ exit ─► child dies,   │ │            │ afterwards:                │
│  │ X dies with it        │ │            │   echo $X  ─► 5            │
│  └───────────────────────┘ │            │   pwd      ─► /tmp         │
│        │                   │            └────────────────────────────┘
│        ▼                   │
│ afterwards:                │            Use source for: module load,
│   echo $X  ─► (empty)      │            activating environments,
│   pwd      ─► unchanged    │            reading config files.
└────────────────────────────┘

4.15 Exit-status algebra — formal treatment

Every command finishes with an exit status \(e \in \{0, 1, \dots, 255\}\). Bash defines truth inversely to C: a command counts as "true" iff \(e = 0\).

The operators && and || short-circuit:

  • c1 && c2 — run c2 only if \(e(c_1) = 0\).
  • c1 || c2 — run c2 only if \(e(c_1) \neq 0\).

The status $? of the whole chain is the status of the last command that actually ran.

Truth table (using true with \(e = 0\) and false with \(e = 1\)):

\(e(c_1)\) \(e(c_2)\) c1 && c2 runs c2? $? after c1 \|\| c2 runs c2? $? after
0 0 yes 0 no 0
0 1 yes 1 no 0
1 0 no 1 yes 0
1 1 no 1 yes 1

Worked example 1 — trace $? after every line:

cd /nonexistent && echo "entered"   # cd fails: e=1 -> echo skipped, chain keeps 1
echo $?                             # prints 1
cd /tmp && echo "entered"           # cd ok: e=0 -> echo runs, its e=0 wins
echo $?                             # prints 0

Worked example 2 — the A && B || C trap. The chain A && B || C is not an if/else:

true && false || echo "C runs"      # prints "C runs"!

Trace: true gives \(e=0\)&& runs false → chain status is now 1 → || sees 1 → runs echo. So C runs when A fails or when B fails. A genuine if/else must be written if A; then B; else C; fi.

Worked example 3 — guard pattern from real job scripts:

grep -q ERROR run.log && echo "errors found" || echo "clean"

If run.log contains ERROR: grep -q returns 0 → first echo runs → final $? is 0. If not: grep returns 1 → first echo skipped (chain carries 1) → second echo runs → final $? is 0.

বাংলায়: Bash-এ সত্য-মিথ্যার হিসাব C-এর উল্টো: exit status 0 মানে সফল/সত্য, আর শূন্য ছাড়া যেকোনো মান মানে ব্যর্থ। তাই && মানে "আগেরটা সফল হলে চালাও", || মানে "আগেরটা ব্যর্থ হলে চালাও"। আর A && B || C-কে if/else ভাবা পরীক্ষার সবচেয়ে বড় ফাঁদ — B ব্যর্থ হলেও C চলে যায়!

4.16 Arithmetic & comparison — the formal rules

Bash integer arithmetic lives inside $(( )) / (( )). Operators follow C precedence: ** (highest), then * / %, then + -, then comparisons, then && ||.

Worked examples (integer-only!):

echo $(( 17/5 ))      # 3   (integer division truncates toward zero)
echo $(( 17%5 ))      # 2   (remainder: 17 = 3*5 + 2)
echo $(( 2**10 ))     # 1024
echo $(( 3+4*2 ))     # 11  (precedence: * before +)
echo $(( (3+4)*2 ))   # 14
echo $(( 7/2*2 ))     # 6   (left-to-right: 7/2=3, 3*2=6 — NOT 7)
\[17 \div 5 = 3 \text{ rem } 2 \quad\Rightarrow\quad \left\lfloor \tfrac{17}{5} \right\rfloor = 3,\qquad 17 \bmod 5 = 2\]

There are no floats in bash arithmetic: echo $((1/3)) is 0. For real numbers delegate to bc or awk: awk 'BEGIN{printf "%.4f\n", 1/3}'.

The string-vs-numeric comparison trap.

Intent Numeric (correct) String (trap!)
equal [ "$a" -eq "$b" ] [ "$a" = "$b" ]
less than [ "$a" -lt "$b" ] [[ "$a" < "$b" ]] (lexicographic!)

Worked trap: [[ 10 < 9 ]] is true — string comparison goes character by character, and "1" < "9". Numerically [ 10 -lt 9 ] is false, as expected. Same disease as sort without -n.

[[ 10 < 9 ]] && echo "string: 10 < 9 is TRUE"     # prints!
[ 10 -lt 9 ] || echo "numeric: 10 < 9 is FALSE"   # prints

বাংলায়: Bash-এ < দুই অর্থে চলে: [[ ]]-এর ভেতরে string তুলনা (অভিধান-ক্রম), আর -lt সংখ্যা তুলনা। "10 < 9 সত্য" — এই অদ্ভুত ফল দেখলেই বুঝবে string তুলনা হচ্ছে। সংখ্যার জন্য সবসময় -eq -lt -gt পরিবার, নাহলে (( a < b )) — arithmetic context-এ < সংখ্যাই বোঝে।

4.17 Control-flow & environment diagrams

(a) if/elif/else + loop flow of a typical job script:

            ┌─────────────┐
            │ parse args  │
            └──────┬──────┘
          ┌─────────────────┐    no   ┌──────────────┐
          │ $# >= 2 ?       ├────────►│ usage; exit 1│
          └────────┬────────┘         └──────────────┘
                   │ yes
          ┌─────────────────┐    no
          │ [ -d "$SRC" ] ? ├────────► error; exit 1
          └────────┬────────┘
                   │ yes
          ┌──────────────────────────┐
          │ for case in case_*       │◄────────┐
          │   run solver > log 2>&1  │         │ next case
          │   check $? → log status  ├─────────┘
          └────────────┬─────────────┘
                       ▼ after loop
                 summary; exit 0

(b) ./script.sh (subshell) vs source script.sh (current shell):

   ./script.sh  (or bash script.sh)          source script.sh  (or . script.sh)
   ┌─────────────────────────┐               ┌─────────────────────────┐
   │  your shell (PID 4000)  │               │  your shell (PID 4000)  │
   │      │ fork+exec        │               │  runs commands HERE     │
   │      ▼                  │               │  X=5 persists  [yes]    │
   │  child bash (PID 4123)  │               │  cd /tmp persists [yes] │
   │  X=5 set here…          │               └─────────────────────────┘
   │  child exits → X gone   │
   └─────────────────────────┘
   Rule: scripts that should MODIFY your session (module load, conda
   activate, ~/.bashrc) must be sourced, not executed.

(c) getopts parsing flow for ./backup.sh -v -s src dst:

 argv:  -v   -s   src   dst
        │    │     │     │
 ┌──────▼────▼─────┼─────┼───────────────────────────┐
 │ while getopts ":vs" opt                           │
 │   pass 1: opt=v  (OPTIND→2)                       │
 │   pass 2: opt=s  (OPTIND→3)                       │
 │   pass 3: not an option → loop ends               │
 └───────────────────┬───────────────────────────────┘
       shift $((OPTIND-1))   # drops -v -s
       now $1=src  $2=dst    # positional args clean

বাংলায়: getopts-এর পুরো নাটক তিন অঙ্কে: (১) লুপ অপশনগুলো এক-একটা করে খায় আর OPTIND এগোয়, (২) : দিয়ে শুরু OPTSTRING মানে error নিজে সামলাবে, (৩) লুপ শেষে shift $((OPTIND-1)) দিলে বাকি থাকে শুধু আসল argument। এই shift ভুলে গেলে $1 তখনও -v — পরীক্ষার প্রিয় bug।


5. Command / Syntax / Code Breakdown

#!/bin/bash

Always first line. Without it the script may run with sh (more limited).

set -euo pipefail

-e exit on error, -u undefined var = error, -o pipefail failure inside a pipe propagates. Strongly recommended in HPC scripts.

[ -d "${SOURCE}" ]

File test. Quote always to handle paths with spaces.

[[ "${NUM}" =~ ^[0-9]+$ ]]

Bash regex test (only inside [[ ]]).

(( a+b ))

Arithmetic. No $ needed for vars. Exit status 0 if result non-zero, 1 if zero.

$(cmd)

Modern command substitution; nestable.

Function definition

greet() {
    local name="${1}"
    echo "Hi $name"
}

Always declare locals and quote params.

read -p "Q: " ANS

Prompt and read one line.

printf "%-10s %8.3f\n" "$name" "$num"

C-style formatted print. Better than echo for tables.

trap

trap 'rm -f "$TMPFILE"' EXIT

Run cleanup on any exit.


6. Mandatory Practical Examples

Example 6.1 — Hello world (example02.sh)

#!/bin/bash
echo "Hello World"
exit 0

Run: bash example02.shHello World. Demonstrates shebang, echo, exit.

Example 6.2 — Variables and arguments (example03.sh)

#!/bin/bash
LOCALVAR="Hello World"
echo "LOCALVAR= ${LOCALVAR}"
echo "FIRST ARGUMENT = ${1}"
echo "SECOND ARGUMENT = ${2}"
exit 0

Run: bash example03.sh foo bar

LOCALVAR= Hello World
FIRST ARGUMENT = foo
SECOND ARGUMENT = bar

Example 6.3 — Quoting (example04.sh)

#!/bin/bash
LOCAL="Hello"
echo "${LOCAL}World"       # → HelloWorld
echo "$LOCALWorld"         # → (empty) - tries to expand $LOCALWorld
echo "$LOCAL World"        # → Hello World

Lesson: use ${VAR} to delimit the variable name.

Example 6.4 — File test (example05.sh)

#!/bin/bash
if [ -f ${1} ]; then
    echo "File ${1} exists !"
else
    echo "File doesn't exist !"
fi
exit 0

bash example05.sh /etc/passwd → "File /etc/passwd exists !".

Example 6.5 — if-elif-else (example06.sh)

#!/bin/bash
if [ ${1} -lt 2006 ]; then
    echo "The person is above 18 years old"
elif [ ${1} -eq 2006 ]; then
    echo "The person 18 years old"
else
    echo "The person is below 18 years old"
fi

Run: bash example06.sh 2000 → above 18.

Example 6.6 — Bash regex (example07.sh)

#!/bin/bash
PATTERN='^#[[:digit:]]{4}$'
if [[ ${1} =~ ${PATTERN} ]]; then
    echo "matches"
else
    echo "no match"
fi

bash example07.sh "#1234" → matches.

Example 6.7 — case (example08.sh)

See 4.6 above. Run interactively: enter 1/2/3.

Example 6.8 — Array (example09.sh)

See 4.8.

Example 6.9 — while/for/until (example10–13.sh)

See 4.7.

Example 6.10 — getopts (example14.sh)

#!/bin/bash
OPTSTRING=":ab"
while getopts ${OPTSTRING} opt; do
    case ${opt} in
        a) echo "Option -a was triggered." ;;
        b) echo "Option -b was triggered." ;;
        ?) echo "Invalid option: -${OPTARG}."; exit 1 ;;
    esac
done

Run: bash example14.sh -a -b → both messages. bash example14.sh -c → Invalid.

Example 6.11 — getopts with arguments (example15.sh)

OPTSTRING=":a:b:"
while getopts ${OPTSTRING} opt; do
    case ${opt} in
        a) echo "-a $OPTARG" ;;
        b) echo "-b $OPTARG" ;;
        :) echo "Option -${OPTARG} requires an argument."; exit 1 ;;
        ?) echo "Invalid option."; exit 1 ;;
    esac
done

bash example15.sh -a hello -b world-a hello / -b world.

Example 6.12 — Verbose adder (example16.sh)

Adds -v flag, two args, prints sum.

Example 6.13 — Command substitution (example17.sh)

Uses LINE=$(sed -n '2p' file) to grab line 2.

Example 6.14 — Function (example18.sh)

is_even returns 1 if even, 0 if odd. Captured via RES=$?.

Example 6.15 — Prime number (E06 Task 1 solution)

#!/bin/bash
PRIME=1
read -p "Enter a number : " NUMBER
if [[ "${NUMBER}" =~ ^[0-9]+$ ]]; then
    if [ "${NUMBER}" -lt 2 ]; then
        PRIME=0
    else
        for (( i=2 ; i<NUMBER ; i++ )); do
            if (( NUMBER % i == 0 )); then PRIME=0; break; fi
        done
    fi
    if [ "${PRIME}" -ne 0 ]; then
        echo "${NUMBER} is a Prime"
    else
        echo "${NUMBER} is NOT a Prime"
    fi
else
    echo "Must be positive integer"; exit 1
fi

(The lecture's regex ^[0-9]*.$ is buggy — it allows trailing punctuation. The corrected regex is ^[0-9]+$. Mention this bug fix in the exam answer.)

বাংলায়: এই prime-checker-টা পরীক্ষার সম্পূর্ণ প্যাকেজ: input validation (regex), special case (<2), লুপে ভাগ-পরীক্ষা আর early break। lecture-এর regex-এর bug-টা ধরিয়ে দিলে extra নম্বর।

Example 6.16 — Extended backup script (E06 Task 2 solution)

Adds -s for sync (rsync) on top of -v:

OPTSTRING=":vs"
VERBOSE=0; SYNC=0
while getopts ${OPTSTRING} opt; do
    case ${opt} in
        v) VERBOSE=1 ;;
        s) SYNC=1 ;;
        ?) USAGE "Invalid"; exit 1 ;;
    esac
done
shift $((OPTIND-1))
SOURCE=${1}; DEST=${2}

if [ -d "$SOURCE" ] && [ -d "$DEST" ]; then
    if [ $SYNC -eq 1 ]; then
        if [ $VERBOSE -eq 1 ]; then rsync -aPv "$SOURCE" "$DEST"
        else                        rsync -aP "$SOURCE" "$DEST"; fi
    else
        if [ $VERBOSE -eq 1 ]; then cp -vr "$SOURCE" "$DEST"
        else                        cp -r "$SOURCE" "$DEST"; fi
    fi
else
    USAGE "Invalid directories"; exit 1
fi

Real-Life HPC/CFD Meaning. Identical pattern in production cluster scripts: copy results of long simulations from $WORK to $HOME or to a project share, with optional sync mode.


7. Real HPC/CFD Workflow

#!/bin/bash
set -euo pipefail
RES=()
for case in case_*; do
    cd "$case"
    echo "Running $case"
    nohup ./solver > run.log 2>&1 &
    RES+=($!)
    cd ..
done
wait "${RES[@]}"
echo "All cases done"

# Aggregate residuals
for case in case_*; do
    awk '/^Time =/ {print $3, $7}' "$case/run.log" > "$case/residuals.dat"
done

# Compress
tar czf $(date +%F)-results.tgz case_*/residuals.dat

বাংলায়: এটা একটা ছোট job-orchestrator: প্রতিটা case background-এ (&) চালিয়ে PID গুলো array-তে রাখা হয় ($!), তারপর wait সবগুলোর শেষ হওয়া পর্যন্ত দাঁড়ায়। set -euo pipefail থাকায় কোনো ধাপ ব্যর্থ হলেই script থেমে যায় — নীরবে ভুল ফল তৈরি হয় না।


8. Exercises and Solutions

Both Exercise 6 tasks are fully solved (6.15, 6.16).

Marking scheme — Task 1 (10 marks):

  • 1: shebang.
  • 1: read -p user input.
  • 1: regex check positive integer.
  • 1: special-case <2.
  • 2: divisibility loop.
  • 1: break early.
  • 1: result message.
  • 2: clean exit codes.

Marking scheme — Task 2 (10 marks):

  • 1: extend OPTSTRING.
  • 1: parse -s.
  • 1: directory validation.
  • 2: branch sync vs copy.
  • 2: branch verbose.
  • 1: rsync -aP flag.
  • 1: error handling on $?.
  • 1: USAGE refresh.

Common mistakes.

  • Spaces around = in assignment: VAR = 5 is wrong → VAR=5.
  • Forgetting ; before then: if [ x ]; then.
  • $1 mixed with literal text: echo $1file expands $1file; use ${1}file.
  • cp -vr paths with spaces unquoted.
  • if [ $a==$b ] (no spaces) → always true.
  • Using the function keyword inconsistently; prefer POSIX fn() { … }.

Harder version of Task 1. Validate also negative numbers, accept a comma-separated list and report each:

read -p "Enter numbers comma-separated: " LINE
IFS=, read -ra NUMS <<< "$LINE"
for n in "${NUMS[@]}"; do
    if ! [[ "$n" =~ ^[0-9]+$ ]]; then echo "skip $n"; continue; fi
    PRIME=1
    (( n<2 )) && PRIME=0
    for ((i=2; i*i<=n; i++)); do (( n%i==0 )) && { PRIME=0; break; }; done
    (( PRIME )) && echo "$n: PRIME" || echo "$n: not"
done

9. Written Exam Focus

9.1 Short Answers

Q. Difference between $@ and $*. A. Both expand to all positional parameters. "$@" produces individual quoted args; "$*" produces one single string joined by IFS. Use "$@" when iterating to preserve word boundaries.

Q. What is $?? A. Exit status of the previous command (0 success, non-zero failure).

Q. Why use [[ ]] over [ ]? A. [[ ]] is a Bash builtin: handles empty/unquoted variables safely, supports &&, ||, regex =~, glob match.

Q. What does shift do? A. Drops $1 and shifts the rest down (so $2 becomes $1). With shift N, drops N. Used after getopts with OPTIND-1.

Q. Difference between > and | in scripts. A. > redirects stdout to a file; | pipes stdout to another command's stdin.

9.2 Medium Answers

Q. (8 marks) Explain getopts with an example.

A. getopts "$OPTSTRING" var reads the next short option into var, advancing OPTIND. Inside OPTSTRING, a letter alone means flag; letter+: means it expects an argument (placed in OPTARG). A leading : enables silent error mode. After the loop, shift $((OPTIND-1)) consumes the parsed options. Example (example15.sh): OPTSTRING=":a:b:" parses -a value -b value.

Q. (5 marks) Compare for, while, and until loops.

A. for x in list iterates over a finite list (or C-style for ((;;))). while [ cond ] repeats while cond is true. until [ cond ] repeats until cond becomes true (negated while). Choose for for known iteration count, while for condition-driven repetition, until to loop until something succeeds.

9.3 Long Answer (12 marks)

Q. Walk through backup-data.sh and explain each step. (Use 4.13.)

9.4 Output Prediction

example03.sh foo bar baz

LOCALVAR= Hello World
FIRST ARGUMENT = foo
SECOND ARGUMENT = bar

example07.sh "#1234" → "matches".

example09.sh → 23 / 45 / 56 / "23 45 56 76" / 4.

example13.sh → Count = 1..4.

A=1; B=2
echo "$((A+B))"             # 3
echo "$((A*10+B))"          # 12

9.5 Comparison

while vs until — opposite condition. for (list) vs for ((;;)) — list-driven vs count-driven.

Script Binary
Execution Interpreter line by line CPU-native
Edit-run cycle Fast Need recompile
Speed Slow for math Fast
Use glue, automation solvers, kernels
Form Strengths
[ ] POSIX, portable
[[ ]] Bash, regex, safer
(( )) Arithmetic, no $, C-like

9.6 Templates

Script template:

#!/bin/bash
set -euo pipefail
usage() { echo "usage: $0 SRC DST" >&2; }
[[ $# -lt 2 ]] && { usage; exit 1; }
SOURCE="${1}"; DEST="${2}"
process() { :; }
process

getopts template: see 4.12. Loop template: for x in "${ARR[@]}"; do …; done.

9.7 Marking Scheme — "Write a script that backs up a folder" (10 marks)

  • 1 shebang.
  • 1 args check.
  • 1 dir test.
  • 2 cp/rsync execution.
  • 2 exit code handling.
  • 1 verbose option (getopts).
  • 1 usage message.
  • 1 quoting / safety.

10. Very Hard Questions

Beginner

  1. How to make a script executable? → chmod +x s.sh.
  2. Print first arg. → echo $1.
  3. Number of args. → $#.
  4. Exit success. → exit 0.
  5. Print array length. → ${#ARRAY[@]}.

Intermediate

  1. Test if file exists. → [ -f f ].
  2. Loop over *.cpp. → for f in *.cpp; do ...; done.
  3. Read user input. → read -p "?: " ans.
  4. Run cmd in subshell. → ( cd dir; ls ).
  5. Capture command output. → x=$(cmd).

Hard

  1. Why does cd dir; cmd differ from (cd dir; cmd)? → Subshell scope; the parenthesised version doesn't change the current shell's pwd.
  2. Rename all .txt to .bak in current dir. → for f in *.txt; do mv -- "$f" "${f%.txt}.bak"; done.
  3. getopts with required args & combined flags. → see 4.12.
  4. Predict the exit status of (( 0 )). → 1 (zero is "false" in arithmetic context).
  5. How to source vs run? → . file.sh or source file.sh runs in the current shell.

Very Hard

  1. Why is for f in $(ls) worse than for f in *? → Word splitting breaks filenames with spaces; globs are safe.
  2. Explain set -e pitfalls in pipes. → A failing command in the middle of a pipe doesn't trigger -e unless pipefail is set.
  3. Implement memoization in bash. → Use associative arrays (declare -A cache).

Deep Integration

  1. Combine awk + bash to re-run only failed cases. → awk '/FAIL/{print $1}' results | xargs -n1 ./rerun.sh.
  2. Use trap to clean tmp files on Ctrl-C. → trap 'rm -f "$tmp"' EXIT INT TERM.

Coding/Command

  1. Script that prints the total size of a directory in bytes. → du -sb "$1" | cut -f1.
  2. Loop over 1..10 printing squares. → for i in {1..10}; do echo $((i*i)); done.

Debugging

  1. if [ $x = "yes" ] fails when x is empty. → Use [ "$x" = "yes" ] or [[ ]].
  2. for f in *.cpp runs with literal *.cpp when no matches. → shopt -s nullglob or guard with [ -e "$f" ] || continue.

Long Written

  1. (250 words) Walk through the extended backup-data.sh from 6.16 explaining how getopts, case, dir tests, and rsync cooperate.

11. Debugging and Mistake Analysis

Mistake Why wrong Correct Explanation
VAR = 5 Spaces → runs VAR as command VAR=5 Bash assignment
[ $a==$b ] Always true (single string) [ "$a" = "$b" ] spaces & =
[ -f $f ] with empty $f "too many arguments" [ -f "${f}" ] quote
function fn() { … } non-portable mix fn() { … } POSIX form
cp $1 $2 with spaces breaks into extra args cp "$1" "$2" quote
return 100 for a value return sets exit code only echo & capture $( ) scope
No local in functions leaks vars to caller local x hygiene
set -e after the error doesn't undo put at top order
Forgot chmod +x "Permission denied" chmod +x run-bit
Using $1 right after getopts still sees -v shift $((OPTIND-1)) options

বাংলায়: এই টেবিল থেকে পরীক্ষায় সবচেয়ে বেশি আসে প্রথম তিনটা: assignment-এ স্পেস, ==-এর দুপাশে স্পেস না দেওয়া, আর খালি variable unquoted রাখা। তিনটারই ওষুধ এক: সবসময় "${VAR}" লেখো আর [ ]-এর ভেতরে প্রতিটা token-এর মাঝে স্পেস দাও।


12. Mini Project for Mastery

Goal: "Sweep" script that runs a CFD case for many Reynolds numbers in parallel.

#!/bin/bash
set -euo pipefail
TEMPLATE="${1:-case_template}"
RE_LIST=(500 1000 2000 5000 10000)
SLOTS=4
i=0
for RE in "${RE_LIST[@]}"; do
    DIR="case_Re${RE}"
    rm -rf "$DIR"
    cp -r "$TEMPLATE" "$DIR"
    sed -i -E "s/^(Re *= *).*/\1${RE}/" "$DIR/in.dat"
    (
      cd "$DIR"
      ./solver > run.log 2>&1
    ) &
    ((i++))
    if (( i % SLOTS == 0 )); then wait; fi
done
wait
echo "All cases done."

Connection to exam: positional arg with default ${1:-…}, arrays, loops, sed, redirection, parallel & + wait, arithmetic — all of Chapter 8 in one script.


13. Final Chapter Cheat Sheet

Item Memorise
Shebang #!/bin/bash
Strict mode set -euo pipefail
Args $1 $2 $# $@ $* $? $$ $!
Quote always "${VAR}"
if if [ ... ]; then ... fi
elif/else elif, else
case case x in pat) ... ;; esac
for for i in {1..10}; do ...; done
while while [ cond ]; do ...; done
until until [ cond ]; do ...; done
arr A=(a b c); "${A[@]}"; ${#A[@]}
function fn(){ local x=$1; ...; }
read read -p "?: " v
arith (( x = y + 1 )); int-only; ** % precedence
exit-status algebra && runs on 0, \|\| runs on non-0; A && B \|\| C ≠ if/else
substitution x=$(cmd)
getopts while getopts ":a:b:" o; do ...; done; shift $((OPTIND-1))
file tests -f -d -e -r -w -x -s -L
string tests -z -n = != =~
numeric tests -eq -ne -lt -le -gt -ge
string-vs-num trap [[ 10 < 9 ]] true (lexicographic); use -lt
trap trap 'cleanup' EXIT
Trap [ $a==$b ] always true
Top phrase "Bash script = shell commands + control flow + variables + functions; saved with shebang and executable bit."

14. Mock Exam — Four Levels

Level 1 — Basic (definitions & syntax)

Q1. Write the first line every bash script should have, and the command making run.sh executable.

Solution: #!/bin/bash and chmod +x run.sh.

Q2. What do $#, $0, and $$ expand to?

Solution: $# number of arguments, $0 script name, $$ PID of the current shell.

Q3. Write a for loop printing 1 to 5.

Solution: for i in {1..5}; do echo $i; done

Q4. How do you read a value into variable name with the prompt "Name: "?

Solution: read -p "Name: " name

Q5. What does exit 2 do inside a script?

Solution: Terminates the script immediately with exit status 2 (a failure code visible to the caller via $?).

Level 2 — Intuitive (predict the output / explain why)

Q1. Predict the output: x=5; ( x=99 ); echo $x

Solution: 5( ) runs in a subshell; the assignment dies with it.

Q2. Predict: [ -z "$UNSET_VAR" ] && echo empty || echo full

Solution: empty-z is true for a zero-length string; an unset variable expands to empty (quoted).

Q3. Why does if [ $n -gt 10 ] crash with "unary operator expected" when n is empty, but [[ $n -gt 10 ]] doesn't?

Solution: [ ] performs word-splitting first: empty $n vanishes, leaving [ -gt 10 ] — malformed. [[ ]] is parsed as syntax, not words, so the empty operand is handled.

Q4. Predict: echo $(( 10/4 )) $(( 10%4 )) $(( 10**2 ))

Solution: 2 2 100 — integer division truncates; modulo gives the remainder; ** is power.

Q5. false; echo $?; echo $? — what prints?

Solution: 1 then 0 — the first echo reports false's status; the second reports the (successful) first echo's status. $? always refers to the IMMEDIATELY previous command.

Level 3 — Hard (exam level)

Q1. (8 marks) Trace $? after each line:

grep -q kappa missing.txt
echo A
test -d /etc && false
echo B

Solution: line1: file missing → grep exits 2 → $?=2. line2: echo prints A, succeeds → $?=0. line3: /etc is a dir (status 0) → && runs false$?=1. line4: echo prints B → $?=0. বাংলা ইঙ্গিত: chain-এর $? হলো শেষ-চলা কমান্ডের status — &&-এর ডান দিক চললে সেটাই, না চললে বাঁ দিকেরটা।

Q2. (8 marks) Write a script check_args.sh that requires exactly 2 arguments (else usage+exit 1), requires arg1 to be an existing readable file, and arg2 to be a positive integer. Use [[ ]] and a regex.

Solution:

#!/bin/bash
set -euo pipefail
usage() { echo "usage: $0 FILE COUNT" >&2; exit 1; }
[[ $# -eq 2 ]] || usage
[[ -f "$1" && -r "$1" ]] || { echo "no readable file: $1" >&2; exit 1; }
[[ "$2" =~ ^[1-9][0-9]*$ ]] || { echo "COUNT must be positive int" >&2; exit 1; }
echo "ok"
বাংলা ইঙ্গিত: "positive integer" regex-এ ^[1-9][0-9]*$^[0-9]+$ দিলে 0-ও পাশ করে যেত; এই সূক্ষ্মতা-ই নম্বর বাঁচায়।

Q3. (10 marks) What is wrong with this loop, and write the fixed version?

for f in $(ls *.dat); do
    mv $f results/$f.bak
done

Solution: Three sins: $(ls) word-splits filenames with spaces; $f unquoted (splits again); ls output parsing is fragile. Fix:

for f in *.dat; do
    [ -e "$f" ] || continue
    mv -- "$f" "results/${f}.bak"
done
বাংলা ইঙ্গিত: for f in $(ls ...) দেখলেই লাল পতাকা — glob নিজেই list দেয়, ls লাগেই না; আর সব expansion-এ quote।

Q4. (10 marks) Write a function retry that runs a given command up to N times until it succeeds, sleeping 2 s between attempts: retry 5 curl -s http://host/data.

Solution:

retry() {
    local n=$1; shift
    local i
    for (( i=1; i<=n; i++ )); do
        "$@" && return 0
        echo "attempt $i failed" >&2
        (( i < n )) && sleep 2
    done
    return 1
}
"$@" preserves the command + args exactly; the function's status is the command's last status. বাংলা ইঙ্গিত: shift-এর পরে "$@"-ই পুরো কমান্ড — eval লাগবে না; আর সফল হলেই return 0, এটাই retry-প্যাটার্নের হৃৎপিণ্ড।

Q5. (10 marks) Predict the full output, then explain the two getopts subtleties:

./s.sh -v -n 3 file.dat        # OPTSTRING=":vn:"
# inside: v) V=1 ;;  n) N=$OPTARG ;;  then shift $((OPTIND-1)); echo "$V $N $1"

Solution: Output: 1 3 file.dat. Subtleties: (a) n: means -n consumes the NEXT word as OPTARG (the 3); (b) OPTIND ends at 4, so shift 3 removes -v -n 3, making file.dat the new $1. বাংলা ইঙ্গিত: OPTIND সবসময় "পরের unprocessed argument"-এর index — তাই shift হয় OPTIND−1 টা।

Level 4 — Beyond the lecture (transfer + coding)

Q1. Write a bash snippet that launches ./solver on every case_*/ directory, at most 4 concurrently, collecting nonzero exit codes, and finally exits 1 if ANY case failed. (No GNU parallel.)

Solution:

#!/bin/bash
set -uo pipefail
pids=(); dirs=()
fail=0
for d in case_*/; do
    ( cd "$d" && ./solver > run.log 2>&1 ) &
    pids+=($!); dirs+=("$d")
    (( ${#pids[@]} % 4 == 0 )) && wait -n || true
done
for i in "${!pids[@]}"; do
    wait "${pids[$i]}" || { echo "FAILED: ${dirs[$i]}" >&2; fail=1; }
done
exit $fail
বাংলা ইঙ্গিত: wait <pid> সেই process-এর exit status ফেরত দেয় — এটাই per-case ব্যর্থতা ধরার চাবি; set -e এখানে ইচ্ছে করেই নেই, নাহলে প্রথম fail-এই script মরে যেত।

Q2. Your sweep script must edit in.dat setting Re from an array, but a colleague's file uses either Re = 100 or Re=100 (spaces vary). Write the robust sed call, and explain why you must use double quotes around the sed program.

Solution:

sed -i -E "s/^(Re[[:space:]]*=[[:space:]]*).*/\1${RE}/" in.dat
[[:space:]]* absorbs any spacing; double quotes (not single) let ${RE} expand inside the sed program while \1 stays escaped. বাংলা ইঙ্গিত: shell variable sed-এ ঢোকাতে হলে double quote — single quote-এ ${RE} আক্ষরিক থেকে যায়; এই এক লাইনেই দুই chapter-এর জ্ঞান মেশে।

Q3. A nightly cron job runs backup.sh, which works interactively but fails in cron with "command not found: rsync". Diagnose and give two fixes.

Solution: Cron runs with a minimal environment — no interactive ~/.bashrc, so PATH lacks /usr/local/bin (or module-provided paths). Fixes: (1) use absolute paths: /usr/bin/rsync; (2) set PATH at the top of the script (export PATH=/usr/bin:/bin:/usr/local/bin) or source the needed profile. (Bonus: module load must also be re-done inside the script on clusters.) বাংলা ইঙ্গিত: "টার্মিনালে চলে কিন্তু cron/sbatch-এ চলে না" শুনলেই উত্তর: environment আলাদা — PATH/module/bashrc সেখানে নেই।

Q4. Combine with C++ (Ch 11/12): write build_and_test.sh that recompiles solver.cpp only when the binary is older than the source (Make-like), runs it on test.in, and diffs against expected.out, reporting PASS/FAIL.

Solution:

#!/bin/bash
set -euo pipefail
if [ ! -x solver ] || [ solver.cpp -nt solver ]; then
    g++ -O2 -std=c++17 -Wall solver.cpp -o solver
    echo "rebuilt"
fi
./solver < test.in > actual.out
if diff -q actual.out expected.out > /dev/null; then
    echo PASS
else
    echo FAIL >&2; exit 1
fi
-nt ("newer than") is bash's timestamp comparison — exactly Make's rebuild rule in one operator. বাংলা ইঙ্গিত: [ src -nt bin ] = Make-এর timestamp নিয়ম bash-এ — দুই chapter জুড়ে দেওয়া প্রশ্ন পরীক্ষায় খুব "german university" স্টাইল।


End of Chapter 8.