Bash History Expansion: !! dan !$
Pahami enam ekspansi history Bash, termasuk !! untuk mengulang perintah, !$ untuk argumen terakhir, dan :p untuk mencetaknya sebelum dijalankan.
Fungsi history expansion pada bash
History expansion pada Bash menyusun kembali baris perintah sebelumnya dari riwayat shell sebelum Bash menjalankannya. !! mengulangi perintah sebelumnya. !$ menyisipkan argumen terakhir dari perintah sebelumnya. Substitusi ini bersifat tekstual dan dilakukan terlebih dahulu, sebelum Bash memisahkan baris menjadi beberapa kata. Karena itu, hasilnya berisi karakter yang sama persis seperti saat pertama kali Anda mengetikkannya.
Enam bentuk berikut mencakup hampir semua penggunaan Anda di server:
!!mengulangi seluruh baris sebelumnya, sedangkansudo !!menjalankannya kembali sebagai root.!$adalah argumen terakhir dari baris sebelumnya.!*adalah semua argumen dari baris sebelumnya.!nmenjalankan entri riwayat bernomor n, sedangkan!-nmenghitung mundur dari posisi Anda saat ini.!stringmenjalankan perintah terbaru yang diawali denganstring.^old^newmenjalankan kembali baris sebelumnya denganoldpertama diganti olehnew.
Semua contoh di bawah ini diketik pada prompt interaktif di server Anda sendiri. History expansion dinonaktifkan dalam script. Bagian terakhir menjelaskan hal tersebut.
Satu path panjang, cukup diketik sekali
Berikut kasus yang langsung menghemat waktu. Anda sedang menyiapkan direktori rilis, dan path-nya cukup panjang sehingga mengetiknya untuk kedua kali berisiko menimbulkan salah ketik.
sudo mkdir -p /srv/www/app/releases/2026-08-07
sudo chown -R deploy:deploy !$
ls -ld !$
sudo -u deploy nano !$/config.envSebelum menjalankan setiap baris, bash mencetak baris hasil ekspansi:
sudo chown -R deploy:deploy /srv/www/app/releases/2026-08-07Cetakan tersebut adalah pemeriksaan Anda. Baca sebelum membaca output perintah, karena itu satu-satunya kesempatan untuk melihat keputusan bash terhadap arti !$.
Rangkaian ini berhasil karena bash menyimpan baris yang sudah diekspansi dalam history, bukan !$ yang Anda ketik. Jadi, baris 3 mengambil argumen terakhir dari baris 2 yang sudah diekspansi, dan baris 4 mengambilnya dari baris 3. Baris 4 juga menunjukkan bahwa teks dapat mengikuti designator: !$/config.env menjadi path dengan /config.env di bagian akhir, karena word designator berhenti pada /.
Berikut contoh lain dari session yang sama. Setelah mengedit file, Anda ingin mencantumkan direktori yang memuatnya:
ls -l !$:h:h adalah head modifier. Modifier ini menghapus komponen terakhir dari path, sama seperti yang dilakukan dirname. :t hanya menyisakan komponen terakhir, :r menghapus ekstensi, dan :e hanya menyisakan ekstensi.
Ulangi perintah terakhir dengan !! dan sudo !!
Anda lupa sudo, dan manajer service memberitahukannya:
systemctl restart nginxFailed to restart nginx.service: Interactive authentication required.sudo !!Bash mengganti !! dengan teks baris sebelumnya, sehingga shell menjalankan sudo systemctl restart nginx.
!! selalu merupakan baris sebelumnya, apa pun isi baris tersebut. Di sinilah kesalahan umum terjadi. Anda mengira perintah terakhir adalah perintah yang gagal, tetapi setelah itu Anda menjalankan cd atau history, atau perintah yang gagal berada dua baris sebelumnya. sudo !! kemudian menjalankan perintah yang salah dengan hak istimewa root. Tampilkan perintah tersebut terlebih dahulu jika Anda tidak yakin:
sudo !!:p:p menampilkan baris yang telah diperluas tanpa menjalankannya. Baris yang ditampilkan ditambahkan ke history. Jika isinya sudah benar, jalankan !! biasa pada baris berikutnya untuk mengeksekusinya.
Gunakan argumen terakhir dengan !$
!$ adalah kata terakhir pada baris sebelumnya. Inilah bentuk yang paling sering digunakan karena kata terakhir biasanya merupakan objek yang sedang Anda kerjakan: path atau nama service.
sudo systemctl status nginx
sudo systemctl reload !$Ada dua hal yang sering mengejutkan pengguna.
Pertama, !$ adalah kata terakhir, bukan argumen terakhir yang Anda maksud. Jika baris sebelumnya diakhiri dengan redirection, kata terakhirnya adalah target redirection:
sudo nginx -T > /tmp/nginx-dump.conf
less !$Contoh tersebut berjalan sesuai harapan. Namun, setelah journalctl -u nginx > /tmp/log.txt, !$ adalah /tmp/log.txt, bukan nginx. Perhatikan baris yang ditampilkan kembali.
Kedua, ekspansi ini bersifat tekstual, sehingga variabel dikembalikan tanpa diekspansi. Setelah ls $HOME/backups, !$ menghasilkan karakter $HOME/backups, yang kemudian diekspansi lagi oleh bash sebagai parameter biasa. Ingat urutan ini: ekspansi history berjalan sebelum ekspansi parameter dan sebelum substitusi perintah dengan $( ), sehingga ekspansi history hanya melihat teks, bukan nilainya.
Bentuk lain di sekitarnya juga perlu diketahui. !^ adalah argumen pertama, !:2 adalah argumen kedua, dan !:2-4 adalah suatu rentang. !!:$ adalah cara penulisan panjang untuk !$.
Jika Anda ingin melihat teksnya sebelum menjalankannya, tekan Alt-. (atau Esc lalu .). Readline memasukkan argumen terakhir dari perintah sebelumnya langsung ke prompt, sehingga Anda dapat mengeditnya. Tekan kembali kombinasi tersebut untuk berpindah ke argumen terakhir dari perintah sebelumnya. Tidak ada perintah yang dijalankan sampai Anda menekan Enter.
Teruskan setiap argumen dengan !*
!* adalah setiap kata pada baris sebelumnya, kecuali kata pertama.
stat /srv/www/app/shared/config.env /srv/www/app/shared/secrets.env
sudo chmod 600 !*Frasa "kecuali kata pertama" berperan penting. !* menghapus kata 0 dan tidak menghapus yang lain, sehingga opsi tetap diteruskan bersama path. Setelah ls -l file1 file2, !* adalah -l file1 file2, sehingga sudo chmod 600 !* gagal karena chmod menerima -l sebagai argumen. Nomori kata mulai dari nol dan gunakan irisan sebagai gantinya: !!:2* berarti kata 2 hingga akhir.
Perangkap yang sama juga berlaku untuk sudo. Setelah sudo chown deploy:deploy /srv/www/app, kata 0 adalah sudo dan kata 1 adalah chown, sehingga !* meneruskan chown deploy:deploy /srv/www/app ke perintah berikutnya. Biasanya, itu bukan yang Anda inginkan.
Memilih perintah berdasarkan posisi dengan !n dan !-n
history menampilkan daftar dengan nomor di depan setiap entri.
history 5 512 sudo nginx -t
513 sudo systemctl reload nginx
514 ss -tulpn
515 sudo tail -f /var/log/nginx/error.log
516 history 5!513 menjalankan kembali entri 513. !-2 menjalankan entri dua baris sebelumnya, dihitung dari baris yang sedang Anda ketik, sehingga !-1 dan !! memiliki arti yang sama.
Periksa nomornya tepat sebelum menggunakannya. !-2 menunjuk ke lokasi yang berbeda setiap kali Anda menjalankan sesuatu, termasuk ls yang Anda jalankan tanpa sengaja. Nomor absolut tetap sama selama satu sesi, tetapi nomornya berbeda pada sesi kedua di mesin yang sama. Nomor tersebut juga berbeda setelah login berikutnya membaca kembali file riwayat. Nomor yang Anda ingat kemarin dapat menunjuk ke perintah lain hari ini.
Jalankan ulang perintah berdasarkan awalan dengan !string
!string menjalankan perintah terbaru yang diawali dengan string.
!ssPerintah tersebut menjalankan ulang ss -tulpn dari daftar di atas, yaitu pemeriksaan socket listening yang dijelaskan dalam port mana yang terbuka pada server Linux. !?string? mencocokkan teks di bagian mana pun pada baris, bukan hanya di awal. Ini berguna ketika Anda mengingat argumen, tetapi tidak mengingat nama perintahnya.
Gunakan awalan yang panjang. !s akan cocok dengan ss, sudo, systemctl, atau shutdown, mana pun yang terakhir dijalankan. Anda tidak akan mengetahui perintah yang cocok sampai perintah tersebut dijalankan. !string:p menampilkan hasil pencocokan tanpa menjalankannya. Jika tidak ada yang cocok, bash menampilkan bash: !ss: event not found dan tidak menjalankan apa pun. Ini adalah hasil yang aman.
Memperbaiki satu kesalahan ketik dengan ^old^new
sudo systemctl status ngnixUnit ngnix.service could not be found.^ngnix^nginxShell menjalankan kembali baris sebelumnya dengan ngnix pertama diganti oleh nginx. Hanya kecocokan pertama yang diubah. Untuk mengubah semua kecocokan, gunakan bentuk panjang !!:gs/ngnix/nginx/, dengan s berarti mengganti dan g berarti melakukannya pada seluruh baris.
Cetak hasil ekspansi sebelum menjalankannya
Dua kebiasaan dapat mencegah kecelakaan ketika sebuah ekspansi menjalankan sesuatu yang tidak Anda maksudkan.
Yang pertama adalah :p, yang sudah Anda lihat. Tambahkan ini ke sebuah ekspansi, lalu bash akan mencetak hasilnya alih-alih menjalankannya: !!:p atau !systemctl:p. Baris yang dicetak masuk ke history, sehingga !! setelahnya dapat menjalankan perintah yang baru saja Anda baca.
Cara kedua lebih kuat karena berlaku untuk setiap ekspansi tanpa perlu mengingat apa pun:
shopt -s histverifyMasukkan baris tersebut ke dalam ~/.bashrc. Jika histverify diaktifkan, ekspansi tidak langsung dijalankan saat Anda menekan Enter. Bash menulis kembali baris yang sudah diperluas ke prompt sehingga Anda dapat membaca dan mengeditnya. Anda harus menekan Enter untuk kedua kalinya agar perintah dijalankan. Cara ini hanya memerlukan satu penekanan tombol dan menghilangkan seluruh jenis kecelakaan tersebut. Fitur ini memerlukan readline, sehingga hanya berfungsi pada prompt interaktif.
Ctrl-R adalah cara lain untuk bekerja, dan tidak pernah memperluas apa pun. Tekan Ctrl-R, lalu ketik sebagian perintah. Prompt berubah menjadi prompt reverse-i-search dan menampilkan kecocokan terbaru saat Anda mengetik. Tekan Ctrl-R lagi untuk berpindah ke kecocokan yang lebih lama. Enter menjalankan baris yang sedang ditampilkan. Ctrl-G membatalkan pencarian dan mengembalikan prompt awal. Tombol panah kiri mengakhiri pencarian dan menyisakan baris yang cocok pada prompt agar dapat diedit.
Gunakan Ctrl-R ketika ingin melihat perintah terlebih dahulu. Gunakan !! dan !$ ketika perintah tersebut baru saja dijalankan dan masih dapat dibaca di layar.
Mengapa echo "done!" menampilkan event not found
echo "deploy done!"bash: !": event not foundEkspansi riwayat berjalan sebelum penguraian tanda kutip, dan tanda kutip ganda tidak melindungi karakter !. Hanya tanda kutip tunggal dan garis miring terbalik yang dapat melindunginya. Dengan demikian, echo 'deploy done!' menampilkan hasil yang diharapkan. Garis miring terbalik juga menghentikan ekspansi, tetapi di dalam tanda kutip ganda bash mempertahankan garis miring terbalik tersebut dalam output. Karena itu, tanda kutip tunggal adalah solusi yang paling bersih.
Masalah ini paling sering terjadi pada kata sandi karena kata sandi yang kuat sering berisi !. Perintah seperti mysql -u app -p"S3cret!pass" dapat gagal dengan error event-not-found. Jika ada entri riwayat yang cocok, perintah tersebut juga dapat mengirim string yang berbeda tanpa menampilkan peringatan. Gunakan tanda kutip tunggal. Lebih baik lagi, biarkan tool meminta input agar secret tidak pernah muncul di command line. Kebiasaan ini merupakan bagian dari pengelolaan akun yang baik. Lihat sepuluh menit pertama pada VPS baru untuk mengetahui cara menangani key dan password pada server baru.
Jika Anda akan menempelkan blok teks yang berisi banyak karakter !, nonaktifkan ekspansi untuk sesi tersebut dengan set +H. Aktifkan kembali dengan set -H.
HISTSIZE, HISTFILESIZE, dan lokasi penyimpanan riwayat
Ada dua variabel. Keduanya sering tertukar karena namanya mirip.
HISTSIZEmenentukan jumlah perintah yang disimpan shell yang sedang berjalan di memori.HISTFILESIZEmenentukan jumlah baris yang disimpan dalam file di disk,~/.bash_history, kecualiHISTFILEmenentukan sebaliknya.
File ditulis saat shell keluar, lalu dipangkas menjadi HISTFILESIZE baris pada saat itu. Default Ubuntu, ~/.bashrc, menetapkan nilai 1000 dan 2000. Periksa nilai yang sebenarnya digunakan:
grep HIST ~/.bashrc
echo "$HISTSIZE $HISTFILESIZE $HISTFILE"Naikkan kedua nilai tersebut jika pencarian !string sering tidak menemukan perintah yang Anda tahu pernah dijalankan. Nilai 10000 dan 20000 umum digunakan dan hampir tidak membebani sistem karena file tersebut berupa teks biasa. Nilai negatif berarti tidak ada batas.
Stempel waktu membantu saat Anda kembali ke server setelah seminggu:
export HISTTIMEFORMAT='%F %T 'history kemudian menampilkan tanggal dan waktu di depan setiap entri karena bash mulai menulis baris komentar yang berisi detik epoch sebelum setiap perintah dalam file.
Keluhan yang umum adalah riwayat hilang saat Anda menggunakan lebih dari satu terminal. Setiap shell menyimpan daftarnya sendiri di memori dan menulisnya saat keluar. Jadi, tanpa histappend, shell terakhir yang ditutup akan menimpa riwayat yang disimpan shell lain. Dua pengaturan dapat mengatasinya:
shopt -s histappend
export PROMPT_COMMAND='history -a'histappend membuat shell menambahkan daftarnya ke akhir file, bukan mengganti file tersebut. history -a menambahkan baris baru setelah setiap prompt. Dengan demikian, sesi yang dihentikan secara paksa dan tidak ditutup dengan benar tetap meninggalkan perintahnya. Default Ubuntu, ~/.bashrc, sudah menetapkan histappend. Hal ini semakin penting jika Anda mengelola lebih banyak mesin karena riwayat disimpan per pengguna pada setiap mesin. Jadi, saat mengelola beberapa server dari satu workstation, !$ yang Anda cari mungkin sebenarnya berada di host lain.
Menjaga rahasia agar tidak masuk ke bash history
HISTCONTROL menentukan baris mana yang disimpan.
ignorespacemenghapus setiap baris yang diawali spasi.ignoredupsmenghapus baris yang identik dengan baris sebelumnya.ignorebothmelakukan kedua hal tersebut.erasedupsmenghapus semua salinan lama dari baris yang sedang disimpan.
export HISTCONTROL=ignorebothJika ignorespace aktif, satu spasi di awal membuat perintah tidak masuk ke daftar dalam memori, sehingga perintah tersebut juga tidak pernah masuk ke file. Periksa nilainya sebelum mengandalkannya dengan echo "$HISTCONTROL". User default Ubuntu, .bashrc, menetapkan ignoreboth, tetapi shell root atau image minimal mungkin tidak menetapkannya. Dalam kondisi tersebut, spasi di awal tidak berpengaruh dan baris disimpan seperti baris lainnya.
Pahami dengan jelas hal yang dilindungi oleh pengaturan ini. Baris tersebut tidak masuk ke ~/.bash_history. Namun, baris itu tetap terlihat dalam output ps selama proses berjalan, sehingga user lain pada server dapat membacanya di sana. sudo mencatat perintah yang dijalankan ke system log. File history berupa teks biasa. Tetapkan modenya ke 600 dan ingat bahwa siapa pun yang dapat membaca home directory Anda dapat membaca beberapa ribu perintah terakhir.
Jika sesuatu yang sensitif sudah ada dalam daftar, hapus entri tersebut dan tulis ulang file:
history
history -d 517
history -whistory -d menghapus entri tersebut dari memori, sedangkan history -w menulis daftar saat ini ke file. history -c menghapus seluruh daftar. HISTIGNORE adalah pengaturan terkait untuk noise, bukan rahasia: HISTIGNORE='ls:pwd:history:clear' mencegah baris tersebut masuk ke daftar sehingga pencarian Anda menghasilkan temuan yang relevan.
Mengapa history expansion tidak melakukan apa pun dalam shell script
History expansion hanya berlaku pada shell interaktif. Script berjalan dalam shell noninteraktif, sehingga daftar history tidak diaktifkan dan expansion dinonaktifkan. Akibatnya, !! dan !$ tetap berada di baris sebagai teks biasa. sudo !! dalam script meminta sudo menjalankan perintah yang secara literal bernama !!, sehingga perintah tersebut gagal.
Periksa shell yang sedang Anda gunakan:
echo $-Output tersebut berisi kumpulan flag opsi saat ini, misalnya himBHs. i berarti shell bersifat interaktif, sedangkan H berarti history expansion diaktifkan. Jalankan baris yang sama dalam script. Kedua huruf tersebut tidak akan muncul.
Inilah perbedaan antara dua cara kerja shell Anda. Pada prompt, !$ dan Ctrl-R membantu menghemat pengetikan pada perintah yang masih dapat Anda lihat. Dalam file, Anda menyebutkan objek secara eksplisit. Masukkan path ke dalam variabel, atau ambil output dengan substitusi perintah. Script yang bergantung pada history pribadi Anda akan menghasilkan perilaku berbeda bagi orang berikutnya yang menjalankannya. Hal itu berlawanan dengan tujuan script.
Karena itu, semua contoh di halaman ini ditujukan untuk diketik pada prompt yang sedang aktif. Tidak ada contoh yang berperilaku sama jika ditempelkan ke file .sh.
FAQ
Apa fungsi !! di bash?
!! memperluas teks lengkap dari baris perintah sebelumnya, sehingga sudo !! menjalankan kembali perintah terakhir Anda sebagai root. Perluasan ini bersifat tekstual dan berlangsung sebelum bash mengurai baris tersebut. bash mencetak baris yang sudah selesai tepat sebelum menjalankannya. Jika Anda tidak yakin dengan isi baris sebelumnya, masukkan sudo !!:p terlebih dahulu. :p mencetak hasil perluasan tanpa menjalankannya dan menambahkan baris yang dicetak ke riwayat. Dengan demikian, !! berikutnya akan menjalankannya.
Bagaimana cara menggunakan kembali argumen terakhir dari perintah sebelumnya?
Gunakan !$. Setelah sudo mkdir -p /srv/www/app/releases, baris ls -ld !$ menjadi ls -ld /srv/www/app/releases. Perintah ini mengambil kata terakhir pada baris tersebut. Karena itu, target redirection di akhir baris dihitung sebagai kata terakhir. Anda juga dapat menambahkan teks setelahnya. !$/config.env menambahkan teks ke path karena penentu kata berhenti pada /. Alternatif interaktifnya adalah Alt-.. Perintah ini menyisipkan teks yang sama ke prompt sehingga Anda dapat membacanya sebelum menekan Enter.
Mengapa bash menampilkan "event not found" ketika teks saya berisi tanda seru?
Tanda kutip ganda tidak melindungi ! dari perluasan riwayat. Karena itu, echo "done!" membuat bash mencari event riwayat dan menampilkan bash: !": event not found. Tanda kutip tunggal melindunginya, jadi tulis echo 'done!'. Backslash juga menghentikan perluasan, tetapi di dalam tanda kutip ganda bash membiarkan backslash tersebut tetap ada dalam output. Untuk menempelkan blok panjang yang berisi !, nonaktifkan perluasan selama sesi dengan set +H.
Mengapa !! dan !$ tidak berfungsi dalam shell script saya?
Perluasan riwayat hanya diaktifkan pada shell interaktif. Script berjalan secara noninteraktif, sehingga shell tidak pernah membuat daftar riwayat dan !! dibiarkan sebagai teks biasa pada baris tersebut. Jalankan echo $- untuk melihat jenis shell yang sedang digunakan. Shell interaktif menampilkan flag yang mencakup i dan H, sedangkan script tidak menampilkan keduanya. Dalam script, gunakan variabel atau command substitution.
Bagaimana cara mencegah password tersimpan dalam riwayat bash?
Atur HISTCONTROL=ignorespace atau HISTCONTROL=ignoreboth di ~/.bashrc, lalu awali perintah dengan satu spasi agar perintah tersebut tidak pernah disimpan. Konfirmasikan nilainya terlebih dahulu dengan echo "$HISTCONTROL", karena spasi di awal tidak berpengaruh jika nilainya belum diatur. Cara ini hanya menghapus baris tersebut dari ~/.bash_history. Perintah itu tetap terlihat di ps selama dijalankan, dan sudo mencatat perintah yang dieksekusi. Jika secret sudah tersimpan, cari nomornya dengan history, lalu jalankan history -d <number> diikuti history -w untuk menulis ulang file tersebut.