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


46

ดังนั้นฉันจึงลบโฟลเดอร์บ้านของฉัน (หรืออย่างแม่นยำยิ่งขึ้นไฟล์ทั้งหมดที่ฉันมีสิทธิ์เข้าถึงเพื่อเขียน) สิ่งที่เกิดขึ้นคือฉันมี

build="build"
...
rm -rf "${build}/"*
...
<do other things with $build>

ในสคริปต์ทุบตีและหลังจากต้องไม่$buildเอาการประกาศและการใช้งานได้ทั้งหมด - rmแต่ rm -rf /*ทุบตีอย่างมีความสุขที่จะขยาย ใช่.

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

ตอนนี้ฉันสงสัยว่าอะไรคือเทคนิคในการเขียนสคริปต์ทุบตีเพื่อให้ข้อผิดพลาดดังกล่าวไม่สามารถเกิดขึ้นได้หรืออย่างน้อยก็มีโอกาสน้อย? ตัวอย่างเช่นถ้าฉันเขียน

FileUtils.rm_rf("#{build}/*")

ในสคริปต์ Ruby ผู้แปลก็จะบ่นbuildว่าไม่ได้ถูกประกาศดังนั้นภาษาก็ช่วยปกป้องฉัน

สิ่งที่ฉันได้พิจารณาในการทุบตีนอกเหนือจากการจับกลุ่มrm(ซึ่งเป็นคำตอบมากมายในคำถามที่เกี่ยวข้องพูดถึงไม่ได้เป็น unproblematic):

  1. rm -rf "./${build}/"*
    นั่นคงจะฆ่างานปัจจุบันของฉัน (repo Git) แต่ไม่มีอะไรอื่น
  2. ตัวแปร / การกำหนดพารามิเตอร์ของrmนั้นต้องมีการโต้ตอบเมื่อทำหน้าที่ภายนอกไดเรกทอรีปัจจุบัน (ไม่พบสิ่งใด) ลักษณะพิเศษที่คล้ายกัน

มันเป็นหรือมีวิธีอื่นในการเขียนสคริปต์ทุบตีที่ "แข็งแกร่ง" ในแง่นี้หรือไม่?


3
ไม่มีเหตุผลrm -rf "${build}/*ใดที่ไม่ว่าเครื่องหมายคำพูดจะไปที่ใด จะทำสิ่งเดียวกันเพราะrm -rf "${build} f
Monty Harder

1
การตรวจสอบแบบคงที่ด้วยshellcheck.netเป็นจุดเริ่มต้นที่มั่นคงมาก มีการรวมโปรแกรมแก้ไขเพื่อให้คุณสามารถรับคำเตือนจากเครื่องมือของคุณทันทีที่คุณลบคำจำกัดความของสิ่งที่ยังคงใช้อยู่
Charles Duffy

@CharlesDuffy หากต้องการเพิ่มความอัปยศของฉันฉันได้เขียนสคริปต์นั้นใน IDE ในรูปแบบของ IDEA โดยติดตั้ง BashSupport ไว้ซึ่งจะเตือนในกรณีนั้น ใช่แล้วจุดที่ถูกต้อง แต่ฉันต้องการการยกเลิกอย่างหนัก
กราฟิลส์

2
Gotcha ให้แน่ใจว่าได้ทราบคำเตือนในBashFAQ # set -uไม่เกือบขมวดคิ้วอย่างที่เป็นset -eอยู่ แต่ก็ยังมี gotchas อยู่
Charles Duffy

2
คำตอบเรื่องตลก: เพียงแค่ใส่#! /usr/bin/env rubyที่ด้านบนของเชลล์สคริปต์ทุกและลืมเกี่ยวกับทุบตี;)
Pod

คำตอบ:


75
set -u

หรือ

set -o nounset

สิ่งนี้จะทำให้เชลล์ปัจจุบันถือว่าการขยายตัวแปร unset เป็นข้อผิดพลาด:

$ unset build
$ set -u
$ rm -rf "$build"/*
bash: build: unbound variable

set -uและset -o nounsetมีPOSIX ตัวเลือกเปลือก

ว่างค่าจะไม่ก่อให้เกิดข้อผิดพลาดว่า

สำหรับสิ่งที่ใช้

$ rm -rf "${build:?Error, variable is empty or unset}"/*
bash: build: Error, variable is empty or unset

การขยายตัวของ${variable:?word}จะขยายเป็นค่าของvariableถ้ามันว่างเปล่าหรือ unset ถ้าว่างเปล่าหรือไม่มีการตั้งค่าwordจะแสดงข้อผิดพลาดมาตรฐานและเชลล์จะถือว่าการขยายตัวเป็นข้อผิดพลาด (คำสั่งจะไม่ถูกเรียกใช้งานและหากทำงานในเชลล์ที่ไม่มีการโต้ตอบสิ่งนี้จะสิ้นสุดลง) การปล่อย:ออกไปจะทำให้เกิดข้อผิดพลาดเฉพาะกับค่าที่ยังไม่ได้ตั้งค่าเช่นเดียวกับด้านset -uล่าง

${variable:?word}คือการขยายตัวพารามิเตอร์ POSIX

ทั้งสองอย่างนี้จะทำให้เชลล์เชิงโต้ตอบสิ้นสุดลงยกเว้นset -e(หรือset -o errexit) ด้วย ${variable:?word}ทำให้สคริปต์ออกหากตัวแปรว่างเปล่าหรือไม่ได้ตั้งค่า จะทำให้เกิดสคริปต์เพื่อออกถ้าใช้ร่วมกับset -uset -e


สำหรับคำถามที่สองของคุณ ไม่มีวิธี จำกัด ที่rmจะไม่ทำงานนอกไดเรกทอรีปัจจุบัน

การนำ GNU ไปใช้rmนั้นมี--one-file-systemตัวเลือกที่จะหยุดไม่ให้ลบระบบไฟล์ที่ติดตั้งซ้ำแล้วซ้ำอีก แต่ก็ใกล้เคียงที่สุดเท่าที่ฉันเชื่อว่าเราสามารถทำได้โดยไม่ต้องวางrmสายในฟังก์ชันที่ตรวจสอบข้อโต้แย้งจริง ๆ


ขณะที่ทราบด้าน: ${build}เป็นว่าเทียบเท่ากับเว้นแต่การขยายตัวเกิดขึ้นเป็นส่วนหนึ่งของสตริงที่ตัวละครทันทีต่อไปนี้เป็นตัวละครที่ถูกต้องในชื่อตัวแปรเช่นใน$build"${build}x"


ดีมากขอบคุณ! 1) มันเป็น${build:?msg}หรือ${build?msg}? 2) ในบริบทของการสร้างสคริปต์ฉันคิดว่าการใช้เครื่องมือที่แตกต่างจาก rm สำหรับการลบที่ปลอดภัยกว่านั้นใช้ได้: เรารู้ว่าเราไม่ต้องการทำงานนอกไดเรกทอรีปัจจุบันดังนั้นเราจึงทำให้ชัดเจนโดยใช้ข้อ จำกัด คำสั่ง ไม่จำเป็นต้องทำให้ rm ปลอดภัยกว่าโดยทั่วไป
กราฟิลส์

1
@ ราฟาเอลขออภัยด้วย: ฉันจะแก้ไขข้อผิดพลาดนั้นและฉันจะพูดถึงความสำคัญในภายหลังเมื่อฉันกลับมาที่คอมพิวเตอร์
Kusalananda

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

1
ขอบคุณคำอธิบายช่วยได้! เกี่ยวกับ "local rm": ฉันมักจะรำลึกถึงการหา "แน่นอนนั่นคือ <toolname>" - แต่แน่นอนว่าไม่ได้พยายามช่วยแวมไพร์ให้คุณเขียนมัน! OO ดีแล้วคุณช่วยได้มากพอ! :)
กราฟิลส์

2
@MateuszKonieczny ฉันไม่คิดว่าฉันจะขอโทษ ผมเชื่อว่ามาตรการด้านความปลอดภัยเช่นนี้ควรจะใช้เมื่อมีความจำเป็น เช่นเดียวกับทุกสิ่งที่ทำให้สภาพแวดล้อมปลอดภัยในที่สุดก็จะทำให้ผู้คนประมาทมากขึ้นเรื่อย ๆ และขึ้นอยู่กับมาตรการความปลอดภัย จะเป็นการดีกว่าที่จะรู้ว่ามาตรการความปลอดภัยของทุกคนทำอะไรและนำไปใช้อย่างเหมาะสมตามที่ต้องการ
Kusalananda

12

ฉันจะแนะนำการตรวจสอบความถูกต้องตามปกติโดยใช้test/[ ]

คุณจะปลอดภัยถ้าคุณเขียนสคริปต์เช่น:

build="build"
...
[ -n "${build}" ] || exit 1
rm -rf "${build}/"*
...

[ -n "${build}" ]การตรวจสอบว่า"${build}"เป็นสตริงยาวไม่ใช่ศูนย์

||เป็นตรรกะหรือผู้ประกอบการในการทุบตี มันทำให้คำสั่งอื่นที่จะทำงานถ้าคนแรกล้มเหลว

ด้วยวิธีนี้ได้${build}ว่าง / ไม่ได้กำหนด / ฯลฯ สคริปต์จะออกจาก (ด้วยโค้ดส่งคืน 1 ซึ่งเป็นข้อผิดพลาดทั่วไป)

สิ่งนี้จะช่วยป้องกันคุณในกรณีที่คุณลบการใช้งานทั้งหมด${build}เพราะ[ -n "" ]จะเป็นเท็จเสมอ


ข้อดีของการใช้test/ [ ]มีการตรวจสอบที่มีความหมายอื่น ๆ อีกมากมายที่สามารถใช้งานได้

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

[ -f FILE ] True if FILE exists and is a regular file.
[ -d FILE ] True if FILE exists and is a directory.
[ -O FILE ] True if FILE exists and is owned by the effective user ID.

ยุติธรรม แต่เทอะทะเทอะทะ ฉันทำบางสิ่งขาดหายไปหรือทำเช่นเดียวกันกับ${variable:?word}@Kusalananda
กราฟิลส์

@ ราฟาเอลมันทำงานคล้ายกันในกรณีที่ "ทำสตริงมีค่า" แต่test(เช่น[) มีการตรวจสอบอื่น ๆ อีกมากมายที่เกี่ยวข้องเช่น-d(อาร์กิวเมนต์เป็นไดเรกทอรี), -f(อาร์กิวเมนต์เป็นไฟล์), -O(ไฟล์ เป็นเจ้าของโดยผู้ใช้ปัจจุบัน) และอื่น ๆ
Centimane

1
@ ราฟาเอลการตรวจสอบควรเป็นส่วนหนึ่งของรหัส / สคริปต์ นอกจากนี้โปรดทราบว่าหากคุณลบการสร้างอินสแตนซ์ทั้งหมดคุณจะไม่ได้ลบออก${build:?word}ไปพร้อม ๆ กันด้วยไหม ${variable:?word}รูปแบบไม่ได้ปกป้องคุณถ้าคุณลบทุกกรณีของตัวแปร
Centimane

1
1) ฉันคิดว่ามีความแตกต่างระหว่างการทำให้แน่ใจว่ามีการสมมติสมมติฐาน (มีไฟล์อยู่ ฯลฯ ) และตรวจสอบว่ามีการตั้งค่าตัวแปรไว้หรือไม่ (เป็นหน้าที่ของคอมไพเลอร์ / ล่าม) ถ้าฉันต้องทำด้วยมือข้างหลังจะดีกว่าซินแท็คซ์แบบพิเศษสำหรับมัน - ซึ่งเต็มไปด้วยการเป่าifไม่ได้ 2) "คุณจะไม่ลบ $ {build:? word} ไปด้วยหรือไม่" - สถานการณ์คือฉันพลาดการใช้งานตัวแปร การใช้${v:?w}จะช่วยป้องกันฉันจากความเสียหาย หากฉันลบประเพณีทั้งหมดออกไปแม้การเข้าถึงธรรมดาจะไม่เป็นอันตรายแน่นอน
กราฟิลส์

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

4

ในกรณีเฉพาะของคุณฉันได้ทำใหม่ 'ลบ' ในอดีตเพื่อย้ายไฟล์ / ไดเรกทอรีแทน (สมมติว่า / tmp อยู่ในพาร์ทิชันเดียวกับไดเรกทอรีของคุณ):

# mktemp -d is also a good, reliable choice
trashdir=/tmp/.trash-$USER/trash-`date`
mkdir -p "$trashdir"
...
mv "${build}"/* "$trashdir"
...

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

หรือรายการ cron เพื่อทำความสะอาด /tmp/.trash-$USER เป็นระยะจะป้องกัน / tmp ไม่ให้เติมสำหรับกระบวนการ (เช่นการสร้าง) ที่ใช้พื้นที่ดิสก์จำนวนมาก หากไดเร็กทอรีของคุณอยู่ในพาร์ติชันอื่นเป็น / tmp คุณสามารถสร้างไดเร็กตอรี่ / tmp-like ที่คล้ายกันบนพาร์ติชันของคุณและให้ cron ล้างมันแทน

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


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

1
คุณสามารถใช้mktemp -dเพื่อรับไดเร็กตอรี่ชั่วคราวนั้นโดยไม่เหยียบบนนิ้วเท้าของโพรเซสอื่น (และให้เกียรติอย่างถูกต้อง$TMPDIR)
Toby Speight

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

2

ใช้การแทนค่าพารามิเตอร์ bash paramater เพื่อกำหนดค่าเริ่มต้นเมื่อตัวแปรไม่ได้กำหนดค่าเริ่มต้นเช่น:

rm -rf $ {ตัวแปร: - "/ nonexistent"}


1

#!/bin/bash -ueฉันมักจะพยายามที่จะเริ่มต้นสคริปต์ทุบตีฉันกับบรรทัด

-eหมายถึง "ล้มเหลวในการอีแร้งแรกที่ไม่แคร์";

-uหมายถึง "ล้มเหลวในการใช้งานครั้งแรกของตัวแปรu ndeclared"

ค้นหารายละเอียดเพิ่มเติมในบทความที่ดีใช้อย่างไม่เป็นทางการทุบตีเข้มงวดโหมด (เว้นแต่คุณ looove แก้จุดบกพร่อง) ผู้เขียนแนะนำให้ใช้set -o pipefail; IFS=$'\n\t'แต่สำหรับจุดประสงค์ของฉันนี่คือ overkill


1

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

มีความเป็นไปได้มากที่สุดที่ไม่จำเป็นต้องทำให้กลมเนื้อหาของ$buildไดเรกทอรีเพื่อลบออก แต่ไม่ใช่$buildไดเรกทอรีจริง ดังนั้นหากคุณข้ามสิ่งที่ไม่เกี่ยวข้องออกไปค่าที่ไม่*ได้ตั้งค่าจะกลายrm -rf /เป็นการใช้งาน rm ส่วนใหญ่ในทศวรรษที่ผ่านมาจะปฏิเสธที่จะดำเนินการ (เว้นแต่คุณปิดการใช้งานการป้องกันนี้ด้วย--no-preserve-rootตัวเลือกGNU rm )

การข้ามการติดตาม/เช่นกันจะส่งผลให้เกิดrm ''ข้อความแสดงข้อผิดพลาด:

rm: can't remove '': No such file or directory

นี้ทำงานแม้ว่าคำสั่ง RM /ของคุณไม่ใช้การป้องกันสำหรับ


-2

คุณกำลังคิดในแง่ภาษาการเขียนโปรแกรม แต่ทุบตีเป็นภาษาสคริปต์ :-) ดังนั้นให้ใช้กระบวนทัศน์การเรียนการสอนที่แตกต่างกันโดยสิ้นเชิงสำหรับกระบวนทัศน์ภาษาที่แตกต่างกันทั้งหมด

ในกรณีนี้:

rmdir ${build}

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

rm -rf ${build}/${parameter}
rmdir ${build}

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

rmdir ${build} || build_dir_not_empty "${build}"

วิธีการคิดแบบนี้ทำให้ฉันรับใช้ได้ดีเพราะ ... ใช่แล้วไปทำอย่างนั้น


6
ขอบคุณสำหรับความพยายามของคุณ แต่นี่เป็นเครื่องหมายที่สมบูรณ์ สมมติฐาน "คุณรู้ว่าสมาชิกเหล่านั้น" ไม่ถูกต้อง นึกถึงคอมไพเลอร์เอาท์พุท make cleanจะใช้อักขระตัวแทนบางตัว (เว้นแต่ว่าคอมไพเลอร์ที่ขยันมากจะสร้างรายการไฟล์ทุกไฟล์ที่สร้างขึ้น) นอกจากนี้ดูเหมือนว่าสำหรับฉันที่มีrm -rf ${build}/${parameter}ปัญหาเพียงเลื่อนลงเล็กน้อย
กราฟิลส์

ฮึ่ม ดูวิธีการที่อ่าน "ขอบคุณ แต่นี่คือสิ่งที่ฉันไม่ได้เปิดเผยในคำถามดั้งเดิมดังนั้นฉันจะไม่เพียง แต่ปฏิเสธคำตอบของคุณเท่านั้น แต่ยังลงคะแนนแม้ว่าจะเหมาะสมกับคดีทั่วไปมากขึ้นก็ตาม" ว้าว.
รวย

"ให้ฉันตั้งสมมติฐานเพิ่มเติมนอกเหนือจากที่คุณเขียนไว้ในคำถามแล้วเขียนคำตอบเฉพาะ" * shrug * (FYI ไม่ใช่ว่ามันเป็นธุรกิจของคุณ: ฉันไม่ได้ลงคะแนนเลยฉันควรจะได้เพราะคำตอบนั้นไม่มีประโยชน์ (สำหรับฉันอย่างน้อยที่สุด).
Raphael

ฮึ่ม ฉันไม่ได้ตั้งสมมติฐานและเขียนคำตอบทั่วไป ไม่มีอะไรเกี่ยวกับmakeคำถามเลย การเขียนคำตอบที่ใช้ได้เฉพาะกับmakeจะเป็นสมมติฐานที่เฉพาะเจาะจงและน่าประหลาดใจมาก คำตอบของฉันทำงานได้กับกรณีอื่น ๆ นอกเหนือmakeจากการพัฒนาซอฟต์แวร์นอกเหนือจากปัญหาเฉพาะมือใหม่ที่คุณมีโดยการลบเนื้อหาของคุณ
รวย

1
Kusalananda สอนวิธีการตกปลา คุณเข้ามาและพูดว่า "เมื่อคุณอาศัยอยู่ในที่ราบทำไมคุณไม่กินเนื้อวัวแทน"
กราฟิลส์
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.