0Pricing
Linux Command Line & Bash Scripting Mastery · บทเรียน

การออกแบบฟังก์ชันด้วยขอบเขตภายในและรหัสส่งคืน

เขียนฟังก์ชันที่ใช้ตัวแปรภายใน สถานะการออก และค่าส่งคืนจาก printf แทนตัวแปรส่วนกลางที่เปราะบาง

การออกแบบฟังก์ชันด้วยขอบเขตภายในและรหัสส่งคืน เป็นบทเรียน Linux Command Line & Bash Scripting Mastery ฟรีบน CoddyKit นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน Linux Command Line & Bash Scripting Mastery และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส Linux Command Line & Bash Scripting Mastery มีบทเรียนทั้งหมด 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

เมื่อฟังก์ชันต้องส่งคืน ผลลัพธ์ที่เป็นสตริง รูปแบบมาตรฐานคือพิมพ์ผลลัพธ์ไปยังเอาต์พุตมาตรฐาน แล้วรับค่าไว้ด้วย การแทนที่คำสั่ง $() ควรใช้ 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 (การอ้างอิงชื่อ)

การแทนที่คำสั่ง $() จะทำงานใน เชลล์ย่อย การกำหนดค่าตัวแปรใด ๆ ภายในนั้นจะมองไม่เห็นจากเชลล์แม่ หากคุณต้องการให้ฟังก์ชันเขียนค่าลงในตัวแปรที่ผู้เรียกจัดเตรียมไว้โดยไม่ใช้เชลล์ย่อย ให้ใช้ การอ้างอิงชื่อ (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 (การอ้างอิงชื่อ) เมื่อต้องการเขียนค่าลงในตัวแปรที่ผู้เรียกจัดเตรียมไว้ โดยไม่ต้องเสียต้นทุนจากการใช้เชลล์ย่อย
  • ข้อกำหนดดักตรวจ (ตรวจสอบอาร์กิวเมนต์ตั้งแต่เนิ่น ๆ และส่งคืนทันทีเมื่อข้อมูลเข้าไม่ถูกต้อง) ช่วยให้ฟังก์ชันมีความทนทานและอธิบายการทำงานของตนเองได้
  • ไฟล์ไลบรารี ควรถูกโหลดด้วย source ใช้คำนำหน้าขอบเขตชื่อ ไม่เรียกใช้ exit และป้องกันการโหลดซ้ำ

การเชี่ยวชาญรูปแบบเหล่านี้คือสิ่งที่แยกสคริปต์เฉพาะกิจที่เปราะบางออกจากโค้ดเบส Bash ระดับมืออาชีพที่ดูแลรักษาได้

คำถามที่พบบ่อย

บทเรียน “การออกแบบฟังก์ชันด้วยขอบเขตภายในและรหัสส่งคืน” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “การออกแบบฟังก์ชันด้วยขอบเขตภายในและรหัสส่งคืน” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส Linux Command Line & Bash Scripting Mastery ให้อัปเกรดเป็น CoddyKit PRO คอร์ส Linux Command Line & Bash Scripting Mastery มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “การออกแบบฟังก์ชันด้วยขอบเขตภายในและรหัสส่งคืน”

เขียนฟังก์ชันที่ใช้ตัวแปรภายใน สถานะการออก และค่าส่งคืนจาก printf แทนตัวแปรส่วนกลางที่เปราะบาง คุณปฏิบัติ Linux Command Line & Bash Scripting Mastery ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน Linux Command Line & Bash Scripting Mastery หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน Linux Command Line & Bash Scripting Mastery บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน

บทเรียน “การออกแบบฟังก์ชันด้วยขอบเขตภายในและรหัสส่งคืน” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน Linux Command Line & Bash Scripting Mastery นี้ได้ไหม

ได้ บทเรียน Linux Command Line & Bash Scripting Mastery ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. การออกแบบฟังก์ชันด้วยขอบเขตภายในและรหัสส่งคืน
  2. การสร้างและโหลดไลบรารี Bash ที่นำกลับมาใช้ซ้ำได้
  3. การแยกแฟล็กและอาร์กิวเมนต์ด้วย getopts
  4. การส่งอาร์เรย์และแผนที่เชื่อมโยงระหว่างฟังก์ชัน
← กลับไปที่ Linux Command Line & Bash Scripting Mastery