วิธีการอ่านเชลล์สคริปต์ทั้งหมดก่อนที่จะรันมัน?


35

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

เท่าที่ฉันเข้าใจทุบตี (กระสุนอื่น ๆ ด้วย?) อ่านสคริปต์เพิ่มขึ้นดังนั้นถ้าคุณแก้ไขไฟล์สคริปต์ภายนอกมันเริ่มอ่านสิ่งผิดปกติ มีวิธีป้องกันไหม?

ตัวอย่าง:

sleep 20

echo test

หากคุณรันสคริปต์นี้ทุบตีจะอ่านบรรทัดแรก (พูด 10 ไบต์) และไปที่โหมดสลีป เมื่อดำเนินการต่ออาจมีเนื้อหาต่าง ๆ ในสคริปต์เริ่มต้นที่ 10 ไบต์ ฉันอาจจะอยู่ตรงกลางบรรทัดในสคริปต์ใหม่ ดังนั้นสคริปต์ที่ใช้งานจะเสียหาย


คุณหมายถึงอะไรโดย "แก้ไขสคริปต์ภายนอก"?
maulinglawns

1
อาจมีวิธีห่อเนื้อหาทั้งหมดในฟังก์ชั่นหรือบางสิ่งดังนั้นเชลล์จะอ่านสคริปต์ทั้งหมดก่อน? แต่สิ่งที่เกี่ยวกับบรรทัดสุดท้ายที่คุณเรียกใช้ฟังก์ชั่นมันจะถูกอ่านจน EOF? บางทีการละเว้นครั้งสุดท้าย\nจะเป็นการหลอกลวง บางที subshell ()จะทำอย่างไร ฉันไม่ค่อยมีประสบการณ์กับมันโปรดช่วยด้วย!
VasyaNovikov

@maulinglawns หากสคริปต์มีเนื้อหาเช่นนี้sleep 20 ;\n echo test ;\n sleep 20และฉันเริ่มแก้ไขมันอาจทำงานผิดปกติได้ ตัวอย่างเช่นทุบตีสามารถอ่านสคริปต์ 10 ไบต์แรกเข้าใจsleepคำสั่งและเข้าสู่โหมดสลีป หลังจากดำเนินการต่อแล้วจะมีเนื้อหาต่าง ๆ ในไฟล์เริ่มต้นที่ 10 ไบต์
VasyaNovikov

1
ดังนั้นสิ่งที่คุณกำลังพูดคือคุณกำลังแก้ไขสคริปต์ที่รันอยู่? หยุดสคริปต์ก่อนทำการแก้ไขจากนั้นเริ่มใหม่อีกครั้ง
maulinglawns

@ maulinglawns ใช่มันเป็นแบบนั้น ปัญหาคือฉันไม่สะดวกที่จะหยุดสคริปต์และมันก็ยากที่จะจำได้เสมอ อาจมีวิธีบังคับให้ทุบตีอ่านสคริปต์ทั้งหมดก่อนหรือไม่
VasyaNovikov

คำตอบ:


43

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

คุณจะสังเกตเห็นว่าเมื่อไฟล์ไม่สามารถค้นหาได้ (เช่นไพพ์) ให้bashอ่านทีละหนึ่งไบต์เพื่อให้แน่ใจว่าจะไม่อ่าน\nอักขระที่ผ่านมา เมื่อไฟล์ถูก seekable มันเพิ่มประสิทธิภาพโดยการอ่านบล็อกเต็มรูปแบบได้ตลอดเวลา \nแต่ขอกลับไปหลังจากที่

นั่นหมายความว่าคุณสามารถทำสิ่งต่าง ๆ เช่น:

bash << \EOF
read var
var's content
echo "$var"
EOF

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

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

เพื่อหลีกเลี่ยงคุณสามารถลองและตรวจสอบให้แน่ใจว่าคุณไม่ได้แก้ไขไฟล์ในสถานที่ (ตัวอย่างเช่นแก้ไขสำเนาและย้ายสำเนาในสถานที่ (เช่นsed -iหรือperl -piและบรรณาธิการบางคนทำเช่น))

หรือคุณสามารถเขียนสคริปต์ของคุณเช่น:

{
  sleep 20
  echo test
}; exit

(โปรดทราบว่าเป็นสิ่งสำคัญที่exitจะต้องอยู่ในบรรทัดเดียวกันกับ}; แม้ว่าคุณสามารถใส่ไว้ในวงเล็บปีกกาก่อนที่จะปิดหนึ่ง)

หรือ:

main() {
  sleep 20
  echo test
}
main "$@"; exit

เชลล์จะต้องอ่านสคริปต์จนถึงexitก่อนที่จะเริ่มทำอะไร เพื่อให้แน่ใจว่าเชลล์จะไม่อ่านจากสคริปต์อีกครั้ง

นั่นหมายความว่าสคริปต์ทั้งหมดจะถูกเก็บไว้ในหน่วยความจำ

ที่สามารถส่งผลกระทบต่อการแยกวิเคราะห์สคริปต์

ตัวอย่างเช่นในbash:

export LC_ALL=fr_FR.UTF-8
echo $'St\ue9phane'

จะส่งออกที่ U + 00E9 เข้ารหัสใน UTF-8 อย่างไรก็ตามหากคุณเปลี่ยนเป็น:

{
  export LC_ALL=fr_FR.UTF-8
  echo $'St\ue9phane'
}

\ue9จะได้รับการขยายตัวใน charset ที่อยู่ในผลในเวลาที่ถูกคำสั่งแยกวิเคราะห์ซึ่งในกรณีนี้คือก่อนที่จะexportคำสั่งจะถูกดำเนินการ

โปรดทราบด้วยว่าหากใช้คำสั่งsourceaka .พร้อมกับเชลล์บางตัวคุณจะมีปัญหาแบบเดียวกันกับไฟล์ที่มา

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

if [[ ! $already_sourced ]]; then
  already_sourced=1
  source "$0"; exit
fi

(ฉันจะไม่พึ่งพาสิ่งนั้นแม้ว่าคุณจะจินตนาการได้ว่าเวอร์ชันในอนาคตของbashสามารถเปลี่ยนพฤติกรรมที่สามารถมองเห็นได้ในปัจจุบันเป็นข้อ จำกัด (ทุบตีและ AT&T ksh เป็นเปลือกหอย POSIX เท่านั้นที่ทำงานเช่นนั้นเท่าที่จะบอก) และalready_sourcedกลอุบายนั้นค่อนข้างบอบบางเพราะถือว่าตัวแปรนั้นไม่ได้อยู่ในสภาพแวดล้อมไม่ต้องพูดถึงว่ามันส่งผลกระทบต่อเนื้อหาของตัวแปร BASH_SOURCE)


@ VasyaNovikov ดูเหมือนว่ามีบางอย่างผิดปกติกับ SE ในขณะนี้ (หรืออย่างน้อยสำหรับฉัน) มีเพียงไม่กี่คำตอบเมื่อฉันเพิ่มของฉันและความคิดเห็นของคุณดูเหมือนจะปรากฏขึ้นตอนนี้แม้ว่ามันจะบอกว่ามันถูกโพสต์เมื่อ 16 นาทีที่ผ่านมา (หรือบางทีฉันแค่สูญเสียหินอ่อน) อย่างไรก็ตามโปรดสังเกต "ทางออก" พิเศษที่จำเป็นที่นี่เพื่อหลีกเลี่ยงปัญหาเมื่อขนาดไฟล์เพิ่มขึ้น (ดังที่ระบุไว้ในความคิดเห็นที่ฉันได้เพิ่มไว้ในคำตอบของคุณ)
Stéphane Chazelas

สเตฟานฉันคิดว่าฉันพบวิธีแก้ปัญหาอื่นแล้ว }; exec trueก็คือการใช้ ด้วยวิธีนี้ไม่มีข้อกำหนดในการขึ้นบรรทัดใหม่เมื่อสิ้นสุดไฟล์ซึ่งเป็นมิตรกับบรรณาธิการบางคน (เช่น emacs) การทดสอบทั้งหมดที่ฉันสามารถคิดได้อย่างถูกต้องกับ}; exec true
VasyaNovikov

@ VasyaNovikov ไม่แน่ใจว่าคุณหมายถึงอะไร มันจะดีกว่า}; exitอย่างไร คุณกำลังสูญเสียสถานะการออก
Stéphane Chazelas

ตามที่กล่าวถึงคำถามที่แตกต่าง: เป็นเรื่องปกติที่จะแยกไฟล์ทั้งหมดก่อนแล้วจึงเรียกใช้งานคำสั่งผสมในกรณีที่ใช้คำสั่ง dot ( . script)
schily

@ schily ใช่ฉันพูดถึงว่าในคำตอบนี้เป็นข้อ จำกัด ของ AT&T ksh และทุบตี เชลล์ชนิด POSIX อื่นไม่มีข้อ จำกัด นั้น
Stéphane Chazelas

12

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

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


"ลบ" ไม่ใช่ส่วนหนึ่งของกระบวนการ ถ้าคุณต้องการทำให้มันเป็นปรมาณูอย่างถูกต้องคุณทำการเปลี่ยนชื่อไฟล์ปลายทาง - หากคุณมีขั้นตอนการลบมีความเสี่ยงที่กระบวนการของคุณจะตายหลังจากการลบ แต่ก่อนที่จะเปลี่ยนชื่อไม่มีไฟล์อยู่เลย ( หรือผู้อ่านพยายามเข้าถึงไฟล์ในหน้าต่างนั้นและค้นหาทั้งรุ่นเก่าและรุ่นใหม่ที่ไม่มี)
Charles Duffy

11

วิธีที่ง่ายที่สุด:

{
  ... your code ...

  exit
}

ด้วยวิธีนี้ทุบตีจะอ่าน{}บล็อกทั้งหมดก่อนที่จะดำเนินการและexitคำสั่งจะทำให้แน่ใจว่าไม่มีอะไรจะอ่านนอกบล็อกรหัส

หากคุณไม่ต้องการ "รัน" สคริปต์ แต่ต้องการ "ซอร์ส" สคริปต์คุณต้องมีโซลูชันอื่น สิ่งนี้ควรใช้งานได้แล้ว:

{
  ... your code ...

  return 2>/dev/null || exit
}

หรือหากคุณต้องการควบคุมรหัสออกโดยตรง:

{
  ... your code ...

  ret="$?";return "$ret" 2>/dev/null || exit "$ret"
}

Voila! สคริปต์นี้มีความปลอดภัยในการแก้ไขแหล่งและดำเนินการ คุณยังต้องแน่ใจว่าคุณไม่ได้แก้ไขในมิลลิวินาทีเหล่านั้นเมื่อเริ่มอ่าน


1
สิ่งที่ฉันพบคือมันไม่เห็น EOF และหยุดอ่านไฟล์ แต่มันจะพันกันในการประมวลผล "สตรีมบัฟเฟอร์" และจบลงด้วยการค้นหาผ่านจุดสิ้นสุดของไฟล์ซึ่งเป็นสาเหตุที่ทำให้ดูดีถ้าขนาดของ ไฟล์จะเพิ่มขึ้นไม่มาก แต่ดูไม่ดีเมื่อคุณสร้างไฟล์ให้ใหญ่กว่าสองเท่าก่อนหน้านี้ ฉันจะรายงานข้อผิดพลาดต่อผู้ดูแลระบบ bash ในไม่ช้า
Stéphane Chazelas


ความคิดเห็นไม่ได้มีไว้สำหรับการอภิปรายเพิ่มเติม การสนทนานี้ได้รับการย้ายไปแชท
terdon

5

พิสูจน์แนวคิด นี่คือสคริปต์ที่ปรับเปลี่ยนตัวเอง:

cat <<EOF >/tmp/scr
#!/bin/bash
sed  s/[k]ept/changed/  /tmp/scr > /tmp/scr2

# this next line overwites the on disk copy of the script
cat /tmp/scr2 > /tmp/scr
# this line ends up changed.
echo script content kept
EOF
chmod u+x /tmp/scr
/tmp/scr

เราเห็นการพิมพ์เวอร์ชันที่เปลี่ยนไป

นี่เป็นเพราะการโหลด bash ช่วยให้ตัวจัดการไฟล์เปิดให้สคริปต์ดังนั้นการเปลี่ยนแปลงไฟล์จะเห็นได้ทันที

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

วิธีหนึ่งในการทำเช่นนั้นคือการใช้ sed -i

sed -i '' filename

พิสูจน์แนวคิด

cat <<EOF >/tmp/scr
#!/bin/bash
sed  s/[k]ept/changed/  /tmp/scr > /tmp/scr2

# this next line unlinks the original and creates a new copy.
sed -i ''  /tmp/scr

# now overwriting it has no immediate effect
cat /tmp/scr2 > /tmp/scr
echo script content kept
EOF

chmod u+x /tmp/scr
/tmp/scr

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


2
ไม่มีไม่ได้เปิดไฟล์ที่มีbash mmap()เป็นการระมัดระวังในการอ่านทีละบรรทัดตามต้องการเช่นเดียวกับเมื่อได้รับคำสั่งจากอุปกรณ์ปลายทางเมื่อมีการโต้ตอบ
Stéphane Chazelas

2

การตัดสคริปต์ของคุณในบล็อก{}น่าจะเป็นตัวเลือกที่ดีที่สุด แต่ต้องเปลี่ยนสคริปต์ของคุณ

F=$(mktemp) && cp test.sh $F && bash $F; rm $F;

จะเป็นตัวเลือกที่ดีที่สุดที่สอง (สมมติว่าtmpfs ) ข้อเสียคือมันแบ่ง $ 0 ถ้าสคริปต์ของคุณใช้มัน

การใช้บางอย่างเช่นF=test.sh; tail -n $(cat "$F" | wc -l) "$F" | bashนั้นไม่เหมาะอย่างยิ่งเพราะจะต้องเก็บไฟล์ทั้งหมดไว้ในหน่วยความจำและแบ่งเป็น $ 0

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

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


ทุกสิ่งที่ทำสำเนาจะใช้ได้ผล tac test.sh | tac | bash
Jasen
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.