0Pricing
DevOps Bootcamp · درس

تصميم الدوال باستخدام النطاق المحلي ورموز الإرجاع

اكتب دوالًا تستخدم متغيرات محلية وحالات خروج وقيم إرجاع مبنية على printf بدلًا من المتغيرات العامة الهشة.

تصميم الدوال باستخدام النطاق المحلي ورموز الإرجاع درس مجاني في DevOps Bootcamp على CoddyKit. هذا هو الدرس 1 من أصل 4. يمكنك قراءة الدرس كاملاً أدناه مجاناً — ثم تمرن عليه مباشرة في المتصفح باستخدام محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7. هذا الدرس جزء من مسار التعلم في DevOps Bootcamp، وتقدمك يتزامن عبر الويب وتطبيق CoddyKit. تتضمن دورة DevOps Bootcamp 4 دروس في المجموع.

أهمية نطاق الدالة

تكون المتغيرات في Bash عالمية افتراضيًا. ويتسرّب المتغير الذي يُعيّن داخل دالة إلى نطاق المستدعي ما لم تصرّح به صراحةً باستخدام local. وهذا مصدر شائع للأخطاء الدقيقة في نصوص الصدفة البرمجية.

  • يمكن للدوال التي لا تستخدم local أن تستبدل متغيرات المستدعي بصمت.
  • تُدمّر متغيرات local عند عودة الدالة.
  • تجعل حدود النطاق الواضحة الدوال قابلة لإعادة الاستخدام والاختبار بمعزل عن غيرها.

يعلّمك هذا الدرس كتابة دوال مستقلة بذاتها: تستخدم متغيرات local، وتتواصل بالنتائج عبر رموز الخروج وprintf، ولا تعتمد مطلقًا على حالة عالمية ضمنية.

مشكلة تسرّب المتغيرات العامة

إليك مثالًا عمليًا على تسرّب متغير عام. تضبط الدالة set_name متغيرًا يُسمّى result، فتستبدل بصمت متغير result الخاص بالوظيفة المستدعية.

شغّل هذا السكربت ولاحظ الناتج غير المتوقع — إذ يختفي متغير result الخاص بالوظيفة المستدعية بعد استدعاء الدالة.

#!/usr/bin/env bash

set_name() {
    result="Alice"   # No 'local' — this is GLOBAL
}

result="important data"
echo "Before: $result"

set_name

echo "After:  $result"   # Prints 'Alice', not 'important data'

تعريف المتغيرات المحلية باستخدام 'local'

تقيّد التعليمة المضمّنة local نطاق المتغير بالدالة المحيطة وأي دوال تستدعيها. خارج الدالة، يكون المتغير إما غير مضبوط أو يحتفظ بقيمته السابقة.

  • local varname — يعرّف المتغير دون إسناد قيمة إليه.
  • local varname="value" — يعرّف المتغير ويسند إليه قيمة في خطوة واحدة.
  • local -i count=0 — يعرّف متغيرًا محليًا من نوع عدد صحيح.
  • local -r PI=3.14159 — يعرّف ثابتًا محليًا للقراءة فقط.

أفضل ممارسة: عرّف كل متغير داخل الدالة باستخدام local، ما لم تكن تحتاج إليه عمدًا ليكون عامًا.

#!/usr/bin/env bash

greet() {
    local name="$1"          # local — safe
    local greeting="Hello, ${name}!"
    echo "$greeting"
}  # 'name' and 'greeting' vanish here

name="global value"
greet "Bob"
echo "name is still: $name"   # Prints 'global value'

رموز الخروج كقيم إرجاع

لا تستطيع دوال Bash إرجاع سلاسل نصية باستخدام return — إذ تضبط return فقط حالة خروج صحيحة (من 0 إلى 255). ووفقًا للعرف:

  • return 0 — نجاح
  • return 1 (أو أي قيمة غير صفرية) — فشل

تقرأ الوظيفة المستدعية حالة الخروج عبر $? مباشرة بعد الاستدعاء، أو تستخدم الدالة مباشرةً في شرط if. وتُعد رموز الخروج الطريقة الاصطلاحية للإشارة إلى النجاح أو الفشل من دالة.

#!/usr/bin/env bash

is_even() {
    local -i n="$1"
    (( n % 2 == 0 ))   # arithmetic command: exits 0 if true, 1 if false
}

for num in 2 3 7 10; do
    if is_even "$num"; then
        echo "$num is even"
    else
        echo "$num is odd"
    fi
done

تمرير نتائج السلاسل النصية عبر printf

عندما تحتاج الدالة إلى إرجاع نتيجة نصية، فإن النمط القياسي هو الطباعة إلى stdout والتقاط الناتج باستخدام استبدال الأوامر $(). ويُفضّل استخدام printf بدلًا من echo للأسباب التالية:

  • لا يضيف printf سطرًا جديدًا في النهاية افتراضيًا (إلا إذا أدرجت \n).
  • سلوك printf متسق ومحدد وفق POSIX، بينما يختلف echo بين الصدَفات.
  • يزيل استبدال الأوامر الأسطر الجديدة اللاحقة، لذا فإن printf '%s' "$value" دقيق.
#!/usr/bin/env bash

to_uppercase() {
    local input="$1"
    printf '%s' "${input^^}"   # Bash 4+ parameter expansion
}

word="hello"
upper=$(to_uppercase "$word")
echo "Original: $word"
echo "Upper:    $upper"

الجمع بين رموز الخروج والناتج في stdout

يمكن للدالة المصممة جيدًا أن تطبع نتيجة (عند النجاح) وأن تشير إلى الفشل (عبر رمز الخروج) في الوقت نفسه. وتحدد الوظيفة المستدعية الإجراء الذي ستتخذه بناءً على رمز الخروج قبل الوثوق بالناتج.

يُستخدم النمط التالي على نطاق واسع في مكتبات Bash الحقيقية:

  • عند النجاح: اطبع النتيجة باستخدام printf ونفّذ return 0.
  • عند الفشل: اكتب رسالة تشخيصية إلى stderr (وليس stdout) ونفّذ return 1.
  • يُبقي إرسال الأخطاء إلى stderr مخرج stdout نظيفًا لتمريره عبر الأنابيب.
#!/usr/bin/env bash

divide() {
    local -i numerator="$1"
    local -i denominator="$2"

    if (( denominator == 0 )); then
        printf 'Error: division by zero\n' >&2
        return 1
    fi

    printf '%d' $(( numerator / denominator ))
    return 0
}

if result=$(divide 20 4); then
    echo "20 / 4 = $result"
else
    echo "Division failed."
fi

if result=$(divide 10 0); then
    echo "10 / 0 = $result"
else
    echo "Division failed (caught the error)."
fi

استخدام 'local' لحماية الدوال التكرارية

يُعد الاستدعاء التكراري من أوضح الأمثلة على أهمية local. يحصل كل استدعاء تكراري على نسخة مستقلة خاصة به من كل متغير local في مكدس الاستدعاءات. ومن دون local، سيستبدل كل استدعاء المتغير العام نفسه، ما يؤدي إلى نتائج خاطئة.

تُعد دالة المضروب أدناه آمنة لأن n وsub محليان لكل إطار في المكدس.

#!/usr/bin/env bash

factorial() {
    local -i n="$1"
    local -i sub

    if (( n <= 1 )); then
        printf '1'
        return 0
    fi

    sub=$(factorial $(( n - 1 )))
    printf '%d' $(( n * sub ))
}

for i in 1 2 3 4 5 6; do
    echo "${i}! = $(factorial $i)"
done

تجنّب فخ الصدفة الفرعية باستخدام local -n (Nameref)

يُنفّذ استبدال الأوامر $() داخل صدفة فرعية. وتكون أي إسنادات للمتغيرات داخله غير مرئية للصدفة الأصلية. عندما تحتاج إلى أن تكتب دالة في متغير يقدمه المستدعي دون استخدام صدفة فرعية، فاستخدم مرجعًا اسميًا (local -n)، وهو متاح في Bash 4.3 والإصدارات الأحدث.

  • يجعل local -n ref="$1" من ref اسمًا مستعارًا للمتغير الذي تُخزّن قيمته الاسمية في $1.
  • يؤدي إسناد قيمة إلى ref داخل الدالة إلى تغيير متغير المستدعي مباشرةً.
  • يؤدي ذلك إلى تجنّب الصدفة الفرعية مع إبقاء تفاصيل التنفيذ محلية.
#!/usr/bin/env bash

# Fills caller's array by reference — no subshell needed
read_csv_line() {
    local -n _out="$1"    # nameref to caller's variable
    local line="$2"
    local IFS=','
    read -ra _out <<< "$line"
}

declare -a fields
read_csv_line fields "alice,30,engineer"

echo "Name:  ${fields[0]}"
echo "Age:   ${fields[1]}"
echo "Role:  ${fields[2]}"

إنشاء مكتبة دوال صغيرة

تقسّم مشاريع Bash الواقعية الدوال القابلة لإعادة الاستخدام إلى ملفات مكتبة تستوردها السكربتات باستخدام source (أو عامل النقطة .). قواعد تصميم المكتبات الجيدة:

  • يجب أن يكون كل متغير داخل دالة مكتبة local.
  • لا تنفّذ دوال المكتبة exit مطلقًا — بل تنفّذ return حتى يظل المستدعي قيد التشغيل.
  • استخدم بادئة مساحة أسماء متسقة (مثل str_ وlog_) لتجنّب تعارض الأسماء.
  • امنع الاستيراد المزدوج باستخدام متغير حارس.

فيما يلي مكتبة أدوات سلاسل نصية بسيطة تتبع هذه الاصطلاحات.

#!/usr/bin/env bash
# lib/str.sh  — string utility library

[[ -n "${_LIB_STR_LOADED:-}" ]] && return 0
_LIB_STR_LOADED=1

str_trim() {
    local str="$1"
    str="${str#"${str%%[![:space:]]*}"}"
    str="${str%"${str##*[![:space:]]}"}" 
    printf '%s' "$str"
}

str_repeat() {
    local -i times="$2"
    local char="$1"
    local -i i
    for (( i = 0; i < times; i++ )); do
        printf '%s' "$char"
    done
}

str_contains() {
    local haystack="$1"
    local needle="$2"
    [[ "$haystack" == *"$needle"* ]]
}

# --- self-test when executed directly ---
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
    trimmed=$(str_trim "   hello world   ")
    echo "Trimmed: '${trimmed}'"
    str_repeat '-' 20; echo
    if str_contains "bash scripting" "script"; then
        echo "Contains: yes"
    fi
fi

التحقق من الوسائط داخل الدوال

ينبغي للدوال التي تستقبل وسائط أن تتحقق منها مبكرًا وأن تعيد رمز خروج محددًا عند وجود إدخال غير صالح. يُسمّى هذا نمط شرط الحراسة — الفشل بسرعة وبوضوح.

  • تحقق من عدد الوسائط باستخدام $#.
  • تحقق من الأنواع أو التنسيقات قبل تنفيذ أي عمل.
  • اطبع الرسائل التشخيصية إلى stderr فقط، وليس إلى stdout مطلقًا.
  • استخدم رموز إرجاع غير صفرية مميزة (مثلًا: 1 = وسائط غير صحيحة، و2 = الملف غير موجود) حتى يتمكن المستدعون من الاستجابة بطرق مختلفة لأنماط الفشل المختلفة.
#!/usr/bin/env bash

file_line_count() {
    if (( $# != 1 )); then
        printf 'Usage: file_line_count <file>\n' >&2
        return 1
    fi

    local file="$1"

    if [[ ! -f "$file" ]]; then
        printf 'Error: not a file: %s\n' "$file" >&2
        return 2
    fi

    if [[ ! -r "$file" ]]; then
        printf 'Error: cannot read: %s\n' "$file" >&2
        return 3
    fi

    local -i count
    count=$(wc -l < "$file")
    printf '%d' "$count"
    return 0
}

# Test with /etc/hosts (exists on every Linux/macOS system)
if lines=$(file_line_count /etc/hosts); then
    echo "/etc/hosts has $lines lines"
else
    echo "Failed with exit code: $?"
fi

جمع كل شيء معًا: مثال واقعي

إليك سكربت كامل مستقل بذاته يوضّح عمل جميع مفاهيم هذا الدرس معًا:

  • متغيرات local في كل دالة.
  • رموز الخروج للإشارة إلى النجاح أو الفشل.
  • استخدام printf لتمرير النتائج النصية.
  • كتابة الأخطاء إلى stderr والنتائج إلى stdout.
  • استخدام شروط الحراسة للتحقق من الوسائط.

ادرس مسار التنفيذ: تستخرج parse_version البيانات، وتقارنها version_ge، ثم تستخدم main كلتيهما بطريقة سليمة.

#!/usr/bin/env bash

# Parse a semver string into components via nameref
parse_version() {
    local -n _major="$2" _minor="$3" _patch="$4"
    local version="$1"
    local IFS='.'
    local -a parts
    read -ra parts <<< "$version"
    _major="${parts[0]:-0}"
    _minor="${parts[1]:-0}"
    _patch="${parts[2]:-0}"
}

# Return 0 if version $1 >= version $2
version_ge() {
    local -i maj_a min_a pat_a
    local -i maj_b min_b pat_b
    parse_version "$1" maj_a min_a pat_a
    parse_version "$2" maj_b min_b pat_b

    if   (( maj_a != maj_b )); then (( maj_a > maj_b ))
    elif (( min_a != min_b )); then (( min_a > min_b ))
    else                             (( pat_a >= pat_b ))
    fi
}

require_bash_version() {
    local required="$1"
    local actual="${BASH_VERSION%%(*}"
    if version_ge "$actual" "$required"; then
        printf 'Bash %s satisfies >= %s\n' "$actual" "$required"
        return 0
    else
        printf 'Error: need Bash >= %s, got %s\n' "$required" "$actual" >&2
        return 1
    fi
}

main() {
    require_bash_version "4.3" || return 1
    require_bash_version "99.0" || true   # demonstrates failure path
}

main

اختبار المعرفة: المتغيرات المحلية وقيم الإرجاع

تأمل دالة Bash التالية. ما الطريقة الصحيحة لالتقاط نتيجتها النصية في الوظيفة المستدعية، وما العبارة الصحيحة بشأن المتغير tmp؟

transform() {
    local tmp="${1,,}"   # lowercase
    printf '%s' "$tmp"
    return 0
}

مراجعة: الدوال ذات النطاق المحلي ورموز الإرجاع

تعلّمت في هذا الدرس كيفية كتابة دوال Bash نظيفة وقابلة للتركيب وآمنة:

  • استخدم local دائمًا للمتغيرات داخل الدوال لمنع تلويث نطاق المستدعي.
  • استخدم رموز الخروج (return 0/1/N) للإشارة إلى النجاح أو الفشل — فهي تتكامل طبيعيًا مع if و&& و||.
  • استخدم printf إلى stdout لتمرير النتائج النصية، والتقطها باستخدام $() في الوظيفة المستدعية.
  • اكتب الأخطاء إلى stderr (>&2) حتى يظل stdout نظيفًا لتدفق البيانات وتمريرها عبر الأنابيب.
  • استخدم local -n (nameref) عندما تحتاج إلى الكتابة في متغير يقدمه المستدعي دون كلفة الصدفة الفرعية.
  • تجعل شروط الحراسة (التحقق من الوسائط مبكرًا والإرجاع فورًا عند الإدخال غير الصالح) الدوال متينة وموضحة لذاتها.
  • ينبغي استيراد ملفات المكتبة، واستخدام بادئات مساحات الأسماء، وعدم استدعاء exit مطلقًا، ومنع الاستيراد المزدوج.

إن إتقان هذه الأنماط هو ما يميّز بين السكربتات الهشة المعدّة لمرة واحدة وقواعد شيفرة Bash الاحترافية القابلة للصيانة.

الأسئلة الشائعة

هل درس «تصميم الدوال باستخدام النطاق المحلي ورموز الإرجاع» مجاني؟

نعم — نص درس «تصميم الدوال باستخدام النطاق المحلي ورموز الإرجاع» كامل متاح مجاناً هنا على الويب. لتمرينه بشكل تفاعلي (محرر أكواد مدمج ومدرس ذكاء اصطناعي متاح 24/7) وفتح باقي دورة DevOps Bootcamp، انتقل إلى CoddyKit PRO. تتضمن دورة DevOps Bootcamp 4 دروس في المجموع.

ماذا ستتعلم في «تصميم الدوال باستخدام النطاق المحلي ورموز الإرجاع»؟

اكتب دوالًا تستخدم متغيرات محلية وحالات خروج وقيم إرجاع مبنية على printf بدلًا من المتغيرات العامة الهشة. تتمرن على DevOps Bootcamp مع أكواد عملية تشغلها مباشرة في المتصفح، ومدرس ذكاء اصطناعي متاح 24/7 يجيب على أسئلتك أثناء عملك.

هل أحتاج إلى خبرة سابقة لأبدأ DevOps Bootcamp؟

لا تُشترط خبرة سابقة. DevOps Bootcamp على CoddyKit منظم للمبتدئين حتى المتقدمين، لذا يمكنك البدء من هنا أو من البداية والتقدم بسرعتك الخاصة. هذا هو الدرس 1 من أصل 4.

كم من الوقت يستغرق درس «تصميم الدوال باستخدام النطاق المحلي ورموز الإرجاع»؟

معظم دروس CoddyKit تستغرق حوالي 5–10 دقائق. كل منها موجز وتفاعلي، لذا تحرز تقدماً مستمراً وتستأنف من حيث توقفت عبر الويب والتطبيق.

هل يمكنني كتابة وتشغيل أكواد في درس DevOps Bootcamp هذا؟

نعم. كل درس في DevOps Bootcamp يتضمن محرر أكواد مدمج، لذا تكتب وتشغل أكواداً حقيقية مباشرة في متصفحك وتحصل على تعليقات فورية من الذكاء الاصطناعي — بدون إعداد محلي.

جميع الدروس في هذه الدورة

  1. تصميم الدوال باستخدام النطاق المحلي ورموز الإرجاع
  2. بناء مكتبات Bash قابلة لإعادة الاستخدام وتحميلها
  3. تحليل الأعلام والوسائط باستخدام getopts
  4. تمرير المصفوفات والخرائط الترابطية بين الدوال
← العودة إلى DevOps Bootcamp