Bash $() vs backticks: alin ang dapat gamitin?
Alamin kung bakit nawawala ang cd at variables sa subshell, bakit mahirap i-nest ang backticks, at kailan makatutulong ang process substitution sa Bash.
Ano ang ginagawa ng Bash command substitution
Pinapalitan ng Bash command substitution ang $(command) ng text na ipinadala ng command na iyon sa standard output. Pareho rin ang ginagawa ng mas lumang anyo na gumagamit ng backticks. Ang mga pag-uugaling nakalilito ay nagmumula sa dalawang bagay: tumatakbo ang command sa hiwalay na process na tinatawag na subshell, at inaalis ang bawat newline sa dulo ng output nito.
mkdir -p /tmp/subst-demo
cd /tmp/subst-demo
printf 'alpha\nbeta\ngamma\n' > three.txt
count=$(wc -l < three.txt)
echo "$count"3Iyon lang ang buong feature. Nag-print ang wc -l ng 3 na sinundan ng newline, inalis ang newline, at naglalaman ang count ng dalawang character na kailangan mo. Tandaan na simpleng numero ang ipinapakita ng wc -l < three.txt dahil walang filename na maipi-print ang GNU wc kapag nagbabasa ito mula sa standard input. Gamitin ang wc -l three.txt sa halip at makukuha mo ang 3 three.txt, na ibang string at karaniwang sanhi ng arithmetic na nagfa-fail sa bandang huli.
Ang natitirang bahagi ng gabay na ito ay tumatalakay sa mga pag-uugaling hindi inaasahan ng karamihan, dahil hiwalay na process ang subshell at hindi mababago ng hiwalay na process ang shell na ginagamit mo.
Gamitin ang $() at itigil ang paggamit ng backticks
Parehong valid ang dalawang anyo. Nasa POSIX ang $(), kaya sinusuportahan ito ng dash, ash, at busybox sh. Wala nang dahilan tungkol sa portability para gumamit pa ng backticks, at may dalawang konkretong dahilan para hindi gamitin ang mga ito.
Hindi maaaring mag-nest ang backticks
echo "$(echo "$(echo hi)")"
echo "`echo `echo hi``"hi
echo hiLiteral na mga salitang echo hi ang inilimbag ng ikalawang linya. Hinahanap ng shell ang kasunod na backtick na hindi naka-escape, kaya isinara ng ikalawang backtick na inilagay mo ang unang backtick. Ang aktuwal na command na pinatakbo nito ay echo na walang arguments, kaya nag-print ito ng isang walang lamang linya na tinanggalan naman ng newline hanggang sa maging ganap na walang laman. Naiwan ang mga salitang echo hi bilang plain text, at nagpatakbo ang huling pares ng backticks ng isang command na walang laman.
Para mag-nest ng backticks, kailangan mong i-escape ang bawat panloob na backtick:
echo "`echo \`echo hi\``"hiSa bawat karagdagang level, nadodoble muli ang escaping. Hindi ito kailangan sa $() dahil parentheses ang tinutugma ng parser sa halip na mag-scan para sa isang delimiter character.
Binabago ng backticks ang iyong mga backslash bago patakbuhin ang command
echo "$(echo 'a\\b')"
echo "`echo 'a\\b'`"a\\b
a\bMagkaibang output ang ginawa ng parehong inner command. Sa loob ng backticks, inaalis ng shell ang isang layer ng backslash escaping bago ma-parse ang inner text, kaya walang naprotektahan ang single quotes. Sa loob ng $(), ang text sa pagitan ng parentheses ay bina-parse bilang ordinaryong script, kaya gumagana ang single quotes sa paraang inaasahan mo. Pinakamalaking problema ito sa sed at awk one-liner, kung saan ang nawawalang backslash ay nagbabago sa gumaganang pattern at nagiging ibang pattern nang walang malinaw na error.
Nagsisimula rin muli ang quoting sa loob ng $(), kaya maaari kang mag-nest ng double quotes nang hindi ine-escape ang mga ito:
path=/etc/nginx/nginx.conf
echo "$(dirname "$path")"/etc/nginxKailangan ng katumbas na anyo gamit ang backticks ang \" sa paligid ng $path. Bawat escape ay pagkakataong magkamali.
Bakit hindi binabago ng cd sa loob ng $() ang iyong shell
Dahil ang $(...) ay nagfa-fork ng bagong proseso. Nakakakuha ang subshell ng kopya ng iyong mga variable at kopya ng iyong working directory. Binabago nito ang sarili nitong kopya, nagpi-print ng output, at nag-e-exit. Kasama nitong nawawala ang kopya.
cd /tmp/subst-demo
pwd
target=$(cd /etc && pwd)
echo "$target"
pwd/tmp/subst-demo
/etc
/tmp/subst-demoWalang nag-fail. Gumana ang cd, at talagang nag-print ang pwd sa loob ng subshell ng /etc. Wala lang paraan para bumalik ang pagbabagong iyon, dahil ang standard output at exit status lamang ang ipinapasa ng subshell sa parent nito.
Pareho rin ang pag-uugali ng variable assignments:
count=0
msg=$(count=99; echo "inside: $count")
echo "$msg"
echo "outside: $count"inside: 99
outside: 0Ipinapaliwanag din ng parehong rule ang mas madalas na bersyon ng bug na ito, na gumagamit ng pipeline sa halip na substitution:
n=0
cat three.txt | while read -r line; do n=$((n+1)); done
echo "$n"0Tumatakbo ang bawat stage ng pipeline sa sarili nitong subshell, kaya dinagdagan ng loop na while ang kopya ng n at pagkatapos ay nag-exit. Kapag pinalitan ng redirect ang pipe, tatakbo ang loop sa iyong shell:
n=0
while read -r line; do n=$((n+1)); done < three.txt
echo "$n"3Maaaring patakbuhin ng Bash ang huling stage ng pipeline sa kasalukuyang shell gamit ang shopt -s lastpipe, pero gagana lamang ito kapag naka-off ang job control, na hindi kailanman totoo sa interactive shell. Gamitin ang redirect.
Saan napunta ang prompt? Mga interactive command sa loob ng $()
Nire-redirect ng command substitution ang standard output sa isang pipe at hindi nito ginagalaw ang standard input. Kapag ipinapakita ng isang program ang tanong nito sa standard output at pagkatapos ay naghihintay ng sagot, nawawala ang tanong pero hindi ang paghihintay. Mukhang nag-freeze ang terminal.
ask() { printf 'Username: '; read -r u; printf '%s\n' "$u"; }
askUsername: deploy
deployAng salitang deploy sa unang linya ang iyong na-type. Ngayon, patakbuhin ang parehong function sa loob ng substitution:
v=$(ask)deployAng sarili mo lamang na keystroke ang lumilitaw. Ine-echo ito ng terminal driver, hindi ng program. Napunta sa ibang lugar ang prompt:
echo "$v"Username: deployNasa loob na ngayon ng variable ang prompt at nakadikit ito sa sagot, dahil nakuha ng $() ang lahat ng isinulat ng function sa standard output. Umabot pa rin ang iyong pagta-type sa read dahil hindi ginalaw ang standard input. Ito ang eksaktong pattern ng bug report na nagsasabing “nagha-hang ang script ko at walang ipinapakita.”
May ilang tool na nagsusulat ng prompt sa standard error o direkta sa /dev/tty upang hindi ito mawala sa ganitong sitwasyon. Marami ang hindi gumagawa nito. Kung kailangang manatili ang isang function sa loob ng substitution, ikaw mismo ang magpadala ng prompt sa standard error:
ask() { printf 'Username: ' >&2; read -r u; printf '%s\n' "$u"; }
v=$(ask)
echo "$v"Username: deploy
deployHindi kino-capture ang standard error, kaya nakakarating ang prompt sa terminal mo at ang sagot lamang ang napupunta sa v.
Bakit iba ang hitsura ng ls sa loob ng $()?
Dahil tinatawag ng ls ang isatty sa file descriptor 1 at binabago ang format ng output batay sa sagot. Sa prompt, ang descriptor na iyon ay ang terminal mo, kaya inaayos ng ls ang mga pangalan sa mga column sa linya. Sa loob ng substitution, pipe ito, kaya lumilipat ang ls sa tig-isang pangalan bawat linya.
mkdir -p /tmp/tty-demo
cd /tmp/tty-demo
touch alpha beta delta gamma
echo "$(ls)"alpha
beta
delta
gammaAng parehong pagsusuri ang nag-o-off ng kulay sa grep --color=auto at nag-o-off ng pager sa git. Feature ito. Ibig sabihin, nakakakuha ang isang script ng stable at machine-readable na output nang hindi na kailangang tahasang hilingin iyon.
Sinasagot din nito ang isang tanong tungkol sa pipelines. Parehong output ang ipinapakita ng ls | sort sa prompt at ng $(ls | sort), dahil may pipe ang output ng ls sa parehong pagkakataon. Ang nagbabago sa loob ng substitution ay ang huling stage ng pipeline. Hindi kailanman nagsusuri ang sort kung terminal ang output nito, kaya hindi nagbabago ang output nito. Kung terminal-aware na command ang ilalagay sa dulo ng pipeline, magbabago ang output nito. Ito ang dahilan kung bakit maaaring iba ang asal ng pipeline na sinuri mo nang manu-mano kapag binalot mo ito sa $().
May isang babalang kasunod nito: huwag i-parse ang output ng ls sa isang script kahit mukhang maginhawa ito. Maaaring maglaman ng spaces at bagong linya ang mga filename. Gumamit ng glob, o find -print0 kasama ng read -d ''.
Mga trailing newline na tahimik na nawawala
Inaalis ng command substitution ang lahat ng newline sa dulo ng output. Hindi lang ang huli. Lahat ng newline.
cd /tmp/subst-demo
printf 'hello\n\n\n' > blanks.txt
wc -c < blanks.txt
v=$(cat blanks.txt)
printf '%s' "$v" | wc -c8
5Naglalaman ang file ng hello at tatlong newline, kaya 8 bytes ito. Naglalaman naman ang variable ng hello, kaya 5 bytes ito. Tatlong byte ang nawala nang walang babala.
Sinasadya ang pag-alis na ito at kadalasan ay nakatutulong. Dahil dito, naglalabas ang stamp=$(date -u +%Y%m%dT%H%M%SZ) ng magagamit na bahagi ng filename sa halip na pangalan na may line break. Kaya ligtas ang pattern sa isang bagay na gaya ng scheduled restic backup script:
stamp=$(date -u +%Y%m%dT%H%M%SZ)
printf 'backup-%s.tar.gz\n' "$stamp"backup-20260804T031500Z.tar.gzMagkakaiba ang timestamp na makukuha mo. Ang mahalaga ay isang linya ang pangalan.
Ang kapalit nito ay hindi mo magagamit ang $() para ilipat ang eksaktong bytes ng isang file. Kung kailangan mong mapanatili ang mga trailing newline, magdagdag ng sentinel character sa loob ng substitution at alisin ito pagkatapos:
v=$(cat blanks.txt; printf x)
v=${v%x}
printf '%s' "$v" | wc -c8Nasa dulo ng mga newline ang x, kaya wala nang trailing newline na aalisin. Pagkatapos, inaalis ng ${v%x} ang sentinel at iniiwan ang orihinal na bytes.
Dalawa pang kaugnay na detalye. Para basahin ang buong file, ginagawa ng v=$(<blanks.txt) ang parehong trabaho nang hindi pinapatakbo ang cat, dahil ang bash mismo ang nagbubukas ng file. Pareho nitong inaalis ang mga trailing newline. Sa kabilang direksyon naman gumagana ang here-strings: nagdaragdag ang mga ito ng newline na hindi mo isinulat:
wc -c <<< 'abc'4Quote the result, kung hindi ay hahatiin at ise-glob ito ng bash
Ang substitution na walang quote ay dumaraan sa word splitting at pagkatapos ay sa pathname expansion. Ang may quote ay hindi dumaraan sa alinman sa mga ito.
printf 'a b\tc\nd\n' > words.txt
echo $(cat words.txt)
echo "$(cat words.txt)"a b c d
a b c
dKapag walang quote, hinati ng bash ang output ayon sa mga character sa IFS, na bilang default ay space, tab, at newline. Pagkatapos, pinagsama ng echo ang apat na bahagi gamit ang mga single space. Kapag may quote, dumating ang text bilang isang word, at nanatiling buo ang tab at ang internal newline.
Mas mapanganib ang globbing:
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
*Ligtas ang mismong assignment dahil ang mga assignment ay hindi dumaraan sa word splitting o globbing. Nangyari ang problema sa echo $p, kung saan na-expand ang * batay sa kasalukuyang directory. Kapag nagbasa ang script ng pattern mula sa config file at nakalimutang gumamit ng quotes, maaari nitong aksyunan ang bawat file na nakikita nito. Gamitin ang quotes sa bawat expansion upang mawala ang buong uri ng bug na ito. Alisin lamang ang quotes kapag talagang kailangan mo ang splitting, na bihira.
Bakit laging nagbabalik ng 0 ang local x=$(cmd)?
Dahil ang local mismo ay isang command, at iniuulat ng $? ang status ng local, hindi ang status ng substitution na naglalaman dito.
check_bad() { local out=$(false); echo "status: $?"; }
check_badstatus: 0Nag-exit ang false nang 1, naging matagumpay ang local sa pagdeklara ng variable, at nawala ang 1. Pareho rin ang kilos ng declare, export, typeset, at readonly. Hindi rin ito mahuhuli ng set -e dahil, mula sa pananaw ng shell, walang nag-fail.
Ihiwalay ang declaration sa assignment:
check_good() { local out; out=$(false); echo "status: $?"; }
check_goodstatus: 1Sa top level, awtomatikong iniuulat ng plain assignment ang status ng huling command substitution nito:
out=$(exit 3)
echo $?3Mahalaga ito lalo na sa isang health check na tumatakbo sa ilalim ng isang systemd service at timer, kung saan nagrereport ang unit ng success sa bawat run dahil natatakpan ang exit code, kahit hindi naman nangyari ang gawaing dapat nitong i-verify.
Mga in-shell na alternatibong talagang kailangan mo
Karaniwang ginagamit ng mga tao ang $(...) dahil gusto nila ng data sa isang variable. Ngunit madalas, input ang talagang kailangan nila, hindi capture. Pinananatili ng apat na form na ito ang state sa kasalukuyang shell.
Redirect sa loop
cd /tmp/subst-demo
while read -r line; do printf 'got: %s\n' "$line"; done < three.txtgot: alpha
got: beta
got: gammaWalang ginagawang process para sa input, kaya nananatili ang anumang itakda ng loop body pagkatapos ng loop.
Process substitution
while read -r line; do printf 'got: %s\n' "$line"; done < <(sort -r three.txt)got: gamma
got: beta
got: alphaBinibigyan ka ng <(command) ng path, gaya ng /dev/fd/63, na nagbabasa ng output ng command. Tumatakbo pa rin ang command sa sarili nitong process. Hindi naman ginagawa iyon ng loop na while, at iyon ang mahalagang punto. Kailangan ang space sa < <(: binabasa ang <<( bilang simula ng here-document at hindi ito magpa-parse. Feature ng bash ang process substitution, kaya ang script na may #!/bin/sh sa Ubuntu o Debian ay tumatakbo sa ilalim ng dash at mabibigo rito. Gamitin ang #!/bin/bash.
Here-strings
read -r first rest <<< 'alpha beta gamma'
echo "$first"
echo "$rest"alpha
beta gammaIpinapasa ng <<< ang isang string sa standard input ng command. Tumatakbo ang read sa iyong shell, kaya nase-set ang dalawang variable kung saan mo magagamit ang mga ito. Ang read -r a b <<< "$(some-command)" ang karaniwang paraan para kunin ang dalawang field mula sa isang linya ng output.
mapfile para sa buong file
mapfile -t lines < three.txt
echo "${#lines[@]}"
echo "${lines[1]}"3
betaAng mapfile, na tinatawag ding readarray, ay nagbabasa ng file papunta sa isang array sa kasalukuyang shell. Inaalis ng -t ang trailing newline sa bawat elemento. Kailangan nito ang bash 4 o mas bago, at ang Ubuntu 24.04 ay may bash 5.2, kaya available ito sa anumang kasalukuyang server image.
Checklist bago i-commit ang script
- Isulat ang
$(command), at i-quote ito bilang"$(command)"maliban kung partikular mong kailangan ang paghahati. - Ipagpalagay na nawala ang mga trailing newline. Magdagdag ng sentinel character kung kailangan mong maibalik ang mga ito.
- Huwag ilagay ang mga interactive command sa substitutions, o ipadala ang mga prompt ng mga ito sa standard error.
- Isulat ang
local outsa sarili nitong linya kapag mahalaga ang exit status ngout=$(command). - Para magtakda ng mga variable mula sa input, gumamit ng redirect o process substitution sa halip na pipe.
Lumilitaw ang mga pattern na ito sa mga unang simpleng script na isinusulat ng mga tao kapag nagse-set up ng bagong VPS, at tahimik na nananatili ang mga failure. Ang backup script na nakakuha ng prompt bilang filename, o ang health check na nagtago ng exit code, ay patuloy na nag-uulat ng tagumpay. Mas lumalaki ang epekto kapag pinapatakbo ang parehong script sa ilang server, dahil ang output na hindi mo binabasa ay output na hindi mo binabasa sa dalawampung machine.
FAQ
Bakit hindi nagbabago ang kasalukuyang directory kapag gumagamit ng cd sa loob ng $()?
Pinapatakbo ng $(...) ang command nito sa isang subshell. Hiwalay itong process na may sariling kopya ng working directory at mga variable. Binabago ng cd ang kopyang iyon, pagkatapos ay nag-e-exit ang process at itinatapon ang kopya. Standard output at exit status lamang ang maaaring ibalik ng subshell. Kaya walang mekanismo para makarating ang pagbabago ng directory sa parent shell. Kung kailangan mo mismo ang directory, kunin ito gamit ang target=$(cd /etc && pwd) at gamitin ang "$target". Kung gusto mong ilipat ang shell mo, direktang patakbuhin ang cd nang walang substitution.
Ano ang pagkakaiba ng $() at backticks sa bash?
Pareho ang resultang ibinibigay ng mga ito para sa mga simpleng command. May dalawang mahalagang pagkakaiba. Direktang nagne-nest ang $() dahil parentheses ang minamatch ng parser. Sa backticks, kailangan ng escaped backtick sa bawat level ng nesting. Tinatanggal din ng backticks ang isang layer ng backslash escaping bago ma-parse ang inner command. Kaya ang ` echo 'a\\b' prints a\b while $(echo 'a\\b') prints a\\b. $() is in POSIX and works in dash and busybox sh`, kaya walang dahilan sa portability para gamitin ang backticks.
Bakit nagha-hang ang script ko at walang prompt kapag may command na nagtatanong?
Nire-redirect ng command substitution ang standard output sa isang pipe, pero nananatiling nakakonekta ang standard input sa terminal mo. Kapag ang program ay nagpi-print ng prompt sa standard output, nakukuha ang prompt sa variable. Samantala, patuloy na naghihintay ang read sa input mo. Ang mga character na tina-type mo lamang ang ipinapakita ng terminal, na ine-echo ng terminal driver. Ilabas ang tanong mula sa substitution, o ipasulat ang prompt sa standard error gamit ang printf 'Username: ' >&2 para hindi ito makuha.
Bakit nawala ang mga blank line sa dulo ng variable ko?
Tinatanggal ng command substitution ang lahat ng trailing newline, hindi lamang ang pinakahuli. Iniiwan ng printf 'hello\n\n\n' > f; v=$(cat f) na may limang byte ang v, habang walong byte ang nasa file. Para mapanatili ang mga ito, magdagdag ng sentinel sa loob ng substitution at alisin ito pagkatapos gamit ang v=$(cat f; printf x) na sinusundan ng v=${v%x}. Nasa likod ng mga newline ang sentinel, kaya wala nang nasa dulo na maaaring alisin ng bash.
Bakit palaging success ang nirereport ng local out=$(cmd)?
Ang local ay isang command mismo, at inirereport ng $? pagkatapos ng linyang iyon kung nagtagumpay ang local sa pagdeklara ng variable. Kinokonsumo at itinatapon ang exit status ng substitution. Nangangahulugan din ito na hindi ihihinto ng set -e ang script. Pareho ang behavior ng declare, export, typeset at readonly. Isulat ang local out sa isang linya at ang out=$(cmd) sa kasunod na linya. Pagkatapos, inirereport ng $? ang aktuwal na status.