Mengapa bash
Ada array dan safe mode di bash. Ketika digunakan dengan benar, bash hampir konsisten dengan praktik pengkodean yang aman.
Lebih sulit membuat kesalahan pada ikan, tetapi tidak ada mode aman. Oleh karena itu, membuat prototipe pada ikan dan kemudian menerjemahkan dari ikan ke bash harus menjadi ide yang baik jika Anda tahu cara melakukannya dengan benar.
Kata Pengantar
Panduan ini menyertai ShellHarden, tetapi penulis juga merekomendasikan
ShellCheck sehingga aturan ShellHarden tidak menyimpang dari ShellCheck.
Bash bukan bahasa di mana cara
paling tepat untuk menyelesaikan masalah pada saat yang sama adalah yang termudah . Jika Anda mengikuti ujian pemrograman bash safe, maka aturan pertama
BashPitfalls adalah: selalu menggunakan tanda kutip.
Hal utama yang perlu Anda ketahui tentang pemrograman di bash
Tanda kutip manik! Variabel yang tidak dikutip harus dianggap sebagai bom cocked: meledak pada kontak dengan ruang. Ya, itu meledak dalam arti
membagi string menjadi sebuah array . Secara khusus, ekstensi variabel seperti
$var
dan substitusi perintah seperti
$(cmd)
oleh
kata ketika string bagian dalam diperluas menjadi array karena pemisahan dalam variabel
$IFS
khusus dengan ruang default. Ini biasanya tidak terlihat, karena paling sering hasilnya adalah array dari 1 elemen, tidak dapat dibedakan dari string yang diharapkan.
Tidak hanya ini diperluas, tetapi juga wildcard (
*?
). Proses ini terjadi setelah memisahkan kata, jadi jika kata tersebut memiliki setidaknya satu wildcard, kata tersebut berubah menjadi wildcard yang berlaku untuk setiap jalur file yang sesuai. Jadi fitur ini mulai berlaku untuk sistem file!
Kutipan menekan pemisahan kata dan perluasan pola untuk variabel dan penggantian perintah.
Ekstensi variabel:
- Bagus:
"$my_var"
- Buruk:
$my_var
Substitusi perintah:
- Bagus:
"$(cmd)"
- Buruk:
$(cmd)
Ada pengecualian dengan tanda kutip opsional, tetapi tanda kutip tidak akan menyakiti, dan aturan umum adalah berhati-hati untuk tidak mengutip variabel yang tidak dikutip, jadi kami tidak akan mencari pengecualian perbatasan untuk keuntungan Anda. Tampaknya salah, dan praktik yang salah cukup luas untuk menimbulkan kecurigaan: banyak skrip telah ditulis dengan pemrosesan yang rusak dari nama file dan spasi di dalamnya ...
ShellHarden hanya menyebutkan sedikit pengecualian - apakah variabel-variabel ini dengan konten numerik seperti
$?
,
$#
dan
${#array[@]}
.
Apakah saya perlu menggunakan backticks?
Pergantian perintah juga dapat memiliki bentuk berikut:
- Benar:
"`cmd`"
- Buruk:
`cmd`
Meskipun gaya ini dapat digunakan dengan benar, itu terlihat kurang nyaman dalam tanda kutip dan kurang dapat dibaca saat bersarang. Konsensus di sini cukup jelas: hindari.
ShellHarden menulis ulang tanda centang dalam tanda kurung dalam dolar.
Apakah kurung kurawal perlu digunakan?
Kurung digunakan untuk menyisipkan string, sehingga biasanya berlebihan:
- Buruk:
some_command $arg1 $arg2 $arg3
- Miskin dan verbose:
some_command ${arg1} ${arg2} ${arg3}
- Bagus, tetapi verbose:
some_command "${arg1}" "${arg2}" "${arg3}"
- Bagus:
some_command "$arg1" "$arg2" "$arg3"
Secara teoritis, selalu menggunakan kurung kurawal bukanlah masalah, tetapi menurut pengalaman penulis Anda, ada korelasi negatif yang kuat antara penggunaan kurung kurawal yang tidak perlu dan penggunaan tanda kutip yang benar - hampir semua orang memilih bentuk "buruk dan bertele-tele" alih-alih bentuk "baik tetapi bertulang"!
Teori penulis Anda:
- Karena takut melakukan sesuatu yang salah: alih-alih bahaya nyata (kurangnya tanda kutip), pemula mungkin khawatir bahwa variabel
$prefix
akan menyebabkan variabel "$prefix_postfix"
meluas, tetapi tidak berfungsi seperti itu. - Kultus muatan: menulis kode dalam perjanjian ketakutan salah yang mendahuluinya.
- Kurung bersaing dengan tanda kutip untuk batas verbositas yang diizinkan.
Oleh karena itu, diputuskan untuk melarang kurung kurawal yang tidak perlu: ShellHarden mengganti opsi ini dengan bentuk yang paling sederhana.
Dan sekarang tentang interpolasi string, di mana kurung kurawal sangat berguna:
- Buruk (gabungan):
$var1"more string content"$var2
- Bagus (gabungan):
"$var1""more string content""$var2"
- Bagus (interpolasi):
"${var1}more string content${var2}"
Penggabungan dan interpolasi dalam bash adalah setara bahkan dalam array (yang konyol).
Karena ShellHarden tidak memformat gaya, itu tidak seharusnya mengubah kode yang benar. Ini berlaku untuk opsi "baik (interpolasi)": dari sudut pandang ShellHarden, ini akan menjadi bentuk yang benar secara kanonik.
ShellHarden sekarang menambahkan dan menghapus kurung kurawal sesuai kebutuhan: dalam contoh yang buruk, var1 disediakan dengan kurung, tetapi mereka tidak diizinkan untuk var2 bahkan dalam kasus "baik (interpolasi)", karena mereka tidak pernah diperlukan pada akhir baris. Persyaratan terakhir mungkin dapat dibalik.
Gotcha: argumen bernomor
Tidak seperti nama
pengenal variabel normal (dalam regex:
[_a-zA-Z][_a-zA-Z0-9]*
), argumen bernomor memerlukan tanda kurung (interpolasi garis tidak). ShellCheck mengatakan:
echo "$10" ^-- SC1037: Braces are required for positionals over 9, eg ${10}.
ShellHarden menolak untuk memperbaikinya (menganggap perbedaannya terlalu halus).
Karena tanda kurung diizinkan hingga 9, ShellHarden memungkinkannya untuk semua argumen bernomor.
Menggunakan Array
Untuk dapat mengutip semua variabel, Anda harus menggunakan array nyata, bukan string pseudo-masif yang dipisahkan oleh spasi.
Sintaksnya adalah verbose, tetapi Anda harus menanganinya. Bashism ini hanyalah salah satu alasan untuk meninggalkan kompatibilitas POSIX untuk sebagian besar skrip shell.
Baik:
array=( a b ) array+=(c) if [ ${#array[@]} -gt 0 ]; then rm -- "${array[@]}" fi
Buruk:
pseudoarray=" \ a \ b \ " pseudoarray="$pseudoarray c" if ! [ "$pseudoarray" = '' ]; then rm -- $pseudoarray fi
Itu sebabnya array adalah fungsi dasar untuk shell:
argumen perintah pada dasarnya adalah array (dan skrip shell adalah perintah dan argumen). Kita dapat mengatakan bahwa shell, yang secara artifisial tidak memungkinkan untuk melewati beberapa argumen, akan menjadi lucu dan tidak berharga. Beberapa shell umum dari kategori ini termasuk
Dash dan Busybox Ash. Ini adalah shell kompatibel POSIX minimal - tetapi apa gunanya kompatibilitas jika hal terpenting
tidak ada pada POSIX?
Kasus luar biasa ketika Anda benar-benar akan melanggar batas
Contoh dengan
\v
sebagai pemisah data (perhatikan kejadian kedua):
IFS=$'\v' read -d '' -ra a < <(printf '%s\v' "$s") || true
Dengan cara ini kami menghindari perluasan templat, dan metode ini berfungsi bahkan jika pemisah data adalah
\n
. Munculnya kedua pemisah data melindungi elemen terakhir jika ternyata menjadi spasi. Untuk beberapa alasan, opsi
-d
harus
-rad ''
, jadi
-rad ''
opsi di
-rad ''
menggoda, tetapi itu tidak akan berhasil. Karena baca mengembalikan nilai bukan nol dalam kasus ini, itu harus dilindungi dari errexit (
|| true
), jika diaktifkan. Diuji dalam bash 4.0, 4.1, 4.2, 4.3 dan 4.4.
Alternatif untuk bash 4.4:
readarray -td $'\v' a < <(printf '%s\v' "$s")
Di mana memulai skrip bash
Dari sesuatu seperti ini:
Ini termasuk:
- Shebang:
- Masalah portabilitas: Path absolut ke
env
mungkin lebih baik untuk portabilitas daripada path absolut ke bash
. Anda dapat melihat contoh NixOS . POSIX membutuhkan env , tetapi bukan bash. - Masalah Keamanan: Tanpa bahasa, opsi seperti
-euo pipefail
tidak akan diterima dengan baik di -euo pipefail
! Ini menjadi tidak mungkin ketika menggunakan pengalihan env
, tetapi bahkan jika shebang Anda mulai dengan #!/bin/bash
, ini bukan tempat untuk parameter yang mempengaruhi nilai skrip, karena mereka dapat diganti, yang akan memungkinkan untuk mengeksekusi skrip secara salah. Namun, sebagai bonus, opsi yang tidak memengaruhi nilai skrip, seperti set -x
, jika digunakan, dapat didefinisikan ulang.
- Apa yang kita butuhkan dari mode ketat Bash tidak resmi , dengan
set -u
fitur set -u
-check. Kami tidak memerlukan semua mode Bash yang ketat, karena kompatibilitas shellcheck / shellharden berarti mengutip segala sesuatu dan segala sesuatu yang jauh lebih ketat. Selain itu, opsi set -u
tidak boleh digunakan di Bash 4.3 dan sebelumnya. Karena opsi ini menganggap array kosong sebagai dibuang di versi itu, array tidak dapat digunakan untuk tujuan yang dijelaskan di sini. Menggunakan array adalah tip terpenting kedua dari panduan ini (setelah tanda kutip) dan satu-satunya alasan kami mengorbankan kompatibilitas dengan POSIX, jadi ini sama sekali tidak dapat diterima: tidak menggunakan set -u
, atau menggunakan Bash 4.4 atau shell normal lainnya seperti Zsh. Ini lebih mudah diucapkan daripada dilakukan, karena ada kemungkinan seseorang akan tetap menjalankan skrip Anda di versi kuno Bash. Untungnya, semua yang bekerja dengan set -u
akan bekerja tanpanya (untuk set -e
Anda tidak bisa mengatakan itu). Inilah sebabnya mengapa penting untuk menggunakan pemeriksaan versi. Waspadalah terhadap asumsi bahwa pengujian dan pengembangan berlangsung di shell yang kompatibel dengan Bash 4.4 (sehingga aspek set -u
diuji). Jika ini mengganggu Anda, maka opsi lain adalah menolak kompatibilitas (skrip gagal saat verifikasi versi gagal), atau menolak set -u
. shopt -s nullglob
memaksa for f in *.txt
berfungsi dengan benar jika *.txt
tidak menemukan file. Perilaku default (alias passglob ) melewati templat tidak berubah, yang dalam kasus hasil nol berbahaya karena beberapa alasan. Untuk globstar, ini mengaktifkan pencarian rekursif. Pergantian lebih mudah digunakan daripada find
. Jadi, gunakan itu.
Tapi tidak:
IFS='' set -f shopt -s failglob
- Mengatur pembatas bidang internal ke string kosong membuat tidak mungkin untuk membagi kata. Kedengarannya seperti solusi sempurna. Sayangnya, ini adalah pengganti yang tidak lengkap untuk mengutip variabel dan penggantian perintah, dan karena Anda akan menggunakan tanda kutip, itu tidak memberikan apa pun. Alasan mengapa kutipan masih perlu digunakan adalah karena jika string kosong menjadi array kosong (seperti dalam
test $x = ""
) dan perluasan template tidak langsung masih dimungkinkan. Selain itu, masalah dengan variabel ini juga akan menyebabkan masalah dengan perintah seperti read
, yang memecah konstruksi seperti cat /etc/fstab | while read -r dev mnt fs opt dump pass; do echo "$fs"; done'
cat /etc/fstab | while read -r dev mnt fs opt dump pass; do echo "$fs"; done'
cat /etc/fstab | while read -r dev mnt fs opt dump pass; do echo "$fs"; done'
. - Ekstensi templat dinonaktifkan: tidak hanya ekstensi tidak langsung yang terkenal, tetapi juga ekstensi langsung tanpa gangguan, yang, seperti yang saya katakan, harus Anda gunakan. Jadi sulit diterima. Dan ini juga sepenuhnya opsional untuk skrip yang kompatibel dengan shellcheck / shellharden.
- Tidak seperti nullglob , failglob gagal dengan hasil nol. Meskipun untuk sebagian besar perintah, ini masuk akal, misalnya,
rm -- *.txt
(karena untuk sebagian besar perintah masih tidak diharapkan untuk mengeksekusi dengan hasil nol), jelas failglob dapat digunakan hanya jika Anda tidak mengharapkan hasil nol. Ini berarti bahwa biasanya Anda tidak akan menempatkan templat grup dalam argumen perintah kecuali Anda menganggap yang sama. Tapi yang selalu bisa terjadi adalah menggunakan nullglob dan memperluas template ke argumen null dalam konstruk yang dapat mengambilnya, seperti loop atau menetapkan nilai ke array ( txt_files=(*.txt)
).
Cara menyelesaikan skrip bash
Status keluar skrip adalah status dari perintah terakhir yang dijalankan. Pastikan itu menunjukkan keberhasilan atau kegagalan nyata.
Yang terburuk adalah meninggalkan solusi ke kondisi yang tidak terkait dalam bentuk daftar DAN di akhir skrip. Jika kondisinya salah, maka perintah yang dieksekusi terakhir adalah kondisinya sendiri.
Untuk errexit, kondisi dalam bentuk daftar DAN tidak pernah digunakan di tempat pertama. Jika errexit tidak digunakan, pertimbangkan menangani kesalahan bahkan untuk perintah terakhir, jadi status keluarnya tidak akan ditutup-tutupi jika kode tambahan ditambahkan ke skrip.
Buruk:
condition && extra_stuff
Bagus (opsi errexit):
if condition; then extra_stuff fi
Bagus (opsi penanganan kesalahan):
if condition; then extra_stuff || exit fi exit 0
Cara menggunakan errexit
Seperti
set -e
.
Tingkat Program Pembersihan Tertunda
Jika errexit berfungsi sebagaimana mestinya, gunakan ini untuk menginstal pembersihan yang diperlukan saat keluar.
tmpfile="$(mktemp -t myprogram-XXXXXX)" cleanup() { rm -f "$tmpfile" } trap cleanup EXIT
Tertangkap: errexit diabaikan dalam argumen perintah
Ini adalah "bom" percabangan yang sangat rumit, yang pengertiannya sangat berarti bagi saya. Skrip build saya berfungsi dengan baik pada mesin pengembangan yang berbeda, tetapi membuat server build berlutut:
set -e
Benar (pergantian perintah dalam tugas):
set -e
Peringatan: perintah built-in
local
dan
export
tetap menjadi perintah, jadi ini masih salah:
set -e
ShellCheck hanya memperingatkan tentang perintah khusus seperti
local
dalam kasus ini.
Untuk menggunakan
local
, pisahkan deklarasi dari pekerjaan:
set -e
Tertangkap: errexit diabaikan tergantung pada konteks pemanggil
Terkadang POSIX mengerikan. Errexit diabaikan dalam fungsi, perintah grup, dan bahkan subkulit jika penelepon memeriksa keberhasilannya. Semua contoh ini mencetak
Unreachable
dan
Great success
, betapapun anehnya itu tampak.
Subshell:
( set -e false echo Unreachable ) && echo Great success
Tim Grup:
{ set -e false echo Unreachable } && echo Great success
Fungsi:
f() { set -e false echo Unreachable } f && echo Great success
Karena itu, bash dengan errexit secara praktis tidak cocok untuk menautkan: ya,
dimungkinkan untuk membungkus fungsi errexit agar berfungsi, tetapi ada keraguan bahwa upaya yang disimpan (penanganan kesalahan eksplisit) sepadan. Sebagai gantinya, pertimbangkan untuk memisah menjadi skrip yang sepenuhnya otonom.
Menghindari memanggil shell dengan kutipan yang salah
Saat menjalankan perintah dari bahasa pemrograman lain, paling mudah untuk membuat kesalahan dan secara implisit memanggil shell. Jika perintah shell ini statis, itu bagus - itu berfungsi atau tidak. Tetapi jika program Anda entah bagaimana memproses baris untuk membangun perintah ini, maka Anda perlu memahami - Anda
membuat skrip shell ! Saya jarang ingin melakukan ini, dan sangat melelahkan untuk mengatur semuanya dengan benar:
- mengutip setiap argumen;
- keluar dari karakter yang sesuai dalam argumen.
Tidak peduli apa bahasa pemrograman Anda melakukan ini, setidaknya ada tiga cara untuk membangun tim dengan benar. Dalam urutan pilihan:
Paket A: lakukan tanpa shell
Jika ini hanya perintah dengan argumen (yaitu, tidak ada fungsi shell seperti pipelining atau redirect), lalu pilih opsi array.
- Buruk (python3):
subprocess.check_call('rm -rf ' + path)
- Bagus (python3):
subprocess.check_call(['rm', '-rf', path])
Buruk (C ++):
std::string cmd = "rm -rf "; cmd += path; system(cmd);
Baik (C / POSIX), minus penanganan kesalahan:
char* const args[] = {"rm", "-rf", path, NULL}; pid_t child; posix_spawnp(&child, args[0], NULL, NULL, args, NULL); int status; waitpid(child, &status, 0);
Paket B: skrip shell statis
Jika shell diperlukan, biarkan argumen menjadi argumen. Anda mungkin berpikir bahwa menulis skrip shell khusus di file Anda sendiri tidak praktis dan mengaksesnya sampai Anda melihat trik seperti itu:
Buruk (python3):
subprocess.check_call('docker exec {} bash -ec "printf %s {} > {}"'.format(instance, content, path))
Bagus (python3):
subprocess.check_call(['docker', 'exec', instance, 'bash', '-ec', 'printf %s "$0" > "$1"', content, path])
Bisakah Anda perhatikan skrip shell?
Itu benar, perintah printf diarahkan. Perhatikan argumen bernomor yang dikutip dengan benar. Menerapkan skrip shell statis baik-baik saja.
Contoh-contoh ini berjalan di Docker karena kalau tidak mereka tidak akan begitu berguna, tetapi Docker juga merupakan contoh yang bagus dari perintah yang menjalankan perintah lain berdasarkan argumen. Tidak seperti Ssh, seperti yang akan kita lihat nanti.
Opsi terakhir: pemrosesan garis
Jika
harus berupa string (misalnya, karena harus bekerja melalui
ssh
), maka itu tidak dapat dilewati. Anda harus mengutip setiap argumen dan melarikan diri karakter apa pun yang diperlukan untuk keluar dari kutipan ini. Cara termudah adalah beralih ke tanda kutip tunggal, karena mereka memiliki aturan pelarian paling sederhana. Hanya satu aturan:
'
โ
'\"
.
Nama file kutipan tunggal:
echo 'Don'\''t stop (12" dub mix).mp3'
Bagaimana cara menggunakan trik ini untuk menjalankan perintah ssh dengan aman? Ini tidak mungkin! Nah, inilah solusi "sering benar":
- Solusi "sering benar" (python3):
subprocess.check_call(['ssh', 'user@host', "sha1sum '{}'".format(path.replace("'", "'\\''"))])
Kita sendiri harus menggabungkan semua argumen menjadi string sehingga Ssh tidak melakukannya dengan salah: jika Anda mencoba untuk melewati beberapa argumen ssh, itu akan mulai menggabungkan argumen tersebut tanpa tanda kutip.
Alasan ini biasanya tidak mungkin adalah karena keputusan yang tepat tergantung pada preferensi pengguna di ujung yang lain, yaitu remote shell, yang bisa berupa apa saja. Pada dasarnya, itu bahkan bisa menjadi ibumu. โSering benarโ untuk mengasumsikan bahwa shell jarak jauh adalah bash atau shell yang kompatibel dengan POSIX, tetapi
fish tidak kompatibel pada tahap ini .