SSD Nodes Learn 🎉 VPS mulai $5.50/bln
Panduan Matt ConnorOleh Matt Connor · Diperbarui 2026-08-13

Substitusi Perintah Bash: $() vs Backtick

Pahami mengapa cd dan variabel hilang di subshell, backtick sulit dinest, serta trik process substitution agar perubahan tetap berlaku di shell utama.

Verified Every command ran end-to-end on a fresh Ubuntu 24.04 server, August 4, 2026.

Fungsi substitusi perintah bash

Substitusi perintah Bash mengganti $(command) dengan teks yang dicetak perintah tersebut ke standard output. Penulisan lama menggunakan backtick memiliki fungsi yang sama. Hal-hal yang sering mengejutkan pengguna berasal dari dua fakta: perintah dijalankan dalam proses terpisah yang disebut subshell, dan setiap newline di akhir output akan dibuang.

mkdir -p /tmp/subst-demo
cd /tmp/subst-demo
printf 'alpha\nbeta\ngamma\n' > three.txt
count=$(wc -l < three.txt)
echo "$count"
3

Itulah seluruh fungsi ini. wc -l mencetak 3 yang diikuti newline, newline tersebut dihapus, dan count berisi dua karakter yang Anda inginkan. Perhatikan bahwa wc -l < three.txt mencetak angka tanpa tambahan teks karena GNU wc yang membaca standard input tidak memiliki nama file untuk dicetak. Gunakan wc -l three.txt sebagai gantinya untuk menangkap 3 three.txt. Keduanya merupakan string yang berbeda dan sering menyebabkan operasi aritmetika gagal pada tahap berikutnya.

Bagian selanjutnya membahas perilaku yang biasanya tidak diperkirakan pengguna, karena subshell adalah proses terpisah dan proses terpisah tidak dapat mengubah shell yang sedang Anda gunakan.

Gunakan $() dan berhenti menggunakan backtick

Kedua bentuk tersebut valid. $() merupakan bagian dari POSIX, sehingga dash, ash, dan busybox sh semuanya mendukungnya. Tidak ada lagi alasan terkait portabilitas untuk menulis backtick, dan ada dua alasan konkret untuk tidak menggunakannya.

Backtick tidak dapat disarang

echo "$(echo "$(echo hi)")"
echo "`echo `echo hi``"
hi
echo hi

Baris kedua mencetak kata literal echo hi. Shell memindai ke depan hingga menemukan backtick berikutnya yang tidak di-escape, sehingga backtick kedua yang Anda ketik menutup backtick pertama. Perintah yang sebenarnya dijalankan adalah echo tanpa argumen, yang mencetak baris kosong, lalu karakter baris barunya dihapus hingga tidak menyisakan apa pun. Kata echo hi tertinggal sebagai teks biasa, dan pasangan backtick terakhir menjalankan perintah kosong.

Untuk menyarang backtick, Anda harus meng-escape setiap backtick bagian dalam:

echo "`echo \`echo hi\``"
hi

Setiap tingkat tambahan menggandakan escape lagi. $() tidak memerlukan semua itu karena parser mencocokkan tanda kurung, bukan memindai karakter pembatas.

Backtick mengubah backslash sebelum perintah dijalankan

echo "$(echo 'a\\b')"
echo "`echo 'a\\b'`"
a\\b
a\b

Perintah bagian dalam yang sama menghasilkan output yang berbeda. Di dalam backtick, shell menghapus satu lapisan escape backslash sebelum teks bagian dalam diparse, sehingga tanda kutip tunggal tidak melindungi apa pun. Di dalam $(), teks di antara tanda kurung diparse sebagai skrip biasa, sehingga tanda kutip tunggal berfungsi seperti yang diharapkan. Masalah ini paling sering muncul pada one-liner sed dan awk, ketika backslash yang hilang mengubah pattern yang semula berfungsi menjadi pattern lain tanpa pesan error.

Quoting juga dimulai ulang di dalam $(), sehingga Anda dapat menyarang tanda kutip ganda tanpa meng-escape-nya:

path=/etc/nginx/nginx.conf
echo "$(dirname "$path")"
/etc/nginx

Padanan backtick memerlukan \" di sekitar $path. Setiap escape merupakan peluang untuk membuat kesalahan.

Mengapa cd di dalam $() tidak mengubah shell Anda

Karena $(...) membuat proses baru. Subshell menerima salinan variabel dan salinan direktori kerja Anda. Subshell mengubah salinannya, mencetak sesuatu, lalu keluar. Salinan tersebut ikut hilang saat prosesnya berakhir.

cd /tmp/subst-demo
pwd
target=$(cd /etc && pwd)
echo "$target"
pwd
/tmp/subst-demo
/etc
/tmp/subst-demo

Tidak ada yang gagal. cd berhasil dijalankan, dan pwd di dalam subshell memang mencetak /etc. Perubahan tersebut tidak memiliki jalur untuk kembali karena satu-satunya hal yang diberikan subshell kepada proses induknya adalah standard output dan status keluar.

Penetapan variabel berperilaku dengan cara yang sama:

count=0
msg=$(count=99; echo "inside: $count")
echo "$msg"
echo "outside: $count"
inside: 99
outside: 0

Aturan yang sama menjelaskan bentuk bug ini yang jauh lebih sering ditemui, yaitu yang melibatkan pipeline, bukan substitusi:

n=0
cat three.txt | while read -r line; do n=$((n+1)); done
echo "$n"
0

Setiap tahap pipeline berjalan di subshell-nya sendiri. Karena itu, loop while menaikkan salinan n, lalu keluar. Ganti pipe dengan redirect agar loop berjalan di shell Anda:

n=0
while read -r line; do n=$((n+1)); done < three.txt
echo "$n"
3

Bash dapat menjalankan tahap terakhir pipeline di shell saat ini dengan shopt -s lastpipe, tetapi hanya jika job control tidak aktif. Kondisi ini tidak pernah berlaku pada shell interaktif. Gunakan redirect.

Ke mana perginya prompt? Perintah interaktif di dalam $()

Substitusi perintah mengalihkan standard output ke pipe dan membiarkan standard input tetap seperti semula. Program yang menampilkan pertanyaan ke standard output lalu menunggu jawaban akan kehilangan pertanyaannya, tetapi tetap menunggu. Terminal tampak macet.

ask() { printf 'Username: '; read -r u; printf '%s\n' "$u"; }
ask
Username: deploy
deploy

Kata deploy pada baris pertama adalah input yang Anda ketikkan. Sekarang jalankan fungsi yang sama di dalam substitusi:

v=$(ask)
deploy

Yang terlihat hanya ketikan Anda sendiri, yang di-echo oleh terminal driver, bukan oleh program. Prompt tersebut pergi ke tempat lain:

echo "$v"
Username: deploy

Sekarang prompt berada di dalam variabel dan menempel pada jawaban, karena $() menangkap semua keluaran yang ditulis fungsi ke standard output. Ketikan Anda tetap diterima oleh read karena standard input tidak pernah diubah. Inilah ciri khas laporan bug yang berbunyi, "skrip saya macet dan tidak menampilkan apa pun".

Beberapa alat menulis prompt ke standard error atau langsung ke /dev/tty agar prompt tetap terlihat. Banyak alat lainnya tidak melakukannya. Jika fungsi harus tetap berada di dalam substitusi, kirim prompt-nya ke standard error secara manual:

ask() { printf 'Username: ' >&2; read -r u; printf '%s\n' "$u"; }
v=$(ask)
echo "$v"
Username: deploy
deploy

Standard error tidak ditangkap, sehingga prompt sampai ke terminal Anda dan hanya jawaban yang masuk ke v.

Mengapa output ls terlihat berbeda di dalam $()?

Karena ls memanggil isatty pada file descriptor 1 dan mengubah format output berdasarkan jawabannya. Pada prompt, descriptor tersebut adalah terminal Anda, sehingga ls menyebarkan nama ke seluruh baris dalam beberapa kolom. Di dalam substitusi, descriptor tersebut adalah pipe, sehingga ls beralih ke satu nama per baris.

mkdir -p /tmp/tty-demo
cd /tmp/tty-demo
touch alpha beta delta gamma
echo "$(ls)"
alpha
beta
delta
gamma

Pemeriksaan yang sama menonaktifkan warna pada grep --color=auto dan menonaktifkan pager pada git. Ini merupakan fitur. Dengan demikian, skrip memperoleh output yang stabil dan dapat dibaca mesin tanpa harus memintanya secara khusus.

Hal ini juga menjawab pertanyaan yang sering muncul tentang pipeline. ls | sort pada prompt dan $(ls | sort) mencetak hal yang sama karena ls memiliki pipe pada output-nya dalam kedua kasus. Yang berubah di dalam substitusi adalah tahap terakhir pipeline. sort tidak pernah memeriksa keberadaan terminal, sehingga output-nya tidak pernah berubah. Jika Anda menempatkan perintah yang mendeteksi terminal di akhir pipeline, hasilnya memang berubah. Karena itu, pipeline yang berfungsi saat diuji secara langsung dapat berperilaku berbeda saat dibungkus dalam $().

Perhatikan hal berikut: jangan mengurai output ls dalam skrip meskipun terlihat praktis. Nama file dapat berisi spasi dan baris baru. Gunakan glob, atau find -print0 dengan read -d ''.

Newline di akhir yang diam-diam dihapus

Substitusi perintah menghapus semua newline di akhir output. Bukan hanya newline terakhir. Semuanya.

cd /tmp/subst-demo
printf 'hello\n\n\n' > blanks.txt
wc -c < blanks.txt
v=$(cat blanks.txt)
printf '%s' "$v" | wc -c
8
5

File tersebut berisi hello ditambah tiga newline, sehingga berukuran 8 byte. Variabel tersebut berisi hello, sehingga berukuran 5 byte. Tiga byte hilang tanpa peringatan.

Penghapusan ini disengaja dan biasanya membantu. Inilah yang membuat stamp=$(date -u +%Y%m%dT%H%M%SZ) menghasilkan fragmen nama file yang dapat digunakan, bukan nama yang berisi line break. Karena itu, pola ini aman digunakan dalam sesuatu seperti skrip pencadangan restic terjadwal:

stamp=$(date -u +%Y%m%dT%H%M%SZ)
printf 'backup-%s.tar.gz\n' "$stamp"
backup-20260804T031500Z.tar.gz

Timestamp Anda akan berbeda. Yang penting, nama tersebut terdiri dari satu baris.

Konsekuensinya, Anda tidak dapat menggunakan $() untuk memindahkan byte persis dari suatu file. Jika Anda perlu mempertahankan newline di akhir, tambahkan karakter sentinel di dalam substitusi, lalu hapus karakter tersebut setelahnya:

v=$(cat blanks.txt; printf x)
v=${v%x}
printf '%s' "$v" | wc -c
8

x berada setelah newline, sehingga tidak ada newline di akhir yang perlu dihapus. ${v%x} kemudian menghapus sentinel dan mempertahankan byte asli.

Ada dua detail terkait. Untuk membaca seluruh file, v=$(<blanks.txt) melakukan hal yang sama tanpa menjalankan cat, karena bash membuka file tersebut sendiri. Perintah ini menghapus newline di akhir dengan cara yang sama. Sebaliknya, here-string menambahkan newline yang tidak Anda tulis:

wc -c <<< 'abc'
4

Kutip hasilnya, atau bash akan memisah dan melakukan glob

Substitusi yang tidak diberi tanda kutip akan melalui pemisahan kata, lalu ekspansi nama path. Substitusi yang diberi tanda kutip tidak melalui keduanya.

printf 'a b\tc\nd\n' > words.txt
echo $(cat words.txt)
echo "$(cat words.txt)"
a b c d
a b	c
d

Tanpa tanda kutip, bash memisahkan output berdasarkan karakter dalam IFS, yang secara default adalah spasi, tab, dan baris baru. Kemudian, echo menggabungkan keempat bagian tersebut dengan spasi tunggal. Dengan tanda kutip, teks diterima sebagai satu kata, dan tab serta baris baru di dalamnya tetap dipertahankan.

Globbing adalah bagian yang lebih berbahaya:

mkdir -p /tmp/glob-demo
cd /tmp/glob-demo
touch one.txt two.txt
printf '*\n' > pattern.txt
p=$(cat pattern.txt)
echo $p
echo "$p"
one.txt pattern.txt two.txt
*

Assignment itu sendiri aman karena assignment tidak melakukan pemisahan kata atau globbing. Kerusakan terjadi pada echo $p, saat * diekspansi berdasarkan direktori saat ini. Skrip yang membaca pola dari file konfigurasi lalu lupa menggunakan tanda kutip dapat dengan mudah memproses setiap file yang dapat diaksesnya. Beri tanda kutip pada setiap ekspansi agar seluruh kelas bug ini tidak terjadi. Hilangkan tanda kutip hanya jika Anda memang menginginkan pemisahan tersebut, yang jarang diperlukan.

Mengapa local x=$(cmd) selalu menghasilkan 0?

Karena local adalah command itu sendiri, sedangkan $? melaporkan status local, bukan status substitution yang ada di dalamnya.

check_bad() { local out=$(false); echo "status: $?"; }
check_bad
status: 0

false berakhir dengan status 1, local berhasil mendeklarasikan variabel, dan nilai 1 tersebut dibuang. declare, export, typeset, dan readonly juga berperilaku sama. set -e juga tidak akan menangkapnya karena dari sudut pandang shell tidak ada yang gagal.

Pisahkan deklarasi dari assignment:

check_good() { local out; out=$(false); echo "status: $?"; }
check_good
status: 1

Assignment biasa pada level teratas sudah melaporkan status command substitution terakhir:

out=$(exit 3)
echo $?
3

Hal ini terutama penting dalam health check yang berjalan di bawah service dan timer systemd, karena exit code yang tertutupi membuat unit melaporkan keberhasilan pada setiap eksekusi, sementara pekerjaan yang seharusnya diverifikasi tidak pernah dijalankan.

Alternatif dalam shell yang benar-benar Anda perlukan

Sebagian besar orang menggunakan $(...) karena ingin menyimpan data dalam variabel. Namun, sering kali yang sebenarnya diperlukan adalah input, bukan capture. Keempat bentuk ini mempertahankan state dalam shell saat ini.

Redirect pada loop

cd /tmp/subst-demo
while read -r line; do printf 'got: %s\n' "$line"; done < three.txt
got: alpha
got: beta
got: gamma

Tidak ada proses yang dibuat untuk input tersebut. Karena itu, semua nilai yang ditetapkan oleh isi loop tetap tersedia setelah loop selesai.

Substitusi proses

while read -r line; do printf 'got: %s\n' "$line"; done < <(sort -r three.txt)
got: gamma
got: beta
got: alpha

<(command) memberi Anda sebuah path, seperti /dev/fd/63, yang membaca output perintah. Perintah tersebut tetap berjalan dalam prosesnya sendiri. Namun, loop while tidak berjalan dalam proses terpisah. Inilah tujuan utamanya. Spasi dalam < <( wajib ada. <<( akan dibaca sebagai awal here-document dan tidak dapat diuraikan. Substitusi proses adalah fitur bash. Karena itu, skrip dengan #!/bin/sh pada Ubuntu atau Debian berjalan menggunakan dash dan akan gagal. Gunakan #!/bin/bash.

Here-string

read -r first rest <<< 'alpha beta gamma'
echo "$first"
echo "$rest"
alpha
beta gamma

<<< mengirimkan satu string ke standard input sebuah perintah. read berjalan dalam shell Anda. Karena itu, kedua variabel ditetapkan di tempat Anda dapat menggunakannya. read -r a b <<< "$(some-command)" adalah cara standar untuk mengambil dua field dari satu baris output.

mapfile untuk seluruh file

mapfile -t lines < three.txt
echo "${#lines[@]}"
echo "${lines[1]}"
3
beta

mapfile, yang juga ditulis sebagai readarray, membaca file ke dalam array pada shell saat ini. -t menghapus newline di akhir setiap elemen. Fitur ini memerlukan bash 4 atau yang lebih baru. Ubuntu 24.04 menyediakan bash 5.2, sehingga fitur ini tersedia pada image server saat ini.

Checklist sebelum menerapkan script

  • Tulis $(command), lalu beri tanda kutip sebagai "$(command)" kecuali Anda memang ingin melakukan pemisahan.
  • Anggap newline di akhir sudah hilang. Tambahkan karakter sentinel jika Anda perlu mempertahankannya.
  • Jangan gunakan perintah interaktif dalam substitusi, atau kirim prompt-nya ke standard error.
  • Tulis local out pada baris tersendiri jika status keluar dari out=$(command) penting.
  • Untuk menetapkan variabel dari input, gunakan redirect atau process substitution, bukan pipe.

Pola-pola ini muncul dalam script kecil pertama yang biasanya ditulis orang saat menyiapkan VPS baru, dan kegagalannya tidak terlihat. Script backup yang menangkap prompt ke dalam nama file, atau health check yang menyamarkan kode keluar, tetap melaporkan keberhasilan. Dampaknya membesar setelah Anda menjalankan script yang sama di beberapa server, karena output yang sebelumnya tidak Anda baca kini menjadi output yang tidak Anda baca pada dua puluh mesin.

FAQ

Mengapa cd di dalam $() tidak mengubah direktori saat ini?

$(...) menjalankan perintahnya dalam subshell, yaitu proses terpisah yang menyimpan salinan direktori kerja dan variabel Anda. cd mengubah salinan tersebut, lalu proses berakhir dan salinan itu dibuang. Subshell hanya dapat mengembalikan output standar dan status keluar, sehingga tidak ada mekanisme untuk meneruskan perubahan direktori ke shell induk. Jika Anda menginginkan direktori tersebut, ambil dengan target=$(cd /etc && pwd) lalu gunakan "$target". Jika Anda ingin shell berpindah direktori, jalankan cd secara langsung tanpa membungkusnya dalam substitusi.

Apa perbedaan antara $() dan backtick dalam bash?

Keduanya menghasilkan hasil yang sama untuk perintah sederhana, tetapi berbeda dalam dua hal penting. $() dapat disarangkan secara langsung karena parser mencocokkan tanda kurung, sedangkan backtick memerlukan backtick yang di-escape pada setiap tingkat penyarangan. Backtick juga menghapus satu lapisan escape backslash sebelum perintah di dalamnya diuraikan, sehingga ` echo 'a\\b' prints a\b while $(echo 'a\\b') prints a\\b. $() is in POSIX and works in dash and busybox sh`, jadi tidak ada alasan portabilitas untuk menggunakan backtick.

Mengapa script saya berhenti tanpa menampilkan prompt ketika sebuah perintah meminta jawaban?

Command substitution mengalihkan output standar ke pipe, tetapi tetap menghubungkan input standar ke terminal Anda. Program yang menampilkan prompt ke output standar akan membuat prompt tersebut ditangkap ke dalam variabel, sedangkan read di baliknya tetap menunggu input Anda. Terminal hanya menampilkan karakter yang Anda ketik, yang digaungkan oleh driver terminal. Pindahkan pertanyaan ke luar substitusi, atau buat prompt ditulis ke standard error dengan printf 'Username: ' >&2 agar tidak ditangkap.

Mengapa baris kosong di akhir variabel saya hilang?

Command substitution menghapus semua newline di bagian akhir, bukan hanya newline terakhir. printf 'hello\n\n\n' > f; v=$(cat f) membuat v berisi lima byte, sedangkan file tersebut berisi delapan byte. Untuk mempertahankannya, tambahkan sentinel di dalam substitusi, lalu hapus sentinel tersebut setelahnya dengan v=$(cat f; printf x) yang diikuti v=${v%x}. Sentinel berada setelah newline, sehingga tidak ada lagi karakter di bagian akhir yang dapat dihapus bash.

Mengapa local out=$(cmd) selalu melaporkan keberhasilan?

local adalah perintah tersendiri, dan $? setelah baris tersebut melaporkan apakah local berhasil mendeklarasikan variabel. Status keluar dari substitusi dikonsumsi dan dibuang, yang juga berarti set -e tidak akan menghentikan script. declare, export, typeset, dan readonly berperilaku sama. Tulis local out pada satu baris dan out=$(cmd) pada baris berikutnya, lalu $? akan melaporkan status sebenarnya.

#bash#shell-scripting#linux#subshell#coreutils