Bash for Oracle DBAs Part 7: Functions and Reusability in Bash

Bash برای Oracle DBAها — بخش ۷: Functions و استفاده مجدد در Bash

تا اینجا با متغیرها، شرط‌ها، حلقه‌ها، exit status و command substitution آشنا شدیم. اما وقتی اسکریپت‌های Oracle DBA بزرگ‌تر و پیچیده‌تر می‌شوند — مثلاً یک اسکریپت Audit که ده‌ها check مختلف انجام می‌دهد — تکرار کد و دشواری نگهداری مشکلات جدی ایجاد می‌کند. راه‌حل استاندارد در Bash، استفاده از Functions است.

در این مقاله با تعریف تابع، ارسال پارامتر، متغیرهای محلی، بازگشت مقدار و الگوهای رایج در اسکریپت‌های Audit/Remediation آشنا می‌شویم.

۱) چرا Functions؟

بدون تابع، اگر یک بلوک منطق (مثلاً بررسی وجود یک فایل و چاپ پیام PASS/FAIL) در چند جای اسکریپت لازم باشد، باید آن را کپی کنید. این یعنی:

  • کد تکراری و مستعد خطا؛
  • اگر منطق تغییر کند، باید همه‌ی نسخه‌ها را دستی اصلاح کنید؛
  • خوانایی کاهش می‌یابد.

Function به ما اجازه می‌دهد یک بلوک کد را یک‌بار تعریف کنیم و بارها صدا بزنیم. این دقیقاً همان فلسفه‌ی PROCEDURE و FUNCTION در Oracle PL/SQL است.

۲) تعریف Function — دو شکل سینتکس

در Bash دو روش برای تعریف تابع وجود دارد:

# شکل ۱: با کلیدواژه‌ی function
function greet {
    echo "Hello, Oracle DBA!"
}

# شکل ۲: با پرانتز (POSIX-compatible)
greet() {
    echo "Hello, Oracle DBA!"
}

هر دو کار می‌کنند، اما شکل دوم (name() { ... }) قابل‌حمل‌تر است و در اکثر shellها پشتیبانی می‌شود. در اسکریپت‌های حرفه‌ای معمولاً همین شکل ترجیح داده می‌شود.

۳) صدا زدن Function

برای صدا زدن یک تابع، فقط نام آن را بنویسید — بدون پرانتز:

greet() {
    echo "Hello, Oracle DBA!"
}

greet   # صدا زدن تابع

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

۴) پارامترها و آرگومان‌ها

تابع‌های Bash پارامترهای نام‌دار نمی‌گیرند (مثل PL/SQL). بلکه آرگومان‌ها به‌صورت positional parameters درون تابع در دسترس هستند:

  • $1, $2, ... : آرگومان اول، دوم، ...
  • $@ : تمام آرگومان‌ها به‌صورت لیست
  • "$@" : تمام آرگومان‌ها به‌صورت لیست safe (هر کدام quote شده)
  • $# : تعداد آرگومان‌ها
  • $0 : نام اسکریپت (نه نام تابع)
check_file() {
    local file="$1"
    if [[ -f "$file" ]]; then
        echo "OK: $file exists."
    else
        echo "WARN: $file not found."
    fi
}

check_file /etc/ssh/sshd_config
check_file /etc/oratab

در این مثال، $1 داخل تابع به اولین آرگومان پاس‌داده‌شده اشاره دارد. این همان الگویی است که در اسکریپت‌های Audit برای بررسی فایل‌های config استفاده می‌شود.

۵) متغیرهای محلی: local

به‌طور پیش‌فرض، تمام متغیرهای داخل تابع global هستند. یعنی اگر در تابع count=5 بنویسید، مقدار متغیر count در کل اسکریپت تغییر می‌کند. برای جلوگیری از این اثر جانبی، از کلیدواژه‌ی local استفاده می‌کنیم:

my_func() {
    local temp="temporary value"
    echo "Inside: $temp"
}

my_func
echo "Outside: $temp"   # خالی است، چون temp محلی بود

استفاده از local در اسکریپت‌های DBA یک best practice حیاتی است؛ چون بدون آن، یک تابع می‌تواند به‌طور ناخواسته متغیرهای سراسری اسکریپت را overwrite کند.

۶) بازگشت مقدار از Function

در Bash، تابع‌ها مانند PL/SQL مقدار را با RETURN برنمی‌گردانند. بلکه دو روش رایج وجود دارد:

روش ۱: return برای exit status

return یک عدد برمی‌گرداند (مانند exit status). فقط اعداد ۰ تا ۲۵۵ مجاز هستند:

is_root() {
    if [[ "$EUID" -eq 0 ]]; then
        return 0   # true
    else
        return 1   # false
    fi
}

if is_root; then
    echo "Running as root."
else
    echo "Not root."
fi

روش ۲: echo + command substitution برای مقدار متنی

اگر می‌خواهید یک رشته یا عدد را از تابع بگیرید، آن را echo کنید و با $(...) بگیرید:

get_oracle_home() {
    local home=""
    if [[ -f /etc/oratab ]]; then
        home=$(grep -E '^\s*+ORCL:' /etc/oratab | cut -d: -f2)
    fi
    echo "$home"
}

OH=$(get_oracle_home)
echo "Oracle Home: $OH"

این الگو در اسکریپت‌های DBA برای گرفتن مقادیری مثل ORACLE_HOME، نسخه‌ی دیتابیس، یا وضعیت یک service بسیار کاربردی است.

۷) Function برای لاگ‌کردن

یکی از پرکاربردترین الگوها در اسکریپت‌های Audit و Remediation، داشتن یک تابع log است که پیام‌ها را با برچسب PASS/FAIL/INFO چاپ می‌کند:

log_pass() {
    local msg="$1"
    echo -e "  \e[32m[PASS]\e[0m $msg"
}

log_fail() {
    local msg="$1"
    echo -e "  \e[31m[FAIL]\e[0m $msg"
}

log_info() {
    local msg="$1"
    echo -e "  \e[34m[INFO]\e[0m $msg"
}

# استفاده
log_pass "SSH config is compliant."
log_fail "Crypto policy is not set to FUTURE."
log_info "Checking crypto policies..."

این الگو دقیقاً همان سبکی است که در اسکریپت audit_07_System_Crypto_Policy.sh با echo -e و رنگ‌ها دیده می‌شود. اگر آن اسکریپت از Functions استفاده می‌کرد، این‌گونه بازنویسی می‌شد:

#!/bin/bash
set -euo pipefail

FAIL_COUNT=0

log_pass() { echo -e "  \e[32m[PASS]\e[0m $1"; }
log_fail() { echo -e "  \e[31m[FAIL]\e[0m $1"; FAIL_COUNT=$((FAIL_COUNT + 1)); }

check_crypto_policy() {
    local current
    current=$(update-crypto-policies --show 2>/dev/null)
    if [[ "$current" == "FUTURE" || "$current" == "FIPS" ]]; then
        log_pass "System Crypto Policy is $current."
    else
        log_fail "System Crypto Policy is '$current', expected FUTURE or FIPS."
    fi
}

check_crypto_policy

همانطور که می‌بینید، استفاده از Functions باعث می‌شود منطق هر check مجزا، خوانا و قابل‌نگهداری باشد.

۸) Function برای بررسی Root

در اسکریپت remediate_07_System_Crypto_Policy.sh این الگو دیده می‌شود:

if [ "$EUID" -ne 0 ]; then echo "Please run as root"; exit 1; fi

با Functions می‌توان این را به یک تابع قابل‌استفاده‌مجدد تبدیل کرد:

require_root() {
    if [[ "$EUID" -ne 0 ]]; then
        echo "ERROR: This script must be run as root."
        exit 1
    fi
}

require_root   # صدا زدن در ابتدای اسکریپت

حالا این تابع را می‌توان در هر اسکریپت remediation استفاده کرد.

۹) Function با چند آرگومان

تابع‌ها می‌توانند چندین آرگومان بگیرند. در این مثال، یک تابع برای بررسی تطبیق یک مقدار با یک الگو:

check_value() {
    local label="$1"
    local actual="$2"
    local expected="$3"

    if [[ "$actual" == "$expected" ]]; then
        log_pass "$label is '$actual'."
    else
        log_fail "$label is '$actual', expected '$expected'."
    fi
}

check_value "Crypto Policy" "$CURRENT_POLICY" "FUTURE"
check_value "SELinux Mode" "$(getenforce)" "Enforcing"

با این الگو، تمام checkهای مشابه با یک خط کد انجام می‌شوند و منطق فقط در یک‌جا نگهداری می‌شود.

۱۰) متغیرهای ویژه‌ی Bash در Function

چند متغیر ویژه که درون تابع مفید هستند:

  • $FUNCNAME : نام تابع جاری (مفید برای debug)؛
  • $BASH_LINENO : شماره‌ی خطی که تابع از آن صدا زده شده؛
  • $? : exit status آخرین command داخل تابع.
debug_func() {
    echo "Called from function: $FUNCNAME"
    echo "At line: ${BASH_LINENO[0]}"
}

debug_func

۱۱) خواندن از stdin در Function

تابع‌ها می‌توانند ورودی را از stdin بخوانند. این الگو برای پردازش خط‌به‌خط خروجی یک command در یک تابع مفید است:

process_lines() {
    local count=0
    while IFS= read -r line; do
        ((count++))
    done
    echo "Total lines: $count"
}

df -h | process_lines

۱۲) الگوی Export و Reuse

اگر یک مجموعه‌ی تابع مفید دارید که در چند اسکریپت استفاده می‌کنید، می‌توانید آن‌ها را در یک فایل جداگانه قرار دهید و source کنید:

# File: /opt/oracle/scripts/lib/dba_functions.sh

log_pass() { echo -e "  \e[32m[PASS]\e[0m $1"; }
log_fail() { echo -e "  \e[31m[FAIL]\e[0m $1"; }
require_root() { [[ "$EUID" -ne 0 ]] && { echo "Run as root"; exit 1; }; }
#!/bin/bash
source /opt/oracle/scripts/lib/dba_functions.sh

require_root
log_pass "Functions loaded successfully."

source (یا .) محتوای فایل را در shell جاری اجرا می‌کند، طوری که انگار آن کد مستقیماً در اسکریپت نوشته شده است. این دقیقاً مشابه @import یا INCLUDE در سایر زبان‌هاست.

۱۳) خطاهای رایج

۱. فراموش کردن local:

# خطرناک: i سراسری است و ممکن است با متغیر بیرون تداخل کند
count_lines() {
    i=0
    while IFS= read -r _; do ((i++)); done
    echo "$i"
}

# امن
count_lines() {
    local i=0
    while IFS= read -r _; do ((i++)); done
    echo "$i"
}

۲..quote نکردن "$@" در حلقه:

# نادرست: اگر آرگومان فاصله داشته باشد، می‌شکند
for arg in $@; do echo "$arg"; done

# درست
for arg in "$@"; do echo "$arg"; done

۳. صدا زدن تابع قبل از تعریف:

check_root   # خطا: تابع هنوز تعریف نشده!

require_root() { ... }

همیشه توابع را در ابتدای اسکریپت تعریف کنید و منطق اصلی را در انتها قرار دهید.

۱۴) ساختار پیشنهادی یک اسکریپت DBA

#!/bin/bash
set -euo pipefail

# ── 1. Constants ──────────────────────────
ORACLE_BASE="/u01/app/oracle"
LOG_FILE="/var/log/oracle_audit.log"

# ── 2. Function Definitions ───────────────
log_pass()    { ... }
log_fail()    { ... }
require_root() { ... }
check_file()  { ... }

# ── 3. Main Logic ─────────────────────────
require_root
check_file "/etc/ssh/sshd_config"
check_file "$ORACLE_BASE/network/admin/sqlnet.ora"

# ── 4. Summary ───────────────────────────
echo "Audit complete. Failures: $FAIL_COUNT"

این ساختار، اسکریپت را تمیز، خوانا و قابل‌نگهداری نگه می‌دارد.

۱۵) جمع‌بندی

Functions ابزار اصلی ساختارمند کردن اسکریپت‌های Bash هستند. در این بخش یاد گرفتیم که:

  • تابع با name() { ... } تعریف می‌شود؛
  • آرگومان‌ها با $1، $2، "$@" و $# در دسترس‌اند؛
  • local از تداخل متغیرها جلوگیری می‌کند؛
  • return برای exit status و echo + $(...) برای مقدار متنی استفاده می‌شود؛
  • الگوهای log_pass، log_fail و require_root در اسکریپت‌های Audit/Remediation بسیار پرکاربردند؛
  • source اجازه می‌دهد Functions را بین اسکریپت‌ها به‌اشتراک بگذاریم.

در مقاله بعدی به سراغ Redirection، Pipes و Streams می‌رویم؛ یعنی چطور stdin، stdout و stderr را کنترل کنیم، خروجی‌ها را به فایل هدایت کنیم، و چند command را با pipe به هم متصل کنیم — مفاهیمی که در اسکریپت‌های Hardening به‌وفور دیده می‌شوند.


مقاله قبلی: Bash برای Oracle DBAها — بخش ۶: Command Substitution و Here Docs در Bash

مقاله بعدی: Bash برای Oracle DBAها — بخش ۸: Redirection، Pipes و Streams در Bash