Bagaimana saya bisa mengeraskan skrip bash agar tidak menyebabkan kerusakan saat diubah di masa mendatang?

46

Jadi, saya menghapus folder rumah saya (atau, lebih tepatnya, semua file yang saya tulis aksesnya). Apa yang terjadi adalah yang saya miliki

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

dalam skrip bash dan, setelah tidak perlu lagi $build, menghapus deklarasi dan semua penggunaannya - tetapi rm. Bash dengan senang hati mengembang rm -rf /*. Ya.

Saya merasa bodoh, menginstal cadangan, redid pekerjaan yang hilang. Mencoba melewati rasa malu.

Sekarang, saya bertanya-tanya: apa teknik untuk menulis skrip bash sehingga kesalahan seperti itu tidak bisa terjadi, atau setidaknya lebih kecil kemungkinannya? Misalnya, sudahkah saya menulis

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

dalam naskah Ruby, penerjemah akan mengeluh karena buildtidak dideklarasikan, jadi bahasa itu melindungi saya.

Apa yang telah saya pertimbangkan dalam bash, selain mengoreksi rm(yang, sebagaimana banyak jawaban dalam pertanyaan terkait menyebutkan, tidak bermasalah):

  1. rm -rf "./${build}/"*
    Itu akan membunuh pekerjaan saya saat ini (repo Git) tetapi tidak ada yang lain.
  2. Varian / parameterisasi rmyang memerlukan interaksi ketika bertindak di luar direktori saat ini. (Tidak dapat menemukan.) Efek serupa.

Apakah itu, atau adakah cara lain untuk menulis skrip bash yang "kuat" dalam pengertian ini?

Raphael
sumber
3
Tidak ada alasan untuk ke rm -rf "${build}/*mana pun tanda kutip pergi. rm -rf "${build} akan melakukan hal yang sama karena f.
Monty Harder
1
Pemeriksaan statis dengan shellcheck.net adalah tempat yang sangat solid untuk memulai. Ada integrasi editor yang tersedia, sehingga Anda bisa mendapatkan peringatan dari alat Anda segera setelah Anda menghapus definisi dari sesuatu yang masih digunakan.
Charles Duffy
@CharlesDuffy Untuk menambah rasa malu saya, saya menulis skrip itu dalam IDE gaya IDEA dengan BashSupport diinstal, yang memang memperingatkan dalam kasus itu. Jadi ya, benar, tapi saya benar-benar butuh pembatalan yang sulit.
Raphael
2
Gotcha. Pastikan untuk mencatat peringatan di BashFAQ # 112 . set -utidak mengernyit set -e, tetapi ia masih memiliki gotcha.
Charles Duffy
2
lelucon jawaban: Cukup taruh #! /usr/bin/env rubydi bagian atas setiap skrip shell dan lupakan bash;)
Pod

Jawaban:

75
set -u

atau

set -o nounset

Ini akan membuat shell memperlakukan ekspansi variabel yang tidak disetel sebagai kesalahan:

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

set -udan set -o nounsetmerupakan opsi shell POSIX .

Sebuah kosong nilai akan tidak memicu kesalahan sekalipun.

Untuk itu, gunakan

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

Perluasan ${variable:?word}akan diperluas ke nilai variablekecuali jika itu kosong atau tidak disetel. Jika kosong atau tidak disetel, worditu akan ditampilkan pada kesalahan standar dan shell akan memperlakukan ekspansi sebagai kesalahan (perintah tidak akan dieksekusi, dan jika berjalan di shell non-interaktif, ini akan berakhir). Membiarkan :keluar akan memicu kesalahan hanya untuk nilai yang tidak disetel, seperti di bawah set -u.

${variable:?word}adalah ekspansi parameter POSIX .

Tidak satu pun dari ini akan menyebabkan shell interaktif berakhir kecuali set -e(atau set -o errexit) juga berlaku. ${variable:?word}menyebabkan skrip keluar jika variabel kosong atau tidak disetel. set -uakan menyebabkan skrip keluar jika digunakan bersama dengan set -e.


Adapun pertanyaan kedua Anda. Tidak ada cara untuk membatasi rmuntuk tidak bekerja di luar direktori saat ini.

Implementasi GNU rmmemiliki --one-file-systemopsi yang menghentikannya dari menghapus filesystem yang dipasang secara rekursif, tapi itu sedekat yang saya percaya bisa kita dapatkan tanpa membungkus rmpanggilan dalam fungsi yang benar-benar memeriksa argumen.


Sebagai catatan samping: ${build}persis sama dengan $buildkecuali jika ekspansi terjadi sebagai bagian dari string di mana karakter yang mengikuti berikut adalah karakter yang valid dalam nama variabel, seperti pada "${build}x".

Kusalananda
sumber
Terima kasih banyak! 1) Apakah itu ${build:?msg}atau ${build?msg}? 2) Dalam konteks sesuatu seperti membangun skrip, saya pikir menggunakan alat yang berbeda dari rm untuk penghapusan yang lebih aman akan baik-baik saja: kita tahu kita tidak ingin bekerja di luar direktori saat ini, jadi kami membuatnya secara eksplisit dengan menggunakan dibatasi perintah. Tidak perlu membuat rm lebih aman secara umum.
Raphael
1
@ Raphael Maaf, harus dengan:. Saya akan memperbaiki kesalahan ketik itu sekarang dan saya akan menyebutkan maknanya nanti ketika saya kembali ke komputer saya.
Kusalananda
2
@ Raphael Ok, saya telah menambahkan kalimat pendek tentang apa yang terjadi tanpa :(itu akan memicu kesalahan hanya untuk variabel yang tidak disetel ). Saya tidak berani mencoba menulis alat yang akan mengatasi menghapus file di bawah jalur tertentu secara eksklusif, dalam semua keadaan. Parsing paths dan caring / not caring tentang tautan simbolik dll. Agak terlalu fiddly untuk saya saat ini.
Kusalananda
1
Terima kasih, penjelasannya membantu! Mengenai "rm lokal": Saya kebanyakan merenung, mungkin memancing untuk "yakin, itu <toolname>" - tetapi tentu saja tidak mencoba untuk membantu-vampir Anda dalam menulisnya! OO Semua bagus, Anda sudah cukup membantu! :)
Raphael
2
@MateuszKonieczny Saya tidak berpikir saya akan, maaf. Saya percaya bahwa langkah-langkah keamanan seperti ini harus digunakan saat dibutuhkan . Seperti segala sesuatu yang membuat lingkungan aman, pada akhirnya akan membuat orang semakin ceroboh dan bergantung pada langkah-langkah keamanan. Lebih baik untuk mengetahui apa yang dilakukan masing-masing dan setiap tindakan keselamatan dan kemudian menerapkannya secara selektif sesuai kebutuhan.
Kusalananda
12

Saya akan menyarankan pemeriksaan validasi normal menggunakan test/[ ]

Anda akan aman jika Anda menulis skrip Anda seperti itu:

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

The [ -n "${build}" ]cek yang "${build}"adalah panjang string non-nol .

The ||adalah logis operator OR di bash. Ini menyebabkan perintah lain dijalankan jika yang pertama gagal.

Dengan cara ini, telah ${build}kosong / tidak terdefinisi / dll. skrip akan keluar (dengan kode pengembalian 1, yang merupakan kesalahan umum).

Ini juga akan melindungi Anda jika Anda menghapus semua penggunaan ${build}karena [ -n "" ]akan selalu salah.


Keuntungan menggunakan test/ [ ]adalah ada banyak pemeriksaan lain yang lebih bermakna yang juga dapat digunakan.

Sebagai contoh:

[ -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.
Centimane
sumber
Adil, tapi agak sulit. Apakah saya melewatkan sesuatu, atau apakah itu hampir sama dengan yang ${variable:?word}diusulkan @Kusalananda?
Raphael
@Raphael berfungsi sama dalam kasus "apakah string memiliki nilai" tetapi test(yaitu [) memiliki banyak pemeriksaan lain yang relevan, seperti -d(argumennya adalah direktori), -f(argumennya adalah file), -O(file tersebut dimiliki oleh pengguna saat ini) dan seterusnya.
Centimane
1
@Raphael juga, validasi harus menjadi bagian normal dari kode / skrip apa pun. Perhatikan juga, jika Anda menghapus semua instance build, apakah Anda juga tidak akan menghapusnya ${build:?word}? The ${variable:?word}format tidak melindungi Anda jika Anda menghapus semua contoh variabel.
Centimane
1
1) Saya pikir ada perbedaan antara memastikan asumsi tetap (file ada, dll) dan memeriksa apakah variabel diatur (pekerjaan imho dari kompiler / juru bahasa). Jika saya harus melakukan yang terakhir dengan tangan, lebih baik ada sintaks ultra-manis untuk itu - yang full-blown iftidak. 2) "Anda tidak akan menghapus $ {build:? Word} bersamaan dengan itu" - skenario adalah bahwa saya melewatkan penggunaan variabel. Menggunakan ${v:?w}akan melindungi saya dari kerusakan. Seandainya saya menghapus semua penggunaan, bahkan akses biasa tidak akan berbahaya, jelas.
Raphael
Bahwa semua kata, jawaban Anda adalah adil dan respon helful untuk pertanyaan tituler umum: memastikan asumsi ditahan adalah penting untuk script yang tinggal di sekitar. Masalah khusus dalam badan pertanyaan adalah, imho, lebih baik dijawab oleh Kusalananda. Terima kasih!
Raphael
4

Dalam kasus spesifik Anda, saya telah mengerjakan 'penghapusan' di masa lalu untuk memindahkan file / direktori sebagai gantinya (dengan asumsi / tmp berada pada partisi yang sama dengan direktori Anda):

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

Di belakang layar, ini memindahkan referensi file / dir tingkat atas dari sumber ke $trashdirstruktur direktori tujuan semua pada partisi yang sama, dan tidak menghabiskan waktu berjalan struktur direktori dan membebaskan blok-blok disk per-file saat itu juga. Ini menghasilkan pembersihan yang jauh lebih cepat ketika sistem sedang digunakan aktif, sebagai ganti untuk reboot yang sedikit lebih lambat (/ tmp dibersihkan pada reboot).

Atau, entri cron untuk membersihkan /tmp/.trash-$USER secara berkala akan menjaga / tmp dari pengisian, untuk proses (mis. Build) yang menghabiskan banyak ruang disk. Jika direktori Anda berada di partisi berbeda sebagai / tmp, Anda bisa membuat direktori serupa / tmp di partisi Anda dan memiliki cron clean that gantinya.

Namun yang paling penting, jika Anda mengacaukan variabel dengan cara apa pun, Anda dapat memulihkan konten sebelum pembersihan terjadi.

pengguna117529
sumber
2
"Ini menghasilkan pembersihan jauh lebih cepat saat sistem sedang digunakan aktif" - bukan? Saya pikir keduanya hanya mengubah inode yang terpengaruh saja. Tentu tidak lebih cepat jika /tmpada di partisi lain (saya pikir itu selalu untuk saya, setidaknya untuk skrip yang berjalan di ruang pengguna); kemudian, folder tempat sampah perlu diubah (dan tidak akan mendapat untung dari penanganan OS /tmp).
Raphael
1
Anda bisa menggunakan mktemp -duntuk mendapatkan direktori sementara itu tanpa menginjak-injak jari kaki proses lain (dan untuk menghormati dengan benar $TMPDIR).
Toby Speight
Menambahkan catatan Anda yang akurat dan bermanfaat dari Anda berdua, terima kasih. Saya pikir saya awalnya memposting ini terlalu cepat tanpa mempertimbangkan poin yang Anda kemukakan.
user117529
2

Gunakan substitusi bash paramater untuk menetapkan default ketika variabel tidak diinisialisasi, misalnya:

rm -rf $ {variabel: - "/ nonexistent"}

pemeras
sumber
1

Saya selalu mencoba memulai skrip Bash saya dengan sebuah baris #!/bin/bash -ue.

-eberarti "gagal pada e rror pertama yang tidak disentuh";

-uberarti "gagal pada penggunaan pertama u ndeclared variabel".

Temukan lebih banyak detail di artikel hebat Gunakan Mode Ketetapan Bash Tidak Resmi (Kecuali Anda Mengecewakan) . Penulis juga merekomendasikan menggunakan set -o pipefail; IFS=$'\n\t'tetapi untuk tujuan saya ini berlebihan.

niya3
sumber
1

Saran umum untuk memeriksa apakah variabel Anda ditetapkan, adalah alat yang berguna untuk mencegah masalah semacam ini. Tetapi dalam hal ini ada solusi yang lebih sederhana.

Kemungkinan besar tidak perlu menggumpal isi $builddirektori untuk menghapusnya tetapi tidak pada $builddirektori itu sendiri. Jadi jika Anda melewatkan extraneous *maka nilai yang tidak disetel akan berubah rm -rf /yang secara default sebagian besar implementasi rm dalam dekade terakhir akan menolak untuk melakukan (kecuali jika Anda menonaktifkan perlindungan ini dengan --no-preserve-rootopsi GNU rm ).

Melompati trailing /juga akan menghasilkan rm ''pesan kesalahan:

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

Ini berfungsi bahkan jika perintah rm Anda tidak menerapkan perlindungan untuk /.

eschwartz
sumber
-2

Anda berpikir dalam istilah bahasa pemrograman, tetapi bash adalah bahasa scripting :-) Jadi, gunakan paradigma instruksi yang sama sekali berbeda untuk paradigma bahasa yang sama sekali berbeda .

Pada kasus ini:

rmdir ${build}

Karena rmdirakan menolak untuk menghapus direktori yang tidak kosong, Anda harus menghapus anggota terlebih dahulu. Anda tahu apa itu anggota, kan? Itu mungkin parameter untuk skrip Anda, atau berasal dari parameter, jadi:

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

Sekarang, jika Anda meletakkan beberapa file atau direktori lain di sana seperti file temp atau sesuatu yang seharusnya tidak Anda miliki, rmdirakan menimbulkan kesalahan. Tangani dengan benar, lalu:

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

Cara berpikir ini telah membantu saya dengan baik karena ... ya, sudah ada, melakukan itu.

Kaya
sumber
6
Terima kasih atas usaha Anda, tetapi ini sepenuhnya melenceng. Asumsi "Anda tahu apa itu anggota" adalah salah; pikirkan output kompiler. make cleanakan menggunakan beberapa wildcard (kecuali jika kompiler yang sangat rajin membuat daftar setiap file yang dibuatnya). Juga, bagi saya tampaknya rm -rf ${build}/${parameter}hanya sedikit memindahkan masalah.
Raphael
Hm Lihat bagaimana bunyinya. "Terima kasih, tapi ini untuk sesuatu yang tidak aku ungkapkan dalam pertanyaan aslinya jadi aku tidak hanya akan menolak jawabanmu tetapi juga menurunkannya, meskipun itu lebih berlaku untuk kasus umum." Wow.
Kaya
"Biarkan saya membuat asumsi tambahan di luar apa yang Anda tulis dalam pertanyaan dan kemudian menulis jawaban khusus." * mengangkat bahu * (FYI, bukan itu urusan Anda: saya tidak downvote. Mungkin saya seharusnya, karena jawabannya tidak berguna (bagi saya, setidaknya).)
Raphael
Hm Saya tidak membuat asumsi, dan menulis seorang jenderal jawaban . Tidak ada sama sekali tentang makepertanyaan itu. Menulis jawaban yang hanya dapat diterapkan makeakan menjadi asumsi yang sangat spesifik dan mengejutkan. Jawaban saya berfungsi untuk kasus selain make, selain pengembangan perangkat lunak, selain masalah khusus pemula yang Anda alami dengan menghapus barang-barang Anda.
Kaya
1
Kusalananda mengajari saya cara memancing. Anda datang dan berkata, "Karena Anda tinggal di dataran, mengapa Anda tidak makan daging sapi saja?"
Raphael