Bash मध्ये $() आणि Backticks: फरक काय?
Bash मध्ये $() subshell मध्ये चालत असल्याने cd आणि variables का टिकत नाहीत, backticks nesting मध्ये का अडचणीचे आहेत आणि shell state जपणारे process substitution उपाय जाणून घ्या.
Bash command substitution काय करते
Bash command substitution मध्ये $(command) ने command ने standard output वर छापलेला मजकूर ठेवला जातो. Backticks वापरणारी जुनी syntax देखील हेच काम करते. लोकांना गोंधळात टाकणाऱ्या सर्व बाबी दोन तथ्यांमुळे घडतात: command subshell नावाच्या स्वतंत्र process मध्ये चालतो आणि त्याच्या output च्या शेवटी असलेली प्रत्येक newline काढून टाकली जाते.
mkdir -p /tmp/subst-demo
cd /tmp/subst-demo
printf 'alpha\nbeta\ngamma\n' > three.txt
count=$(wc -l < three.txt)
echo "$count"3हीच या सुविधेची संपूर्ण कार्यपद्धती आहे. wc -l ने 3 छापले आणि त्यानंतर newline दिली. ती newline काढून टाकली गेली आणि count मध्ये तुम्हाला हवे असलेले दोन characters ठेवले गेले. लक्षात ठेवा, GNU wc standard input वाचत असल्यामुळे छापण्यासाठी filename नसतो; म्हणून wc -l < three.txt फक्त संख्या छापते. त्याऐवजी wc -l three.txt लिहिल्यास 3 three.txt capture होते. हा वेगळा string आहे आणि नंतर arithmetic fail होण्याचे हे एक सामान्य कारण आहे.
या मार्गदर्शकाचा उर्वरित भाग अशा वर्तनाविषयी आहे ज्याची बहुतेकांना अपेक्षा नसते. subshell हा स्वतंत्र process असतो आणि स्वतंत्र process तुम्ही ज्या shell मध्ये command टाइप करत आहात त्या shell मध्ये बदल करू शकत नाही.
$() वापरा आणि backticks वापरणे थांबवा
दोन्ही पद्धती वैध आहेत. $() हे POSIX मध्ये आहे, त्यामुळे dash, ash आणि busybox sh हे सर्व त्याला support करतात. Backticks लिहिण्याचे portability संबंधी कोणतेही कारण आता उरलेले नाही. तसे न करण्याची दोन ठोस कारणे आहेत.
Backticks एकमेकांमध्ये nest करता येत नाहीत
echo "$(echo "$(echo hi)")"
echo "`echo `echo hi``"hi
echo hiदुसऱ्या ओळीने echo hi हे शब्द अक्षरशः print केले. Shell पुढील unescaped backtick पर्यंत scan करतो. त्यामुळे तुम्ही टाइप केलेला दुसरा backtick पहिल्या backtick ची जोडी पूर्ण करतो. प्रत्यक्षात चालवलेली command echo ही कोणत्याही arguments शिवाय होती. तिने रिकामी ओळ print केली आणि तिचा newline काढून टाकल्यावर काहीही उरले नाही. echo hi हे शब्द plain text म्हणून तसेच राहिले. शेवटच्या backticks च्या जोडीने रिकामी command चालवली.
Backticks nest करण्यासाठी प्रत्येक आतील backtick escape करावा लागतो:
echo "`echo \`echo hi\``"hiप्रत्येक अतिरिक्त nesting level साठी escaping पुन्हा दुप्पट करावे लागते. $() मध्ये ही गरज नसते, कारण parser delimiter character शोधण्याऐवजी parentheses ची जुळणी करतो.
Command चालण्यापूर्वी Backticks तुमचे backslashes बदलतात
echo "$(echo 'a\\b')"
echo "`echo 'a\\b'`"a\\b
a\bत्याच inner command ने वेगळे output दिले. Backticks च्या आत shell inner text parse करण्यापूर्वी backslash escaping चा एक स्तर काढून टाकतो. त्यामुळे single quotes ने काहीही सुरक्षित ठेवले नाही. $() च्या आत parentheses मधील text ordinary script म्हणून parse केला जातो. त्यामुळे single quotes अपेक्षेप्रमाणे कार्य करतात. sed आणि awk one-liners मध्ये ही समस्या विशेषतः गंभीर असते. हरवलेल्या backslash मुळे कार्यरत pattern शांतपणे वेगळ्या pattern मध्ये बदलतो.
$() च्या आत quoting नव्याने सुरू होते. त्यामुळे double quotes escape न करता nest करता येतात:
path=/etc/nginx/nginx.conf
echo "$(dirname "$path")"/etc/nginxBacktick ची समतुल्य पद्धत \" ला $path भोवती आवश्यक करते. प्रत्येक escape ही चूक होण्याची एक संधी असते.
$() मधील cd तुमच्या shell मध्ये बदल का करत नाही
कारण $(...) नवीन process fork करते. Subshell ला तुमच्या variables ची आणि working directory ची प्रत मिळते. ते स्वतःची प्रत बदलते, काहीतरी print करते आणि बाहेर पडते. त्याच्यासोबत ती प्रतही नष्ट होते.
cd /tmp/subst-demo
pwd
target=$(cd /etc && pwd)
echo "$target"
pwd/tmp/subst-demo
/etc
/tmp/subst-demoकाहीही अपयशी झाले नाही. cd यशस्वीपणे चालले आणि subshell मधील pwd ने खरोखर /etc print केले. हा बदल parent पर्यंत पोहोचण्यासाठी कोणताही मार्ग नाही. कारण subshell parent कडे फक्त standard output आणि exit status पाठवते.
Variable assignments याच प्रकारे वागतात:
count=0
msg=$(count=99; echo "inside: $count")
echo "$msg"
echo "outside: $count"inside: 99
outside: 0याच नियमामुळे या bug ची अधिक सामान्य आवृत्ती स्पष्ट होते. त्यात substitution ऐवजी pipeline वापरलेली असते:
n=0
cat three.txt | while read -r line; do n=$((n+1)); done
echo "$n"0Pipeline मधील प्रत्येक stage स्वतःच्या subshell मध्ये चालतो. त्यामुळे while loop ने n ची प्रत increment केली आणि नंतर तो बाहेर पडला. Pipe ऐवजी redirect वापरल्यास loop तुमच्या shell मध्ये चालतो:
n=0
while read -r line; do n=$((n+1)); done < three.txt
echo "$n"3Bash shopt -s lastpipe वापरून pipeline चा शेवटचा stage current shell मध्ये चालवू शकते. मात्र हे फक्त job control बंद असताना शक्य असते. Interactive shell मध्ये job control कधीही बंद नसते. त्यामुळे redirect वापरा.
प्रॉम्प्ट कुठे गेला? $() मधील परस्परसंवादी commands
Command substitution standard output ला pipe मध्ये पाठवते आणि standard input तसाच ठेवते. एखादा program आपला प्रश्न standard output वर छापून उत्तराची वाट पाहत असेल, तर प्रश्न दिसत नाही; प्रतीक्षा मात्र सुरूच राहते. Terminal गोठल्यासारखा दिसतो.
ask() { printf 'Username: '; read -r u; printf '%s\n' "$u"; }
askUsername: deploy
deployपहिल्या ओळीवरील deploy हा तुम्ही टाइप केलेला मजकूर आहे. आता त्याच function ला substitution मध्ये चालवा:
v=$(ask)deployफक्त तुमच्या स्वतःच्या keystrokes दिसतात. ते program ने नव्हे, तर terminal driver ने echo केलेले असतात. Prompt दुसरीकडे गेला:
echo "$v"Username: deployआता prompt variable मध्येच आहे आणि answer सोबत जोडलेला आहे, कारण $() ने function ने standard output वर लिहिलेली प्रत्येक गोष्ट capture केली. तुमचे typing मात्र read पर्यंत पोहोचले, कारण standard input ला स्पर्शही झाला नाही. “माझी script hang होते आणि काहीही print होत नाही” अशा bug report चे हे अचूक लक्षण आहे.
काही tools prompts standard error वर किंवा थेट /dev/tty वर लिहितात, त्यामुळे ते या समस्येतून वाचतात. अनेक tools असे करत नाहीत. एखादा function substitution मध्येच ठेवणे आवश्यक असल्यास, त्याचा prompt स्वतः standard error कडे पाठवा:
ask() { printf 'Username: ' >&2; read -r u; printf '%s\n' "$u"; }
v=$(ask)
echo "$v"Username: deploy
deployStandard error capture होत नाही. त्यामुळे prompt तुमच्या terminal पर्यंत पोहोचतो आणि फक्त answer v मध्ये साठतो.
$() मध्ये ls चे स्वरूप वेगळे का दिसते?
कारण ls फाइल descriptor 1 वर isatty ला कॉल करते आणि त्याला मिळालेल्या उत्तरानुसार output format बदलते. Prompt वर तो descriptor तुमचे terminal असतो. त्यामुळे ls नावे एका ओळीवर columns मध्ये मांडते. Substitution मध्ये तो pipe असतो. त्यामुळे ls प्रत्येक ओळीवर एक नाव दाखवते.
mkdir -p /tmp/tty-demo
cd /tmp/tty-demo
touch alpha beta delta gamma
echo "$(ls)"alpha
beta
delta
gammaयाच तपासणीमुळे grep --color=auto मधील रंग बंद होतो आणि git मधील pager बंद होतो. हे एक feature आहे. त्यामुळे script ला वेगळे सांगण्याची गरज न पडता स्थिर, machine-readable output मिळते.
यामुळे pipelines संदर्भातील एक सामान्य प्रश्नाचे उत्तरही मिळते. Prompt वर ls | sort आणि $(ls | sort) समान output दाखवतात, कारण दोन्ही प्रसंगी ls च्या output ला pipe जोडलेला होता. Substitution मध्ये बदलणारा भाग म्हणजे pipeline चा शेवटचा stage. sort terminal तपासत नाही. त्यामुळे त्याचे output कधीही बदलत नाही. Pipeline च्या शेवटी terminal-aware command ठेवल्यास output बदलते. म्हणून डोळ्यांनी तपासलेली pipeline $() मध्ये wrap केल्यावर लगेच वेगळी वागू शकते.
यावरून एक महत्त्वाची सूचना मिळते: script मध्ये ls चे output सोयीचे दिसते म्हणून parse करू नका. Filename मध्ये spaces आणि newlines असू शकतात. Glob वापरा किंवा find -print0 सोबत read -d '' वापरा.
शेवटी शांतपणे नाहीशा होणाऱ्या नवीन ओळी
Command substitution आउटपुटच्या शेवटी असलेली प्रत्येक 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
5फाइलमध्ये hello आणि त्यानंतर तीन newline आहेत, त्यामुळे तिचा आकार 8 bytes आहे. Variable मध्ये hello आहे, त्यामुळे तिचा आकार 5 bytes आहे. कोणतीही सूचना न देता तीन bytes नाहीशा झाले.
हे stripping जाणीवपूर्वक केले जाते आणि सामान्यतः उपयुक्त असते. त्यामुळे stamp=$(date -u +%Y%m%dT%H%M%SZ) line break असलेले नाव तयार करण्याऐवजी वापरता येणारा filename fragment तयार करते. म्हणून scheduled restic backup script सारख्या ठिकाणी हा pattern सुरक्षित असतो:
stamp=$(date -u +%Y%m%dT%H%M%SZ)
printf 'backup-%s.tar.gz\n' "$stamp"backup-20260804T031500Z.tar.gzतुमचा timestamp वेगळा असेल. नाव एका ओळीत आहे, हे महत्त्वाचे आहे.
याची किंमत अशी की फाइलमधील अचूक bytes हलवण्यासाठी तुम्ही $() वापरू शकत नाही. शेवटच्या newline जतन करायच्या असल्यास substitution मध्ये एक sentinel character जोडा आणि नंतर तो काढून टाका:
v=$(cat blanks.txt; printf x)
v=${v%x}
printf '%s' "$v" | wc -c8x newline नंतर असल्यामुळे काढून टाकण्यासाठी कोणतीही trailing newline उरत नाही. त्यानंतर ${v%x} sentinel काढून टाकते आणि मूळ bytes शिल्लक ठेवते.
यासंबंधी आणखी दोन मुद्दे आहेत. संपूर्ण फाइल वाचण्यासाठी v=$(<blanks.txt), cat चालवण्याची गरज न पडता तेच काम करते, कारण bash स्वतः फाइल उघडते. ते trailing newline याच पद्धतीने काढते. याउलट here-string तुम्ही न लिहिलेली एक newline जोडते:
wc -c <<< 'abc'4परिणामाभोवती quotes द्या; अन्यथा bash त्याचे splitting आणि globbing करेल
Unquoted substitution वर word splitting आणि त्यानंतर pathname expansion लागू होतात. Quoted substitution वर यापैकी कोणतीही प्रक्रिया लागू होत नाही.
printf 'a b\tc\nd\n' > words.txt
echo $(cat words.txt)
echo "$(cat words.txt)"a b c d
a b c
dUnquoted स्थितीत bash ने IFS मधील characters वर output विभाजित केले. Default स्थितीत हे space, tab आणि newline असतात. त्यानंतर echo ने चार भाग single spaces ने जोडले. Quoted स्थितीत text एकाच word म्हणून पोहोचला आणि tab तसेच आतील newline जसेच्या तसे राहिले.
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
*Assignment स्वतः सुरक्षित होते, कारण assignments वर word splitting किंवा globbing लागू होत नाही. नुकसान echo $p येथे झाले. तेथे * चा विस्तार current directory च्या आधारावर झाला. एखादी script config file मधून pattern वाचते आणि quotes विसरते, तर ती दिसणाऱ्या प्रत्येक file वर सहजपणे कृती करेल. प्रत्येक expansion भोवती quotes दिल्यास या संपूर्ण प्रकारातील bug टाळता येतो. Splitting खरोखर आवश्यक असेल तेव्हाच quotes वगळा. अशी गरज क्वचितच असते.
local x=$(cmd) नेहमी 0 का परत करते?
कारण local ही स्वतः एक command आहे आणि $? हे त्यात असलेल्या substitution चा status नव्हे, तर local चा status परत करते.
check_bad() { local out=$(false); echo "status: $?"; }
check_badstatus: 0false ने 1 परत केला. local ने variable घोषित करणे यशस्वीपणे पूर्ण केले आणि 1 discard झाला. declare, export, typeset आणि readonly यांचे वर्तनही असेच असते. set -e देखील हा error पकडणार नाही, कारण shell च्या दृष्टीने कोणतीही command अयशस्वी झालेली नसते.
Declaration आणि assignment वेगळे करा:
check_good() { local out; out=$(false); echo "status: $?"; }
check_goodstatus: 1Top level वरील साधे assignment त्याच्या शेवटच्या command substitution चा status आधीच परत करते:
out=$(exit 3)
echo $?3systemd service आणि timer अंतर्गत चालणाऱ्या health check मध्ये हे विशेष महत्त्वाचे असते. लपवलेला exit code असल्यास unit प्रत्येक run मध्ये success report करते, जरी तपासायचे काम प्रत्यक्षात झालेले नसते.
तुम्हाला प्रत्यक्षात हवे असलेले in-shell पर्याय
बहुतेक जण $(...) वापरतात, कारण त्यांना एखाद्या variable मध्ये data हवा असतो. प्रत्यक्षात त्यांना अनेकदा capture नव्हे, तर input हवे असते. हे चार प्रकार current shell मधील state कायम ठेवतात.
loop वर redirect
cd /tmp/subst-demo
while read -r line; do printf 'got: %s\n' "$line"; done < three.txtgot: alpha
got: beta
got: gammainput साठी कोणतीही process तयार होत नाही. त्यामुळे loop body ने सेट केलेली कोणतीही गोष्ट loop नंतरही उपलब्ध राहते.
Process substitution
while read -r line; do printf 'got: %s\n' "$line"; done < <(sort -r three.txt)got: gamma
got: beta
got: alpha<(command) तुम्हाला /dev/fd/63 सारखा एक path देते. हा path command चे output वाचतो. Command मात्र स्वतःच्या process मध्ये चालते. while loop स्वतःच्या process मध्ये चालत नाही. हाच यामागचा मुख्य उद्देश आहे. < <( मधील space आवश्यक आहे. <<( ला here-document ची सुरुवात म्हणून वाचले जाते आणि ते parse होत नाही. Process substitution हे bash चे feature आहे. त्यामुळे Ubuntu किंवा Debian वरील #!/bin/sh असलेली script dash अंतर्गत चालते आणि अयशस्वी होते. #!/bin/bash वापरा.
Here-strings
read -r first rest <<< 'alpha beta gamma'
echo "$first"
echo "$rest"alpha
beta gamma<<< एक string command च्या standard input ला देते. read तुमच्या shell मध्ये चालते. त्यामुळे दोन्ही variables तुम्ही वापरू शकता अशा scope मध्ये सेट होतात. एका output line मधून दोन fields काढण्याची सामान्य पद्धत read -r a b <<< "$(some-command)" आहे.
संपूर्ण files साठी mapfile
mapfile -t lines < three.txt
echo "${#lines[@]}"
echo "${lines[1]}"3
betamapfile, ज्याला readarray असेही लिहितात, current shell मधील array मध्ये file वाचते. -t प्रत्येक element मधील शेवटचा newline काढते. यासाठी bash 4 किंवा त्यानंतरची आवृत्ती आवश्यक आहे. Ubuntu 24.04 मध्ये bash 5.2 येते, त्यामुळे ते कोणत्याही current server image वर उपलब्ध आहे.
स्क्रिप्ट commit करण्यापूर्वीची तपासणी यादी
$(command)लिहा आणि विशेषतः splitting हवे नसल्यास त्याला"$(command)"म्हणून quote करा.- शेवटच्या newlines काढल्या जातात असे गृहीत धरा. त्या परत हव्या असल्यास sentinel character जोडा.
- substitutions मध्ये interactive commands ठेवू नका किंवा त्यांचे prompts standard error कडे पाठवा.
out=$(command)चा exit status महत्त्वाचा असल्यासlocal outस्वतंत्र ओळीवर लिहा.- input मधून variables सेट करण्यासाठी pipe ऐवजी redirect किंवा process substitution वापरा.
नवीन VPS सेट करताना लोक लिहितात त्या सुरुवातीच्या छोट्या scripts मध्ये हे नमुने वारंवार दिसतात आणि त्यांतील failures शांतपणे घडतात. एखाद्या backup script ने prompt filename मध्ये capture केला असेल किंवा health check ने exit code लपवला असेल, तरी तो success report करत राहतो. तुम्ही तोच script अनेक servers वर चालवता, तेव्हा हा खर्च वाढतो, कारण तुम्ही न वाचलेला output आता वीस machines वरही न वाचलेला output असतो.
FAQ
$() मधील cd मुळे माझी सध्याची directory का बदलत नाही?
$(...) आपली command subshell मध्ये चालवते. Subshell ही स्वतंत्र process असते आणि तिच्याकडे तुमच्या working directory ची तसेच variables ची प्रत असते. cd त्या प्रतीमध्ये बदल करते. त्यानंतर process बंद होते आणि ती प्रत नष्ट होते. Subshell फक्त तिचे standard output आणि exit status परत करू शकते. त्यामुळे directory मधील बदल parent shell पर्यंत पोहोचवण्याची कोणतीही यंत्रणा नसते. तुम्हाला directory स्वतः मिळवायची असल्यास ती target=$(cd /etc && pwd) ने capture करा आणि "$target" वापरा. तुमच्या shell ने directory बदलावी असे असल्यास substitution न वापरता cd थेट चालवा.
bash मध्ये $() आणि backticks यांच्यात काय फरक आहे?
सोप्या commands साठी दोन्ही समान परिणाम देतात. मात्र दोन महत्त्वाचे फरक आहेत. $() थेट nest करता येते, कारण parser parentheses जुळवतो. Backticks साठी nesting च्या प्रत्येक स्तरावर escaped backtick आवश्यक असतो. Inner command parse होण्यापूर्वी backticks backslash escaping चा एक स्तर काढून टाकतात. त्यामुळे ` echo 'a\\b' prints a\b while $(echo 'a\\b') prints a\\b. $() is in POSIX and works in dash and busybox sh` असे परिणाम मिळतात. त्यामुळे backticks वापरण्याच्या portability च्या बाजूने कोणताही आधार नाही.
एखादी command प्रश्न विचारते तेव्हा prompt न दिसता माझी script का अडकते?
Command substitution standard output एका pipe कडे redirect करते. मात्र standard input तुमच्या terminal शी जोडलेलेच ठेवते. Prompt standard output वर दाखवणारा program तो prompt variable मध्ये capture करतो. त्यामागील read तुमच्याकडून input ची प्रतीक्षा करत राहते. Terminal driver मुळे तुम्ही टाइप केलेले characters एवढेच terminal वर दिसतात. प्रश्न substitution च्या बाहेर विचारा. किंवा prompt printf 'Username: ' >&2 ने standard error वर लिहा, म्हणजे तो capture होणार नाही.
माझ्या variable च्या शेवटी असलेल्या रिकाम्या lines का नाहीशा झाल्या?
Command substitution फक्त शेवटची newline नव्हे, तर शेवटी असलेल्या प्रत्येक newline काढून टाकते. printf 'hello\n\n\n' > f; v=$(cat f) मध्ये v पाच bytes ठेवते, तर file मध्ये आठ bytes असतात. त्या newline ठेवायच्या असल्यास substitution मध्ये एक sentinel जोडा. त्यानंतर v=$(cat f; printf x) वापरून तो sentinel काढा आणि v=${v%x} ने प्रक्रिया पूर्ण करा. Sentinel newline नंतर येतो. त्यामुळे bash कडे शेवटी काढून टाकण्यासाठी काहीही उरत नाही.
local out=$(cmd) ने नेहमी success का दाखवते?
local ही स्वतंत्र command आहे. त्या line नंतरचे $? local ने variable घोषित करणे यशस्वी झाले का ते दाखवते. Substitution चा exit status वापरून टाकला जातो. त्यामुळे set -e देखील script थांबवणार नाही. declare, export, typeset आणि readonly यांचे वर्तनही असेच असते. local out एका line वर आणि out=$(cmd) पुढील line वर लिहा. त्यानंतर $? प्रत्यक्ष status दाखवेल.