Galat Bash dan penyalinan berkas aman: validasi, trap, dan pipefail
Skrip berkas yang lebih aman memvalidasi masukan serta tujuan, menangani kegagalan secara eksplisit, dan tidak menyajikan keluaran yang belum lengkap sebagai hasil jadi. Jangan hanya mengandalkan set -e: tentukan arti kegagalan pada setiap tahap, cara membersihkan data sementara, dan aturan penimpaan.
Lanjutkan skrip Bash pertama dengan cabang kegagalan dan batasan operasi berkas. Tinjau &&, ||, dan aturan pipeline pada alur eksekusi perintah shell.
Menetapkan batas keamanan contoh
Contoh menyalin satu berkas biasa ke nama baru dalam direktori tepercaya yang Anda kendalikan, lalu membandingkan isinya. Direktori tujuan harus sudah ada dan tidak boleh dapat ditulis oleh pengguna yang tidak dipercaya. Contoh bukan batas keamanan terhadap perubahan jalur secara bersamaan oleh penyerang dan bukan alat backup database. Jalankan tanpa hak administrator.
Membuat skrip penyalinan lengkap dengan pemeriksaan
Gunakan Bash, GNU Coreutils untuk mktemp, cp, dirname, ln, rm, serta rmdir, dan cmp dari GNU Diffutils. Simpan berkas dengan akhir baris LF dan jalankan blok-blok latihan dalam sesi Bash yang sama. Rangkaian && pada persiapan menghentikan pembuatan berkas jika direktori sementara atau perpindahan direktori gagal. Program di dalam here-document baru dijalankan melalui perintah bash checked-copy.sh di bawahnya.
lab=$(mktemp -d "$HOME/linux-safe-copy.XXXXXX") &&
cd "$lab" &&
mkdir output &&
printf "important sample\n" > "source file.txt" &&
cat > checked-copy.sh <<'EOF'
#!/usr/bin/env bash
set -u
set -o pipefail
umask 077
if (( $# != 2 )); then
printf "Usage: %s SOURCE DESTINATION\n" "$0" >&2
exit 2
fi
source_file=$1
destination=$2
if [[ ! -f "$source_file" || -L "$source_file" ]]; then
printf "Source must be a regular non-symlink file\n" >&2
exit 1
fi
if [[ -e "$destination" || -L "$destination" ]]; then
printf "Destination already exists\n" >&2
exit 1
fi
parent=$(dirname -- "$destination")
if [[ ! -d "$parent" ]]; then
printf "Destination directory is missing\n" >&2
exit 1
fi
work=$(mktemp -d "$parent/.copy-work.XXXXXX") || exit 1
cleanup() {
result=$?
trap - EXIT
rm -f -- "$work/payload" || result=1
rmdir -- "$work" || result=1
exit "$result"
}
trap cleanup EXIT
trap 'exit 130' INT
trap 'exit 143' TERM
if ! cp -- "$source_file" "$work/payload"; then
printf "Copy failed\n" >&2
exit 1
fi
if ! cmp -s -- "$source_file" "$work/payload"; then
printf "Verification failed\n" >&2
exit 1
fi
if ! ln -T -- "$work/payload" "$destination"; then
printf "Destination creation failed\n" >&2
exit 1
fi
printf "Created: %s\n" "$destination"
EOF
bash -n checked-copy.sh
bash checked-copy.sh "source file.txt" "output/copy file.txt"
cmp "source file.txt" "output/copy file.txt"
Skrip membuat direktori sementara di dalam direktori induk tujuan, menyalin isi ke payload, lalu membandingkannya dengan sumber. Hard link adalah nama lain untuk berkas yang sama, bukan salinan kedua dari datanya. Setelah perbandingan berhasil, ln -T membuat nama tujuan sebagai hard link ke payload.
Penyimpanan sementara berada pada sistem berkas tujuan karena hard link tidak dapat melintasi batas sistem berkas. Opsi GNU -T membuat argumen tujuan diperlakukan sebagai nama tepat, bukan sebagai direktori untuk menampung payload. Tanpa opsi penimpaan, nama yang sudah ada menyebabkan ln gagal. Jika sistem berkas tidak mendukung hard link, tahap akhir gagal; tinjau kembali rancangan dan kebutuhannya.
Penanganan EXIT menghapus hanya nama sementara payload, lalu menghapus direktori kerja jika sudah kosong; link tujuan tetap ada. Jika ada berkas tak terduga di direktori kerja, rmdir gagal dan mempertahankannya. Fungsi cleanup menyimpan status semula dan mengembalikan kegagalan bila pembersihan gagal. Pesan Created: menunjukkan nama tujuan telah dibuat, tetapi pemanggil tetap harus memeriksa status akhir karena pembersihan terjadi setelah pesan itu.
set -u menolak penggunaan variabel yang belum disetel, sedangkan umask 077 membatasi izin awal berkas baru untuk grup dan pengguna lain pada sistem berkas Linux yang mendukungnya. Contoh memeriksa isi, bukan pelestarian pemilik, daftar kontrol akses (ACL), atribut tambahan, atau ketahanan data setelah listrik mati. Contoh juga tidak cocok untuk sumber yang terus berubah. Pembersihan dibatasi pada direktori kerja baru yang berhasil dibuat mktemp, bukan jalur sumber atau tujuan dari pengguna.
Menguji kegagalan yang diharapkan
bash checked-copy.sh "source file.txt" "output/copy file.txt"
printf "exit=%s\n" "$?"
bash checked-copy.sh missing.txt output/new.txt
printf "exit=%s\n" "$?"
find output -maxdepth 1 -name ".copy-work.*" -print
Mengulangi salinan yang sudah berhasil seharusnya mengembalikan status 1 dan mempertahankan tujuan yang ada. Sumber yang tidak ditemukan juga harus gagal. Setelah keluar normal atau kegagalan yang ditangani, find seharusnya tidak menampilkan direktori kerja sementara. Jika muncul pesan pembersihan gagal, periksa jalur itu dan isinya sebelum mengambil tindakan; jangan menghapusnya secara massal.
Trap tidak dapat berjalan setelah SIGKILL atau kehilangan daya. Data sementara yang tertinggal memerlukan kebijakan pembersihan tersendiri. Setelah uji kegagalan, bandingkan lagi sumber dan salinan pertama dengan cmp "source file.txt" "output/copy file.txt". Keluaran kosong dengan status 0 menunjukkan isinya sama; jangan menganggap keluaran kosong tanpa memeriksa status sebagai bukti keberhasilan.
Memahami batas errexit dan pipefail
bash -c 'false | cat; printf "status=%s\n" "$?"'
bash -o pipefail -c 'false | cat; printf "status=%s\n" "$?"'
Perintah pertama biasanya menampilkan status=0 karena cat, perintah terakhir pipeline, berhasil. Perintah kedua menampilkan status=1 karena pipefail meneruskan kegagalan komponen. Dengan pipefail, status pipeline berasal dari perintah paling kanan yang gagal, atau 0 jika semuanya berhasil. Opsi ini mengubah penerusan status, bukan melakukan pemulihan otomatis.
set -e, juga disebut errexit, memiliki pengecualian sesuai konteks, termasuk pengujian kondisi dan pipeline. Uji konteks tepat tempat Anda mengharapkan program berhenti. Rangkaian yang status pipeline-nya disimpan atau dicetak juga harus membedakan status pipeline dari status printf yang dijalankan sesudahnya.
if ! command; then memudahkan cabang kegagalan, tetapi $? di dalam cabang itu mencerminkan hasil negasi, bukan status asli perintah. Jika status asli diperlukan, simpan segera dalam cabang else, misalnya if command; then ...; else rc=$?; ...; fi. Tentukan kode kembali dan pesan galat yang dapat dibedakan oleh pemanggil.
Memeriksa asumsi sebelum otomatisasi
- Lindungi ekspansi dengan tanda kutip dan gunakan
--sebelum argumen nama berkas pada perintah yang mendukungnya. - Pisahkan pratinjau dari tindakan yang mengubah data dan pastikan pratinjau tidak menulis data.
- Tinjau pemilik, izin, tautan simbolis, eksekusi bersamaan, dan perubahan sumber selama penyalinan.
- Uji keberhasilan, kegagalan, serta sinyal yang ditangani, dan pertahankan backup yang dapat dipulihkan.




