2 पॉइंट द्वारा GN⁺ 2024-03-03 | 1 टिप्पणियां | WhatsApp पर शेयर करें
  • जब Bash स्क्रिप्ट उम्मीद के मुताबिक काम नहीं करती, तो चल रहे कमांड्स को जैसा है वैसा देखना भर से भी कारण ढूंढना आसान हो जाता है
  • set -x हर लाइन को variable expansion के बाद प्रिंट करता है, जिससे पता चलता है कि स्क्रिप्ट वास्तव में कौन-से कमांड चला रही है
  • कमांड लाइन से bash -x script.sh चलाने पर script.sh की शुरुआत में set -x जोड़ने जैसा ही असर होता है
  • DEBUG trap और read को साथ में इस्तेमाल करने पर हर लाइन चलने से पहले रुककर फ़ाइल नाम, लाइन नंबर और अगला कमांड देखा जा सकता है
  • die() { echo $1 >&2; exit 1; } फ़ंक्शन किसी असफल कमांड के बाद जोड़कर standard error message दिखाने के बाद बाहर निकलने के flow को सरल बनाता है

execution flow को आँखों से देखना

  • set -x स्क्रिप्ट द्वारा चलाई जा रही लाइनों को प्रिंट करता है, और variables को expanded values के साथ दिखाता है
  • इसे स्क्रिप्ट की शुरुआत में set -x डालकर इस्तेमाल किया जा सकता है
  • यही काम कमांड लाइन से भी किया जा सकता है
    • $ bash -x script.sh
    • यह script.sh के सबसे ऊपर set -x जोड़ने के बराबर है

हर लाइन पर रुककर देखना

  • DEBUG trap हर code line के execute होने से पहले चलता है
  • अगर स्क्रिप्ट की शुरुआत में नीचे दिया गया code डालें, तो अगला कमांड चलाने से पहले Enter input का इंतज़ार होगा
    • trap '(read -p "\[$BASH_SOURCE: $LINENO] $BASH_COMMAND")' DEBUG
    • read -p message दिखाता है और Enter input का इंतज़ार करता है
    • $BASH_SOURCE स्क्रिप्ट फ़ाइल का नाम है
    • $LINENO line number है
    • $BASH_COMMAND अगला चलने वाला command है

failure पर message देकर बाहर निकलना

  • die फ़ंक्शन का उपयोग command failure होने पर message प्रिंट करके program बंद करने के लिए किया जा सकता है
    • die() { echo $1 >&2; exit 1; }
    • किसी fail हो सकने वाले command के बाद some_command || die "oh no!" की तरह जोड़ सकते हैं
  • यह फ़ंक्शन message को standard error पर भेजता है और exit 1 के साथ बंद हो जाता है

1 टिप्पणियां

 
GN⁺ 2024-03-03
Hacker News की राय
  • ZFSBootMenu में debugging में मदद के लिए कुछ अच्छे custom functions इस्तेमाल किए जाते हैं
    कोड में कई जगह zdebug logging function डाला गया है: https://github.com/zbm-dev/zfsbootmenu/blob/master/zfsbootme...
    debug logging चालू होने पर main menu में Ctrl-T दबाने से ऐसा स्क्रीन दिखता है: https://i.imgur.com/Ge75zkP.png
    साथ ही https://github.com/zbm-dev/zfsbootmenu/blob/master/zfsbootme... से चालू की जा सकने वाली flamegraph profiling भी है, और serial port पर dump किए गए डेटा को फिर से जोड़कर https://raw.githubusercontent.com/zbm-dev/zfsbootmenu/master... जैसा ग्राफ बनाया जा सकता है
    Bash उम्मीद से ज़्यादा flexible है
  • set -x इस्तेमाल करते समय PS4='+ ${BASH_SOURCE:-}:${FUNCNAME[0]:-}:L${LINENO:-}: ' बहुत काम का है
    इससे filename, function name, line number दिखते हैं, इसलिए बड़े Bash scripts को debug करते समय काफ़ी मदद मिलती है
  • shellcheck की भी सिफारिश है। यह भले सीधे समस्या न पकड़ पाए, लेकिन संभावित समस्याओं की ओर इशारा करता है
    और script को किसी दूसरी language में फिर से लिखने की भी सलाह है। कंपनी में Bash scripts को Rust में बदला जा रहा है; शुरुआती लागत ज़्यादा है, लेकिन नतीजे में code काफ़ी ज़्यादा maintainable और reliable होता है
    तेज़ scripts के लिए Bash अब भी अच्छा है, लेकिन 100 lines के आसपास पहुँचते ही ऐसी language लेना ठीक है जो मज़बूत guarantees दे सके
    • सहमत हूँ, लेकिन यही बात CI/CD engineering और YAML pipelines पर भी लागू होती है
  • exit codes को इस तरह इस्तेमाल करने से debugging और बेहतर हो सकती है
    die() एक helper function है जो error message को standard error पर प्रिंट करके दिए गए error code के साथ exit करता है
    और ज़्यादा shell script exit codes और helper functions यहाँ हैं: https://github.com/SixArm/unix-shell-script-kit/blob/main/un...
    • यह अच्छी सूची है, और अनुभवी users के पास शायद अपने helper functions होते ही हैं
      लेकिन उस die की philosophy को लेकर थोड़ी आपत्ति है। die function को मूल रूप से failed command का exit code पास करना चाहिए, और उस command का error output भी छिपाना नहीं चाहिए
      अगर बड़े script में command failure को मैं खुद कोई अर्थ देना चाहूँ, तो उसके लिए अलग, ज़्यादा specific die इस्तेमाल करूँगा। मेरा die लगभग __errex "$?" "${LINENO}" "$0" जैसा है, जो fatal error, line number, script name और message प्रिंट करके उसी exit code के साथ खत्म करता है
  • अगर आप Bash functions का बहुत इस्तेमाल करते हैं, तो एक तरह का stack trace बनाना भी संभव है
    इसका एक implementation example यहाँ है: https://github.com/TritonDataCenter/sdc-headnode/blob/master...
    • stack trace का एक और implementation है: https://github.com/runag/runag/blob/main/lib/fail.sh
      some-command || fail "message" की तरह इस्तेमाल करने पर, जब some-command non-zero exit status लौटाता है, तो यह stack trace बनाता है और shell को बंद कर देता है
      अगर function के भीतर stack trace बनाकर return करना हो, तो some-command || softfail "message" || return $? की तरह इस्तेमाल किया जा सकता है
  • यह जानने की जिज्ञासा है कि Bash अब भी de facto shell scripting language क्यों है, और क्या इसकी वजह सिर्फ legacy inertia से ज़्यादा कुछ है
    ज़रूरी काम तो हो जाता है, लेकिन यह भद्दी है और इसका syntax भी भयानक है। जब script एक खास size या complexity तक पहुँचती है, तो यह आपको किसी proper language में जाने को मजबूर कर देती है; शायद यह किसी तरह का intentional design भी हो सकता है
    • legacy usage इसकी popularity का बड़ा हिस्सा है, यह सही है
      modern distributions में आम तौर पर Bash का काफ़ी नया version होता है, और अगर arrays जैसी चीज़ें इस्तेमाल नहीं करनी हैं, तो version की ज़्यादा चिंता भी नहीं करनी पड़ती
      Bash की ताकत दूसरी languages और tools के बीच उसकी जगह में है। यह दूसरे tools को जोड़ने के लिए ideal है, operating system के काफ़ी क़रीब है इसलिए सुविधाजनक है, और Python की तरह library installation की ज़रूरत भी नहीं पड़ती
      अक्सर कहा जाता है कि ज़्यादा complex scripting को Python जैसी language में ले जाओ, लेकिन लंबी अवधि में इससे complexity की एक अतिरिक्त परत जुड़ सकती है जो मददगार न हो। 20 साल पहले लिखे Bash scripts आज भी ठीक चलते हैं, लेकिन 20 साल पुराने Python programs में version problems होने की संभावना काफ़ी ज़्यादा है
    • Bourne shell scripting काफ़ी अच्छी है, इसलिए इसे replace करना लगभग असंभव है
      Plan 9 का rc थोड़ा ज़्यादा साफ़-सुथरा है, लेकिन “लगभग वही, बस ज़्यादा साफ़” होने से कोई switch नहीं करता। आप अभी https://pkgsrc.se/shells से इसी तरह की लेकिन बेहतर चीज़ install कर सकते हैं, फिर भी लोग इस्तेमाल नहीं करते, और दूसरे लोगों के लिए इसे चलाने का तरीका भी नहीं बदलता
      किसी स्थापित technology को replace करने के लिए किसी चीज़ का core aspects में कई गुना बेहतर होना ज़रूरी है। Plan 9 भी UNIX परिवार से बेहतर था, लेकिन replace करने लायक पर्याप्त बेहतर नहीं था
      Bourne shell scripting की niche को replace करने लायक कुछ बनाना मुश्किल है। उससे पहले ही वह Perl, Python, Ruby जैसी असली scripting languages के ecological position या problem space में पहुँच जाता है
      संकीर्ण problem spaces में local optimum सारी हवा खींच लेता है, जिससे सैद्धांतिक global optimum के क़रीब कोई competitor उभरना मुश्किल हो जाता है
    • मुझे सच में लगता है कि इसकी वजह legacy और inertia ही है

हाल में जो अतिरिक्त फीचर्स जुड़े हैं, वे sh/Bash के ऊपर ठीक से बैठ जाते हैं, लेकिन आखिरकार shell scripting एक मकसद को पूरा करने का साधन है और इसे सामान्य programming language की तुलना में कहीं अधिक धीरे evolve होना चाहिए
Bash/sh की मुख्य खासियत यह है कि यह anti-entropic है। इसमें development या evolution लगभग नहीं के बराबर होता है, इसलिए dependency या नए features की वजह से सिरदर्द होने की संभावना कम रहती है, और जो चीज़ें 20 साल पहले चलती थीं वे आज भी बुनियादी tools बनी रहती हैं
इसके design के कारण यह बदलाव का विरोध करने वाला system बन जाता है, और जब लोग इसकी सीमाओं से टकराते हैं तो उन्हें इसके बाहर जाने की प्रेरणा मिलती है

  • यह वाकई Bash है, इस पर यकीन नहीं
    FreeBSD की ज़्यादातर scripts sh के लिए लिखी जाती हैं, और sh POSIX standard का हिस्सा है, इसलिए मुझे लगता है कि उसका support कहीं ज़्यादा व्यापक है। Bash बस लोकप्रिय है, ऐसा मानता हूँ
  • इसका हर जगह उपलब्ध होना बड़ी बात है
    लेकिन Bash इतना खराब था कि Groovy scripts इस्तेमाल करने के लिए मैंने namespace कम किए हुए utilities का एक ढेर बना लिया था। उन्हें IDE में develop किया जा सकता था, library system भी सुरक्षित था, और Groovy ने Java की लगभग सारी असुविधाएँ काफी हद तक सुधार दी थीं, इसलिए वह कहीं बेहतर था
  • एक काफ़ी ताकतवर gdb-style असली debugger भी है: https://bashdb.sourceforge.net/
  • थोड़ा संबंधित self-promo करूँ तो, मैंने पहले एक Bash pipeline debugger बनाया था जो intermediate output को सुरक्षित रखता है
    इसकी कुछ सीमाएँ हैं, लेकिन आम तौर पर उपयोगी हो सकता है: https://github.com/ketancmaheshwari/pd
  • die() तकनीक अच्छी है, लेकिन Bash में एक परेशान करने वाली विशेषता है। अगर आप subshell के अंदर exit करने की कोशिश करते हैं, तो सिर्फ subshell बंद होता है और बाकी script चलती रहती है
    उदाहरण के लिए, अगर cat myfile | while read line; do ... die "Found match" ... done जैसी pipeline के भीतर die को call किया जाए, तो बाद का echo "I don't want this line" फिर भी print हो जाएगा
    कई मामलों में subshell से बचा जा सकता है, और इस उदाहरण में shellcheck का UUOC बताना सही है; उसे ठीक कर देने से subshell के अंदर die वाली समस्या भी सुलझ जाती है
    लेकिन कभी-कभी subshell से बचना संभव नहीं होता, या उससे बचने के लिए script बहुत जटिल हो जाती है। ऐसे में script की शुरुआत में MYPID=$$ से PID पकड़कर रखा जा सकता है और die() { echo "$1" >&2; kill -9 $MYPID; exit 1; } जैसे तरीके से process को मारा जा सकता है
    बेशक, इसमें भी trade-off है। इस तरह मारने का तरीका काफ़ी rough है, और पता नहीं क्यों, यह पूरी तरह भरोसेमंद भी नहीं लगा
    • सिर्फ set -e जोड़ देने से भी, जब subshell non-zero error code के साथ बंद होता है, script भी साथ में बंद हो जाती है
      समझ नहीं आता कि किसी भी shell script में set -e न रखने की वजह क्या हो सकती है
    • उस तरह PID को मारने से क्या zombie process नहीं बन सकते?
  • मैं Bash script के सबसे ऊपर हमेशा set -euxo pipefail डालता हूँ
    इससे condition tests थोड़े मुश्किल हो जाते हैं, लेकिन खासकर pipefail ने अकेले ही कई बार बहुत काम किया है
    • https://mywiki.wooledge.org/BashPitfalls#set_-euo_pipefail
    • इसे कई lines के लिए चालू करके set +x से बंद भी किया जा सकता है
      इसे लगातार चालू छोड़ दें तो यह काफ़ी उबाऊ हो जाता है
    • यह तो life-saver जैसा setting है
      हाँ, -x को मैं तब तक बचाकर रखता हूँ जब तक सच में सारी गंदी debug output देखने की ज़रूरत न पड़े