SSD Nodes Learn 🎉 VPS dari $5.50/bln
Panduan Matt ConnorOleh Matt Connor · Dikemas kini 2026-08-13

Perbezaan Bash Command Substitution: $() vs Backticks

Ketahui mengapa $(...) menggunakan subshell yang memadamkan pemboleh ubah dan sebab backticks sukar disarangkan. Dapatkan penyelesaian untuk mengekalkan status shell anda.

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

Fungsi penggantian perintah bash

Penggantian perintah Bash menggantikan $(command) dengan teks yang dicetak oleh perintah tersebut ke output standard. Ejaan lama yang menggunakan backtick melakukan tugas yang sama. Segala perkara yang mengejutkan pengguna berpunca daripada dua fakta: perintah tersebut dijalankan dalam proses berasingan yang dipanggil subshell, dan setiap baris baharu pada penghujung outputnya 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 keseluruhan ciri tersebut. wc -l mencetak 3 diikuti dengan baris baharu, baris baharu tersebut dibuang, dan count memegang dua aksara yang anda inginkan. Perhatikan bahawa wc -l < three.txt mencetak nombor kosong kerana GNU wc yang membaca input standard tidak mempunyai nama fail untuk dicetak. Tulis wc -l three.txt sebaliknya dan anda akan menangkap 3 three.txt, yang merupakan rentetan berbeza dan punca biasa bagi pengiraan yang gagal kemudiannya.

Selebihnya panduan ini adalah mengenai gelagat yang tidak dijangkakan oleh sesiapa, kerana subshell adalah proses berasingan dan proses berasingan tidak boleh mengubah shell yang sedang anda taip.

Gunakan $() dan berhenti menggunakan backtick

Kedua-dua bentuk adalah sah. $() terkandung dalam POSIX, jadi dash, ash dan busybox sh semuanya menyokongnya. Tiada lagi sebab keserasian untuk menulis backtick, dan terdapat dua sebab konkrit untuk tidak menggunakannya.

Backtick tidak boleh disarangkan (nest)

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

Baris kedua mencetak perkataan literal echo hi. Shell mengimbas ke hadapan untuk mencari backtick seterusnya yang tidak di-escape, jadi backtick kedua yang anda taip menutup yang pertama. Perintah yang sebenarnya dijalankan ialah echo tanpa argumen, yang mencetak baris kosong yang kemudiannya dibuang aksara baris baharunya. Perkataan echo hi ditinggalkan sebagai teks biasa, dan pasangan backtick terakhir menjalankan perintah kosong.

Untuk menyarangkan backtick, anda mesti melakukan escape pada setiap backtick dalaman:

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

Setiap tahap tambahan menggandakan keperluan escape sekali lagi. $() tidak memerlukan semua ini, kerana parser memadankan tanda kurungan dan bukannya mengimbas aksara pembatas.

Backtick mengubah backslash anda sebelum perintah dijalankan

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

Perintah dalaman yang sama menghasilkan output yang berbeza. Di dalam backtick, shell membuang satu lapisan escape backslash sebelum teks dalaman dihurai (parsed), jadi tanda petik tunggal tidak melindungi apa-apa. Di dalam $(), teks di antara tanda kurungan dihurai sebagai skrip biasa, jadi tanda petik tunggal berkelakuan seperti yang anda jangkakan. Ini paling memberi kesan dalam baris tunggal sed dan awk, di mana backslash yang hilang menukar corak yang berfungsi kepada corak yang berbeza secara senyap.

Penggunaan tanda petik juga bermula semula di dalam $(), yang bermaksud anda boleh menyarangkan tanda petik berganda tanpa perlu melakukan escape:

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

Setara backtick memerlukan \" di sekeliling $path. Setiap escape adalah ruang untuk melakukan kesilapan.

Mengapa cd di dalam $() tidak menukar shell anda

Kerana $(...) melakukan fork kepada proses baharu. Subshell tersebut mendapat salinan pemboleh ubah dan salinan direktori kerja anda. Ia menukar salinannya, mencetak sesuatu, kemudian keluar. Salinan itu mati bersamanya.

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

Tiada apa-apa yang gagal. cd telah berfungsi, dan pwd di dalam subshell benar-benar mencetak /etc. Cuma tiada laluan untuk perubahan itu kembali ke shell asal, kerana satu-satunya perkara yang diserahkan oleh subshell kepada induknya ialah output standard dan status keluar.

Penugasan pemboleh ubah berkelakuan dengan cara yang sama:

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

Peraturan yang sama menjelaskan versi pepijat ini yang sering dihadapi oleh pengguna, yang melibatkan pipeline dan bukannya substitution:

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

Setiap peringkat pipeline berjalan dalam subshellnya sendiri, jadi gelung while menambah salinan n dan kemudian keluar. Gantikan pipe dengan redirect dan gelung tersebut akan berjalan dalam shell anda:

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

Bash boleh menjalankan peringkat terakhir pipeline dalam shell semasa dengan shopt -s lastpipe, tetapi hanya apabila kawalan kerja (job control) dimatikan, yang mana tidak pernah berlaku dalam shell interaktif. Gunakan redirect.

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

Penggantian perintah (command substitution) mengubah hala output standard ke dalam paip dan membiarkan input standard tidak disentuh. Program yang mencetak soalannya ke output standard dan kemudian menunggu jawapan akan menyebabkan soalan tersebut hilang, bukannya proses menunggu itu. Terminal kelihatan seolah-olah terhenti (frozen).

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

Perkataan deploy pada baris pertama ialah apa yang anda taip. Sekarang jalankan fungsi yang sama di dalam penggantian:

v=$(ask)
deploy

Hanya ketukan kekunci anda sendiri yang muncul, dipaparkan oleh pemacu terminal dan bukannya oleh program tersebut. Prompt itu telah pergi ke tempat lain:

echo "$v"
Username: deploy

Prompt itu kini berada di dalam pemboleh ubah, melekat pada jawapan, kerana $() menangkap segala yang ditulis oleh fungsi tersebut ke output standard. Taipan anda masih sampai ke read, kerana input standard tidak pernah disentuh. Itulah tanda tepat bagi laporan pepijat yang menyatakan "skrip saya tergantung dan tidak mencetak apa-apa".

Sesetengah alatan menulis prompt ke ralat standard (standard error) atau terus ke /dev/tty supaya ia terselamat daripada perkara ini. Banyak alatan tidak berbuat demikian. Jika sesuatu fungsi mesti kekal di dalam penggantian, hantar promptnya ke ralat standard sendiri:

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

Ralat standard tidak ditangkap, jadi prompt sampai ke terminal anda dan hanya jawapan yang mendarat di dalam v.

Mengapa ls kelihatan berbeza di dalam $()?

Kerana ls memanggil isatty pada file descriptor 1 dan mengubah format keluarannya berdasarkan jawapan tersebut. Pada prompt, descriptor itu ialah terminal anda, jadi ls menyusun nama merentasi baris dalam bentuk lajur. Di dalam penggantian (substitution), ia merupakan pipe, jadi ls bertukar kepada satu nama bagi setiap baris.

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

Semakan yang sama mematikan warna dalam grep --color=auto dan mematikan pager dalam git. Ini adalah satu ciri. Ia bermakna skrip mendapat output stabil yang boleh dibaca oleh mesin tanpa perlu memintanya.

Ini juga menjawab soalan yang sering ditanya orang mengenai pipeline. ls | sort pada prompt dan $(ls | sort) mencetak perkara yang sama, kerana ls mempunyai pipe pada keluarannya dalam kedua-dua kes. Perkara yang berubah di dalam penggantian ialah peringkat terakhir pipeline tersebut. sort tidak pernah menyemak terminal, jadi keluarannya tidak pernah berubah. Letakkan arahan yang peka-terminal di hujung pipeline dan ia akan berubah; itulah sebabnya pipeline yang anda uji dengan mata kasar boleh berkelakuan berbeza sebaik sahaja anda membungkusnya dalam $().

Satu amaran yang timbul daripada perkara ini: jangan menghuraikan (parse) output ls dalam skrip walaupun ia kelihatan mudah. Nama fail mungkin mengandungi ruang dan baris baharu. Gunakan glob, atau find -print0 dengan read -d ''.

Baris baharu di hujung yang hilang secara senyap

Penggantian arahan (command substitution) membuang setiap baris baharu di hujung output. Bukan hanya yang terakhir. Kesemuanya.

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

Fail tersebut mengandungi hello berserta tiga baris baharu, jadi saiznya ialah 8 bait. Pemboleh ubah tersebut menyimpan hello, iaitu 5 bait. Tiga bait hilang tanpa sebarang amaran.

Proses pembuangan ini adalah disengajakan dan biasanya membantu. Inilah yang menyebabkan stamp=$(date -u +%Y%m%dT%H%M%SZ) menghasilkan fragmen nama fail yang boleh digunakan dan bukannya nama yang mengandungi pemisah baris, sebab itulah corak ini selamat digunakan dalam skrip sandaran restic berjadual:

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

Cap masa anda akan berbeza. Perkara yang penting ialah nama tersebut berada dalam satu baris.

Kekurangannya ialah anda tidak boleh menggunakan $() untuk memindahkan bait tepat sesuatu fail. Jika anda memerlukan baris baharu di hujung, tambahkan aksara penanda (sentinel) di dalam penggantian tersebut dan buang ia kemudiannya:

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

x diletakkan selepas baris-baris baharu tersebut, jadi tiada baris baharu di hujung yang tinggal untuk dibuang. ${v%x} kemudian membuang aksara penanda tersebut dan meninggalkan bait asal.

Dua perincian berkaitan. Untuk membaca keseluruhan fail, v=$(<blanks.txt) melakukan kerja yang sama tanpa menjalankan cat, kerana bash membuka fail itu sendiri. Ia membuang baris baharu di hujung dengan cara yang sama. Manakala here-strings bergerak ke arah bertentangan, dengan menambah baris baharu yang tidak anda tulis:

wc -c <<< 'abc'
4

Petik hasil output, atau bash akan melakukan pemisahan dan globbing

Substitusi yang tidak dipetik akan melalui proses pemisahan perkataan dan kemudian pengembangan nama laluan (pathname expansion). Substitusi yang dipetik tidak melalui kedua-dua proses tersebut.

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 petik, bash memisahkan output berdasarkan aksara dalam IFS, yang secara lalai ialah ruang (space), tab, dan baris baharu (newline), dan echo kemudian mencantumkan empat bahagian tersebut dengan ruang tunggal. Apabila dipetik, teks tersebut diterima sebagai satu perkataan dengan tab dan baris baharu dalaman kekal utuh.

Globbing adalah bahagian 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
*

Tugasan (assignment) itu sendiri adalah selamat, kerana tugasan tidak melakukan pemisahan perkataan atau globbing. Kerosakan berlaku pada echo $p, di mana * dikembangkan berdasarkan direktori semasa. Skrip yang membaca corak daripada fail konfigurasi dan terlupa meletakkan tanda petik akan bertindak ke atas setiap fail yang dapat dilihatnya. Petik setiap pengembangan dan seluruh kelas pepijat ini akan hilang. Jangan gunakan tanda petik hanya apabila anda benar-benar mahukan pemisahan, yang jarang berlaku.

Mengapa local x=$(cmd) sentiasa mengembalikan 0?

Kerana local itu sendiri merupakan satu perintah, dan $? melaporkan status bagi local, bukan status bagi penggantian (substitution) yang terkandung di dalamnya.

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

local x=$(false)
echo $?

status: 0

false keluar dengan kod 1, local berjaya mengisytiharkan pemboleh ubah tersebut, dan nilai 1 itu telah dibuang. declare, export, typeset dan readonly semuanya berkelakuan dengan cara yang sama. set -e juga tidak akan mengesannya, kerana dari sudut pandangan shell, tiada apa-apa yang gagal.

Asingkan pengisytiharan daripada tugasan (assignment):

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

local x
x=$(false)
echo $?

status: 1

Tugasan biasa pada peringkat atas (top level) sudah pun melaporkan status bagi penggantian perintah terakhirnya:

out=$(exit 3)
echo $?

x=$(false)
echo $?

3

Perkara ini paling penting dalam pemeriksaan kesihatan (health check) yang dijalankan di bawah servis dan pemasa systemd, di mana kod keluar yang bertopeng (masked exit code) bermakna unit tersebut melaporkan kejayaan pada setiap kali dijalankan, sedangkan kerja yang sepatutnya disahkan tidak pernah berlaku.

Alternatif dalam shell yang sebenarnya anda perlukan

Kebanyakan orang menggunakan $(...) kerana mereka mahukan data dalam pemboleh ubah. Selalunya, apa yang mereka perlukan sebenarnya ialah input, bukan tangkapan (capture). Empat bentuk ini mengekalkan status dalam shell semasa anda.

Ubah hala pada gelung (loop)

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

Tiada proses baharu dicipta untuk input tersebut, jadi sebarang tetapan yang dibuat oleh badan gelung akan kekal selepas gelung tamat.

Penggantian proses (process substitution)

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

<(command) memberikan anda satu laluan, seperti /dev/fd/63, yang membaca output arahan tersebut. Arahan itu masih berjalan dalam prosesnya sendiri. Gelung while tidak berjalan dalam proses berasingan, dan itulah tujuan utamanya. Ruang dalam < <( adalah wajib: <<( akan dibaca sebagai permulaan here-document dan tidak akan dihurai (parse). Penggantian proses ialah ciri bash, jadi skrip dengan #!/bin/sh pada Ubuntu atau Debian akan berjalan di bawah dash dan akan gagal. Gunakan #!/bin/bash.

Here-strings

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

<<< menyalurkan satu rentetan (string) ke input standard sesuatu arahan. read berjalan dalam shell anda, jadi kedua-dua pemboleh ubah ditetapkan di tempat yang boleh anda gunakan. read -r a b <<< "$(some-command)" ialah cara biasa untuk mengambil dua medan daripada satu baris output.

mapfile untuk keseluruhan fail

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

mapfile, yang juga dikenali sebagai readarray, membaca fail ke dalam tatasusunan (array) dalam shell semasa. -t membuang aksara baris baharu (newline) di hujung setiap elemen. Ia memerlukan bash 4 atau lebih baharu, dan Ubuntu 24.04 membekalkan bash 5.2, jadi ia tersedia pada mana-mana imej pelayan semasa.

Senarai semak sebelum anda melakukan commit skrip

  • Tulis $(command), dan petik ia sebagai "$(command)" melainkan anda secara khusus mahukan pemisahan.
  • Anggap baris baharu di hujung telah tiada. Tambah aksara sentinel jika anda memerlukannya semula.
  • Jauhkan arahan interaktif daripada penggantian, atau hantar promptnya ke ralat standard.
  • Tulis local out pada barisnya sendiri apabila status keluar out=$(command) adalah penting.
  • Untuk menetapkan pemboleh ubah daripada input, gunakan ubah hala atau penggantian proses dan bukannya pipe.

Bentuk-bentuk ini muncul dalam skrip kecil pertama yang ditulis orang semasa mereka menyediakan VPS baharu, dan kegagalannya kekal senyap. Skrip sandaran yang menangkap prompt ke dalam nama fail, atau pemeriksaan kesihatan yang menutup kod keluar, terus melaporkan kejayaan. Kosnya meningkat sebaik sahaja anda menjalankan skrip yang sama merentasi beberapa pelayan, kerana output yang tidak pernah anda baca kini merupakan output yang tidak pernah anda baca pada dua puluh mesin.

FAQ

Mengapa cd di dalam $() tidak menukar direktori semasa saya?

$(...) menjalankan arahannya dalam subshell, iaitu proses berasingan yang memegang salinan direktori kerja dan pemboleh ubah anda. cd menukar salinan tersebut, kemudian proses itu tamat dan salinan itu dibuang. Subshell hanya boleh mengembalikan output standard dan status tamat, jadi tiada mekanisme untuk perubahan direktori sampai ke shell induk. Jika anda mahukan direktori tersebut, tangkap ia dengan target=$(cd /etc && pwd) dan gunakan "$target". Jika anda mahu shell anda berpindah, jalankan cd secara terus, tanpa penggantian di sekelilingnya.

Apakah perbezaan antara $() dan backtick dalam bash?

Kedua-duanya menghasilkan keputusan yang sama untuk arahan mudah, dan berbeza dalam dua cara yang penting. $() bersarang secara terus, kerana parser memadankan tanda kurung, manakala backtick memerlukan backtick yang di-escape untuk setiap tahap sarang. Backtick juga membuang satu lapisan backslash escaping sebelum arahan dalaman di-parse, jadi ` echo 'a\\b' prints a\b while $(echo 'a\\b') prints a\\b. $() is in POSIX and works in dash and busybox sh`, jadi tiada hujah keserasian untuk backtick.

Mengapa skrip saya tergantung tanpa prompt apabila arahan bertanyakan soalan?

Penggantian arahan (command substitution) mengubah hala output standard ke dalam pipe tetapi membiarkan input standard bersambung dengan terminal anda. Program yang mencetak prompt ke output standard akan menyebabkan prompt tersebut ditangkap ke dalam pemboleh ubah, manakala read di belakangnya masih menunggu anda. Terminal hanya menunjukkan aksara yang anda taip, yang diulang oleh pemacu terminal. Alihkan soalan ke luar penggantian, atau buat prompt menulis ke ralat standard dengan printf 'Username: ' >&2 supaya ia tidak ditangkap.

Mengapa baris kosong di hujung pemboleh ubah saya hilang?

Penggantian arahan membuang setiap baris baharu (newline) di hujung, bukan hanya yang terakhir. printf 'hello\n\n\n' > f; v=$(cat f) menyebabkan v memegang lima bait manakala fail memegang lapan. Untuk mengekalkannya, tambahkan sentinel di dalam penggantian dan buang ia kemudian dengan v=$(cat f; printf x) diikuti oleh v=${v%x}. Sentinel itu berada selepas baris baharu, jadi tiada apa-apa di hujung untuk dibuang oleh bash.

Mengapa local out=$(cmd) sentiasa melaporkan kejayaan?

local ialah arahan yang berdiri sendiri, dan $? selepas baris itu melaporkan sama ada local berjaya mengisytiharkan pemboleh ubah tersebut. Status tamat penggantian digunakan dan dibuang, yang juga bermakna set -e tidak akan menghentikan skrip. declare, export, typeset dan readonly berkelakuan dengan cara yang sama. Tulis local out pada satu baris dan out=$(cmd) pada baris seterusnya, dan $? kemudian akan melaporkan status sebenar.

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