การออกแบบฟังก์ชันด้วยขอบเขตภายในและรหัสส่งคืน
เขียนฟังก์ชันที่ใช้ตัวแปรภายใน สถานะการออก และค่าส่งคืนจาก printf แทนตัวแปรส่วนกลางที่เปราะบาง
การออกแบบฟังก์ชันด้วยขอบเขตภายในและรหัสส่งคืน เป็นบทเรียน DevOps Bootcamp ฟรีบน CoddyKit นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 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
เมื่อฟังก์ชันต้องส่งคืน ผลลัพธ์ที่เป็นสตริง รูปแบบมาตรฐานคือพิมพ์ผลลัพธ์ไปยังเอาต์พุตมาตรฐาน แล้วรับค่าไว้ด้วย การแทนที่คำสั่ง $() ควรใช้ 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) และปลดล็อคส่วนที่เหลือของคอร์ส DevOps Bootcamp ให้อัปเกรดเป็น CoddyKit PRO คอร์ส DevOps Bootcamp มีบทเรียนทั้งหมด 4 บทเรียน
คุณจะเรียนรู้อะไรในบทเรียน “การออกแบบฟังก์ชันด้วยขอบเขตภายในและรหัสส่งคืน”
เขียนฟังก์ชันที่ใช้ตัวแปรภายใน สถานะการออก และค่าส่งคืนจาก printf แทนตัวแปรส่วนกลางที่เปราะบาง คุณปฏิบัติ DevOps Bootcamp ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน
คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน DevOps Bootcamp หรือไม่
ไม่จำเป็นต้องมีประสบการณ์มาก่อน DevOps Bootcamp บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน
บทเรียน “การออกแบบฟังก์ชันด้วยขอบเขตภายในและรหัสส่งคืน” ใช้เวลานานแค่ไหน
บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย
ฉันเขียนและรันโค้ดในบทเรียน DevOps Bootcamp นี้ได้ไหม
ได้ บทเรียน DevOps Bootcamp ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ
บทเรียนทั้งหมดในหลักสูตรนี้
- การออกแบบฟังก์ชันด้วยขอบเขตภายในและรหัสส่งคืน
- การสร้างและโหลดไลบรารี Bash ที่นำกลับมาใช้ซ้ำได้
- การแยกแฟล็กและอาร์กิวเมนต์ด้วย getopts
- การส่งอาร์เรย์และแผนที่เชื่อมโยงระหว่างฟังก์ชัน