پرش به محتویات

۱۴. Bash و پردازش متن

اسکریپت عملیاتی که با خطا ساکت می‌شود، خطرناک‌تر از اسکریپت نبودن است. این فصل ابزار پردازش متن و الگوی نوشتن اسکریپت ایمن را می‌دهد.

راهنمای سریع

ابزار کاربرد
grep -E/-w/-c/-v فیلتر خطوط
cut -d: -f1 / awk '{print $N}' انتخاب ستون
sort / uniq -c / sort -nr مرتب‌سازی و شمارش
sed 's/a/b/g' جایگزینی (سیال)
tr / column -t / paste / comm / diff تبدیل، جدول، مقایسه
jq پردازش JSON
xargs -0 / find -print0 اجرای فرمان روی فهرست امن
tee / watch / timeout تقسیم خروجی، تکرار، محدودیت زمانی

خروجی و pipe

command > output.txt
command >> output.txt
command 2> errors.log
command >all.log 2>&1
command1 | command2
command | tee -a run.log          # هم نمایش، هم ذخیره
command 2>&1 | less
diff <(sort a.txt) <(sort b.txt)  # process substitution

> فایل را بازنویسی می‌کند و >> به انتها می‌افزاید. برای خروجی حساس یا مهم، مسیر مقصد را بررسی کنید. (set -o noclobber از بازنویسی ناخواسته جلوگیری می‌کند.)

grep، cut، sort و uniq

grep -in 'failed' app.log
grep -Rni --include='*.conf' 'listen' /etc/nginx
grep -E 'error|warn' app.log
grep -v '^#' /etc/ssh/sshd_config | grep -v '^$'      # بدون کامنت و خط خالی
cut -d: -f1 /etc/passwd
sort names.txt | uniq -c
sort -nr counts.txt | head
sort -k2 -n -u data.txt

uniq معمولاً ردیف‌های تکراری مجاور را ادغام می‌کند؛ برای شمارش همه موارد، ابتدا sort کنید.

awk و sed

awk '{print $1}' access.log
awk '$9 == 500 {print $0}' access.log
awk -F: '$3 >= 1000 {print $1, $3}' /etc/passwd          # جداکنندهٔ دلخواه
awk '{s += $10} END {printf "%.1f MB\n", s/1024/1024}' access.log   # جمع یک ستون
sed -n '1,40p' file.txt
sed 's/old/new/g' file.txt
sed -i.bak 's/old/new/g' file.conf                       # درجا + نسخهٔ پشتیبان

sed بالا فایل را تغییر نمی‌دهد و نتیجه را به stdout می‌فرستد. برای تغییر درجا از sed -i فقط پس از تهیه نسخه پشتیبان (-i.bak) استفاده کنید و قبلش خروجی را بدون -i ببینید.

JSON با jq

curl -s https://api.example.com/status | jq .
jq -r '.items[] | "\(.name)\t\(.status)"' data.json
docker inspect CONTAINER | jq '.[0].State'
journalctl -u myapp -o json -n 100 | jq -r '.MESSAGE'

xargs و null separator

find . -type f -name '*.log' -print0 | xargs -0 -r ls -lh
find . -type f -name '*.log' -print0 | xargs -0 -r -P4 gzip      # -P4: چهار کار موازی

استفاده از -print0 و -0 نام فایل‌های دارای فاصله یا نویسه‌های خاص را ایمن‌تر مدیریت می‌کند. -r از اجرا با ورودی خالی جلوگیری می‌کند.

متغیرها، نقل‌قول و شرط‌ها

name="backup"
printf 'Task: %s\n' "$name"
if [ -f /etc/hosts ]; then echo "hosts exists"; fi
[[ "$USER" == root ]] && echo "root"          # [[ ]] در bash: امن‌تر
echo "${LOG_DIR:-/var/log}"                   # مقدار پیش‌فرض
echo "${TARGET:?TARGET باید تنظیم شود}"       # اگر خالی بود، خطا و توقف
files=(a.txt b.txt "c d.txt")                 # آرایه
for f in "${files[@]}"; do echo "$f"; done
while IFS= read -r line; do echo "$line"; done < input.txt   # خواندن امن خط‌به‌خط

متغیرها را معمولاً داخل نقل‌قول دوتایی بگذارید: "$variable"؛ این کار از شکستن ناخواسته بر اساس فاصله و glob جلوگیری می‌کند. $(...) را به‌جای backtick بنویسید.

قالب امن اسکریپت

#!/usr/bin/env bash
# نمونه: قالب اسکریپت عملیاتی ایمن
set -Eeuo pipefail
umask 077

LOG_TAG="example-task"
WORKDIR="$(mktemp -d)"
LOCK=/var/lock/example-task.lock

log()  { logger -t "$LOG_TAG" -- "$*"; printf '[%s] %s\n' "$(date -Is)" "$*"; }
die()  { log "ERROR: $*"; exit 1; }
cleanup() { rm -rf -- "${WORKDIR:?}"; }
trap cleanup EXIT
trap 'die "خطا در خط $LINENO (کد $?)"' ERR

usage() { echo "usage: $0 [-n] TARGET_DIR"; exit 2; }
DRY=0
while getopts ':n' opt; do case $opt in n) DRY=1;; *) usage;; esac; done
shift $((OPTIND - 1))
[ $# -eq 1 ] || usage
TARGET="$1"

exec 9>"$LOCK"; flock -n 9 || die "نمونهٔ دیگری در حال اجراست"
[ -d "$TARGET" ] || die "مسیر نامعتبر: $TARGET"

log "شروع (dry-run=$DRY)"
if [ "$DRY" -eq 1 ]; then
  find "$TARGET" -type f -mtime +30 -print
else
  find "$TARGET" -type f -mtime +30 -delete
fi
log "پایان"

نکات:

  • set -e رفتار توقف را در بعضی زمینه‌ها تغییر می‌دهد (داخل if، &&، ||)؛ آن را جایگزین مدیریت خطا و بررسی exit code ندانید. set -u متغیر تعریف‌نشده را خطا می‌کند؛ set -o pipefail خطای فرمان‌های میانی pipeline را پنهان نمی‌کند.
  • اسکریپت را ابتدا با داده آزمایشی، گزینهٔ dry-run و کاربر محدود آزمایش کنید.
  • قبل از کار مخرب، ورودی را اعتبارسنجی کنید (مسیر خالی، /، mount نبودن).

ابزارهای کیفیت

bash -n script.sh               # فقط بررسی نحو
bash -x script.sh               # اجرا با ردیابی هر خط
shellcheck script.sh            # تحلیل ایستا (نصب: apt install shellcheck) ← بیشتر دام‌ها را می‌گیرد

دام pipefail با grep -q / head

cmd | grep -q x با pipefail ممکن است به‌خاطر SIGPIPE کد خطا بدهد حتی وقتی تطبیق یافت شده. برای شرط‌ها از if cmd | grep -q x; then دقت کنید یا خروجی را در متغیر بگیرید.

کد خروج

some_command
echo "$?"
some_command || echo "شکست خورد"

کد 0 معمولاً موفقیت و غیرصفر خطا/وضعیت خاص را نشان می‌دهد. برای pipeline، set -o pipefail کمک می‌کند خطای فرمان‌های میانی پنهان نشود. کدهای رایج: 1 عمومی، 2 سوءاستفاده از دستور، 126 اجرا نشد (مجوز)، 127 پیدا نشد، 130 Ctrl+C، 137 کشته شد با KILL/OOM (128+9)، 143 با TERM (128+15).

ابزارهای کمکی روزمره

watch -n 2 'df -h /var'                 # تکرار هر ۲ ثانیه
timeout 10 curl -sS https://example.com # قطع بعد از ۱۰ ثانیه
column -t -s: /etc/passwd | head
date -d '2 days ago' +%F
printf '%s\n' "نمونه" | wc -m           # شمارش نویسه (UTF-8)

خطاهای رایج

نشانه علت احتمالی راه‌حل
: command not found با ^M پایان خط ویندوز (CRLF) sed -i 's/\r$//' script.sh ؛ dos2unix
unbound variable set -u و متغیر تنظیم‌نشده ${VAR:-} یا تعریف متغیر
اسکریپت با فاصله در نام فایل می‌شکند نقل‌قول فراموش شده "$var" ؛ -print0/xargs -0
rm -rf "$DIR/"* روی /* رفت DIR خالی بود "${DIR:?}"
[: too many arguments متغیر بدون نقل‌قول با فاصله [[ ]] یا "$var"
pipeline خطا را نمی‌گیرد بدون pipefail set -o pipefail
sed -i فایل را خراب کرد regex اشتباه، بدون بکاپ -i.bak ؛ اول بدون -i

خلاصهٔ اجرایی

  1. set -Eeuo pipefail + trap + flock + dry-run برای هر اسکریپت.
  2. همه چیز را نقل‌قول کنید؛ ${VAR:?} قبل از حذف.
  3. shellcheck همیشه.
  4. sed -i.bak، find -print قبل از -delete.