แม้ว่าวิศวกรซอฟต์แวร์จะใช้บรรทัดคำสั่งเป็นประจำสำหรับการพัฒนาในหลายแง่มุม แต่อาร์เรย์ก็เป็นหนึ่งในคุณลักษณะที่ไม่ชัดเจนของบรรทัดคำสั่ง (แม้ว่าจะไม่ใช่ คลุมเครือเหมือนโอเปอเรเตอร์ regex =~
). แต่ไวยากรณ์ที่ไม่ชัดเจนและน่าสงสัยนั้น อาร์เรย์ของ Bash นั้นทรงพลังมาก
เดี๋ยวก่อน แต่ทำไม
การเขียนเกี่ยวกับ Bash เป็นเรื่องที่ท้าทายเพราะเป็นเรื่องง่ายอย่างน่าทึ่งสำหรับบทความที่จะพัฒนาเป็นคู่มือที่เน้นที่ความแปลกประหลาดของไวยากรณ์ อย่างไรก็ตาม โปรดวางใจว่าเจตนาของบทความนี้คือการหลีกเลี่ยงการให้ RTFM กับคุณ
ตัวอย่างจริง (มีประโยชน์จริง)
ให้พิจารณาสถานการณ์จริงและวิธีที่ Bash สามารถช่วยคุณได้:คุณกำลังนำความพยายามครั้งใหม่มาสู่บริษัทของคุณในการประเมินและเพิ่มประสิทธิภาพรันไทม์ของไปป์ไลน์ข้อมูลภายในของคุณ ในขั้นแรก คุณต้องทำการกวาดพารามิเตอร์เพื่อประเมินว่าไปป์ไลน์ใช้ประโยชน์จากเธรดได้ดีเพียงใด เพื่อความง่าย เราจะถือว่าไพพ์ไลน์เป็นกล่องดำ C++ ที่คอมไพล์แล้ว โดยพารามิเตอร์เดียวที่เราปรับแต่งได้คือจำนวนเธรดที่สงวนไว้สำหรับการประมวลผลข้อมูล:./pipeline --threads 4
.
พื้นฐาน
สิ่งแรกที่เราจะทำคือกำหนดอาร์เรย์ที่มีค่าของ --threads
พารามิเตอร์ที่เราต้องการทดสอบ:
allThreads=(1 2 4 8 16 32 64 128)
ในตัวอย่างนี้ องค์ประกอบทั้งหมดเป็นตัวเลข แต่ไม่จำเป็นต้องเป็นเช่นนั้น อาร์เรย์ใน Bash สามารถมีทั้งตัวเลขและสตริงได้ เช่น myArray=(1 2 "three" 4 "five")
เป็นนิพจน์ที่ถูกต้อง และเช่นเดียวกับตัวแปร Bash อื่นๆ อย่าลืมเว้นช่องว่างรอบเครื่องหมายเท่ากับ มิฉะนั้น Bash จะถือว่าชื่อตัวแปรเป็นโปรแกรมที่จะดำเนินการ และ =
เป็นพารามิเตอร์แรก!
ตอนนี้เราได้เริ่มต้นอาร์เรย์แล้ว มาดูองค์ประกอบบางส่วนกัน คุณจะสังเกตเห็นว่าเพียงแค่ทำ echo $allThreads
จะแสดงผลเฉพาะองค์ประกอบแรกเท่านั้น
เพื่อให้เข้าใจว่าเหตุใดจึงเป็นเช่นนั้น ให้ย้อนกลับไปทบทวนว่าเรามักจะส่งออกตัวแปรใน Bash อย่างไร พิจารณาสถานการณ์ต่อไปนี้:
type="article"
echo "Found 42 $type"
พูดตัวแปร $type
ให้เราเป็นคำนามเอกพจน์และเราต้องการเพิ่ม s
ในตอนท้ายของประโยคของเรา เราไม่สามารถเพิ่ม s
. ได้ง่ายๆ ถึง $type
เพราะนั่นจะเปลี่ยนเป็นตัวแปรอื่น $types
. และถึงแม้ว่าเราจะใช้การบิดเบือนโค้ดได้ เช่น echo "Found 42 "$type"s"
วิธีที่ดีที่สุดในการแก้ปัญหานี้คือการใช้วงเล็บปีกกา:echo "Found 42 ${type}s"
ซึ่งช่วยให้เราสามารถบอก Bash ได้ว่าชื่อของตัวแปรเริ่มต้นและสิ้นสุดที่ใด (น่าสนใจ นี่เป็นรูปแบบเดียวกับที่ใช้ใน JavaScript/ES6 เพื่อฉีดตัวแปรและนิพจน์ในตัวอักษรของเทมเพลต)
ดังนั้นแม้ว่าโดยทั่วไปตัวแปร Bash จะไม่ต้องการวงเล็บปีกกา แต่ก็จำเป็นสำหรับอาร์เรย์ ในทางกลับกัน ทำให้เราสามารถระบุดัชนีที่จะเข้าถึงได้ เช่น echo ${allThreads[1]}
ส่งคืนองค์ประกอบที่สองของอาร์เรย์ ไม่รวมวงเล็บ เช่น echo $allThreads[1]
, นำ Bash ไปปฏิบัติต่อ [1]
เป็นสตริงและส่งออกเป็นเช่นนี้
ใช่ อาร์เรย์ Bash มีไวยากรณ์แปลก ๆ แต่อย่างน้อยก็ไม่มีการจัดทำดัชนี ไม่เหมือนกับภาษาอื่น ๆ (ฉันกำลังดูคุณอยู่ R
)
วนซ้ำผ่านอาร์เรย์
แม้ว่าในตัวอย่างข้างต้น เราใช้ดัชนีจำนวนเต็มในอาร์เรย์ของเรา ลองพิจารณาสองครั้งที่กรณีนี้ไม่เป็นเช่นนั้น:อันดับแรก หากเราต้องการ $i
- องค์ประกอบของอาร์เรย์ โดยที่ $i
เป็นตัวแปรที่มีดัชนีที่น่าสนใจ เราสามารถดึงองค์ประกอบนั้นโดยใช้:echo ${allThreads[$i]}
. ประการที่สอง ในการเอาท์พุตองค์ประกอบทั้งหมดของอาร์เรย์ เราแทนที่ดัชนีตัวเลขด้วย @
สัญลักษณ์ (คุณสามารถนึกถึง @
ที่ยืนหยัดเพื่อ all
):echo ${allThreads[@]}
.
วนซ้ำผ่านองค์ประกอบอาร์เรย์
ด้วยเหตุนี้ เรามาวนซ้ำ $allThreads
และเปิดไปป์ไลน์สำหรับแต่ละค่าของ --threads
:
for t in ${allThreads[@]}; do
./pipeline --threads $t
done
วนรอบดัชนีอาร์เรย์
ต่อไป ลองพิจารณาแนวทางที่แตกต่างออกไปเล็กน้อย แทนที่จะวนรอบอาร์เรย์ elements เราสามารถวนรอบอาร์เรย์ ดัชนี :
for i in ${!allThreads[@]}; do
./pipeline --threads ${allThreads[$i]}
done
มาแยกกัน:ตามที่เราเห็นด้านบน ${allThreads[@]}
แสดงถึงองค์ประกอบทั้งหมดในอาร์เรย์ของเรา การเพิ่มเครื่องหมายอัศเจรีย์เพื่อทำให้เป็น ${!allThreads[@]}
จะส่งคืนรายการดัชนีอาร์เรย์ทั้งหมด (ในกรณีของเราคือ 0 ถึง 7) กล่าวอีกนัยหนึ่ง for
วนซ้ำผ่านดัชนีทั้งหมด $i
และอ่าน $i
-th องค์ประกอบจาก $allThreads
เพื่อตั้งค่าของ --threads
พารามิเตอร์
สิ่งนี้ทำให้ดวงตาดูแข็งกระด้างกว่ามาก ดังนั้นคุณอาจสงสัยว่าทำไมฉันต้องแนะนำมันตั้งแต่แรก นั่นเป็นเพราะมีบางครั้งที่คุณต้องรู้ทั้งดัชนีและค่าภายในลูป เช่น หากคุณต้องการละเว้นองค์ประกอบแรกของอาร์เรย์ การใช้ดัชนีช่วยให้คุณไม่ต้องสร้างตัวแปรเพิ่มเติมที่คุณเพิ่มภายในลูป .
กำลังเติมอาร์เรย์
จนถึงตอนนี้ เราสามารถเปิดไปป์ไลน์สำหรับแต่ละ --threads
ที่น่าสนใจ ตอนนี้ สมมติว่าผลลัพธ์ไปยังไปป์ไลน์ของเราคือรันไทม์ในหน่วยวินาที เราต้องการจับเอาท์พุตนั้นในการวนซ้ำแต่ละครั้งและบันทึกไว้ในอาร์เรย์อื่น เพื่อให้เราสามารถดำเนินการเปลี่ยนแปลงต่างๆ ในตอนท้ายได้
ไวยากรณ์ที่เป็นประโยชน์
แต่ก่อนที่จะดำดิ่งลงไปในโค้ด เราต้องแนะนำรูปแบบไวยากรณ์เพิ่มเติม อันดับแรก เราต้องสามารถดึงข้อมูลผลลัพธ์ของคำสั่ง Bash ได้ ในการดำเนินการดังกล่าว ให้ใช้ไวยากรณ์ต่อไปนี้:output=$( ./my_script.sh )
ซึ่งจะเก็บผลลัพธ์ของคำสั่งของเราลงในตัวแปร $output
.
บิตที่สองของไวยากรณ์ที่เราต้องการคือการผนวกค่าที่เราเพิ่งดึงข้อมูลไปยังอาร์เรย์ ไวยากรณ์ที่จะทำซึ่งจะดูคุ้นเคย:
myArray+=( "newElement1" "newElement2" )
การกวาดพารามิเตอร์
เมื่อนำทุกอย่างมารวมกัน นี่คือสคริปต์สำหรับเรียกใช้การกวาดพารามิเตอร์ของเรา:
allThreads=(1 2 4 8 16 32 64 128)
allRuntimes=()
for t in ${allThreads[@]}; do
runtime=$(./pipeline --threads $t)
allRuntimes+=( $runtime )
done
ว้าว!
ได้อะไรอีก
ในบทความนี้ เราได้กล่าวถึงสถานการณ์ของการใช้อาร์เรย์สำหรับการกวาดพารามิเตอร์ แต่ฉันสัญญาว่ามีเหตุผลมากกว่านี้ในการใช้ Bash arrays—นี่คือตัวอย่างอีกสองตัวอย่าง
การแจ้งเตือนบันทึก
ในสถานการณ์นี้ แอปของคุณจะถูกแบ่งออกเป็นโมดูล โดยแต่ละโมดูลจะมีไฟล์บันทึกของตัวเอง เราสามารถเขียนสคริปต์งาน cron เพื่อส่งอีเมลถึงผู้ที่เหมาะสมเมื่อมีสัญญาณของปัญหาในบางโมดูล:
# List of logs and who should be notified of issues
logPaths=("api.log" "auth.log" "jenkins.log" "data.log")
logEmails=("jay@email" "emma@email" "jon@email" "sophia@email")
# Look for signs of trouble in each log
for i in ${!logPaths[@]};
do
log=${logPaths[$i]}
stakeholder=${logEmails[$i]}
numErrors=$( tail -n 100 "$log" | grep "ERROR" | wc -l )
# Warn stakeholders if recently saw > 5 errors
if [[ "$numErrors" -gt 5 ]];
then
emailRecipient="$stakeholder"
emailSubject="WARNING: ${log} showing unusual levels of errors"
emailBody="${numErrors} errors found in log ${log}"
echo "$emailBody" | mailx -s "$emailSubject" "$emailRecipient"
fi
done
การสืบค้น API
สมมติว่าคุณต้องการสร้างการวิเคราะห์ว่าผู้ใช้คนใดแสดงความคิดเห็นมากที่สุดในโพสต์สื่อของคุณ เนื่องจากเราไม่มีการเข้าถึงฐานข้อมูลโดยตรง SQL จึงไม่มีปัญหา แต่เราสามารถใช้ API ได้!
เพื่อหลีกเลี่ยงการสนทนาที่ยาวนานเกี่ยวกับการตรวจสอบสิทธิ์ API และโทเค็น เราจะใช้ JSONPlaceholder ซึ่งเป็นบริการทดสอบ API ที่เปิดเผยต่อสาธารณะเป็นปลายทางแทน เมื่อเราสอบถามแต่ละโพสต์และดึงอีเมลของทุกคนที่แสดงความคิดเห็น เราสามารถต่อท้ายอีเมลเหล่านั้นในอาร์เรย์ผลลัพธ์ของเรา:
endpoint="https://jsonplaceholder.typicode.com/comments"
allEmails=()
# Query first 10 posts
for postId in {1..10};
do
# Make API call to fetch emails of this posts's commenters
response=$(curl "${endpoint}?postId=${postId}")
# Use jq to parse the JSON response into an array
allEmails+=( $( jq '.[].email' <<< "$response" ) )
done
โปรดทราบว่าฉันกำลังใช้ jq
เครื่องมือในการแยก JSON จากบรรทัดคำสั่ง ไวยากรณ์ของ jq
อยู่นอกเหนือขอบเขตของบทความนี้ แต่ฉันขอแนะนำให้คุณพิจารณาเป็นอย่างยิ่ง
อย่างที่คุณอาจจินตนาการได้ มีสถานการณ์อื่นๆ อีกนับไม่ถ้วนที่การใช้อาร์เรย์ Bash สามารถช่วยได้ และฉันหวังว่าตัวอย่างที่สรุปไว้ในบทความนี้จะช่วยให้คุณมีความคิด หากคุณมีตัวอย่างอื่นที่จะแบ่งปันจากงานของคุณเอง โปรดแสดงความคิดเห็นด้านล่าง
แต่เดี๋ยวก่อน ยังมีอีก!
เนื่องจากเราได้กล่าวถึงไวยากรณ์อาร์เรย์ไปบ้างในบทความนี้ ต่อไปนี้คือบทสรุปของสิ่งที่เรากล่าวถึง พร้อมกับลูกเล่นขั้นสูงที่เราไม่ได้กล่าวถึง:
ไวยากรณ์ | ผลลัพธ์ |
---|---|
arr=() | สร้างอาร์เรย์ว่าง |
arr=(1 2 3) | เริ่มต้นอาร์เรย์ |
${arr[2]} | ดึงองค์ประกอบที่สาม |
${arr[@]} | ดึงองค์ประกอบทั้งหมด |
${!arr[@]} | ดึงดัชนีอาร์เรย์ |
${#arr[@]} | คำนวณขนาดอาร์เรย์ |
arr[0]=3 | เขียนทับองค์ประกอบที่ 1 |
arr+=(4) | เพิ่มค่าต่อท้าย |
str=$(ls) | บันทึก ls เอาต์พุตเป็นสตริง |
arr=( $(ls) ) | บันทึก ls เอาต์พุตเป็นอาร์เรย์ของไฟล์ |
${arr[@]:s:n} | ดึง n องค์ประกอบ starting at index s |
ความคิดสุดท้าย
ตามที่เราได้ค้นพบ อาร์เรย์ของ Bash นั้นมีรูปแบบไวยากรณ์ที่แปลก แต่ฉันหวังว่าบทความนี้จะทำให้คุณเชื่อได้ว่าพวกมันมีประสิทธิภาพมาก เมื่อคุณคุ้นเคยกับไวยากรณ์แล้ว คุณจะพบว่าตัวเองใช้อาร์เรย์ Bash ค่อนข้างบ่อย
Bash หรือ Python?
ซึ่งทำให้เกิดคำถาม:เมื่อใดที่คุณควรใช้ Bash arrays แทนภาษาสคริปต์อื่น ๆ เช่น Python
สำหรับฉันแล้ว ทั้งหมดขึ้นอยู่กับการพึ่งพา—หากคุณสามารถแก้ปัญหาได้โดยใช้เพียงการเรียกไปยังเครื่องมือบรรทัดคำสั่ง คุณก็อาจใช้ Bash เช่นกัน แต่สำหรับเวลาที่สคริปต์ของคุณเป็นส่วนหนึ่งของโครงการ Python ที่ใหญ่กว่า คุณอาจใช้ Python ด้วยเช่นกัน
ตัวอย่างเช่น เราอาจหันไปใช้ Python เพื่อใช้การกวาดพารามิเตอร์ แต่สุดท้ายแล้วเราก็จบลงด้วยการเขียน wrapper รอบ Bash:
import subprocess
all_threads = [1, 2, 4, 8, 16, 32, 64, 128]
all_runtimes = []
# Launch pipeline on each number of threads
for t in all_threads:
cmd = './pipeline --threads {}'.format(t)
# Use the subprocess module to fetch the return output
p = subprocess.Popen(cmd, stdout=subprocess.PIPE, shell=True)
output = p.communicate()[0]
all_runtimes.append(output)
เนื่องจากในตัวอย่างนี้ไม่มีการใช้บรรทัดคำสั่ง จึงควรใช้ Bash โดยตรง
ถึงเวลาของปลั๊กไร้ยางอาย
บทความนี้อิงจากการพูดคุยของฉันที่ OSCON ซึ่งฉันได้นำเสนอเวิร์กชอปการเขียนโค้ดแบบสด You Don't Know Bash . ไม่มีสไลด์ ไม่มีตัวคลิก มีเพียงฉันและผู้ชมที่พิมพ์คำสั่งที่บรรทัดคำสั่ง สำรวจโลกมหัศจรรย์ของ Bash