Advanced Shell Scripting (Part 2)
Real-World Automation, DevOps & Production-Grade Bash

Advanced Shell Scripting (Part 2)
Real-World Automation, DevOps & Production-Grade Bash
▶️ **YouTube , [📸 Instagram](https://www.instagram.com/devops_voice) , [💼 LinkedIn](https://www.linkedin.com/in/tushar-jadhav29/) , [✍️ Medium](https://medium.com/@tushar.jadhav29)**
Non-Member= Click HERE!
This guide is designed for engineers who already understand:
- Variables
- Loops
- Functions
- Arrays
- Conditions
- File Handling
- Cron Jobs
Now we’ll focus on production-grade automation used by:
- Linux Administrators
- DevOps Engineers
- Cloud Engineers
- SREs
- Platform Engineers
1. Strict Mode (Production Scripts)
Most production-grade Bash scripts start with:
#!/bin/bash
set -euo pipefail
This enables Strict Mode, helping prevent common scripting mistakes and making scripts safer and more predictable.
Why Use Strict Mode?
Without strict mode:
- Scripts may continue after failures.
- Undefined variables may go unnoticed.
- Pipeline errors may be hidden.
- Production incidents become harder to troubleshoot.
With strict mode:
- Fail fast.
- Detect bugs early.
- Improve reliability and maintainability.
Exit on Error (set -e)
Purpose : Stops script execution immediately when a command exits with a non-zero status.
#!/bin/bash
set -e
cp file.txt backup/
rm file.txt
echo "Completed"
Problem Without set -e
If:
cp file.txt backup/ ### fails because the file does not exist:
cp: cannot stat 'file.txt' ## the script still executes:
rm file.txt ## which may lead to data loss or unexpected behavior.
With set -e
cp: cannot stat 'file.txt' ## Script stops immediately and nothing else runs.
Undefined Variable Check ( set -u )
Purpose : Treats undefined variables as errors.
## Example:
#!/bin/bash
set -u
echo "$USERNAME"
## If USERNAME is not defined:
## Script exits immediately.
./script.sh: line 5: USERNAME: unbound variable
## Without set -u
echo "$USERNAME"
Output:
(empty value)
This can cause:
- Incorrect paths
- Empty filenames
- Accidental deletions
## DevOPs Example
rm -rf "$APP_HOME/logs" ## If APP_HOME is empty:
rm -rf /logs ### Potentially dangerous.
Pipeline Validation (set -o pipefail)
Purpose : Makes a pipeline fail if any command in the pipeline fails.
set -o pipefail
Example:
cat file.txt | grep test | sort
Without pipefail:
## Example
cat file.txt | grep test | sort
file.txt ## does not exist.
## Without pipefail
cat file.txt | grep test | sort
Output:
cat: file.txt: No such file or directory
## But pipeline exit code:
echo $?
0
# Because sort succeeded.
# The script incorrectly believes everything worked.
Combined Example
#!/bin/bash
set -euo pipefail
BACKUP_DIR="/backup"
cp data.txt "$BACKUP_DIR/"
cat data.txt | grep ERROR > errors.log
echo "Backup completed successfully"
Behavior:
| ---------------------- | ------------ |
| Situation | Result |
| ---------------------- | ------------ |
| Copy fails | Script exits |
| Variable missing | Script exits |
| grep fails in pipeline | Script exits |
| Everything successful | Continues |
| ---------------------- | ------------ |
Template
#!/bin/bash
set -euo pipefail
main() {
echo "Starting script..."
# Script logic here
echo "Completed successfully."
}
main "$@"
2. Advanced Error Handling
Production scripts should not only fail safely but also provide meaningful error messages that help quickly identify and troubleshoot issues.
Error Function
A common pattern is creating a reusable error handling function.
#!/bin/bash
set -euo pipefail
error_exit() {
echo "[ERROR] $1"
exit 1
}
Usage:
[ -f config.txt ] || error_exit "Config Missing"
## Output
[ERROR] Config file missing
Benefits
- Consistent error messages
- Cleaner code
- Easier maintenance
- Reusable across scripts
Enhanced Error Function
Include timestamp and script name.
error_exit() {
echo "$(date '+%Y-%m-%d %H:%M:%S') [ERROR] [$0] $1"
exit 1
}
# Example:
[ -d /backup ] || error_exit "Backup directory not found"
# Output
2026-06-23 10:30:12 [ERROR] [backup.sh] Backup directory not found
Centralized Error Handler Using trap
Instead of manually checking every command, use Bash traps.
#!/bin/bash
set -euo pipefail
handle_error() {
echo "Error occurred on line $1"
}
trap 'handle_error $LINENO' ERR
Production-level debugging.
#!/bin/bash
set -euo pipefail
handle_error() {
echo "Error occurred on line $1"
}
trap 'handle_error $LINENO' ERR
cp missing.txt /backup/
echo "Completed"
## Output:
cp: cannot stat 'missing.txt': No such file or directory
Error occurred on line 10
Production-Grade Error Handler
Capture:
- Line number
- Failed command
- Exit code
- Script name
- Timestamp
#!/bin/bash
set -euo pipefail
handle_error() {
local line_no=$1
local exit_code=$2
echo "================================="
echo "Timestamp : $(date)"
echo "Script : $0"
echo "Line : $line_no"
echo "Command : $BASH_COMMAND"
echo "Exit Code : $exit_code"
echo "================================="
}
trap 'handle_error ${LINENO} $?' ERR
## Example Output:
=================================
Timestamp : Tue Jun 23 10:35:12 UTC 2026
Script : backup.sh
Line : 22
Command : cp missing.txt /backup/
Exit Code : 1
=================================
Logging Errors to a File
LOGFILE="/var/log/app.log"
handle_error() {
echo "$(date) ERROR at line $1: $BASH_COMMAND" >> "$LOGFILE"
}
trap 'handle_error $LINENO' ERR
## Output in log:
2026-06-23 10:40:12 ERROR at line 18: cp missing.txt /backup/
Cleanup Before Exit
Use trap EXIT for cleanup tasks.
cleanup() {
rm -f /tmp/mytempfile
}
trap cleanup EXIT
Multiple Traps
trap cleanup EXIT
trap 'handle_error $LINENO' ERR
trap 'echo "Interrupted"; exit 130' INT
trap 'echo "Terminated"; exit 143' TERM
Signals
| ------ | --------------- |
| Signal | Description |
| ------ | --------------- |
| EXIT | Script exits |
| ERR | Command failure |
| INT | Ctrl+C |
| TERM | Kill request |
| HUP | Terminal closed |
| ------ | --------------- |
Production Error Handling Template
#!/bin/bash
set -euo pipefail
LOGFILE="/var/log/script.log"
log() {
echo "$(date '+%F %T') [INFO] $1" | tee -a "$LOGFILE"
}
error_exit() {
echo "$(date '+%F %T') [ERROR] $1" | tee -a "$LOGFILE"
exit 1
}
handle_error() {
local line_no=$1
local exit_code=$2
echo "$(date '+%F %T') [ERROR] Script failed at line ${line_no}" \
| tee -a "$LOGFILE"
echo "$(date '+%F %T') [ERROR] Command: ${BASH_COMMAND}" \
| tee -a "$LOGFILE"
echo "$(date '+%F %T') [ERROR] Exit Code: ${exit_code}" \
| tee -a "$LOGFILE"
}
cleanup() {
log "Cleanup completed"
}
trap 'handle_error ${LINENO} $?' ERR
trap cleanup EXIT
log "Script started"
[ -f config.txt ] || error_exit "Config file missing"
log "Processing completed"
3. Logging Framework
Production scripts should use structured logging instead of plain echo statements. A logging framework makes it easier to troubleshoot issues, monitor deployments, and integrate with log management tools.
Production scripts should use structured logging instead of plain echo statements. A logging framework makes it easier to troubleshoot issues, monitor deployments, and integrate with log management tools.
Why Use a Logging Framework?
Instead of:
echo "Starting deployment..."
echo "Database backup completed"
echo "Deployment failed"
Create logger:
LOGFILE=/var/log/deploy.log
log() {
echo "$(date '+%F %T') [$1] $2" | tee -a $LOGFILE
}
Use structured logs:
2026-01-01 10:00:00 [INFO] Deployment Started
2026-01-01 10:01:15 [INFO] Database Backup Completed
2026-01-01 10:03:42 [ERROR] Deployment Failed
Benefits
- Consistent log format
- Easy debugging
- Timestamped events
- Log file persistence
- Compatible with monitoring tools (Grafana, ELK, Splunk, Loki, etc.)
Basic Logging Function
#!/bin/bash
LOGFILE="/var/log/deploy.log"
log() {
echo "$(date '+%F %T') [$1] $2" | tee -a "$LOGFILE"
}
## Usage
log INFO "Deployment Started"
log WARNING "Disk Usage High"
log ERROR "Database Connection Failed"
## Output
2026-01-01 10:00:00 [INFO] Deployment Started
2026-01-01 10:05:12 [WARNING] Disk Usage High
2026-01-01 10:07:45 [ERROR] Database Connection Failed
Log Levels
| -------- | ----------------- | -------------------------- |
| Level | Purpose | Example |
| -------- | ----------------- | -------------------------- |
| INFO | Normal operations | Service started |
| WARNING | Potential issue | Disk usage above 80% |
| ERROR | Operation failed | Database connection failed |
| DEBUG | Troubleshooting | Variable values |
| CRITICAL | Severe failure | System unavailable |
| -------- | ----------------- | -------------------------- |
## Example:
log INFO "Backup started"
log DEBUG "Server IP: $HOSTNAME"
log WARNING "Memory usage is above 90%"
log ERROR "Unable to connect to database"
log CRITICAL "Deployment aborted"
## Improved Logger with Script Name
#!/bin/bash
LOGFILE="/var/log/deploy.log"
log() {
local level="$1"
shift
echo "$(date '+%F %T') [$level] [$0] $*" | tee -a "$LOGFILE"
}
## Output:
2026-01-01 10:15:32 [INFO] [deploy.sh] Deployment Started
Logging to File Only
This writes only to the log file without displaying output on the terminal.
log() {
echo "$(date '+%F %T') [$1] $2" >> "$LOGFILE"
}
Logging to Console and File
This is the most common approach in production because logs are visible in real time and stored for later review.
log() {
local level="$1"
shift
echo "$(date '+%F %T') [$level] $*" | tee -a "$LOGFILE"
}
-------------------------------------------------------
## Add Process ID (PID)
log() {
local level="$1"
shift
echo "$(date '+%F %T') [$level] [PID:$$] $*" | tee -a "$LOGFILE"
}
## Output:
2026-01-01 10:20:11 [INFO] [PID:4521] Deployment Started
-------------------------------------------------------
### Add Hostname
log() {
local level="$1"
shift
echo "$(date '+%F %T') [$(hostname)] [$level] $*" | tee -a "$LOGFILE"
}
## Output:
2026-01-01 10:25:00 [web01] [INFO] Service Started
DevOPs Logging Framework
#!/bin/bash
set -euo pipefail
LOGFILE="/var/log/deploy.log"
log() {
local level="$1"
shift
printf "%s [%s] [%s] [PID:%s] %s\n" \
"$(date '+%F %T')" \
"$level" \
"$(hostname)" \
"$$" \
"$*" | tee -a "$LOGFILE"
}
## Usage:
log INFO "Deployment Started"
log INFO "Creating backup"
log WARNING "Low Disk Space"
log ERROR "Application failed to start"
log CRITICAL "Rolling back deployment"
## Sample output:
2026-01-01 10:00:00 [INFO] [web01] [PID:4521] Deployment Started
2026-01-01 10:01:20 [INFO] [web01] [PID:4521] Creating backup
2026-01-01 10:02:30 [WARNING] [web01] [PID:4521] Low Disk Space
2026-01-01 10:03:15 [ERROR] [web01] [PID:4521] Application failed to start
2026-01-01 10:03:18 [CRITICAL] [web01] [PID:4521] Rolling back deployment
---------------------------------------------
## Integrating with Error Handling
error_exit() {
log ERROR "$1"
exit 1
}
handle_error() {
log ERROR "Error on line $1: ${BASH_COMMAND}"
}
trap 'handle_error ${LINENO}' ERR
## Example:
cp config.txt /backup || error_exit "Backup failed"
## Log output:
2026-01-01 10:30:15 [ERROR] Backup failed
4. Configuration Files
Production scripts should never hardcode values such as ports, hostnames, usernames, or file paths. Instead, store configuration in a separate file so the script can be reused across different environments (Development, Testing, Staging, Produc
Why Use Configuration Files?
❌ Never hardcode values.
## Hardcoded values:
# config.conf
APP_NAME=myapp
PORT=8080
DB_HOST=localhost
**Problems:
- **Difficult to maintain
- Requires script changes for every environment
- Increases risk of errors
- Hard to reuse
Use a Configuration File
Create a file named config.conf:
APP_NAME=myapp
PORT=8080
DB_HOST=localhost
DB_PORT=3306
LOG_DIR=/var/log/myapp
BACKUP_DIR=/backup
Loading the Configuration
#!/bin/bash
set -euo pipefail
source config.conf
## OR try with below example
#!/bin/bash
set -euo pipefail
CONFIG_FILE="./config.conf"
if [[ ! -f "$CONFIG_FILE" ]]; then
echo "Configuration file not found: $CONFIG_FILE"
exit 1
fi
source "$CONFIG_FILE"
Using Configuration Values
echo "$APP_NAME"
echo "$PORT"
echo "$DB_HOST"
## Output:
myapp
8080
localhost
Example Script
config.conf
APP_NAME=InventoryApp
PORT=8080
DB_HOST=db01.example.com
LOG_DIR=/var/log/inventory
deploy.sh
#!/bin/bash
set -euo pipefail
source config.conf
echo "Application : $APP_NAME"
echo "Port : $PORT"
echo "Database : $DB_HOST"
echo "Logs : $LOG_DIR"
## Output:
Application : InventoryApp
Port : 8080
Database : db01.example.com
Logs : /var/log/inventory
---------------------------------------
#!/bin/bash
set -euo pipefail
CONFIG_FILE="./config.conf"
[[ -f "$CONFIG_FILE" ]] || {
echo "Configuration file missing"
exit 1
}
source "$CONFIG_FILE"
echo "Deploying $APP_NAME..."
echo "Connecting to $DB_HOST:$DB_PORT"
echo "Application will listen on port $PORT"
5. Parsing YAML
Modern DevOps tools such as Kubernetes, Ansible, GitHub Actions, Argo CD, Helm, and Docker Compose use YAML for configuration. Instead of manually parsing YAML with grep or awk, use **yq**, a command-line YAML processor.
Why Use yq?
❌ Avoid parsing YAML like this:
grep environment config.yaml | awk '{print $2}'
Problems:
- Breaks with nested YAML
- Doesn’t handle arrays or complex structures
- Difficult to maintain
✅ Use yq, which understands YAML syntax.
Sample YAML File
environment: production
app:
name: myapp
port: 8080
database:
host: db.example.com
port: 3306
servers:
- web01
- web02
Install yq
## Ubuntu/Debian (via Snap)
sudo snap install yq
## download the binary
sudo wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 \
-O /usr/local/bin/yq
sudo chmod +x /usr/local/bin/yq
## macOS (Homebrew)
brew install yq
## Verify installation:
yq --version
Read a Value
yq '.environment' config.yaml
## Output:
production
## Read Nested Values
yq '.app.name' config.yaml
## Output:
myapp
## Read Array Elements
yq '.servers[0]' config.yaml
## Output:
web01
## Display all servers:
yq '.servers[]' config.yaml
# Output:
web01
web02
Store Values in Variables
APP_NAME=$(yq '.app.name' config.yaml)
DB_HOST=$(yq '.database.host' config.yaml)
PORT=$(yq '.app.port' config.yaml)
echo "$APP_NAME"
echo "$DB_HOST"
echo "$PORT"
Loop Through an Array
for server in $(yq '.servers[]' config.yaml); do
echo "Deploying to $server"
done
## Output:
Deploying to web01
Deploying to web02
Update a YAML Value
Modify a field in place:
yq -i '.environment = "staging"' config.yaml
# Updated file:
environment: staging
Production Example
config.yaml
application:
name: inventory-service
version: "1.2.0"
database:
host: mysql-prod
port: 3306
deployment:
replicas: 3
deploy.sh
#!/bin/bash
set -euo pipefail
APP_NAME=$(yq '.application.name' config.yaml)
VERSION=$(yq '.application.version' config.yaml)
REPLICAS=$(yq '.deployment.replicas' config.yaml)
echo "Deploying $APP_NAME"
echo "Version: $VERSION"
echo "Replicas: $REPLICAS"
## Output:
Deploying inventory-service
Version: 1.2.0
Replicas: 3
Common yq Commands
| Task | Command |
| ------------------ | ------------------------------------------- |
| Read a value | `yq '.environment' config.yaml` |
| Read nested value | `yq '.database.host' config.yaml` |
| Read array | `yq '.servers[]' config.yaml` |
| Read first element | `yq '.servers[0]' config.yaml` |
| Update value | `yq -i '.environment = "prod"' config.yaml` |
| Pretty-print YAML | `yq '.' config.yaml` |
6. JSON Automation with jq
Most REST APIs return JSON. Use **jq** to parse it reliably.
Sample JSON
{
"status": "success",
"count": 25,
"users": [
{
"id": 1,
"name": "John"
}
]
}
## Read Values
jq -r '.status' response.json
## Output
success
jq '.count' response.json
## Output
25
Read Nested Value
jq -r '.users[0].name' response.json
## Output
John
Parse API Response
response=$(curl -s https://api.example.com/users)
status=$(jq -r '.status' <<< "$response")
echo "$status"
7. Advanced Arrays
Indexed Arrays
servers=(
web01
web02
web03
)
Loop:
for server in "${servers[@]}"; do
echo "$server"
done
Associative Arrays
declare -A server_ip
server_ip[web01]="10.0.0.1"
server_ip[web02]="10.0.0.2"
Access:
echo "${server_ip[web01]}"
Loop:
for host in "${!server_ip[@]}"; do
echo "$host -> ${server_ip[$host]}"
done
8. Parallel Processing
Sequential:
backup_server server1
backup_server server2
backup_server server3
Parallel:
backup_server server1 &
backup_server server2 &
backup_server server3 &
wait
Production pattern:
for server in "${servers[@]}"; do
backup_server "$server" &
done
wait
echo "All backups completed"
9. Multi-Server SSH Automation
while read -r server; do
ssh "$server" uptime
done < servers.txt
Use read -r to preserve backslashes and always quote variables.
10. Parallel SSH
while read -r server; do
ssh "$server" uptime &
done < servers.txt
wait
Ideal for:
- Health checks
- Configuration validation
- Patch verification
- Log collection
- Rolling deployments
For large fleets, consider tools like pssh, pdsh, or Ansible.
11. Automated Package Deployment
packages=(
nginx
git
curl
docker.io
)
for pkg in "${packages[@]}"; do
sudo apt-get install -y "$pkg"
done
Production improvement:
sudo apt-get update
for pkg in "${packages[@]}"; do
if ! dpkg -s "$pkg" >/dev/null 2>&1; then
sudo apt-get install -y "$pkg"
fi
done
12. Service Monitoring
Check service:
systemctl is-active nginx
Restart if stopped:
if ! systemctl is-active --quiet nginx; then
systemctl restart nginx
fi
Verify restart:
systemctl is-active --quiet nginx \
&& echo "Service healthy"
13. Database Backup Automation
Avoid passwords on the command line.
Instead of:
mysqldump -u root -pPassword appdb
## Use:
mysqldump --defaults-extra-file=/root/.my.cnf appdb \
> backup.sql
## Compress:
gzip backup.sql
## Add timestamp:
mysqldump appdb \
> backup_$(date +%F).sql
14. Disk Monitoring
usage=$(df -P / | awk 'NR==2 {gsub("%","",$5); print $5}')
Check:
if (( usage > 80 )); then
echo "Disk usage critical: ${usage}%"
fi
Using df -P provides POSIX-compatible output.
15. Memory Monitoring
free -m
Used memory:
free -m | awk 'NR==2 {print $3}'
Memory percentage:
free | awk '/Mem:/ {printf "%.2f%%\n", $3/$2*100}'
16. CPU Monitoring
Avoid parsing top when possible.
Preferred:
mpstat 1 1
If using top:
top -bn1 | grep "Cpu(s)"
17. Email Alerts
echo "Disk Usage Critical" \
| mail -s "Server Alert" admin@company.com
Production alternatives:
- Postfix
- SMTP relay
- Amazon SES
- SendGrid
- Microsoft Graph
18. Slack Notifications
curl -X POST \
-H "Content-Type: application/json" \
-d '{"text":"Deployment completed successfully"}' \
"$WEBHOOK_URL"
Always store the webhook URL in an environment variable or secret manager.
19. REST API Automation
GET:
curl -fsS https://api.example.com/users
POST:
curl -X POST \
-H "Content-Type: application/json" \
-d '{"name":"john"}' \
https://api.example.com/users
Recommended options:
-f→ Fail on HTTP errors-s→ Silent mode-S→ Show errors
20. Retry Logic
Basic:
for i in {1..5}; do
curl -fsS https://api.example.com && break
sleep 5
done
Production version:
max_retry=5
for ((i=1;i<=max_retry;i++)); do
if curl -fsS https://api.example.com; then
break
fi
echo "Retry $i/$max_retry"
sleep 5
done
21. Lock Files
Simple lock:
LOCK=/tmp/backup.lock
if [[ -f "$LOCK" ]]; then
echo "Already running"
exit 1
fi
touch "$LOCK"
trap 'rm -f "$LOCK"' EXIT
Production recommendation:
exec 9>/tmp/backup.lock
flock -n 9 || exit 1
flock avoids race conditions and is more reliable than checking for a file.
22. Signal Handling
cleanup() {
rm -f /tmp/*.tmp
}
trap cleanup EXIT
trap cleanup INT
trap cleanup TERM
Ensures cleanup on normal exit, Ctrl+C, or termination.
23. Advanced sed
Replace:
sed -i 's/dev/prod/g' app.conf
Delete lines:
sed '/ERROR/d' logs.txt
Replace only first occurrence:
sed 's/dev/prod/'
24. Advanced awk
Sum:
awk '{sum+=$3}
END {print sum}' sales.txt
Average:
awk '{sum+=$3}
END {print sum/NR}' sales.txt
Maximum:
awk 'max<$3{max=$3}
END{print max}' sales.txt
25. Docker Automation
Build:
docker build -t myapp:latest .
Run:
docker run -d --name myapp myapp:latest
Stop all running containers:
docker stop $(docker ps -q)
Remove stopped containers:
docker container prune -f
26. Kubernetes Automation
Pods:
kubectl get pods
Restart Deployment:
kubectl rollout restart deployment app
Check Status:
kubectl rollout status deployment app
27. CI/CD Deployment Script
git pull
npm install
npm run build
systemctl restart app
28. Blue-Green Deployment Script
CURRENT=$(cat active_env)
if [ "$CURRENT" = "blue" ]
then
deploy green
else
deploy blue
fi
Switch Traffic:
update_load_balancer
29. Production Health Check Script
#!/bin/bash
check_disk() {
usage=$(df -h / | awk 'NR==2 {print $5}' | tr -d '%')
if [ $usage -gt 80 ]
then
echo "Disk Alert"
fi
}
check_memory() {
free -m
}
check_cpu() {
top -bn1 | head -5
}
check_disk
check_memory
check_cpu
30. Enterprise Deployment Framework
main() {
validate_config
backup_current_release
deploy_application
run_health_checks
switch_traffic
notify_team
}
main
This structure is used in production deployment systems.
Check here for an in-depth learning series on Bash scripting.
👉 Bash Scripting — DAY 2 — Complete Practical Guide for DevOps
👉 Bash Scripting — DAY 3 — Advanced Shell Scripting for DevOps
🚀 100+ Handy Knowledge Hub: All-in-One Linux, DevOps & Automation Blogs
👉 **Click here to read all my topic-wise blogs on a single page**
***🐧 Linux Server Configuration — Complete Administrator’s Guide (Beginner → Advanced → Production)***
***🏆 Ultimate DevOps & SRE Learning Hub (2026 Edition) — 100% Free, Real-World Knowledge***
🌟 Final Note
This single page is designed to be:
- 📌 Bookmarked
- 📌 Shared
- 📌 Used daily
Thank you for reading! 😊🚀
If you’re a Linux admin, DevOps engineer, cloud engineer, or SRE — this page is your personal technical library.
👏 If it helped you, clap & share 💬 Drop a comment if you want a topic-wise PDF or roadmap next
🐳Happy Learning & Troubleshooting!
메타데이터
- post_id
- 224f3c532cad
- slug
- advanced-shell-scripting-part-2-224f3c532cad
- url
- https://blog.devops.dev/advanced-shell-scripting-part-2-224f3c532cad
- canonical_url
- https://blog.devops.dev/advanced-shell-scripting-part-2-224f3c532cad
- author_url
- https://medium.com/@tushar.jadhav29
- status
- ok
- fetched_at
- 2026-07-15 11:23:19