Bash में $() और backticks का सही उपयोग कैसे करें
Bash में $(...) और backticks के बीच का अंतर समझें। जानें कि subshell के कारण cd और variables क्यों खो जाते हैं, backticks नेस्टिंग में क्यों विफल होते हैं और state बनाए रखने के तरीके।
Bash command substitution क्या करता है
Bash command substitution $(command) को उस टेक्स्ट से बदल देता है जिसे उस command ने standard output पर print किया था। backticks के साथ पुराना spelling भी यही काम करता है। जो भी बातें लोगों को हैरान करती हैं, वे दो तथ्यों से उत्पन्न होती हैं: command एक अलग process में चलती है जिसे subshell कहते हैं, और इसके 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 के साथ print किया, newline को हटा दिया गया, और count में वे दो characters आ गए जो आप चाहते थे। ध्यान दें कि wc -l < three.txt एक साधारण number print करता है क्योंकि standard input को पढ़ने वाले GNU wc के पास print करने के लिए कोई filename नहीं होता। इसके बजाय wc -l three.txt लिखें और आप 3 three.txt को capture करेंगे, जो एक अलग string है और बाद में विफल होने वाली arithmetic का एक सामान्य कारण है।
इस guide का बाकी हिस्सा वह व्यवहार है जिसकी किसी को उम्मीद नहीं होती, क्योंकि subshell एक अलग process है और एक अलग process उस shell को नहीं बदल सकता जिसमें आप type कर रहे हैं।
$() का उपयोग करें और backticks का उपयोग बंद करें
दोनों ही रूप मान्य हैं। $() POSIX में है, इसलिए dash, ash और busybox sh सभी इसका समर्थन करते हैं। backticks लिखने का अब कोई पोर्टेबिलिटी कारण नहीं बचा है, और इसके विपरीत दो ठोस कारण मौजूद हैं।
Backticks nest नहीं होते
echo "$(echo "$(echo hi)")"
echo "`echo `echo hi``"hi
echo hiदूसरी लाइन ने literal शब्द echo hi प्रिंट किए। शेल अगले unescaped backtick के लिए आगे स्कैन करता है, इसलिए आपके द्वारा टाइप किया गया दूसरा backtick पहले वाले को बंद कर देता है। जो कमांड वास्तव में चली वह बिना किसी argument के echo थी, जिसने एक खाली लाइन प्रिंट की और फिर उसकी newline को हटा दिया। शब्द echo hi सादे टेक्स्ट के रूप में पीछे रह गए, और backticks की अंतिम जोड़ी ने एक खाली कमांड चलाई।
backticks को nest करने के लिए आपको हर inner backtick को escape करना होगा:
echo "`echo \`echo hi\``"hiहर अतिरिक्त स्तर फिर से escaping को दोगुना कर देता है। $() को इसकी कोई आवश्यकता नहीं है, क्योंकि पार्सर delimiter character को स्कैन करने के बजाय parentheses का मिलान करता है।
Backticks कमांड चलने से पहले आपके backslashes को बदल देते हैं
echo "$(echo 'a\\b')"
echo "`echo 'a\\b'`"a\\b
a\bएक ही inner कमांड ने अलग आउटपुट दिया। backticks के अंदर, शेल inner टेक्स्ट के पार्स होने से पहले backslash escaping की एक परत हटा देता है, इसलिए single quotes ने किसी चीज को सुरक्षित नहीं किया। $() के अंदर, parentheses के बीच के टेक्स्ट को एक सामान्य स्क्रिप्ट के रूप में पार्स किया जाता है, इसलिए single quotes वैसे ही व्यवहार करते हैं जैसा आप उम्मीद करते हैं। यह sed और awk one-liners में सबसे अधिक समस्या पैदा करता है, जहाँ एक खोया हुआ backslash एक काम करने वाले पैटर्न को चुपचाप एक अलग पैटर्न में बदल देता है।
$() के अंदर 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 की एक copy मिलती है। वह अपनी copy को बदलती है, कुछ print करती है और exit हो जाती है। वह copy उसके साथ ही समाप्त हो जाती है।
cd /tmp/subst-demo
pwd
target=$(cd /etc && pwd)
echo "$target"
pwd/tmp/subst-demo
/etc
/tmp/subst-demoकुछ भी विफल नहीं हुआ। cd ने काम किया, और subshell के अंदर pwd ने वास्तव में /etc को print किया। उस बदलाव के वापस आने का कोई रास्ता नहीं है, क्योंकि 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यही नियम इस बग के उस संस्करण को भी समझाता है जिसका सामना लोग अक्सर करते हैं, जिसमें substitution के बजाय pipeline का उपयोग होता है:
n=0
cat three.txt | while read -r line; do n=$((n+1)); done
echo "$n"0Pipeline का हर चरण अपनी subshell में चलता है, इसलिए while loop ने n की एक copy को increment किया और फिर exit हो गया। Pipe को redirect से बदलें और loop आपकी shell में चलेगा:
n=0
while read -r line; do n=$((n+1)); done < three.txt
echo "$n"3Bash pipeline के अंतिम चरण को current shell में shopt -s lastpipe के साथ चला सकता है, लेकिन केवल तब जब job control बंद हो, जो interactive shell में कभी नहीं होता। Redirect का उपयोग करें।
प्रॉम्प्ट कहाँ गया? $() के अंदर इंटरैक्टिव कमांड्स
कमांड सब्स्टीट्यूशन (command substitution) स्टैंडर्ड आउटपुट को पाइप में रीडायरेक्ट कर देता है और स्टैंडर्ड इनपुट को वैसे ही छोड़ देता है। जो प्रोग्राम अपना प्रश्न स्टैंडर्ड आउटपुट पर प्रिंट करता है और फिर उत्तर की प्रतीक्षा करता है, वह प्रश्न को खो देता है, प्रतीक्षा को नहीं। टर्मिनल ऐसा लगता है जैसे फ्रीज हो गया हो।
ask() { printf 'Username: '; read -r u; printf '%s\n' "$u"; }
askUsername: deploy
deployपहली लाइन पर मौजूद शब्द deploy वह है जो आपने टाइप किया था। अब सब्स्टीट्यूशन के अंदर वही फंक्शन चलाएँ:
v=$(ask)deployकेवल आपकी अपनी की-स्ट्रोक्स दिखाई देती हैं, जिन्हें प्रोग्राम के बजाय टर्मिनल ड्राइवर द्वारा इको (echo) किया जाता है। प्रॉम्प्ट कहीं और चला गया:
echo "$v"Username: deployप्रॉम्प्ट अब वेरिएबल के अंदर है, उत्तर के साथ जुड़ा हुआ, क्योंकि $() ने वह सब कुछ कैप्चर कर लिया जो फंक्शन ने स्टैंडर्ड आउटपुट पर लिखा था। आपकी टाइपिंग अभी भी read तक पहुँच गई, क्योंकि स्टैंडर्ड इनपुट को कभी नहीं छुआ गया था। यह उस बग रिपोर्ट का सटीक लक्षण है जो कहती है "मेरी स्क्रिप्ट हैंग हो जाती है और कुछ भी प्रिंट नहीं करती"।
कुछ टूल्स प्रॉम्प्ट को स्टैंडर्ड एरर पर या सीधे /dev/tty पर लिखते हैं ताकि वे इसमें बच सकें। कई ऐसा नहीं करते हैं। यदि किसी फंक्शन को सब्स्टीट्यूशन के अंदर ही रहना है, तो उसके प्रॉम्प्ट को स्वयं स्टैंडर्ड एरर पर भेजें:
ask() { printf 'Username: ' >&2; read -r u; printf '%s\n' "$u"; }
v=$(ask)
echo "$v"Username: deploy
deployस्टैंडर्ड एरर कैप्चर नहीं होता है, इसलिए प्रॉम्प्ट आपके टर्मिनल तक पहुँच जाता है और केवल उत्तर v में जाता है।
$() के अंदर ls अलग क्यों दिखता है?
क्योंकि ls फाइल डिस्क्रिप्टर 1 पर isatty को कॉल करता है और उत्तर के आधार पर अपना आउटपुट फॉर्मेट बदल देता है। प्रॉम्प्ट पर वह डिस्क्रिप्टर आपका टर्मिनल होता है, इसलिए ls नामों को लाइन में कॉलम के अनुसार फैला देता है। सबस्टीट्यूशन के अंदर यह एक पाइप होता है, इसलिए ls प्रति लाइन एक नाम वाले फॉर्मेट पर स्विच हो जाता है।
mkdir -p /tmp/tty-demo
cd /tmp/tty-demo
touch alpha beta delta gamma
echo "$(ls)"alpha
beta
delta
gammaयही चेक grep --color=auto में रंग (colour) को बंद कर देता है और git में पेजर (pager) को बंद कर देता है। यह एक फीचर है। इसका मतलब है कि एक स्क्रिप्ट को बिना मांगे ही स्थिर और मशीन-रीडेबल आउटपुट मिल जाता है।
यह पाइपलाइन के बारे में पूछे जाने वाले एक सवाल का भी जवाब देता है। प्रॉम्प्ट पर ls | sort और $(ls | sort) एक ही चीज प्रिंट करते हैं, क्योंकि ls के आउटपुट पर दोनों ही मामलों में पाइप लगा था। सबस्टीट्यूशन के अंदर जो बदलता है, वह पाइपलाइन का अंतिम चरण है। sort कभी भी टर्मिनल की जांच नहीं करता है, इसलिए उसका आउटपुट कभी नहीं बदलता है। पाइपलाइन के अंत में टर्मिनल-अवेयर कमांड रखें और वह बदल जाएगा, यही कारण है कि जिस पाइपलाइन को आपने देखकर टेस्ट किया था, वह $() में लपेटते ही अलग व्यवहार कर सकती है।
इससे जुड़ी एक चेतावनी: स्क्रिप्ट में ls के आउटपुट को पार्स (parse) न करें, भले ही यह सुविधाजनक लगे। फाइलनाम में स्पेस और नई लाइनें हो सकती हैं। इसके बजाय ग्लोब (glob) का उपयोग करें, या read -d '' के साथ find -print0 का उपयोग करें।
वे trailing newlines जो चुपचाप गायब हो जाते हैं
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 और उसके साथ तीन newlines हैं, यानी कुल 8 bytes। वेरिएबल में hello है, यानी 5 bytes। तीन bytes बिना किसी चेतावनी के गायब हो गए।
यह हटाना जानबूझकर किया गया है और आमतौर पर उपयोगी होता है। यही कारण है कि stamp=$(date -u +%Y%m%dT%H%M%SZ) एक उपयोगी filename fragment बनाता है, न कि ऐसा नाम जिसमें line break हो। इसीलिए यह पैटर्न एक scheduled restic backup script जैसी चीजों में सुरक्षित है:
stamp=$(date -u +%Y%m%dT%H%M%SZ)
printf 'backup-%s.tar.gz\n' "$stamp"backup-20260804T031500Z.tar.gzआपका timestamp अलग हो सकता है। महत्वपूर्ण यह है कि नाम एक ही लाइन में है।
इसकी कीमत यह है कि आप किसी फाइल के सटीक bytes को move करने के लिए $() का उपयोग नहीं कर सकते। यदि आपको trailing newlines की आवश्यकता है, तो substitution के अंदर एक sentinel character जोड़ें और बाद में उसे हटा दें:
v=$(cat blanks.txt; printf x)
v=${v%x}
printf '%s' "$v" | wc -c8x इन newlines के बाद स्थित है, इसलिए हटाने के लिए कोई trailing newline नहीं बचता। ${v%x} फिर sentinel को हटा देता है और मूल bytes को सुरक्षित रखता है।
दो संबंधित विवरण। पूरी फाइल को पढ़ने के लिए, v=$(<blanks.txt) बिना cat को चलाए वही काम करता है, क्योंकि bash स्वयं फाइल को खोलता है। यह भी trailing newlines को उसी तरह हटा देता है। और here-strings विपरीत दिशा में काम करते हैं, जो एक ऐसा newline जोड़ देते हैं जिसे आपने नहीं लिखा था:
wc -c <<< 'abc'4परिणाम को कोट (quote) करें, अन्यथा bash इसे विभाजित और ग्लोब (glob) कर देगा
बिना कोट किया हुआ सब्स्टीट्यूशन (unquoted substitution) पहले वर्ड स्प्लिटिंग (word splitting) और फिर पाथनेम एक्सपेंशन (pathname expansion) से गुजरता है। कोट किया हुआ सब्स्टीट्यूशन इनमें से किसी से नहीं गुजरता।
printf 'a b\tc\nd\n' > words.txt
echo $(cat words.txt)
echo "$(cat words.txt)"a b c d
a b c
dबिना कोट किए, bash ने आउटपुट को IFS में दिए गए कैरेक्टर्स पर विभाजित कर दिया, जो डिफ़ॉल्ट रूप से स्पेस, टैब और न्यूलाइन होते हैं, और echo ने फिर उन चार टुकड़ों को सिंगल स्पेस के साथ जोड़ दिया। कोट करने पर, टेक्स्ट एक शब्द के रूप में प्राप्त हुआ जिसमें टैब और आंतरिक न्यूलाइन सुरक्षित रहे।
ग्लोबिंग (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) स्वयं सुरक्षित था, क्योंकि असाइनमेंट वर्ड-स्प्लिट या ग्लोब नहीं करते हैं। नुकसान echo $p पर हुआ, जहाँ * को वर्तमान डायरेक्टरी के विरुद्ध एक्सपैंड किया गया था। एक स्क्रिप्ट जो कॉन्फ़िगरेशन फ़ाइल से पैटर्न पढ़ती है और कोट्स लगाना भूल जाती है, वह उन सभी फ़ाइलों पर अनजाने में कार्य कर देगी जिन्हें वह देख सकती है। हर एक्सपेंशन को कोट करें और इस श्रेणी की पूरी बग समाप्त हो जाएगी। कोट्स केवल तभी हटाएँ जब आप वास्तव में स्प्लिटिंग चाहते हों, जो कि बहुत कम होता है।
local x=$(cmd) हमेशा 0 क्यों लौटाता है?
क्योंकि local स्वयं एक कमांड है, और $?, local की स्थिति (status) बताता है, न कि उस substitution की स्थिति जो उसके अंदर थी।
check_bad() { local out=$(false); echo "status: $?"; }
check_badlocal x=$(false)
echo $?
status: 0false 1 के साथ exited हुआ, local ने वेरिएबल घोषित करने का कार्य सफलतापूर्वक पूरा किया, और 1 को हटा दिया गया। declare, export, typeset और readonly सभी एक ही तरह से व्यवहार करते हैं। set -e भी इसे नहीं पकड़ पाएगा, क्योंकि शेल के दृष्टिकोण से कुछ भी विफल नहीं हुआ।
घोषणा (declaration) को असाइनमेंट (assignment) से अलग करें:
check_good() { local out; out=$(false); echo "status: $?"; }
check_goodlocal x
x=$(cmd)
status: 1टॉप लेवल पर एक साधारण असाइनमेंट पहले से ही अपने अंतिम कमांड substitution की स्थिति की रिपोर्ट करता है:
out=$(exit 3)
echo $?x=$(cmd)
echo $?
3यह तब सबसे महत्वपूर्ण होता है जब कोई हेल्थ चेक systemd service और timer के तहत चल रहा हो, जहाँ एक masked exit code का मतलब है कि यूनिट हर बार सफलता की रिपोर्ट करती है, जबकि वह कार्य जिसे उसे सत्यापित करना था, कभी हुआ ही नहीं।
इन-शेल विकल्प जिनकी आपको वास्तव में आवश्यकता है
ज्यादातर लोग $(...) का उपयोग करते हैं क्योंकि वे डेटा को एक variable में रखना चाहते हैं। अक्सर उन्हें वास्तव में capture की नहीं, बल्कि input की आवश्यकता होती है। ये चार तरीके आपके current shell में state को बनाए रखते हैं।
लूप पर रीडायरेक्ट
cd /tmp/subst-demo
while read -r line; do printf 'got: %s\n' "$line"; done < three.txtgot: alpha
got: beta
got: gammainput के लिए कोई अलग process नहीं बनाई जाती है, इसलिए लूप की body जो कुछ भी set करती है, वह लूप के बाद भी सुरक्षित रहता है।
प्रोसेस सब्स्टीट्यूशन
while read -r line; do printf 'got: %s\n' "$line"; done < <(sort -r three.txt)got: gamma
got: beta
got: alpha<(command) आपको एक path देता है, जैसे कि /dev/fd/63, जो command के output को read करता है। command अभी भी अपनी खुद की process में चलती है। while लूप ऐसा नहीं करता है, और यही इसका मुख्य उद्देश्य है। < <( में space अनिवार्य है: <<( को here-document की शुरुआत के रूप में पढ़ा जाएगा और यह parse नहीं होगा। प्रोसेस सब्स्टीट्यूशन एक bash feature है, इसलिए Ubuntu या Debian पर #!/bin/sh वाली script dash के तहत चलती है और उस पर विफल हो जाएगी। #!/bin/bash का उपयोग करें।
हियर-स्ट्रिंग्स
read -r first rest <<< 'alpha beta gamma'
echo "$first"
echo "$rest"alpha
beta gamma<<< एक string को command के standard input में भेजता है। read आपके shell में चलता है, इसलिए दोनों variables वहीं set हो जाते हैं जहाँ आप उनका उपयोग कर सकते हैं। read -r a b <<< "$(some-command)" आउटपुट की एक लाइन से दो fields निकालने का सामान्य तरीका है।
पूरी फाइलों के लिए mapfile
mapfile -t lines < three.txt
echo "${#lines[@]}"
echo "${lines[1]}"3
betamapfile, जिसे readarray भी कहा जाता है, एक file को current shell में एक array में read करता है। -t प्रत्येक element से trailing newline को हटा देता है। इसके लिए bash 4 या उससे नया वर्ज़न चाहिए, और Ubuntu 24.04 में bash 5.2 आता है, इसलिए यह किसी भी वर्तमान server image पर उपलब्ध है।
स्क्रिप्ट को कमिट करने से पहले की चेकलिस्ट
$(command)लिखें, और इसे"$(command)"के रूप में कोट करें, जब तक कि आप विशेष रूप से स्प्लिटिंग न करना चाहते हों।- यह मान लें कि ट्रेलिंग न्यूलाइन्स हट चुकी हैं। यदि आपको उन्हें वापस चाहिए, तो एक सेंटिनल कैरेक्टर जोड़ें।
- इंटरैक्टिव कमांड्स को सब्स्टीट्यूशन से बाहर रखें, या उनके प्रॉम्प्ट्स को स्टैंडर्ड एरर पर भेजें।
- जब
out=$(command)का एग्जिट स्टेटस मायने रखता हो, तोlocal outको अपनी अलग लाइन पर लिखें। - इनपुट से वेरिएबल्स सेट करने के लिए, पाइप के बजाय रीडायरेक्ट या प्रोसेस सब्स्टीट्यूशन का उपयोग करें।
ये आकार उन पहली छोटी स्क्रिप्ट्स में दिखाई देते हैं जिन्हें लोग नया VPS सेटअप करते समय लिखते हैं, और विफलताएं शांत रहती हैं। एक बैकअप स्क्रिप्ट जिसने प्रॉम्प्ट को फाइलनाम में कैप्चर किया, या एक हेल्थ चेक जिसने एग्जिट कोड को मास्क किया, सफलता की रिपोर्ट करना जारी रखती है। लागत तब बढ़ती है जब आप एक ही स्क्रिप्ट को कई सर्वर्स पर चलाते हैं, क्योंकि जो आउटपुट आपने कभी नहीं पढ़ा, वह अब ऐसा आउटपुट है जिसे आपने बीस मशीनों पर कभी नहीं पढ़ा।
FAQ
$() के अंदर cd करने पर मेरी current directory क्यों नहीं बदलती?
$(...) अपने कमांड को एक subshell में चलाता है, जो एक अलग process होती है। यह process आपकी working directory और variables की एक copy रखती है। cd उस copy को बदलता है, फिर process समाप्त हो जाती है और वह copy नष्ट हो जाती है। एक subshell केवल अपना standard output और exit status ही वापस भेज सकता है, इसलिए directory परिवर्तन के parent shell तक पहुँचने का कोई तरीका नहीं है। यदि आपको directory का path चाहिए, तो उसे target=$(cd /etc && pwd) के साथ capture करें और "$target" का उपयोग करें। यदि आप चाहते हैं कि आपकी shell move करे, तो cd को सीधे चलाएं, बिना किसी substitution के।
bash में $() और backticks के बीच क्या अंतर है?
साधारण कमांड के लिए दोनों एक ही परिणाम देते हैं, लेकिन दो महत्वपूर्ण अंतर हैं। $() सीधे nest हो जाता है, क्योंकि parser parentheses का मिलान करता है, जबकि backticks में nesting के हर स्तर के लिए escaped backtick की आवश्यकता होती है। Backticks inner command के parse होने से पहले 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 तर्क नहीं बचता।
जब कोई कमांड सवाल पूछता है, तो मेरी script बिना prompt के क्यों अटक जाती है?
Command substitution standard output को एक pipe में redirect कर देता है, लेकिन standard input को आपके terminal से जुड़ा रहने देता है। जो program अपना prompt standard output पर print करता है, वह prompt variable में capture हो जाता है, जबकि उसके पीछे का read अभी भी आपके इनपुट का इंतज़ार करता है। Terminal केवल आपके द्वारा टाइप किए गए characters दिखाता है, जिन्हें terminal driver echo करता है। सवाल को substitution के बाहर ले जाएं, या prompt को printf 'Username: ' >&2 के साथ standard error पर लिखें ताकि वह capture न हो।
मेरे variable के अंत में खाली लाइनें क्यों गायब हो गईं?
Command substitution हर trailing newline को हटा देता है, न कि केवल आखिरी वाली को। printf 'hello\n\n\n' > f; v=$(cat f) के बाद v में पांच bytes बचते हैं जबकि file में आठ होते हैं। उन्हें सुरक्षित रखने के लिए, substitution के अंदर एक sentinel जोड़ें और बाद में v=$(cat f; printf x) के साथ उसे हटाकर v=${v%x} का उपयोग करें। Sentinel newlines के बाद रहता है, इसलिए अंत में bash के हटाने के लिए कुछ नहीं बचता।
local out=$(cmd) हमेशा success क्यों दिखाता है?
local अपने आप में एक कमांड है, और उस लाइन के बाद $? यह बताता है कि local variable घोषित करने में सफल रहा या नहीं। Substitution का exit status consume होकर नष्ट हो जाता है, जिसका अर्थ यह भी है कि set -e script को नहीं रोकेगा। declare, export, typeset और readonly भी इसी तरह व्यवहार करते हैं। local out को एक लाइन पर और out=$(cmd) को अगली लाइन पर लिखें, तब $? वास्तविक status रिपोर्ट करेगा।