Shell programming in Linux means writing commands, variables, conditions, loops and functions in a plain text file (a shell script) that the Linux shell runs for you. Start the file with a shebang such as #!/bin/bash, make it executable with chmod +x script.sh, and run it with ./script.sh.
Power up any Linux development board, open a UART console, and the first thing you meet is not a desktop. It is a blinking prompt. That prompt belongs to the shell, and learning to program it is one of the best skills an embedded or IoT engineer can pick up early.
This introduction to shell programming in Linux keeps everything practical. Here is what you will walk away with:
Let’s start from the ground up.
A shell is a program that reads the commands you type, asks the Linux kernel to carry them out, and shows you the result. It is the bridge between you and the operating system.
Think of it as a translator standing between two people who do not share a language:
You (keyboard) -> Shell (Bash) -> Kernel -> Hardware (CPU, RAM, storage, UART)
Every time you press Enter, the shell quietly does five jobs:
*.c, $HOME and $(date)These three get mixed up constantly. Here is the clean difference:
| Shell | Where you will meet it |
|---|---|
| Bash | Default on most Linux distributions and the best place to start |
| sh (dash) | The lean POSIX shell behind /bin/sh on Debian and Ubuntu |
| Zsh | Rich interactive features; the default shell on macOS |
| Fish | Friendly auto-suggestions and syntax colors for daily use |
| ksh | KornShell, whose features influenced many later shells |
| BusyBox ash | Tiny shell found on space-constrained embedded Linux images |
Check which shell you are running:
echo $SHELL # your default login shell
echo $0 # the shell running right now
cat /etc/shells # valid login shells on this system
Good to know: $SHELL shows your default login shell, while $0 shows the shell you are using at this moment. They can differ.
Shell scripting is the practice of saving a series of shell commands in a text file so they run together, in order, without typing each one by hand. That file is a shell script, and it usually ends with .sh.
Shell scripting vs shell programming:
Where shell scripts show up in real work:
Time to write your first script. You only need a Linux terminal: Ubuntu, Raspberry Pi OS, a virtual machine, or WSL on Windows. Every example in this guide targets Bash.
bash --version
nano hello.sh
#!/bin/bash
# hello.sh - my first shell script
echo "Hello, Embedded World!"
echo "Today is $(date '+%A, %d %B %Y')"
chmod +x hello.sh
./hello.sh
Sample output:
Hello, Embedded World!
Today is Monday, 26 October 2026
What just happened?
#!/bin/bash is the shebang. It tells Linux which interpreter should run the file.# starts a comment, which the shell ignores.echo prints text to the screen.$(date ...) is command substitution: the shell runs the command and drops its output into your text.chmod +x adds execute permission to the file../ runs the file from the current folder, because that folder is normally not in your PATH.Pro tip: You can also run bash hello.sh without chmod. When something misbehaves, run bash -x hello.sh to watch every command as it executes.
Let’s explain shell programming in Linux through the six building blocks that every script is made of.
board="Raspberry Pi 4"
units=3
echo "Board: $board, Units: $units"
=. Writing units = 3 makes the shell hunt for a command named units.$, and wrap it in double quotes to protect spaces.${name} when text sits right next to a variable, for example ${board}_log.txt.export NAME=value to pass a variable on to child processes.#!/bin/bash
read -p "Enter your name: " name
echo "Welcome, $name! Arguments passed: $#. First one: $1"
The special variables you will use every day:
| Variable | Meaning |
|---|---|
| $0 | Name of the script |
$1, $2, … | First, second, … argument |
| $# | Number of arguments |
| $@ | All arguments, each kept as a separate word |
| $? | Exit status of the last command (0 means success) |
| $ | Process ID of the current shell |
#!/bin/bash
if [ -e /dev/ttyUSB0 ]; then
echo "USB-serial adapter detected."
elif [ -e /dev/ttyACM0 ]; then
echo "USB CDC device detected (for example, an Arduino)."
else
echo "No serial device found."
fi
Handy test operators:
-e exists, -f regular file, -d directory, -r readable, -w writable, -x executable-eq, -ne, -gt, -lt, -ge, -le=, !=, -z (empty), -n (not empty)[ -f "$file" ] works, but [-f "$file"] does not# for: go through a list
for i in 1 2 3 4 5; do
echo "Reading sensor $i"
done
# while: repeat as long as the condition is true
count=1
while [ "$count" -le 3 ]; do
echo "Attempt $count"
count=$((count + 1))
done
until is the mirror image of while: it runs until the condition becomes truebreak leaves a loop early, and continue skips to the next round$(( ... )) performs integer arithmetic#!/bin/bash
check_file() {
local path="$1"
if [ -f "$path" ]; then
echo "Found: $path"
return 0
else
echo "Missing: $path"
return 1
fi
}
check_file "/etc/hostname"
check_file "/tmp/nothing_here"
echo "Status of the last call: $?"
local keeps a variable inside the functionreturn gives back a status code from 0 to 255. To hand back data, use echo and capture it with $(...)&& and ||Every command finishes with an exit status: 0 means success, anything else means something went wrong. The shell lets you chain commands using that result:
&& runs the next command only if the previous one succeeded|| runs the next command only if the previous one failed; runs the next command no matter whatmake && echo "Build OK" || echo "Build failed"
Scripts are only as strong as the commands inside them. Keep this quick reference handy:
| Category | Commands | What they do |
|---|---|---|
| Navigation and files | pwd, ls, cd, mkdir, cp, mv, rm, touch | Move around and manage files and folders |
| Viewing content | cat, less, head, tail -f | Read files; tail -f follows a log live |
| Search and text | grep, find, sed, awk, cut, sort, uniq, wc | Search, filter, transform and count text |
| Permissions | chmod, chown, ls -l | Control who can read, write and execute |
| Processes | ps, top, kill, jobs, bg, fg | Inspect and control running programs |
| System info | uname -a, df -h, du -sh, free -h, dmesg | Check kernel, disk, memory and driver messages |
| Networking | ping, ssh, scp, ip a | Reach and manage remote boards |
| Help | man, --help, type | Find out what any command does |
Linux programs are small and focused. The shell snaps them together like building blocks:
| (pipe) sends the output of one command into the next> writes output to a file (and overwrites it)>> appends output to a file< feeds a file into a command as input2> redirects error messages2>&1 sends error messages to the same place as normal output# Count the CPU cores on this board
grep processor /proc/cpuinfo | wc -l
# Show build output on screen and save it to a log file
make 2>&1 | tee build.log
Reading syntax is useful. Running scripts is where it clicks. These four examples move from beginner level to real embedded work.
#!/bin/bash
# backup.sh - archive a project folder with a timestamp
SRC_DIR="$HOME/projects"
PROJECT="firmware"
DEST="$HOME/backups"
STAMP=$(date +%Y%m%d_%H%M%S)
mkdir -p "$DEST"
tar -czf "$DEST/${PROJECT}_$STAMP.tar.gz" -C "$SRC_DIR" "$PROJECT"
echo "Backup saved: $DEST/${PROJECT}_$STAMP.tar.gz"
date${PROJECT} needs braces, otherwise the shell reads $PROJECT_ as one variable name0 2 * * * /home/user/backup.sh through crontab -e#!/bin/bash
# check_nodes.sh - report which IoT nodes answer a ping
NODES=("192.168.1.10" "192.168.1.11" "192.168.1.12")
for node in "${NODES[@]}"; do
if ping -c 1 -W 2 "$node" > /dev/null 2>&1; then
echo "[ UP ] $node"
else
echo "[ DOWN ] $node"
fi
done
"${NODES[@]}" loops over every element safelyping drives the if, and > /dev/null 2>&1 silences its output#!/bin/bash
# temp_logger.sh - log SoC temperature every 5 seconds
ZONE="/sys/class/thermal/thermal_zone0/temp"
LOG="$HOME/templog.csv"
if [ ! -r "$ZONE" ]; then
echo "Thermal sensor not found on this board." >&2
exit 1
fi
[ -f "$LOG" ] || echo "timestamp,temp_c" > "$LOG"
while true; do
raw=$(cat "$ZONE")
temp=$(awk -v t="$raw" 'BEGIN { printf "%.1f", t / 1000 }')
echo "$(date '+%F%T'),$temp" >> "$LOG"
sleep 5
done
/sys virtual filesystem. Linux reports it in millidegrees Celsius, so 45000 means 45.0 °C>&2 sends the message to the error stream, and exit 1 signals failure[ -f "$LOG" ] || echo ... writes the CSV header only when the file does not exist yetwhile true loops forever, so press Ctrl+C to stop it#!/bin/bash
# build_flash.sh - build firmware, show memory usage, flash an STM32 board
set -euo pipefail
ELF="build/firmware.elf"
BIN="build/firmware.bin"
need() { command -v "$1" > /dev/null || { echo "Missing tool: $1" >&2; exit 1; }; }
need make
need arm-none-eabi-size
need st-flash
echo "==> Building..."
make -j"$(nproc)"
echo "==> Memory usage:"
arm-none-eabi-size "$ELF"
echo "==> Flashing..."
st-flash write "$BIN" 0x08000000
echo "Done."
set -euo pipefail stops the script on errors, undefined variables and failed pipelinesneed helper checks that every tool exists before real work begins$(nproc) uses all CPU cores for a faster build0x08000000 is the usual start of flash memory on STM32 devicesbash -x.eval and scripts run as root can open the door to command injection.| Task | Best choice | Why |
|---|---|---|
| Automate build, flash and log capture | Shell | Glues existing tools together in a few lines |
| Boot-time setup on a minimal Linux image | Shell (POSIX sh) | Already present, no extra runtime needed |
| Parse JSON, call APIs, analyze sensor data | Python | Rich libraries and readable data handling |
| Drivers, ISRs and real-time control | C / C++ | Direct hardware access and predictable timing |
| One-off file renaming or log filtering | Shell | Faster than writing and compiling a program |
Rule of thumb: use shell to orchestrate, C to control hardware, and Python to analyze data.
Most guides stop at generic admin examples. On an embedded Linux board, the shell does much more:
dmesg (it may need sudo), lsusb, and i2cdetect -y 1 (the bus number varies by board)/etc/init.d/ on BusyBox-based systems, to mount filesystems, start the network and launch your application/sys, filter it with awk, then publish it to an MQTT broker with mosquitto_pub# Capture 115200-baud UART output into a timestamped log file
stty -F /dev/ttyUSB0 115200 cs8 -parenb -cstopb raw -echo
cat /dev/ttyUSB0 | tee "uart_$(date +%F_%H%M%S).log"
On many distributions you must add your user to the dialout group before you can open serial ports.
A script that runs perfectly on your Ubuntu laptop can fail on a minimal board image. The /bin/sh there may be dash or BusyBox ash, which stay close to the POSIX standard and may not include Bash extensions.
| Feature | Bash | POSIX sh (the safe choice) |
|---|---|---|
| Arrays | Supported | Not part of the standard |
[[ ... ]] tests | Supported | Use [ ... ] |
read -p prompt | Supported | Use printf, then read |
| source file | Supported | Use . file |
== inside [ ] | Accepted | Use = |
Pro tip: If a script must run on a minimal image, use #!/bin/sh, avoid the Bash-only features above, and check it with sh -n script.sh and the ShellCheck tool. Keep timing-critical logic in C and let the shell orchestrate around it.
#!/bin/bash, or #!/usr/bin/env bash for portability"$var" protects against spaces and special charactersset -euo pipefail to stop on errors, unset variables and failed pipelines (learn its quirks before you rely on it)UPPER_CASE for constants and lower_case for local variableslocal inside themmktemp and clean them up with trap*.log over parsing the output of lsA reusable starter template:
#!/usr/bin/env bash
set -euo pipefail
TMP=$(mktemp)
cleanup() { rm -f "$TMP"; }
trap cleanup EXIT
echo "Working file: $TMP"
# your logic goes here
| Mistake | What you see | Quick fix |
|---|---|---|
Spaces around = (x = 5) | x: command not found | Write x=5 |
Forgetting chmod +x | Permission denied | Run chmod +x script.sh |
| Windows (CRLF) line endings | bad interpreter: /bin/bash^M | Run dos2unix script.sh |
| Unquoted variables | Breaks on names with spaces | Write "$var" |
Missing spaces inside [ ] | [: missing ] or command not found | Write [ -f "$file" ] |
Running Bash code with sh | Syntax error: “(” unexpected | Run bash script.sh or fix the shebang |
grep, sed and awk$1 and getopts, case statements, trap and error handlingcron or a systemd timer.sh file, and shell programming adds variables, conditions, loops and functionschmod +x) and a run command (./script.sh)This introduction to shell programming in Linux covered what a shell is, what shell scripting is, the six building blocks, essential commands, practical examples, and the real advantages and disadvantages of shell programming. Your best next step is simple: pick one repetitive task from your own workflow, automate it with a script, and improve it a little every week.
If you want to turn these skills into a career, the Indian Institute of Embedded Systems (IIES) in Bangalore trains engineers in embedded systems, IoT an
Shell programming in Linux is writing scripts made of shell commands, variables, conditions, loops and functions that the Linux shell (usually Bash) executes to automate tasks. It turns repetitive terminal work into one reusable command.
The shell is the program that interprets your commands. The terminal is only the window or serial console that displays the shell. You can run different shells inside the same terminal.
Bash. It is the default on most Linux distributions, has the largest amount of documentation and community help, and the skills carry over to other shells.
Yes. Embedded Linux work involves board bring-up, build automation, log capture, startup scripts and remote maintenance, and shell scripts handle all of these with very little overhead. Real-time and hardware-level code still belongs in C or C++.
Yes. Install WSL (Windows Subsystem for Linux), use Git Bash, or run a Linux virtual machine. A Raspberry Pi also makes an excellent low-cost practice machine.
Indian Institute of Embedded Systems – IIES