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

การส่งอาร์เรย์และแผนที่เชื่อมโยงระหว่างฟังก์ชัน

ใช้ nameref และการขยายค่าแบบอ้อม เพื่อส่งโครงสร้างข้อมูลซับซ้อนเข้าและออกจากฟังก์ชันอย่างปลอดภัย

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

เหตุใดการส่งอาร์เรย์ให้ฟังก์ชันจึงทำได้ยุ่งยาก

ใน Bash อาร์เรย์และอาร์เรย์เชื่อมโยง ไม่ใช่ค่าแบบชั้นหนึ่ง เมื่อคุณพยายามส่งอาร์เรย์ให้ฟังก์ชัน คุณไม่สามารถใช้ $my_array ได้โดยตรง เพราะวิธีนี้จะขยายค่าออกมาเพียงองค์ประกอบแรกเท่านั้น

ลองพิจารณาความพยายามที่ใช้ไม่ได้ต่อไปนี้:

my_func "${my_array[@]}"

ฟังก์ชันจะได้รับรายการคำแบบแบน จึงไม่สามารถทราบได้ว่าองค์ประกอบหนึ่งสิ้นสุดตรงไหนและองค์ประกอบถัดไปเริ่มตรงไหน และโครงสร้างอาร์เรย์จะหายไปทั้งหมด

Bash มีกลไกที่เชื่อถือได้สองวิธีสำหรับส่งโครงสร้างข้อมูลที่ซับซ้อน:

  • การอ้างอิงด้วยชื่อ (declare -n) — การอ้างอิงตัวแปรอื่นด้วยชื่อ (Bash 4.3 ขึ้นไป)
  • การขยายค่าโดยอ้อม (${!varname[@]}) — เข้าถึงตัวแปรที่มีชื่อเก็บอยู่ในตัวแปรอื่น (สำหรับ Bash รุ่นเก่า)

บทเรียนนี้ครอบคลุมทั้งสองเทคนิค เพื่อให้คุณเขียนฟังก์ชันที่ทนทานและนำกลับมาใช้ซ้ำได้ ซึ่งทำงานกับอาร์เรย์และแมป

การส่งอาร์เรย์ด้วยชื่อโดยใช้การอ้างอิงด้วยชื่อ

แนวทางสมัยใหม่ที่สะอาดที่สุดคือ การอ้างอิงด้วยชื่อ: คุณส่ง ชื่อ ของอาร์เรย์เป็นสตริง แล้วภายในฟังก์ชันประกาศตัวแปรเฉพาะที่ซึ่งอ้างอิงไปยังอาร์เรย์นั้น

ไวยากรณ์สำคัญ:

  • declare -n ref="$1" — ขณะนี้ ref เป็นชื่อแทนของตัวแปรใดก็ตามที่ส่งมาเป็นอาร์กิวเมนต์แรก
  • คุณอ่าน วนซ้ำ และแก้ไข ref ได้เช่นเดียวกับอาร์เรย์ต้นฉบับทุกประการ

วิธีนี้หลีกเลี่ยงการคัดลอกอาร์เรย์ทั้งหมด และทำให้ฟังก์ชันทำงานกับอาร์เรย์ที่มีขนาดใดก็ได้

#!/usr/bin/env bash

print_array() {
    declare -n arr="$1"   # nameref: arr -> caller's variable
    echo "Array has ${#arr[@]} elements:"
    for elem in "${arr[@]}"; do
        echo "  - $elem"
    done
}

fruits=(apple banana cherry)
print_array fruits

การแก้ไขอาร์เรย์ของผู้เรียกภายในฟังก์ชัน

เนื่องจากการอ้างอิงด้วยชื่อเป็นชื่อแท้จริง การ เขียนค่าผ่านการอ้างอิงด้วยชื่อจะเปลี่ยนตัวแปรต้นฉบับของผู้เรียก จึงไม่จำเป็นต้องส่งค่ากลับด้วยการแสดงค่า หรือใช้ตัวแปรส่วนกลาง

ด้วยเหตุนี้ การอ้างอิงด้วยชื่อจึงเหมาะอย่างยิ่งกับฟังก์ชันที่แปลงอาร์เรย์โดยแก้ไขอาร์เรย์เดิมโดยตรง เช่น การปรับรูปแบบสตริง การกรององค์ประกอบ หรือการเรียงลำดับ

#!/usr/bin/env bash

to_uppercase() {
    declare -n _arr="$1"
    for i in "${!_arr[@]}"; do
        _arr[$i]="${_arr[$i]^^}"   # ^^ = uppercase in Bash 4+
    done
}

words=(hello world bash)
echo "Before: ${words[@]}"
to_uppercase words
echo "After:  ${words[@]}"

การคืนอาร์เรย์ใหม่ผ่านการอ้างอิงด้วยชื่อ

ฟังก์ชันยังสามารถ เขียนลงในอาร์เรย์ที่ผู้เรียกจัดเตรียมไว้ ได้ โดยรับชื่ออาร์เรย์เป็นพารามิเตอร์ข้อมูลออก ผู้เรียกสร้างอาร์เรย์ว่าง ส่งชื่อของอาร์เรย์นั้นไป แล้วฟังก์ชันจะเติมข้อมูลลงไป

รูปแบบนี้คล้ายสำนวนการเขียนโปรแกรมแบบดั้งเดิมในภาษา C ที่ส่งตัวชี้ไปยังบัฟเฟอร์ผลลัพธ์ แต่ในกรณีนี้ทำทั้งหมดภายในเชลล์

#!/usr/bin/env bash

get_even_numbers() {
    local -a source=("$@")
    # Last argument is the output array name — pop it off
    local out_name="${source[-1]}"
    unset 'source[-1]'

    declare -n _out="$out_name"
    _out=()   # clear
    for n in "${source[@]}"; do
        (( n % 2 == 0 )) && _out+=("$n")
    done
}

declare -a evens
get_even_numbers 1 2 3 4 5 6 7 8 evens
echo "Evens: ${evens[@]}"

กับดักการชนกันของการอ้างอิงด้วยชื่อ

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

ตัวอย่างเช่น หากผู้เรียกตั้งชื่ออาร์เรย์ว่า arr และฟังก์ชันใช้คำสั่ง declare -n arr="$1" การอ้างอิงด้วยชื่อ arr ก็จะชี้กลับมาที่ตัวเอง

หลักปฏิบัติเพื่อหลีกเลี่ยงปัญหานี้: เติมขีดล่างหน้าชื่อตัวแปรการอ้างอิงด้วยชื่อภายใน เช่น _arr, _ref, _out ผู้เรียกแทบไม่ใช้ชื่อในรูปแบบนี้ จึงทำให้การชนกันเกิดขึ้นได้ยากมาก

#!/usr/bin/env bash

# DANGER: caller uses variable named 'arr'
arr=(one two three)

bad_func() {
    declare -n arr="$1"   # collision! arr refers to itself
    echo "${arr[@]}"
}

good_func() {
    declare -n _arr="$1"  # underscore prefix avoids collision
    echo "${_arr[@]}"
}

# bad_func arr   # would hang / error
good_func arr

การส่งอาร์เรย์เชื่อมโยง (แมป) ด้วยชื่อ

อาร์เรย์เชื่อมโยง (declare -A) ทำงานกับการอ้างอิงด้วยชื่อได้ในลักษณะเดียวกัน ส่งชื่อของแมปเป็นสตริง แล้วประกาศการอ้างอิงด้วยชื่อภายในฟังก์ชัน คุณจะเข้าถึงเพื่ออ่านและเขียนคู่คีย์-ค่าได้ทั้งหมด

สิ่งนี้มีประโยชน์อย่างยิ่งสำหรับแมปการกำหนดค่า ตัวแยกวิเคราะห์ตัวเลือก และตารางค้นหาที่ต้องใช้ร่วมกันระหว่างฟังก์ชันในไลบรารี

#!/usr/bin/env bash

print_map() {
    declare -n _map="$1"
    echo "Map contents:"
    for key in "${!_map[@]}"; do
        printf '  %-15s => %s\n' "$key" "${_map[$key]}"
    done
}

declare -A config
config[host]="localhost"
config[port]="5432"
config[db]="myapp"

print_map config

การรวมแมปสองรายการด้วยการอ้างอิงด้วยชื่อ

เนื่องจากการอ้างอิงด้วยชื่อทำให้ฟังก์ชันมีชื่อแทนที่เชื่อมโยงกับข้อมูลจริงอยู่เสมอ คุณจึงสร้างเครื่องมืออย่าง การรวมแมป ได้อย่างเป็นระเบียบ ส่งแมปปลายทางและแมปต้นทางหนึ่งรายการขึ้นไปด้วยชื่อ จากนั้นฟังก์ชันจะวนอ่านคีย์จากต้นทางและเขียนคีย์เหล่านั้นลงในปลายทาง

การอ้างอิงด้วยชื่อทำให้สามารถเขียนฟังก์ชันทั่วไป merge_map เพียงฟังก์ชันเดียว ซึ่งทำงานกับอาร์เรย์เชื่อมโยงใด ๆ ในสคริปต์ของคุณได้

#!/usr/bin/env bash

merge_map() {
    # merge_map dest src
    declare -n _dest="$1"
    declare -n _src="$2"
    for key in "${!_src[@]}"; do
        _dest["$key"]="${_src[$key]}"
    done
}

declare -A defaults=([timeout]=30 [retries]=3 [verbose]=false)
declare -A overrides=([retries]=5 [log_level]=debug)

merge_map defaults overrides

for k in "${!defaults[@]}"; do
    echo "$k = ${defaults[$k]}"
done

การขยายค่าโดยอ้อม: ความเข้ากันได้กับ Bash รุ่นเก่า

หากคุณต้องรองรับ Bash 4.0–4.2 (หรือเชลล์เริ่มต้นรุ่นเก่าของ macOS) จะไม่สามารถใช้ declare -n ได้ ทางเลือกคือ การขยายค่าโดยอ้อมด้วย ${!varname}

การขยายค่าโดยอ้อมจะประเมินค่าของ varname ให้เป็นชื่อตัวแปร แล้วคืนค่าของตัวแปรนั้น สำหรับอาร์เรย์ คุณต้องใช้ร่วมกับไวยากรณ์พิเศษเพื่อรับองค์ประกอบทั้งหมด

ข้อจำกัดเมื่อเทียบกับการอ้างอิงด้วยชื่อ:

  • ในทางปฏิบัติอ่านได้อย่างเดียว (การเขียนค่ากลับต้องใช้ eval ซึ่งมีความเสี่ยงจากการแทรกคำสั่ง)
  • ไม่สามารถวนอ่านคีย์ของอาร์เรย์เชื่อมโยงได้อย่างปลอดภัยหากไม่ใช้ eval
  • ใช้การอ้างอิงด้วยชื่อเมื่อทำได้ และใช้การอ้างอิงโดยอ้อมเป็นทางเลือกสำรองเฉพาะเมื่อจำเป็น
#!/usr/bin/env bash

sum_array() {
    local arr_name="$1"
    # Indirect: build a temporary copy by expanding arr_name[@]
    local -a items
    eval "items=( \"\${${arr_name}[@]}\" )"
    local total=0
    for n in "${items[@]}"; do
        (( total += n ))
    done
    echo "$total"
}

numbers=(10 20 30 40)
result=$(sum_array numbers)
echo "Sum: $result"

การสร้างไลบรารีสคริปต์แบบแยกส่วน

ในโครงการ Bash ระดับมืออาชีพ ฟังก์ชันอรรถประโยชน์จะจัดเก็บไว้ใน ไฟล์ไลบรารี เช่น lib/array_utils.sh และถูกนำเข้ามาโดยสคริปต์ที่ต้องใช้งาน

รูปแบบมีดังนี้:

  • ไฟล์ไลบรารีแต่ละไฟล์กำหนดเฉพาะฟังก์ชันเท่านั้น ไม่มีผลข้างเคียงระดับบนสุด
  • ป้องกันการนำเข้าไฟล์ซ้ำด้วยตัวแปรบอกสถานะ: [[ -n $__LIB_LOADED ]] && return; __LIB_LOADED=1
  • สคริปต์ผู้ใช้ใช้ source หรือ . เพื่อโหลดไลบรารี
  • ฟังก์ชันแลกเปลี่ยนข้อมูลที่ซับซ้อนผ่านการอ้างอิงด้วยชื่อ ทำให้ตรรกะทั้งหมดอยู่ภายในตัวเอง

วิธีนี้ทำให้ได้สคริปต์ที่ทดสอบได้ นำกลับมาใช้ซ้ำได้ และดูแลรักษาได้ ไม่ใช่การแก้ปัญหาเชลล์เฉพาะกิจ

ตัวอย่างเชิงปฏิบัติ: ตัวแยกวิเคราะห์ตัวเลือกที่คืนค่าเป็นแมป

กรณีใช้งานจริง: ตัวแยกวิเคราะห์ตัวเลือกบรรทัดคำสั่งที่เติมข้อมูลลงในอาร์เรย์เชื่อมโยงที่ผู้เรียกจัดเตรียมไว้ ผู้เรียกส่งชื่ออาร์เรย์เป็นอาร์กิวเมนต์แรก ตามด้วยอาร์กิวเมนต์ "$@" จริง

รูปแบบนี้ทำให้สคริปต์ใด ๆ นำตัวแยกวิเคราะห์เดียวกันกลับมาใช้ได้ โดยไม่ต้องคัดลอกโค้ดหรือพึ่งพาตัวแปรส่วนกลาง

#!/usr/bin/env bash

parse_opts() {
    declare -n _opts="$1"
    shift
    while [[ $# -gt 0 ]]; do
        case "$1" in
            --host)   _opts[host]="$2";   shift 2 ;;
            --port)   _opts[port]="$2";   shift 2 ;;
            --user)   _opts[user]="$2";   shift 2 ;;
            *)        echo "Unknown option: $1" >&2; shift ;;
        esac
    done
}

declare -A opts=([host]=localhost [port]=5432 [user]=admin)
parse_opts opts --host db.example.com --port 3306

echo "Connecting to ${opts[host]}:${opts[port]} as ${opts[user]}"

การรวมการอ้างอิงด้วยชื่อของข้อมูลเข้าและข้อมูลออกในฟังก์ชันเดียว

ฟังก์ชันสามารถมี การอ้างอิงด้วยชื่อหลายรายการพร้อมกัน ได้ โดยรายการหนึ่งใช้สำหรับข้อมูลเข้า และอีกรายการใช้สำหรับข้อมูลออก วิธีนี้ทำให้สร้างฟังก์ชันแปลงข้อมูลบริสุทธิ์ได้: ฟังก์ชันอ่านโครงสร้างต้นทาง คำนวณผลลัพธ์ แล้วเขียนลงในโครงสร้างปลายทาง โดยไม่แตะต้องสถานะส่วนกลาง

การใช้ชื่อที่แตกต่างกันและเติมขีดล่างหน้าชื่อของการอ้างอิงด้วยชื่อแต่ละรายการ ทำให้ไม่เกิดการชนกัน แม้ตัวแปรของผู้เรียกจะมีชื่อสั้น ๆ ก็ตาม

#!/usr/bin/env bash

filter_map_by_key_prefix() {
    # Usage: filter_map_by_key_prefix src_map dest_map prefix
    declare -n _src="$1"
    declare -n _dst="$2"
    local prefix="$3"

    _dst=()
    for key in "${!_src[@]}"; do
        if [[ $key == ${prefix}* ]]; then
            _dst["$key"]="${_src[$key]}"
        fi
    done
}

declare -A settings=(
    [db_host]=localhost [db_port]=5432
    [cache_ttl]=300     [cache_driver]=redis
    [app_debug]=true
)
declare -A db_settings

filter_map_by_key_prefix settings db_settings db_

for k in "${!db_settings[@]}"; do
    echo "$k = ${db_settings[$k]}"
done

ตรวจสอบความเข้าใจ: การอ้างอิงด้วยชื่อและการส่งอาร์เรย์

ทดสอบความเข้าใจของคุณเกี่ยวกับการส่งอาร์เรย์และแมประหว่างฟังก์ชันใน Bash

ทบทวน: การส่งโครงสร้างข้อมูลที่ซับซ้อนใน Bash

สิ่งที่คุณได้เรียนรู้ในบทเรียนนี้มีดังนี้:

  • ไม่สามารถส่งอาร์เรย์แบบส่งค่าได้ — ให้ส่ง ชื่อ ของตัวแปรเป็นสตริงแทน
  • declare -n ref="$varname" สร้างการอ้างอิงด้วยชื่อ ซึ่งเป็นชื่อแทนที่เชื่อมโยงกับตัวแปรของผู้เรียกอยู่เสมอ ทั้งการอ่านและการเขียนจะส่งต่อไปยังต้นฉบับ ต้องใช้ Bash 4.3 ขึ้นไป
  • การชนกันของการอ้างอิงด้วยชื่อ เกิดขึ้นเมื่อตัวแปรภายในและภายนอกมีชื่อเดียวกัน ป้องกันได้โดยเติมขีดล่างหน้าชื่อการอ้างอิงด้วยชื่อภายใน เช่น _arr, _map
  • พารามิเตอร์ข้อมูลออกทำงานโดยส่งชื่อของอาร์เรย์หรือแมปผลลัพธ์ที่ว่างเปล่า แล้วให้ฟังก์ชันเติมข้อมูลผ่านการอ้างอิงด้วยชื่อ
  • การอ้างอิงด้วยชื่อหลายรายการในฟังก์ชันเดียว ทำให้แปลงข้อมูลจากข้อมูลเข้าเป็นข้อมูลออกได้โดยไม่มีผลข้างเคียงต่อสถานะส่วนกลาง
  • การขยายค่าโดยอ้อม (${!varname[@]} + eval) เป็นทางเลือกสำรองแบบเก่าสำหรับ Bash < 4.3 แต่มีความเสี่ยงจากการแทรกคำสั่ง และในทางปฏิบัติอ่านได้อย่างเดียว
  • ผสานเทคนิคเหล่านี้เข้ากับ ไฟล์ไลบรารีที่นำเข้า เพื่อสร้างโค้ด Bash ที่เป็นแบบแยกส่วน นำกลับมาใช้ซ้ำได้ และทดสอบได้

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

บทเรียน “การส่งอาร์เรย์และแผนที่เชื่อมโยงระหว่างฟังก์ชัน” ฟรีหรือไม่

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

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

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

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

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