Computer >> คอมพิวเตอร์ >  >> การเขียนโปรแกรม >> การเขียนโปรแกรม BASH

คุณไม่รู้ Bash:บทนำสู่ Bash arrays

แม้ว่าวิศวกรซอฟต์แวร์จะใช้บรรทัดคำสั่งเป็นประจำสำหรับการพัฒนาในหลายแง่มุม แต่อาร์เรย์ก็เป็นหนึ่งในคุณลักษณะที่ไม่ชัดเจนของบรรทัดคำสั่ง (แม้ว่าจะไม่ใช่ คลุมเครือเหมือนโอเปอเรเตอร์ 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