SSD Nodes Learn 🎉 VPS kutoka $5.50/mwezi
Mwongozo Matt ConnorNa Matt Connor · Imeboreshwa 2026-08-13

Bash command substitution: $() dhidi ya backticks

Jifunze kwa nini $(...) hutumia subshell na kusababisha mabadiliko ya cd au vigezo kupotea. Gundua kwa nini backticks ni ngumu kutumia na jinsi ya kuhifadhi hali ya shell.

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

Kazi ya command substitution katika bash

Bash command substitution hubadilisha $(command) na matini ambayo amri hiyo imetoa kwenye standard output. Uandishi wa zamani unaotumia backticks hufanya kazi ileile. Kila kitu kinachowashangaza watu hutokana na ukweli mbili: amri hiyo huendeshwa katika mchakato tofauti unaoitwa subshell, na kila newline iliyo mwishoni mwa matokeo yake huondolewa.

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

Hiyo ndiyo kazi nzima ya kipengele hiki. wc -l ilichapisha 3 ikifuatiwa na newline, newline hiyo iliondolewa, na count inashikilia herufi mbili ulizotaka. Kumbuka kuwa wc -l < three.txt huchapisha namba tupu kwa sababu GNU wc inayosoma standard input haina jina la faili la kuchapisha. Andika wc -l three.txt badala yake na utakamata 3 three.txt, ambayo ni string tofauti na chanzo cha kawaida cha hesabu zinazofeli baadaye.

Sehemu iliyobaki ya mwongozo huu ni kuhusu tabia ambayo hakuna anayeitarajia, kwa sababu subshell ni mchakato tofauti na mchakato tofauti hauwezi kubadilisha shell unayoitumia kuandika.

Tumia $() na uache kutumia alama za backticks

Aina zote mbili ni sahihi. $() inakubalika katika POSIX, kwa hivyo dash, ash na busybox sh zote zinaunga mkono. Hakuna sababu ya kiufundi ya kutumia backticks, na kuna sababu mbili madhubuti za kutozitumia.

Backticks haziwezi kuwekwa ndani ya nyingine (nesting)

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

Mstari wa pili ulichapisha maneno halisi ya echo hi. Shell hutafuta mbele kwa ajili ya backtick inayofuata isiyo na escape, kwa hivyo backtick ya pili uliyochapa ilifunga ile ya kwanza. Amri iliyoendeshwa kwa kweli ilikuwa echo bila hoja yoyote, ambayo ilichapisha mstari mtupu ambao kisha ukaondolewa newline yake. Maneno echo hi yaliachwa kama maandishi ya kawaida, na jozi ya mwisho ya backticks iliendesha amri tupu.

Ili kuweka backticks ndani ya nyingine lazima uweke escape kwa kila moja ya ndani:

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

Kila ngazi ya ziada huongeza mara mbili idadi ya escape. $() haihitaji yote haya, kwa sababu parser inalinganisha mabano badala ya kutafuta herufi ya kikomo.

Backticks hubadilisha backslashes zako kabla ya amri kuendeshwa

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

Amri ile ile ya ndani ilitoa matokeo tofauti. Ndani ya backticks, shell huondoa safu moja ya backslash escape kabla maandishi ya ndani hayajachambuliwa, kwa hivyo single quotes hazikulinda chochote. Ndani ya $(), maandishi yaliyo kati ya mabano huchambuliwa kama script ya kawaida, kwa hivyo single quotes hufanya kazi kama unavyotarajia. Hili huleta shida zaidi katika one-liners za sed na awk, ambapo backslash iliyopotea hubadilisha pattern inayofanya kazi kuwa nyingine kimya kimya.

Uwekaji wa quotes pia huanza upya ndani ya $(), ikimaanisha unaweza kuweka double quotes ndani ya nyingine bila kuzifanyia escape:

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

Njia mbadala ya backtick inahitaji \" kuzunguka $path. Kila escape ni nafasi ya kufanya makosa.

Kwa nini cd ndani ya $() haibadilishi shell yako

Kwa sababu $(...) huunda mchakato mpya (fork). Subshell hupata nakala ya vigezo (variables) vyako na nakala ya saraka yako ya sasa (working directory). Inabadilisha nakala yake, inachapisha kitu, kisha inajifunga. Nakala hiyo hufa pamoja nayo.

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

Hakuna kilichofeli. cd ilifanya kazi, na pwd ndani ya subshell ilichapisha /etc. Hakuna njia ya mabadiliko hayo kurudi kwenye shell mama, kwa sababu vitu pekee ambavyo subshell huipatia mzazi wake ni standard output na exit status.

Uwekaji wa vigezo (variable assignments) hufanya kazi kwa njia ile ile:

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

Kanuni hiyo hiyo inaelezea toleo la hitilafu hii ambalo watu hukutana nalo mara nyingi zaidi, ambalo huhusisha pipeline badala ya substitution:

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

Kila hatua ya pipeline huendeshwa katika subshell yake yenyewe, kwa hivyo kitanzi cha while kiliongeza nakala ya n kisha kikajifunga. Badilisha pipe na redirect na kitanzi kitaendeshwa ndani ya shell yako:

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

Bash inaweza kuendesha hatua ya mwisho ya pipeline katika shell ya sasa kwa kutumia shopt -s lastpipe, lakini pale tu ambapo job control imezimwa, jambo ambalo haliwezi kutokea katika shell shirikishi (interactive shell). Tumia redirect.

Prompt imekwenda wapi? Amri shirikishi ndani ya $()

Ubadilishaji wa amri (command substitution) huelekeza pato la kawaida (standard output) kwenye pipe na kuiacha ingizo la kawaida (standard input) bila kuguswa. Programu inayochapisha swali lake kwenye pato la kawaida kisha ikasubiri jibu, hupoteza swali hilo, lakini haipotezi kusubiri. Terminal huonekana kama imeganda.

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

Neno deploy kwenye mstari wa kwanza ndilo ulilochapa. Sasa endesha kitendaji hicho hicho ndani ya ubadilishaji:

v=$(ask)
deploy

Ni vibonyezo vyako pekee vinavyoonekana, vikirudiwa na terminal driver badala ya programu yenyewe. Prompt imekwenda mahali pengine:

echo "$v"
Username: deploy

Prompt sasa iko ndani ya variable, ikiwa imeshikamana na jibu, kwa sababu $() ilinasa kila kitu ambacho kitendaji kiliandika kwenye pato la kawaida. Uandishi wako bado uliifikia read, kwa sababu ingizo la kawaida halikuguswa kamwe. Hiyo ndiyo alama kamili ya ripoti ya hitilafu inayosema "script yangu inaganda na haichapishi chochote".

Zana nyingine huandika prompt kwenye hitilafu ya kawaida (standard error) au moja kwa moja kwenye /dev/tty ili ziweze kuokoka hali hii. Nyingi hazifanyi hivyo. Ikiwa kitendaji lazima kibaki ndani ya ubadilishaji, tuma prompt yake kwenye hitilafu ya kawaida wewe mwenyewe:

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

Hitilafu ya kawaida hainaswi, kwa hivyo prompt hufika kwenye terminal yako na jibu pekee ndilo linalotua kwenye v.

Kwa nini ls inaonekana tofauti ndani ya $()?

Kwa sababu ls huita isatty kwenye file descriptor 1 na kubadilisha muundo wake wa pato kulingana na jibu hilo. Kwenye prompt, descriptor hiyo ni terminal yako, kwa hivyo ls hupanga majina kwenye safu mlalo kwa nguzo. Ndani ya substitution, hiyo ni pipe, kwa hivyo ls hubadilika na kuonyesha jina moja kwa kila mstari.

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

Ukaguzi huo huo huzima rangi katika grep --color=auto na huzima pager katika git. Hii ni kipengele cha mfumo. Inamaanisha kuwa script hupata pato thabiti linaloweza kusomwa na mashine bila kulazimika kuliomba.

Hii pia inajibu swali ambalo watu huuliza kuhusu pipelines. ls | sort kwenye prompt na $(ls | sort) huchapisha kitu kimoja, kwa sababu ls ilikuwa na pipe kwenye pato lake katika visa vyote viwili. Kinachobadilika ndani ya substitution ni hatua ya mwisho ya pipeline. sort haikagui kamwe kama kuna terminal, kwa hivyo pato lake halibadiliki kamwe. Weka amri inayotambua terminal mwishoni mwa pipeline na itabadilika, ndiyo sababu pipeline uliyojaribu kwa macho inaweza kuishi tofauti pindi tu unapoifunga ndani ya $().

Onyo moja linalotokana na hili: usichambue (parse) pato la ls kwenye script ingawa inaonekana ni rahisi. Majina ya faili yanaweza kuwa na nafasi (spaces) na mistari mipya (newlines). Tumia glob, au find -print0 pamoja na read -d ''.

Mstari mpya wa mwisho unaopotea kimyakimya

Ubadilishaji wa amri (command substitution) huondoa kila mstari mpya (newline) uliopo mwishoni mwa matokeo. Sio ule wa mwisho pekee. Yote huondolewa.

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

Faili lina hello pamoja na mistari mipya mitatu, hivyo lina baiti 8. Kigezo (variable) kinashikilia hello, hivyo kina baiti 5. Baiti tatu zimetoweka bila onyo lolote.

Uondoaji huu ni wa makusudi na mara nyingi huwa na manufaa. Ndio unaofanya stamp=$(date -u +%Y%m%dT%H%M%SZ) kutoa kipande cha jina la faili kinachoweza kutumika badala ya jina lenye mstari mpya, ndiyo maana muundo huu ni salama katika kitu kama hati ya kuhifadhi nakala ya restic iliyopangwa:

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

Muhuri wako wa muda (timestamp) utatofautiana. Muhimu ni kwamba jina hilo ni mstari mmoja.

Gharama yake ni kwamba huwezi kutumia $() kuhamisha baiti kamili za faili. Ikiwa unahitaji mistari mipya ya mwishoni, ongeza herufi ya alama (sentinel character) ndani ya ubadilishaji na uiondoe baadaye:

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

x inakaa baada ya mistari mipya, kwa hivyo hakuna mistari mipya ya mwishoni iliyobaki ya kuondolewa. ${v%x} kisha huondoa ile herufi ya alama na kuacha baiti asilia.

Maelezo mawili yanayohusiana. Kwa kusoma faili zima, v=$(<blanks.txt) hufanya kazi hiyo hiyo bila kuendesha cat, kwa sababu bash hufungua faili yenyewe. Huondoa mistari mipya ya mwishoni kwa njia ile ile. Na here-strings huenda katika mwelekeo tofauti, ikiongeza mstari mpya ambao hukuuandika:

wc -c <<< 'abc'
4

Weka matokeo kwenye alama za kunukuu, au bash itayagawanya na kuyafanyia globbing

Ubadilishaji (substitution) usio na alama za kunukuu hupitia mchakato wa kugawanya maneno (word splitting) na kisha upanuzi wa njia ya faili (pathname expansion). Ule wenye alama za kunukuu haupitii mojawapo ya michakato hiyo.

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

Bila alama za kunukuu, bash iligawanya matokeo kulingana na vibambo vilivyomo kwenye IFS, ambavyo kwa chaguo-msingi ni nafasi (space), tab, na mstari mpya (newline), na echo kisha ikaunganisha vipande vinne hivyo kwa kutumia nafasi moja. Kwa alama za kunukuu, matini ilifika kama neno moja huku tab na mstari mpya wa ndani ukiwa salama.

Globbing ndiyo nusu hatari zaidi:

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
*

Uwekaji wa thamani (assignment) wenyewe ulikuwa salama, kwa sababu uwekaji thamani haufanyi word-splitting wala globbing. Uharibifu ulitokea kwenye echo $p, ambapo * ilipanuliwa kulingana na saraka (directory) ya sasa. Hati (script) inayosoma kigezo (pattern) kutoka kwenye faili ya usanidi na kusahau kuweka alama za kunukuu itafanya kazi kwa furaha kwenye kila faili inayoiona. Weka alama za kunukuu kwenye kila upanuzi na aina hii nzima ya hitilafu itatoweka. Acha alama za kunukuu tu pale unapohitaji kikweli mgawanyo wa maneno, jambo ambalo ni nadra.

Kwa nini local x=$(cmd) hurejesha 0 kila wakati?

Kwa sababu local yenyewe ni amri, na $? huripoti hali ya local, si hali ya substitution iliyokuwa ndani yake.

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

false ilitoka ikiwa na 1, local ilifanikiwa kutangaza variable, na ile 1 ilitupiliwa mbali. declare, export, typeset na readonly zote hufanya kazi kwa njia hiyo hiyo. set -e haitaikamata pia, kwa sababu kwa mtazamo wa shell hakuna kilichofeli.

Tenganisha utangazaji wa variable na uwekaji wa thamani:

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

Uwekaji wa thamani wa kawaida katika ngazi ya juu (top level) huripoti hali ya command substitution yake ya mwisho:

out=$(exit 3)
echo $?
3

Hili ni muhimu zaidi katika ukaguzi wa afya (health check) unaoendeshwa chini ya systemd service na timer, ambapo exit code iliyofichwa inamaanisha kuwa unit huripoti mafanikio katika kila uendeshaji wakati kazi iliyopaswa kuhakikiwa haikutekelezwa kamwe.

Njia mbadala za ndani ya shell unazohitaji kihalisi

Watu wengi hutumia $(...) kwa sababu wanataka data iingie kwenye variable. Mara nyingi, wanachohitaji ni input, si capture. Njia hizi nne huhifadhi state kwenye shell yako ya sasa.

Redirect kwenye loop

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

Hakuna mchakato (process) mpya unaoundwa kwa ajili ya input, kwa hivyo chochote kinachowekwa na mwili wa loop hiyo hubaki baada ya loop kumalizika.

Process substitution

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

<(command) hukupa njia (path), kama vile /dev/fd/63, inayosoma output ya amri husika. Amri hiyo bado huendeshwa katika mchakato wake yenyewe. Loop ya while haifanyi hivyo, na ndiyo maana ya njia hii. Nafasi (space) katika < <( ni ya lazima: <<( husomwa kama mwanzo wa here-document na haitafanya kazi. Process substitution ni kipengele cha bash, kwa hivyo script yenye #!/bin/sh kwenye Ubuntu au Debian huendeshwa chini ya dash na itafeli. Tumia #!/bin/bash.

Here-strings

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

<<< huingiza string moja kwenye standard input ya amri. read huendeshwa ndani ya shell yako, kwa hivyo variable zote mbili huwekwa mahali ambapo unaweza kuzitumia. read -r a b <<< "$(some-command)" ndiyo njia ya kawaida ya kutoa sehemu mbili za data kutoka kwenye mstari mmoja wa output.

mapfile kwa ajili ya faili nzima

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

mapfile, inayojulikana pia kama readarray, husoma faili na kuiweka kwenye array ndani ya shell ya sasa. -t huondoa newline ya mwisho kutoka kwa kila element. Inahitaji bash 4 au mpya zaidi, na Ubuntu 24.04 inakuja na bash 5.2, kwa hivyo inapatikana kwenye image yoyote ya seva ya sasa.

Orodha ya hakiki kabla ya kutekeleza script

  • Andika $(command), na uiweke kwenye alama za kunukuu kama "$(command)" isipokuwa kama unahitaji mgawanyo.
  • Chukulia kuwa mistari mipya ya mwishoni (trailing newlines) imepotea. Ongeza herufi ya alama (sentinel character) ikiwa unahitaji kuirejesha.
  • Epuka kutumia amri za mwingiliano (interactive commands) ndani ya mbadala (substitutions), au tuma maombi yake kwenye standard error.
  • Andika local out kwenye mstari wake pekee wakati hali ya kutoka (exit status) ya out=$(command) ni muhimu.
  • Ili kuweka vigezo (variables) kutoka kwenye ingizo, tumia redirect au process substitution badala ya pipe.

Maumbo haya hujitokeza katika script ndogo za kwanza ambazo watu huandika wanapokuwa wanaweka VPS mpya, na hitilafu zake hubaki kimya. Script ya chelezo (backup script) iliyokamata maombi (prompt) na kuifanya jina la faili, au ukaguzi wa afya ya mfumo (health check) ulioficha exit code, huendelea kuripoti mafanikio. Gharama huongezeka pindi unapo endesha script hiyo hiyo kwenye seva kadhaa, kwa sababu matokeo ambayo hukuwahi kuyasoma sasa ni matokeo ambayo huyasomi kwenye mashine ishirini.

FAQ

Kwa nini cd ndani ya $() haibadilishi saraka yangu ya sasa?

$(...) huendesha amri yake ndani ya subshell, ambayo ni mchakato tofauti unaoshikilia nakala ya saraka yako ya sasa na vigezo vyako. cd hubadilisha nakala hiyo, kisha mchakato huo huisha na nakala hiyo hufutwa. Subshell inaweza tu kurejesha standard output na exit status, kwa hivyo hakuna utaratibu wa mabadiliko ya saraka kufika kwenye shell mama. Ikiwa unataka saraka yenyewe, inase kwa target=$(cd /etc && pwd) na utumie "$target". Ikiwa unataka shell yako ihame, endesha cd moja kwa moja, bila substitution kuizunguka.

Kuna tofauti gani kati ya $() na backticks katika bash?

Hutoa matokeo sawa kwa amri rahisi, na hutofautiana kwa njia mbili muhimu. $() huingiliana moja kwa moja, kwa sababu parser inalinganisha mabano, wakati backticks zinahitaji backtick iliyokwepeshwa (escaped) kwa kila ngazi ya uingiliano. Backticks pia huondoa safu moja ya backslash escaping kabla ya amri ya ndani kuchambuliwa, kwa hivyo ` echo 'a\\b' prints a\b while $(echo 'a\\b') prints a\\b. $() is in POSIX and works in dash and busybox sh`, kwa hivyo hakuna hoja ya utangamano (portability) kwa ajili ya backticks.

Kwa nini hati yangu inakwama bila prompt wakati amri inauliza swali?

Command substitution huelekeza standard output kwenye pipe lakini huacha standard input ikiwa imeunganishwa kwenye terminal yako. Programu inayochapisha prompt yake kwenye standard output hupata prompt hiyo ikiwa imenaswa kwenye kigezo, wakati read iliyo nyuma yake bado inakusubiri wewe. Terminal huonyesha tu vibambo unavyochapa, vinavyoakisiwa na terminal driver. Hamisha swali nje ya substitution, au fanya prompt iandike kwenye standard error kwa printf 'Username: ' >&2 ili isinaswe.

Kwa nini mistari mitupu mwishoni mwa kigezo changu imetoweka?

Command substitution huondoa kila newline ya mwisho, si ile ya mwisho pekee. printf 'hello\n\n\n' > f; v=$(cat f) huacha v ikiwa na baiti tano wakati faili lina nane. Ili kuzihifadhi, ongeza sentinel ndani ya substitution na uiondoe baadaye kwa v=$(cat f; printf x) ikifuatiwa na v=${v%x}. Sentinel hukaa baada ya newlines, kwa hivyo hakuna kitu mwishoni kwa bash cha kuondoa.

Kwa nini local out=$(cmd) huripoti mafanikio kila wakati?

local ni amri inayojitegemea, na $? baada ya mstari huo huripoti ikiwa local imefanikiwa kutangaza kigezo. Exit status ya substitution hutumiwa na kutupwa, jambo ambalo pia linamaanisha set -e haitasimamisha hati. declare, export, typeset na readonly hufanya kazi kwa njia hiyo hiyo. Andika local out kwenye mstari mmoja na out=$(cmd) kwenye mstari unaofuata, na $? itaripoti hali halisi.

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