ใช่เราเห็นหลายสิ่งเช่น:
while read line; do
echo $line | cut -c3
done
หรือแย่กว่านั้น:
for line in `cat file`; do
foo=`echo $line | awk '{print $2}'`
echo whatever $foo
done
(อย่าหัวเราะฉันเคยเห็นหลายคน)
โดยทั่วไปจากผู้เริ่มต้นเชลล์สคริปต์ สิ่งเหล่านี้คือคำแปลที่ไร้เดียงสาของสิ่งที่คุณจะทำในภาษาที่จำเป็นเช่น C หรือ python แต่นั่นไม่ใช่วิธีที่คุณทำสิ่งต่างๆใน shells และตัวอย่างเหล่านั้นไม่มีประสิทธิภาพมากและไม่น่าเชื่อถืออย่างสมบูรณ์ (อาจนำไปสู่ปัญหาด้านความปลอดภัย) เพื่อแก้ไขข้อบกพร่องส่วนใหญ่รหัสของคุณจะอ่านไม่ออก
แนวคิด
ในภาษา C หรือภาษาอื่น ๆ ส่วนใหญ่การสร้างบล็อคนั้นมีเพียงหนึ่งระดับเหนือคำแนะนำคอมพิวเตอร์ คุณบอกโปรเซสเซอร์ของคุณว่าต้องทำอย่างไรและจะทำอย่างไรต่อไป คุณใช้หน่วยประมวลผลด้วยมือของคุณและจัดการกับไมโคร: คุณเปิดไฟล์นั้นคุณอ่านจำนวนไบต์ที่คุณทำเช่นนี้คุณทำกับมัน
เชลล์เป็นภาษาระดับสูงกว่า บางคนอาจพูดว่าไม่ใช่ภาษา พวกมันอยู่ตรงหน้าล่ามบรรทัดคำสั่งทั้งหมด งานทำโดยคำสั่งเหล่านั้นที่คุณเรียกใช้และเชลล์นั้นมีไว้เพื่อติดตั้งให้เท่านั้น
หนึ่งในสิ่งที่ดีที่ใช้ระบบปฏิบัติการยูนิกซ์แนะนำเป็นท่อและผู้เริ่มต้น stdin / stdout / stderr ลำธารว่าคำสั่งทั้งหมดจัดการโดยค่าเริ่มต้น
ใน 45 ปีที่ผ่านมาเราไม่พบว่าดีกว่า API นั้นในการควบคุมพลังของคำสั่งและให้ความร่วมมือกับงาน นั่นอาจเป็นเหตุผลหลักว่าทำไมผู้คนยังคงใช้กระสุนในปัจจุบัน
คุณมีเครื่องมือตัดและเครื่องมือถอดเสียงและคุณสามารถทำได้:
cut -c4-5 < in | tr a b > out
เชลล์กำลังทำหน้าที่วางท่อ (เปิดไฟล์ตั้งค่าไพพ์เรียกใช้คำสั่ง) และเมื่อพร้อมแล้วมันก็จะไหลโดยที่เชลล์ไม่ทำอะไรเลย เครื่องมือทำงานของพวกเขาพร้อมกันอย่างมีประสิทธิภาพตามจังหวะของตัวเองด้วยการบัฟเฟอร์เพียงพอเพื่อที่จะไม่บล็อกสิ่งอื่นมันสวยงามและเรียบง่าย
เรียกใช้เครื่องมือว่ามีค่าใช้จ่าย (และเราจะพัฒนาสิ่งนั้นในจุดประสิทธิภาพ) เครื่องมือเหล่านั้นอาจถูกเขียนด้วยคำแนะนำนับพันใน C. กระบวนการจะต้องมีการสร้างเครื่องมือจะต้องมีการโหลดเริ่มต้นแล้วทำความสะอาดขึ้นกระบวนการทำลายและรอ
การกล่าวอ้างcut
เป็นเหมือนการเปิดลิ้นชักครัวใช้มีดใช้ล้างมันเช็ดให้แห้งใส่มันกลับเข้าไปในลิ้นชัก เมื่อคุณทำ:
while read line; do
echo $line | cut -c3
done < file
มันเหมือนไฟล์แต่ละบรรทัดเอาread
เครื่องมือออกมาจากลิ้นชักห้องครัว (อันที่ซุ่มซ่ามเพราะมันไม่ได้ถูกออกแบบมาสำหรับมัน ) อ่านบรรทัดล้างเครื่องมืออ่านของคุณวางมันกลับเข้าไปในลิ้นชัก จากนั้นกำหนดเวลาการประชุมสำหรับเครื่องมือecho
และcut
รับพวกเขาออกมาจากลิ้นชักเรียกพวกเขาล้างพวกเขาให้แห้งนำพวกเขากลับมาไว้ในลิ้นชักและอื่น ๆ
เครื่องมือเหล่านี้บางตัว ( read
และecho
) ถูกสร้างขึ้นในเชลล์ส่วนใหญ่ แต่แทบจะไม่สามารถสร้างความแตกต่างได้ที่นี่ตั้งแต่echo
และcut
ยังคงต้องทำงานในกระบวนการแยกต่างหาก
มันเหมือนกับการตัดหัวหอม แต่การล้างมีดของคุณและนำกลับมาใส่ในลิ้นชักห้องครัวระหว่างแต่ละชิ้น
วิธีที่ชัดเจนคือการนำcut
เครื่องมือของคุณออกมาจากลิ้นชักหั่นหัวหอมใหญ่ทั้งหมดของคุณแล้วนำกลับมาใส่ในลิ้นชักหลังจากทำงานทั้งหมดเสร็จแล้ว
โดยเฉพาะอย่างยิ่งในการประมวลผลข้อความคุณเรียกใช้ยูทิลิตี้น้อยที่สุดเท่าที่จะเป็นไปได้และให้พวกเขาร่วมมือกับงานไม่ใช่เรียกใช้เครื่องมือหลายพันตัวตามลำดับที่รอให้แต่ละตัวเริ่มต้นรันทำความสะอาดก่อนเรียกใช้งานถัดไป
อ่านเพิ่มเติมในคำตอบที่ดีของบรูซ เครื่องมือภายในการประมวลผลข้อความระดับต่ำในเชลล์ (ยกเว้นอาจจะzsh
) มี จำกัด ยุ่งยากและโดยทั่วไปไม่เหมาะสำหรับการประมวลผลข้อความทั่วไป
ประสิทธิภาพ
ดังที่ได้กล่าวไว้ก่อนหน้านี้การรันหนึ่งคำสั่งมีค่าใช้จ่าย ค่าใช้จ่ายมากถ้าคำสั่งนั้นไม่ได้สร้างขึ้น แต่ถึงแม้ว่าพวกเขาจะสร้างขึ้นในราคาที่มีขนาดใหญ่
และเชลล์ไม่ได้ถูกออกแบบมาให้ทำงานเช่นนั้นพวกเขาไม่มีข้ออ้างที่จะเป็นภาษาโปรแกรมนักแสดง พวกเขาไม่ใช่พวกเขาเป็นเพียงตัวแปลบรรทัดคำสั่ง ดังนั้นการเพิ่มประสิทธิภาพเล็ก ๆ น้อย ๆ ได้ดำเนินการในหน้านี้
นอกจากนี้เชลล์ยังรันคำสั่งในกระบวนการแยกต่างหาก Building Block เหล่านั้นจะไม่แชร์หน่วยความจำหรือสถานะทั่วไป เมื่อคุณทำfgets()
หรือfputs()
ใน C นั่นคือฟังก์ชั่นใน stdio stdio เก็บบัฟเฟอร์ภายในไว้สำหรับอินพุตและเอาต์พุตสำหรับฟังก์ชัน stdio ทั้งหมดเพื่อหลีกเลี่ยงการโทรบ่อยครั้ง
ที่สอดคล้องกันแม้แต่สาธารณูปโภคเปลือก builtin ( read
, echo
, printf
) ไม่สามารถทำเช่นนั้น read
มีวัตถุประสงค์เพื่ออ่านหนึ่งบรรทัด หากอ่านผ่านอักขระบรรทัดใหม่นั่นหมายความว่าคำสั่งถัดไปที่คุณรันจะพลาด ดังนั้นread
ต้องอ่านอินพุตทีละหนึ่งไบต์ (การใช้งานบางอย่างมีการปรับให้เหมาะสมถ้าอินพุตเป็นไฟล์ปกติที่พวกเขาอ่านชิ้นและค้นหากลับ แต่ใช้งานได้กับไฟล์ปกติbash
เท่านั้นและเช่นอ่าน 128 ไบต์ซึ่งเป็น ยังคงน้อยกว่าโปรแกรมอรรถประโยชน์ข้อความอย่างมาก)
ด้านเดียวกับเอาท์พุทecho
ไม่สามารถบัฟเฟอร์เอาต์พุตเพียงอย่างเดียว แต่ต้องส่งออกทันทีเนื่องจากคำสั่งถัดไปที่คุณรันจะไม่แชร์บัฟเฟอร์นั้น
เห็นได้ชัดว่าการรันคำสั่งตามลำดับหมายความว่าคุณต้องรอคำสั่งเหล่านี้มันเป็นการเต้นรำตัวกำหนดตารางเวลาเล็กน้อยที่ให้การควบคุมจากเชลล์และเครื่องมือและด้านหลัง นั่นก็หมายความว่า (ต่างจากการใช้อินสแตนซ์ของเครื่องมือที่ทำงานเป็นเวลานาน) ซึ่งคุณไม่สามารถควบคุมโปรเซสเซอร์หลายตัวในเวลาเดียวกันเมื่อมีให้ใช้งาน
ระหว่างwhile read
ลูปนั้นและเทียบเท่า (ควร) cut -c3 < file
ในการทดสอบอย่างรวดเร็วของฉันมีอัตราส่วนเวลาซีพียูประมาณ 40000 ในการทดสอบของฉัน (หนึ่งวินาทีต่อครึ่งวัน) แต่แม้ว่าคุณจะใช้เชลล์บิลด์อินเท่านั้น:
while read line; do
echo ${line:2:1}
done
(ที่นี่ด้วยbash
) ซึ่งยังคงอยู่ประมาณ 1: 600 (หนึ่งวินาทีกับ 10 นาที)
ความน่าเชื่อถือ / ความชัดเจน
มันยากมากที่จะทำให้รหัสนั้นถูกต้อง ตัวอย่างที่ฉันให้เห็นบ่อยเกินไปในป่า แต่มีข้อบกพร่องมากมาย
read
เป็นเครื่องมือที่มีประโยชน์ที่สามารถทำสิ่งต่าง ๆ มากมาย มันสามารถอ่านอินพุตจากผู้ใช้แบ่งเป็นคำเพื่อเก็บในตัวแปรที่แตกต่างกัน read line
ไม่ได้อ่านบรรทัดของการป้อนข้อมูลหรือบางทีมันอาจจะอ่านบรรทัดในทางที่พิเศษมาก จริงๆแล้วมันจะอ่านคำจากอินพุตที่คำเหล่านั้นคั่นด้วย$IFS
และสามารถใช้แบ็กสแลชเพื่อหลีกเลี่ยงตัวคั่นหรืออักขระขึ้นบรรทัดใหม่
ด้วยค่าเริ่มต้นของ$IFS
บนอินพุตเช่น:
foo\/bar \
baz
biz
read line
จะเก็บ"foo/bar baz"
เข้า$line
ไม่ได้" foo\/bar \"
ที่คุณคาดหวัง
หากต้องการอ่านบรรทัดคุณจำเป็นต้องใช้:
IFS= read -r line
นั่นไม่ใช่วิธีที่ใช้งานง่ายมาก แต่นั่นเป็นวิธีที่จำได้ว่ากระสุนไม่ได้ถูกใช้งานอย่างนั้น
echo
เหมือนกันสำหรับ echo
ขยายลำดับ คุณไม่สามารถใช้มันสำหรับเนื้อหาที่กำหนดเองเช่นเนื้อหาของไฟล์สุ่ม คุณต้องการprintf
ที่นี่แทน
และแน่นอนว่ามีการลืมความหมายทั่วไปของตัวแปรที่ทุกคนพูดถึง ดังนั้นมันจึงมากกว่า:
while IFS= read -r line; do
printf '%s\n' "$line" | cut -c3
done < file
ตอนนี้มีคำเตือนอีกสองสามข้อ:
- ยกเว้น
zsh
ว่าจะไม่ทำงานหากอินพุตมีอักขระ NUL ในขณะที่ยูทิลิตี้ข้อความอย่างน้อย GNU จะไม่มีปัญหา
- หากมีข้อมูลหลังจากขึ้นบรรทัดใหม่แล้วมันจะถูกข้าม
- ภายในลูป stdin จะถูกเปลี่ยนเส้นทางดังนั้นคุณต้องให้ความสนใจว่าคำสั่งในนั้นไม่อ่านจาก stdin
- สำหรับคำสั่งภายในลูปเราไม่ได้สนใจว่ามันจะสำเร็จหรือไม่ โดยปกติแล้วข้อผิดพลาด (ดิสก์เต็มอ่านข้อผิดพลาด ... ) เงื่อนไขจะได้รับการจัดการไม่ดีมักจะไม่ดีกว่าที่เทียบเท่าที่ถูกต้อง
หากเราต้องการแก้ไขปัญหาเหล่านี้บางส่วนข้างต้นนั่นจะกลายเป็น:
while IFS= read -r line <&3; do
{
printf '%s\n' "$line" | cut -c3 || exit
} 3<&-
done 3< file
if [ -n "$line" ]; then
printf '%s' "$line" | cut -c3 || exit
fi
นั่นกลายเป็นชัดเจนน้อยลง
มีปัญหาอื่น ๆ อีกมากมายที่มีการส่งผ่านข้อมูลไปยังคำสั่งผ่านอาร์กิวเมนต์หรือการดึงเอาท์พุทของพวกเขาในตัวแปร:
- ข้อ จำกัด เกี่ยวกับขนาดของข้อโต้แย้ง (การใช้งานยูทิลิตี้ข้อความบางอย่างมีข้อ จำกัด เช่นกันแม้ว่าผลของการเข้าถึงเหล่านั้นโดยทั่วไปจะมีปัญหาน้อยกว่า)
- อักขระ NUL (เช่นปัญหากับยูทิลิตี้ข้อความ)
- อาร์กิวเมนต์ที่ใช้เป็นตัวเลือกเมื่อเริ่มต้นด้วย
-
(หรือ+
บางครั้ง)
- นิสัยใจคอต่างๆของคำสั่งต่างๆมักจะใช้ในคนที่เป็นเหมือนลูป
expr
, test
...
- ตัวดำเนินการจัดการข้อความ (จำกัด ) ของเชลล์ต่างๆที่จัดการอักขระหลายไบต์ด้วยวิธีที่ไม่สอดคล้องกัน
- ...
ข้อพิจารณาด้านความปลอดภัย
เมื่อคุณเริ่มทำงานกับตัวแปรเชลล์และอาร์กิวเมนต์ของคำสั่งคุณกำลังเข้าสู่ฟิลด์ของฉัน
หากคุณลืมที่จะพูดตัวแปรของคุณให้ลืมเครื่องหมายสิ้นสุดตัวเลือกทำงานในโลแคลที่มีอักขระหลายไบต์ (ปกติวันนี้) คุณมั่นใจที่จะแนะนำบั๊กซึ่งไม่ช้าก็เร็วจะกลายเป็นช่องโหว่
เมื่อคุณอาจต้องการใช้ลูป
TBD
yes
เขียนลงไฟล์เร็วขนาดไหน?