[{"data":1,"prerenderedAt":4577},["ShallowReactive",2],{"page-\u002Fbash\u002F13-best-practices":3},{"id":4,"title":5,"body":6,"description":4570,"extension":4571,"meta":4572,"navigation":52,"path":4573,"seo":4574,"stem":4575,"__hash__":4576},"content\u002Fbash\u002F13-best-practices.md","Bash 13 — Production Script Architecture: Structure, Idioms & Safety",{"type":7,"value":8,"toc":4550},"minimark",[9,14,19,1463,1470,1856,1864,1967,1971,2186,2196,2433,2440,2715,2719,3135,3139,3474,3481,3788,3792,3943,3947,4205,4209,4360,4364,4368,4407,4546],[10,11,13],"h1",{"id":12},"_13-production-script-architecture-structure-idioms-safety","13 — Production Script Architecture: Structure, Idioms & Safety",[15,16,18],"h2",{"id":17},"the-production-script-skeleton","The Production Script Skeleton",[20,21,23],"code-wrapper",{"language":22},"bash",[24,25,29],"pre",{"className":26,"code":27,"language":22,"meta":28,"style":28},"language-bash shiki shiki-themes github-light github-dark","#!\u002Fusr\u002Fbin\u002Fenv bash\n# ── 1. Shebang (portable: env finds bash via PATH) ──\n\n# ── 2. Strict mode ──\nset -Eeuo pipefail\n# -E: ERR trap inherits into functions (essential for trap ERR)\n# -e: exit on error\n# -u: error on unset variable\n# -o pipefail: pipeline fails if any command fails\n\n# ── 3. Defensive shopts ──\nshopt -s inherit_errexit 2>\u002Fdev\u002Fnull || true  # Bash 4.4+: subshells inherit set -e\nshopt -s nullglob        # unmatched globs → empty (not literal)\nshopt -s globstar        # ** recursive\nshopt -s extglob         # extended pattern matching\n\n# ── 4. Constants (readonly, UPPER_CASE) ──\nreadonly SCRIPT_NAME=$(basename \"${BASH_SOURCE[0]}\")\nreadonly SCRIPT_DIR=$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\nreadonly VERSION=\"1.0.0\"\nreadonly LOG_FILE=\"${LOG_FILE:-\u002Fvar\u002Flog\u002Fmyapp.log}\"\n\n# ── 5. Global state (declare explicitly) ──\ndeclare -A config\ndeclare -a positionals=()\ntmpdir=\"\"\n\n# ── 6. Utility functions ──\nlog() { printf '[%s] %s\\n' \"$(date -Iseconds)\" \"$*\" >&2; }\ndie() { log \"FATAL: $*\"; exit 1; }\nrequire() { command -v \"$1\" &>\u002Fdev\u002Fnull || die \"missing: $1\"; }\n\n# ── 7. Error handling ──\non_error() {\n    local exit_code=$?\n    local line=$1\n    log \"ERROR at line $line: '$BASH_COMMAND' failed (exit $exit_code)\"\n    exit \"$exit_code\"\n}\ntrap 'on_error $LINENO' ERR\n\ncleanup() {\n    local exit_code=$?\n    [[ -n \"$tmpdir\" && -d \"$tmpdir\" ]] && rm -rf -- \"$tmpdir\"\n    if ((exit_code == 0)); then\n        log \"completed successfully\"\n    else\n        log \"failed (exit $exit_code)\"\n    fi\n    exit \"$exit_code\"\n}\ntrap cleanup EXIT INT TERM\n\n# ── 8. Usage ──\nusage() {\n    cat \u003C\u003CEOF\n$SCRIPT_NAME v$VERSION\n\nUsage: $SCRIPT_NAME [OPTIONS] \u003Ccommand> [args...]\n\nOptions:\n  -h, --help     Show this help\n  -v, --verbose  Verbose output\n  --version      Show version\n\nCommands:\n  build    Build the project\n  test     Run tests\n  deploy   Deploy to environment\nEOF\n}\n\n# ── 9. Command functions ──\ncmd_build() {\n    log \"building...\"\n    # ... build logic ...\n}\n\ncmd_test() {\n    log \"testing...\"\n    # ... test logic ...\n}\n\n# ── 10. Main function ──\nmain() {\n    parse_args \"$@\"\n    dispatch\n}\n\nparse_args() {\n    while [[ $# -gt 0 ]]; do\n        case \"$1\" in\n            -h|--help) usage; exit 0 ;;\n            -v|--verbose) LOG_LEVEL=1; shift ;;\n            --version) echo \"$VERSION\"; exit 0 ;;\n            -*) die \"unknown option: $1\" ;;\n            *) positionals+=(\"$1\"); shift ;;\n        esac\n    done\n}\n\ndispatch() {\n    local cmd=\"${positionals[0]:-}\"\n    [[ -z \"$cmd\" ]] && { usage; exit 1; }\n    case \"$cmd\" in\n        build)  shift_positionals 1; cmd_build \"$@\" ;;\n        test)   shift_positionals 1; cmd_test \"$@\" ;;\n        *)      die \"unknown command: $cmd\" ;;\n    esac\n}\n\nshift_positionals() {\n    shift \"$1\"\n    positionals=(\"${positionals[@]:1}\")\n}\n\n# ── 11. Entry point (only if executed, not sourced) ──\nif [[ \"${BASH_SOURCE[0]}\" == \"${0}\" ]]; then\n    main \"$@\"\nfi\n","",[30,31,32,41,47,54,60,74,80,86,92,98,103,109,138,151,164,177,182,188,219,255,268,309,314,320,332,348,359,364,370,410,438,483,488,494,503,517,531,558,571,577,589,594,602,613,659,680,689,695,707,713,724,729,746,751,757,765,777,789,794,805,810,816,822,828,834,839,845,851,857,863,868,873,878,884,892,900,906,911,916,924,932,938,943,948,954,962,975,981,986,991,999,1022,1037,1065,1093,1119,1141,1168,1174,1180,1185,1190,1198,1221,1249,1263,1288,1313,1331,1337,1342,1347,1355,1367,1400,1405,1410,1416,1445,1457],"code",{"__ignoreMap":28},[33,34,37],"span",{"class":35,"line":36},"line",1,[33,38,40],{"class":39},"sdCPZ","#!\u002Fusr\u002Fbin\u002Fenv bash\n",[33,42,44],{"class":35,"line":43},2,[33,45,46],{"class":39},"# ── 1. Shebang (portable: env finds bash via PATH) ──\n",[33,48,50],{"class":35,"line":49},3,[33,51,53],{"emptyLinePlaceholder":52},true,"\n",[33,55,57],{"class":35,"line":56},4,[33,58,59],{"class":39},"# ── 2. Strict mode ──\n",[33,61,63,67,70],{"class":35,"line":62},5,[33,64,66],{"class":65},"snvgF","set",[33,68,69],{"class":65}," -Eeuo",[33,71,73],{"class":72},"sJ6F3"," pipefail\n",[33,75,77],{"class":35,"line":76},6,[33,78,79],{"class":39},"# -E: ERR trap inherits into functions (essential for trap ERR)\n",[33,81,83],{"class":35,"line":82},7,[33,84,85],{"class":39},"# -e: exit on error\n",[33,87,89],{"class":35,"line":88},8,[33,90,91],{"class":39},"# -u: error on unset variable\n",[33,93,95],{"class":35,"line":94},9,[33,96,97],{"class":39},"# -o pipefail: pipeline fails if any command fails\n",[33,99,101],{"class":35,"line":100},10,[33,102,53],{"emptyLinePlaceholder":52},[33,104,106],{"class":35,"line":105},11,[33,107,108],{"class":39},"# ── 3. Defensive shopts ──\n",[33,110,112,116,119,122,126,129,132,135],{"class":35,"line":111},12,[33,113,115],{"class":114},"sIsaT","shopt",[33,117,118],{"class":65}," -s",[33,120,121],{"class":72}," inherit_errexit",[33,123,125],{"class":124},"svdQ7"," 2>",[33,127,128],{"class":72},"\u002Fdev\u002Fnull",[33,130,131],{"class":124}," ||",[33,133,134],{"class":65}," true",[33,136,137],{"class":39},"  # Bash 4.4+: subshells inherit set -e\n",[33,139,141,143,145,148],{"class":35,"line":140},13,[33,142,115],{"class":114},[33,144,118],{"class":65},[33,146,147],{"class":72}," nullglob",[33,149,150],{"class":39},"        # unmatched globs → empty (not literal)\n",[33,152,154,156,158,161],{"class":35,"line":153},14,[33,155,115],{"class":114},[33,157,118],{"class":65},[33,159,160],{"class":72}," globstar",[33,162,163],{"class":39},"        # ** recursive\n",[33,165,167,169,171,174],{"class":35,"line":166},15,[33,168,115],{"class":114},[33,170,118],{"class":65},[33,172,173],{"class":72}," extglob",[33,175,176],{"class":39},"         # extended pattern matching\n",[33,178,180],{"class":35,"line":179},16,[33,181,53],{"emptyLinePlaceholder":52},[33,183,185],{"class":35,"line":184},17,[33,186,187],{"class":39},"# ── 4. Constants (readonly, UPPER_CASE) ──\n",[33,189,191,194,198,201,204,207,210,213,216],{"class":35,"line":190},18,[33,192,193],{"class":124},"readonly",[33,195,197],{"class":196},"ssxIu"," SCRIPT_NAME",[33,199,200],{"class":124},"=",[33,202,203],{"class":196},"$(",[33,205,206],{"class":114},"basename",[33,208,209],{"class":72}," \"${",[33,211,212],{"class":196},"BASH_SOURCE",[33,214,215],{"class":72},"[0]}\"",[33,217,218],{"class":196},")\n",[33,220,222,224,227,229,231,234,237,240,242,244,247,250,253],{"class":35,"line":221},19,[33,223,193],{"class":124},[33,225,226],{"class":196}," SCRIPT_DIR",[33,228,200],{"class":124},[33,230,203],{"class":196},[33,232,233],{"class":65},"cd",[33,235,236],{"class":72}," \"$(",[33,238,239],{"class":114},"dirname",[33,241,209],{"class":72},[33,243,212],{"class":196},[33,245,246],{"class":72},"[0]}\")\"",[33,248,249],{"class":196}," && ",[33,251,252],{"class":65},"pwd",[33,254,218],{"class":196},[33,256,258,260,263,265],{"class":35,"line":257},20,[33,259,193],{"class":124},[33,261,262],{"class":196}," VERSION",[33,264,200],{"class":124},[33,266,267],{"class":72},"\"1.0.0\"\n",[33,269,271,273,276,278,281,284,287,290,293,296,298,301,304,306],{"class":35,"line":270},21,[33,272,193],{"class":124},[33,274,275],{"class":196}," LOG_FILE",[33,277,200],{"class":124},[33,279,280],{"class":72},"\"${",[33,282,283],{"class":196},"LOG_FILE",[33,285,286],{"class":124},":-\u002F",[33,288,289],{"class":196},"var",[33,291,292],{"class":124},"\u002F",[33,294,295],{"class":196},"log",[33,297,292],{"class":124},[33,299,300],{"class":196},"myapp",[33,302,303],{"class":72},".",[33,305,295],{"class":196},[33,307,308],{"class":72},"}\"\n",[33,310,312],{"class":35,"line":311},22,[33,313,53],{"emptyLinePlaceholder":52},[33,315,317],{"class":35,"line":316},23,[33,318,319],{"class":39},"# ── 5. Global state (declare explicitly) ──\n",[33,321,323,326,329],{"class":35,"line":322},24,[33,324,325],{"class":124},"declare",[33,327,328],{"class":65}," -A",[33,330,331],{"class":196}," config\n",[33,333,335,337,340,343,345],{"class":35,"line":334},25,[33,336,325],{"class":124},[33,338,339],{"class":65}," -a",[33,341,342],{"class":196}," positionals",[33,344,200],{"class":124},[33,346,347],{"class":196},"()\n",[33,349,351,354,356],{"class":35,"line":350},26,[33,352,353],{"class":196},"tmpdir",[33,355,200],{"class":124},[33,357,358],{"class":72},"\"\"\n",[33,360,362],{"class":35,"line":361},27,[33,363,53],{"emptyLinePlaceholder":52},[33,365,367],{"class":35,"line":366},28,[33,368,369],{"class":39},"# ── 6. Utility functions ──\n",[33,371,373,375,378,381,384,386,389,392,395,398,401,404,407],{"class":35,"line":372},29,[33,374,295],{"class":114},[33,376,377],{"class":196},"() { ",[33,379,380],{"class":65},"printf",[33,382,383],{"class":72}," '[%s] %s\\n'",[33,385,236],{"class":72},[33,387,388],{"class":114},"date",[33,390,391],{"class":65}," -Iseconds",[33,393,394],{"class":72},")\"",[33,396,397],{"class":72}," \"",[33,399,400],{"class":65},"$*",[33,402,403],{"class":72},"\"",[33,405,406],{"class":124}," >&2",[33,408,409],{"class":196},"; }\n",[33,411,413,416,418,420,423,425,427,430,433,436],{"class":35,"line":412},30,[33,414,415],{"class":114},"die",[33,417,377],{"class":196},[33,419,295],{"class":114},[33,421,422],{"class":72}," \"FATAL: ",[33,424,400],{"class":65},[33,426,403],{"class":72},[33,428,429],{"class":196},"; ",[33,431,432],{"class":65},"exit",[33,434,435],{"class":65}," 1",[33,437,409],{"class":196},[33,439,441,444,446,449,452,454,457,459,462,465,468,471,474,477,479,481],{"class":35,"line":440},31,[33,442,443],{"class":114},"require",[33,445,377],{"class":196},[33,447,448],{"class":65},"command",[33,450,451],{"class":65}," -v",[33,453,397],{"class":72},[33,455,456],{"class":65},"$1",[33,458,403],{"class":72},[33,460,461],{"class":196}," &",[33,463,464],{"class":124},">",[33,466,467],{"class":196},"\u002Fdev\u002Fnull ",[33,469,470],{"class":124},"||",[33,472,473],{"class":114}," die",[33,475,476],{"class":72}," \"missing: ",[33,478,456],{"class":65},[33,480,403],{"class":72},[33,482,409],{"class":196},[33,484,486],{"class":35,"line":485},32,[33,487,53],{"emptyLinePlaceholder":52},[33,489,491],{"class":35,"line":490},33,[33,492,493],{"class":39},"# ── 7. Error handling ──\n",[33,495,497,500],{"class":35,"line":496},34,[33,498,499],{"class":114},"on_error",[33,501,502],{"class":196},"() {\n",[33,504,506,509,512,514],{"class":35,"line":505},35,[33,507,508],{"class":124},"    local",[33,510,511],{"class":196}," exit_code",[33,513,200],{"class":124},[33,515,516],{"class":65},"$?\n",[33,518,520,522,525,527],{"class":35,"line":519},36,[33,521,508],{"class":124},[33,523,524],{"class":196}," line",[33,526,200],{"class":124},[33,528,530],{"class":529},"sCrzJ","$1\n",[33,532,534,537,540,543,546,549,552,555],{"class":35,"line":533},37,[33,535,536],{"class":114},"    log",[33,538,539],{"class":72}," \"ERROR at line ",[33,541,542],{"class":196},"$line",[33,544,545],{"class":72},": '",[33,547,548],{"class":196},"$BASH_COMMAND",[33,550,551],{"class":72},"' failed (exit ",[33,553,554],{"class":196},"$exit_code",[33,556,557],{"class":72},")\"\n",[33,559,561,564,566,568],{"class":35,"line":560},38,[33,562,563],{"class":65},"    exit",[33,565,397],{"class":72},[33,567,554],{"class":196},[33,569,570],{"class":72},"\"\n",[33,572,574],{"class":35,"line":573},39,[33,575,576],{"class":196},"}\n",[33,578,580,583,586],{"class":35,"line":579},40,[33,581,582],{"class":65},"trap",[33,584,585],{"class":72}," 'on_error $LINENO'",[33,587,588],{"class":72}," ERR\n",[33,590,592],{"class":35,"line":591},41,[33,593,53],{"emptyLinePlaceholder":52},[33,595,597,600],{"class":35,"line":596},42,[33,598,599],{"class":114},"cleanup",[33,601,502],{"class":196},[33,603,605,607,609,611],{"class":35,"line":604},43,[33,606,508],{"class":124},[33,608,511],{"class":196},[33,610,200],{"class":124},[33,612,516],{"class":65},[33,614,616,619,622,624,627,629,632,635,637,639,641,644,647,650,653,655,657],{"class":35,"line":615},44,[33,617,618],{"class":196},"    [[ ",[33,620,621],{"class":124},"-n",[33,623,397],{"class":72},[33,625,626],{"class":196},"$tmpdir",[33,628,403],{"class":72},[33,630,631],{"class":124}," &&",[33,633,634],{"class":124}," -d",[33,636,397],{"class":72},[33,638,626],{"class":196},[33,640,403],{"class":72},[33,642,643],{"class":196}," ]] && ",[33,645,646],{"class":114},"rm",[33,648,649],{"class":65}," -rf",[33,651,652],{"class":65}," --",[33,654,397],{"class":72},[33,656,626],{"class":196},[33,658,570],{"class":72},[33,660,662,665,668,671,674,677],{"class":35,"line":661},45,[33,663,664],{"class":124},"    if",[33,666,667],{"class":196}," ((exit_code ",[33,669,670],{"class":124},"==",[33,672,673],{"class":65}," 0",[33,675,676],{"class":196},")); ",[33,678,679],{"class":124},"then\n",[33,681,683,686],{"class":35,"line":682},46,[33,684,685],{"class":114},"        log",[33,687,688],{"class":72}," \"completed successfully\"\n",[33,690,692],{"class":35,"line":691},47,[33,693,694],{"class":124},"    else\n",[33,696,698,700,703,705],{"class":35,"line":697},48,[33,699,685],{"class":114},[33,701,702],{"class":72}," \"failed (exit ",[33,704,554],{"class":196},[33,706,557],{"class":72},[33,708,710],{"class":35,"line":709},49,[33,711,712],{"class":124},"    fi\n",[33,714,716,718,720,722],{"class":35,"line":715},50,[33,717,563],{"class":65},[33,719,397],{"class":72},[33,721,554],{"class":196},[33,723,570],{"class":72},[33,725,727],{"class":35,"line":726},51,[33,728,576],{"class":196},[33,730,732,734,737,740,743],{"class":35,"line":731},52,[33,733,582],{"class":65},[33,735,736],{"class":72}," cleanup",[33,738,739],{"class":72}," EXIT",[33,741,742],{"class":72}," INT",[33,744,745],{"class":72}," TERM\n",[33,747,749],{"class":35,"line":748},53,[33,750,53],{"emptyLinePlaceholder":52},[33,752,754],{"class":35,"line":753},54,[33,755,756],{"class":39},"# ── 8. Usage ──\n",[33,758,760,763],{"class":35,"line":759},55,[33,761,762],{"class":114},"usage",[33,764,502],{"class":196},[33,766,768,771,774],{"class":35,"line":767},56,[33,769,770],{"class":114},"    cat",[33,772,773],{"class":124}," \u003C\u003C",[33,775,776],{"class":72},"EOF\n",[33,778,780,783,786],{"class":35,"line":779},57,[33,781,782],{"class":196},"$SCRIPT_NAME",[33,784,785],{"class":72}," v",[33,787,788],{"class":196},"$VERSION\n",[33,790,792],{"class":35,"line":791},58,[33,793,53],{"emptyLinePlaceholder":52},[33,795,797,800,802],{"class":35,"line":796},59,[33,798,799],{"class":72},"Usage: ",[33,801,782],{"class":196},[33,803,804],{"class":72}," [OPTIONS] \u003Ccommand> [args...]\n",[33,806,808],{"class":35,"line":807},60,[33,809,53],{"emptyLinePlaceholder":52},[33,811,813],{"class":35,"line":812},61,[33,814,815],{"class":72},"Options:\n",[33,817,819],{"class":35,"line":818},62,[33,820,821],{"class":72},"  -h, --help     Show this help\n",[33,823,825],{"class":35,"line":824},63,[33,826,827],{"class":72},"  -v, --verbose  Verbose output\n",[33,829,831],{"class":35,"line":830},64,[33,832,833],{"class":72},"  --version      Show version\n",[33,835,837],{"class":35,"line":836},65,[33,838,53],{"emptyLinePlaceholder":52},[33,840,842],{"class":35,"line":841},66,[33,843,844],{"class":72},"Commands:\n",[33,846,848],{"class":35,"line":847},67,[33,849,850],{"class":72},"  build    Build the project\n",[33,852,854],{"class":35,"line":853},68,[33,855,856],{"class":72},"  test     Run tests\n",[33,858,860],{"class":35,"line":859},69,[33,861,862],{"class":72},"  deploy   Deploy to environment\n",[33,864,866],{"class":35,"line":865},70,[33,867,776],{"class":72},[33,869,871],{"class":35,"line":870},71,[33,872,576],{"class":196},[33,874,876],{"class":35,"line":875},72,[33,877,53],{"emptyLinePlaceholder":52},[33,879,881],{"class":35,"line":880},73,[33,882,883],{"class":39},"# ── 9. Command functions ──\n",[33,885,887,890],{"class":35,"line":886},74,[33,888,889],{"class":114},"cmd_build",[33,891,502],{"class":196},[33,893,895,897],{"class":35,"line":894},75,[33,896,536],{"class":114},[33,898,899],{"class":72}," \"building...\"\n",[33,901,903],{"class":35,"line":902},76,[33,904,905],{"class":39},"    # ... build logic ...\n",[33,907,909],{"class":35,"line":908},77,[33,910,576],{"class":196},[33,912,914],{"class":35,"line":913},78,[33,915,53],{"emptyLinePlaceholder":52},[33,917,919,922],{"class":35,"line":918},79,[33,920,921],{"class":114},"cmd_test",[33,923,502],{"class":196},[33,925,927,929],{"class":35,"line":926},80,[33,928,536],{"class":114},[33,930,931],{"class":72}," \"testing...\"\n",[33,933,935],{"class":35,"line":934},81,[33,936,937],{"class":39},"    # ... test logic ...\n",[33,939,941],{"class":35,"line":940},82,[33,942,576],{"class":196},[33,944,946],{"class":35,"line":945},83,[33,947,53],{"emptyLinePlaceholder":52},[33,949,951],{"class":35,"line":950},84,[33,952,953],{"class":39},"# ── 10. Main function ──\n",[33,955,957,960],{"class":35,"line":956},85,[33,958,959],{"class":114},"main",[33,961,502],{"class":196},[33,963,965,968,970,973],{"class":35,"line":964},86,[33,966,967],{"class":114},"    parse_args",[33,969,397],{"class":72},[33,971,972],{"class":65},"$@",[33,974,570],{"class":72},[33,976,978],{"class":35,"line":977},87,[33,979,980],{"class":114},"    dispatch\n",[33,982,984],{"class":35,"line":983},88,[33,985,576],{"class":196},[33,987,989],{"class":35,"line":988},89,[33,990,53],{"emptyLinePlaceholder":52},[33,992,994,997],{"class":35,"line":993},90,[33,995,996],{"class":114},"parse_args",[33,998,502],{"class":196},[33,1000,1002,1005,1008,1011,1014,1016,1019],{"class":35,"line":1001},91,[33,1003,1004],{"class":124},"    while",[33,1006,1007],{"class":72}," [[",[33,1009,1010],{"class":65}," $#",[33,1012,1013],{"class":65}," -gt",[33,1015,673],{"class":65},[33,1017,1018],{"class":196}," ]]; ",[33,1020,1021],{"class":124},"do\n",[33,1023,1025,1028,1030,1032,1034],{"class":35,"line":1024},92,[33,1026,1027],{"class":124},"        case",[33,1029,397],{"class":72},[33,1031,456],{"class":65},[33,1033,403],{"class":72},[33,1035,1036],{"class":124}," in\n",[33,1038,1040,1044,1047,1050,1053,1056,1058,1060,1062],{"class":35,"line":1039},93,[33,1041,1043],{"class":1042},"svAP2","            -h",[33,1045,1046],{"class":124},"|",[33,1048,1049],{"class":1042},"--help",[33,1051,1052],{"class":124},")",[33,1054,1055],{"class":114}," usage",[33,1057,429],{"class":196},[33,1059,432],{"class":65},[33,1061,673],{"class":65},[33,1063,1064],{"class":196}," ;;\n",[33,1066,1068,1071,1073,1076,1078,1081,1083,1086,1088,1091],{"class":35,"line":1067},94,[33,1069,1070],{"class":1042},"            -v",[33,1072,1046],{"class":124},[33,1074,1075],{"class":1042},"--verbose",[33,1077,1052],{"class":124},[33,1079,1080],{"class":196}," LOG_LEVEL",[33,1082,200],{"class":124},[33,1084,1085],{"class":72},"1",[33,1087,429],{"class":196},[33,1089,1090],{"class":65},"shift",[33,1092,1064],{"class":196},[33,1094,1096,1099,1101,1104,1106,1109,1111,1113,1115,1117],{"class":35,"line":1095},95,[33,1097,1098],{"class":1042},"            --version",[33,1100,1052],{"class":124},[33,1102,1103],{"class":65}," echo",[33,1105,397],{"class":72},[33,1107,1108],{"class":196},"$VERSION",[33,1110,403],{"class":72},[33,1112,429],{"class":196},[33,1114,432],{"class":65},[33,1116,673],{"class":65},[33,1118,1064],{"class":196},[33,1120,1122,1125,1128,1130,1132,1135,1137,1139],{"class":35,"line":1121},96,[33,1123,1124],{"class":1042},"            -",[33,1126,1127],{"class":124},"*",[33,1129,1052],{"class":124},[33,1131,473],{"class":114},[33,1133,1134],{"class":72}," \"unknown option: ",[33,1136,456],{"class":65},[33,1138,403],{"class":72},[33,1140,1064],{"class":196},[33,1142,1144,1147,1149,1152,1155,1157,1159,1161,1164,1166],{"class":35,"line":1143},97,[33,1145,1146],{"class":124},"            *)",[33,1148,342],{"class":196},[33,1150,1151],{"class":124},"+=",[33,1153,1154],{"class":196},"(",[33,1156,403],{"class":72},[33,1158,456],{"class":65},[33,1160,403],{"class":72},[33,1162,1163],{"class":196},"); ",[33,1165,1090],{"class":65},[33,1167,1064],{"class":196},[33,1169,1171],{"class":35,"line":1170},98,[33,1172,1173],{"class":124},"        esac\n",[33,1175,1177],{"class":35,"line":1176},99,[33,1178,1179],{"class":124},"    done\n",[33,1181,1183],{"class":35,"line":1182},100,[33,1184,576],{"class":196},[33,1186,1188],{"class":35,"line":1187},101,[33,1189,53],{"emptyLinePlaceholder":52},[33,1191,1193,1196],{"class":35,"line":1192},102,[33,1194,1195],{"class":114},"dispatch",[33,1197,502],{"class":196},[33,1199,1201,1203,1206,1208,1210,1213,1216,1219],{"class":35,"line":1200},103,[33,1202,508],{"class":124},[33,1204,1205],{"class":196}," cmd",[33,1207,200],{"class":124},[33,1209,280],{"class":72},[33,1211,1212],{"class":196},"positionals",[33,1214,1215],{"class":72},"[0]",[33,1217,1218],{"class":124},":-",[33,1220,308],{"class":72},[33,1222,1224,1226,1229,1231,1234,1236,1239,1241,1243,1245,1247],{"class":35,"line":1223},104,[33,1225,618],{"class":196},[33,1227,1228],{"class":124},"-z",[33,1230,397],{"class":72},[33,1232,1233],{"class":196},"$cmd",[33,1235,403],{"class":72},[33,1237,1238],{"class":196}," ]] && { ",[33,1240,762],{"class":114},[33,1242,429],{"class":196},[33,1244,432],{"class":65},[33,1246,435],{"class":65},[33,1248,409],{"class":196},[33,1250,1252,1255,1257,1259,1261],{"class":35,"line":1251},105,[33,1253,1254],{"class":124},"    case",[33,1256,397],{"class":72},[33,1258,1233],{"class":196},[33,1260,403],{"class":72},[33,1262,1036],{"class":124},[33,1264,1266,1269,1271,1274,1276,1278,1280,1282,1284,1286],{"class":35,"line":1265},106,[33,1267,1268],{"class":1042},"        build",[33,1270,1052],{"class":124},[33,1272,1273],{"class":114},"  shift_positionals",[33,1275,435],{"class":65},[33,1277,429],{"class":196},[33,1279,889],{"class":114},[33,1281,397],{"class":72},[33,1283,972],{"class":65},[33,1285,403],{"class":72},[33,1287,1064],{"class":196},[33,1289,1291,1294,1296,1299,1301,1303,1305,1307,1309,1311],{"class":35,"line":1290},107,[33,1292,1293],{"class":1042},"        test",[33,1295,1052],{"class":124},[33,1297,1298],{"class":114},"   shift_positionals",[33,1300,435],{"class":65},[33,1302,429],{"class":196},[33,1304,921],{"class":114},[33,1306,397],{"class":72},[33,1308,972],{"class":65},[33,1310,403],{"class":72},[33,1312,1064],{"class":196},[33,1314,1316,1319,1322,1325,1327,1329],{"class":35,"line":1315},108,[33,1317,1318],{"class":124},"        *)",[33,1320,1321],{"class":114},"      die",[33,1323,1324],{"class":72}," \"unknown command: ",[33,1326,1233],{"class":196},[33,1328,403],{"class":72},[33,1330,1064],{"class":196},[33,1332,1334],{"class":35,"line":1333},109,[33,1335,1336],{"class":124},"    esac\n",[33,1338,1340],{"class":35,"line":1339},110,[33,1341,576],{"class":196},[33,1343,1345],{"class":35,"line":1344},111,[33,1346,53],{"emptyLinePlaceholder":52},[33,1348,1350,1353],{"class":35,"line":1349},112,[33,1351,1352],{"class":114},"shift_positionals",[33,1354,502],{"class":196},[33,1356,1358,1361,1363,1365],{"class":35,"line":1357},113,[33,1359,1360],{"class":65},"    shift",[33,1362,397],{"class":72},[33,1364,456],{"class":65},[33,1366,570],{"class":72},[33,1368,1370,1373,1375,1377,1379,1381,1384,1387,1390,1393,1395,1398],{"class":35,"line":1369},114,[33,1371,1372],{"class":196},"    positionals",[33,1374,200],{"class":124},[33,1376,1154],{"class":196},[33,1378,280],{"class":72},[33,1380,1212],{"class":196},[33,1382,1383],{"class":72},"[",[33,1385,1386],{"class":124},"@",[33,1388,1389],{"class":72},"]",[33,1391,1392],{"class":124},":",[33,1394,1085],{"class":196},[33,1396,1397],{"class":72},"}\"",[33,1399,218],{"class":196},[33,1401,1403],{"class":35,"line":1402},115,[33,1404,576],{"class":196},[33,1406,1408],{"class":35,"line":1407},116,[33,1409,53],{"emptyLinePlaceholder":52},[33,1411,1413],{"class":35,"line":1412},117,[33,1414,1415],{"class":39},"# ── 11. Entry point (only if executed, not sourced) ──\n",[33,1417,1419,1422,1425,1427,1429,1431,1434,1436,1439,1441,1443],{"class":35,"line":1418},118,[33,1420,1421],{"class":124},"if",[33,1423,1424],{"class":196}," [[ ",[33,1426,280],{"class":72},[33,1428,212],{"class":196},[33,1430,215],{"class":72},[33,1432,1433],{"class":124}," ==",[33,1435,397],{"class":72},[33,1437,1438],{"class":65},"${0}",[33,1440,403],{"class":72},[33,1442,1018],{"class":196},[33,1444,679],{"class":124},[33,1446,1448,1451,1453,1455],{"class":35,"line":1447},119,[33,1449,1450],{"class":114},"    main",[33,1452,397],{"class":72},[33,1454,972],{"class":65},[33,1456,570],{"class":72},[33,1458,1460],{"class":35,"line":1459},120,[33,1461,1462],{"class":124},"fi\n",[15,1464,1466,1469],{"id":1465},"script_dir-reliable-script-directory-resolution",[30,1467,1468],{},"SCRIPT_DIR",": Reliable Script Directory Resolution",[20,1471,1472],{"language":22},[24,1473,1475],{"className":26,"code":1474,"language":22,"meta":28,"style":28},"# ── Naive: $0 is unreliable ──\n# $0 is the path the script was invoked as:\n#   .\u002Fscript.sh        → $0 = \".\u002Fscript.sh\"\n#   bash script.sh     → $0 = \"script.sh\"\n#   \u002Fusr\u002Flocal\u002Fbin\u002Fscript  → $0 = \"\u002Fusr\u002Flocal\u002Fbin\u002Fscript\" (symlink → \u002Fopt\u002Fapp\u002Fscript)\n# When sourced: $0 = \"bash\" (the shell name)\n\n# ── Better: BASH_SOURCE[0] (works when sourced too) ──\nSCRIPT_DIR=$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\n# But: if BASH_SOURCE[0] is a symlink, this gives the SYMLINK's dir, not the real script's dir.\n\n# ── Full: resolve symlinks ──\nresolve_script_dir() {\n    local source=\"${BASH_SOURCE[0]}\"\n    # Follow symlinks until we reach the real file\n    while [[ -L \"$source\" ]]; do\n        local dir=$(dirname \"$source\")\n        source=$(readlink \"$source\")\n        [[ \"$source\" != \u002F* ]] && source=\"$dir\u002F$source\"  # resolve relative symlink\n    done\n    cd \"$(dirname \"$source\")\" && pwd\n}\nreadonly SCRIPT_DIR=$(resolve_script_dir)\n\n# ── Portable: use realpath if available ──\nif command -v realpath &>\u002Fdev\u002Fnull; then\n    readonly SCRIPT_DIR=$(dirname \"$(realpath \"${BASH_SOURCE[0]}\")\")\nelse\n    readonly SCRIPT_DIR=$(cd \"$(dirname \"${BASH_SOURCE[0]}\")\" && pwd)\nfi\n\n# ── Use SCRIPT_DIR for relative paths ──\nsource \"$SCRIPT_DIR\u002Flib\u002Futils.sh\"\nconfig_file=\"$SCRIPT_DIR\u002Fconfig\u002Fapp.env\"\n",[30,1476,1477,1482,1487,1492,1497,1502,1507,1511,1516,1542,1547,1551,1556,1563,1579,1584,1604,1626,1646,1684,1688,1709,1713,1727,1731,1736,1757,1783,1788,1816,1820,1824,1829,1842],{"__ignoreMap":28},[33,1478,1479],{"class":35,"line":36},[33,1480,1481],{"class":39},"# ── Naive: $0 is unreliable ──\n",[33,1483,1484],{"class":35,"line":43},[33,1485,1486],{"class":39},"# $0 is the path the script was invoked as:\n",[33,1488,1489],{"class":35,"line":49},[33,1490,1491],{"class":39},"#   .\u002Fscript.sh        → $0 = \".\u002Fscript.sh\"\n",[33,1493,1494],{"class":35,"line":56},[33,1495,1496],{"class":39},"#   bash script.sh     → $0 = \"script.sh\"\n",[33,1498,1499],{"class":35,"line":62},[33,1500,1501],{"class":39},"#   \u002Fusr\u002Flocal\u002Fbin\u002Fscript  → $0 = \"\u002Fusr\u002Flocal\u002Fbin\u002Fscript\" (symlink → \u002Fopt\u002Fapp\u002Fscript)\n",[33,1503,1504],{"class":35,"line":76},[33,1505,1506],{"class":39},"# When sourced: $0 = \"bash\" (the shell name)\n",[33,1508,1509],{"class":35,"line":82},[33,1510,53],{"emptyLinePlaceholder":52},[33,1512,1513],{"class":35,"line":88},[33,1514,1515],{"class":39},"# ── Better: BASH_SOURCE[0] (works when sourced too) ──\n",[33,1517,1518,1520,1522,1524,1526,1528,1530,1532,1534,1536,1538,1540],{"class":35,"line":94},[33,1519,1468],{"class":196},[33,1521,200],{"class":124},[33,1523,203],{"class":196},[33,1525,233],{"class":65},[33,1527,236],{"class":72},[33,1529,239],{"class":114},[33,1531,209],{"class":72},[33,1533,212],{"class":196},[33,1535,246],{"class":72},[33,1537,249],{"class":196},[33,1539,252],{"class":65},[33,1541,218],{"class":196},[33,1543,1544],{"class":35,"line":100},[33,1545,1546],{"class":39},"# But: if BASH_SOURCE[0] is a symlink, this gives the SYMLINK's dir, not the real script's dir.\n",[33,1548,1549],{"class":35,"line":105},[33,1550,53],{"emptyLinePlaceholder":52},[33,1552,1553],{"class":35,"line":111},[33,1554,1555],{"class":39},"# ── Full: resolve symlinks ──\n",[33,1557,1558,1561],{"class":35,"line":140},[33,1559,1560],{"class":114},"resolve_script_dir",[33,1562,502],{"class":196},[33,1564,1565,1567,1570,1572,1574,1576],{"class":35,"line":153},[33,1566,508],{"class":124},[33,1568,1569],{"class":196}," source",[33,1571,200],{"class":124},[33,1573,280],{"class":72},[33,1575,212],{"class":196},[33,1577,1578],{"class":72},"[0]}\"\n",[33,1580,1581],{"class":35,"line":166},[33,1582,1583],{"class":39},"    # Follow symlinks until we reach the real file\n",[33,1585,1586,1588,1590,1593,1595,1598,1600,1602],{"class":35,"line":179},[33,1587,1004],{"class":124},[33,1589,1424],{"class":196},[33,1591,1592],{"class":124},"-L",[33,1594,397],{"class":72},[33,1596,1597],{"class":196},"$source",[33,1599,403],{"class":72},[33,1601,1018],{"class":196},[33,1603,1021],{"class":124},[33,1605,1606,1609,1612,1614,1616,1618,1620,1622,1624],{"class":35,"line":184},[33,1607,1608],{"class":124},"        local",[33,1610,1611],{"class":196}," dir",[33,1613,200],{"class":124},[33,1615,203],{"class":196},[33,1617,239],{"class":114},[33,1619,397],{"class":72},[33,1621,1597],{"class":196},[33,1623,403],{"class":72},[33,1625,218],{"class":196},[33,1627,1628,1631,1633,1635,1638,1640,1642,1644],{"class":35,"line":190},[33,1629,1630],{"class":196},"        source",[33,1632,200],{"class":124},[33,1634,203],{"class":196},[33,1636,1637],{"class":114},"readlink",[33,1639,397],{"class":72},[33,1641,1597],{"class":196},[33,1643,403],{"class":72},[33,1645,218],{"class":196},[33,1647,1648,1651,1653,1655,1657,1660,1663,1665,1668,1670,1672,1675,1677,1679,1681],{"class":35,"line":221},[33,1649,1650],{"class":196},"        [[ ",[33,1652,403],{"class":72},[33,1654,1597],{"class":196},[33,1656,403],{"class":72},[33,1658,1659],{"class":124}," !=",[33,1661,1662],{"class":196}," \u002F",[33,1664,1127],{"class":124},[33,1666,1667],{"class":196}," ]] && source",[33,1669,200],{"class":124},[33,1671,403],{"class":72},[33,1673,1674],{"class":196},"$dir",[33,1676,292],{"class":72},[33,1678,1597],{"class":196},[33,1680,403],{"class":72},[33,1682,1683],{"class":39},"  # resolve relative symlink\n",[33,1685,1686],{"class":35,"line":257},[33,1687,1179],{"class":124},[33,1689,1690,1693,1695,1697,1699,1701,1704,1706],{"class":35,"line":270},[33,1691,1692],{"class":65},"    cd",[33,1694,236],{"class":72},[33,1696,239],{"class":114},[33,1698,397],{"class":72},[33,1700,1597],{"class":196},[33,1702,1703],{"class":72},"\")\"",[33,1705,249],{"class":196},[33,1707,1708],{"class":65},"pwd\n",[33,1710,1711],{"class":35,"line":311},[33,1712,576],{"class":196},[33,1714,1715,1717,1719,1721,1723,1725],{"class":35,"line":316},[33,1716,193],{"class":124},[33,1718,226],{"class":196},[33,1720,200],{"class":124},[33,1722,203],{"class":196},[33,1724,1560],{"class":114},[33,1726,218],{"class":196},[33,1728,1729],{"class":35,"line":322},[33,1730,53],{"emptyLinePlaceholder":52},[33,1732,1733],{"class":35,"line":334},[33,1734,1735],{"class":39},"# ── Portable: use realpath if available ──\n",[33,1737,1738,1740,1743,1745,1748,1750,1752,1755],{"class":35,"line":350},[33,1739,1421],{"class":124},[33,1741,1742],{"class":65}," command",[33,1744,451],{"class":65},[33,1746,1747],{"class":72}," realpath",[33,1749,461],{"class":196},[33,1751,464],{"class":124},[33,1753,1754],{"class":196},"\u002Fdev\u002Fnull; ",[33,1756,679],{"class":124},[33,1758,1759,1762,1764,1766,1768,1770,1772,1775,1777,1779,1781],{"class":35,"line":361},[33,1760,1761],{"class":124},"    readonly",[33,1763,226],{"class":196},[33,1765,200],{"class":124},[33,1767,203],{"class":196},[33,1769,239],{"class":114},[33,1771,236],{"class":72},[33,1773,1774],{"class":114},"realpath",[33,1776,209],{"class":72},[33,1778,212],{"class":196},[33,1780,246],{"class":72},[33,1782,218],{"class":196},[33,1784,1785],{"class":35,"line":366},[33,1786,1787],{"class":124},"else\n",[33,1789,1790,1792,1794,1796,1798,1800,1802,1804,1806,1808,1810,1812,1814],{"class":35,"line":372},[33,1791,1761],{"class":124},[33,1793,226],{"class":196},[33,1795,200],{"class":124},[33,1797,203],{"class":196},[33,1799,233],{"class":65},[33,1801,236],{"class":72},[33,1803,239],{"class":114},[33,1805,209],{"class":72},[33,1807,212],{"class":196},[33,1809,246],{"class":72},[33,1811,249],{"class":196},[33,1813,252],{"class":65},[33,1815,218],{"class":196},[33,1817,1818],{"class":35,"line":412},[33,1819,1462],{"class":124},[33,1821,1822],{"class":35,"line":440},[33,1823,53],{"emptyLinePlaceholder":52},[33,1825,1826],{"class":35,"line":485},[33,1827,1828],{"class":39},"# ── Use SCRIPT_DIR for relative paths ──\n",[33,1830,1831,1834,1836,1839],{"class":35,"line":490},[33,1832,1833],{"class":65},"source",[33,1835,397],{"class":72},[33,1837,1838],{"class":196},"$SCRIPT_DIR",[33,1840,1841],{"class":72},"\u002Flib\u002Futils.sh\"\n",[33,1843,1844,1847,1849,1851,1853],{"class":35,"line":496},[33,1845,1846],{"class":196},"config_file",[33,1848,200],{"class":124},[33,1850,403],{"class":72},[33,1852,1838],{"class":196},[33,1854,1855],{"class":72},"\u002Fconfig\u002Fapp.env\"\n",[15,1857,1859,1860,1863],{"id":1858},"anti-pattern-0-for-script-directory","Anti-Pattern: ",[30,1861,1862],{},"$0"," for Script Directory",[20,1865,1866],{"language":22},[24,1867,1869],{"className":26,"code":1868,"language":22,"meta":28,"style":28},"# ❌ NAIVE — breaks with symlinks and sourcing\nSCRIPT_DIR=$(cd \"$(dirname \"$0\")\" && pwd)\n# If $0 is a symlink (\u002Fusr\u002Flocal\u002Fbin\u002Fapp → \u002Fopt\u002Fapp\u002Fbin\u002Fscript):\n#   dirname \"$0\" = \u002Fusr\u002Flocal\u002Fbin (the symlink's dir, NOT the real dir)\n#   Resources in \u002Fopt\u002Fapp\u002F (lib\u002F, config\u002F) won't be found!\n\n# ✅ CORRECT — BASH_SOURCE[0] + symlink resolution\nSCRIPT_DIR=$(cd \"$(dirname \"$(realpath \"${BASH_SOURCE[0]}\")\")\" && pwd)\n# realpath resolves all symlinks → \u002Fopt\u002Fapp\u002Fbin\n# dirname → \u002Fopt\u002Fapp\u002Fbin (the real script's directory)\n",[30,1870,1871,1876,1902,1907,1912,1917,1921,1926,1957,1962],{"__ignoreMap":28},[33,1872,1873],{"class":35,"line":36},[33,1874,1875],{"class":39},"# ❌ NAIVE — breaks with symlinks and sourcing\n",[33,1877,1878,1880,1882,1884,1886,1888,1890,1892,1894,1896,1898,1900],{"class":35,"line":43},[33,1879,1468],{"class":196},[33,1881,200],{"class":124},[33,1883,203],{"class":196},[33,1885,233],{"class":65},[33,1887,236],{"class":72},[33,1889,239],{"class":114},[33,1891,397],{"class":72},[33,1893,1862],{"class":65},[33,1895,1703],{"class":72},[33,1897,249],{"class":196},[33,1899,252],{"class":65},[33,1901,218],{"class":196},[33,1903,1904],{"class":35,"line":49},[33,1905,1906],{"class":39},"# If $0 is a symlink (\u002Fusr\u002Flocal\u002Fbin\u002Fapp → \u002Fopt\u002Fapp\u002Fbin\u002Fscript):\n",[33,1908,1909],{"class":35,"line":56},[33,1910,1911],{"class":39},"#   dirname \"$0\" = \u002Fusr\u002Flocal\u002Fbin (the symlink's dir, NOT the real dir)\n",[33,1913,1914],{"class":35,"line":62},[33,1915,1916],{"class":39},"#   Resources in \u002Fopt\u002Fapp\u002F (lib\u002F, config\u002F) won't be found!\n",[33,1918,1919],{"class":35,"line":76},[33,1920,53],{"emptyLinePlaceholder":52},[33,1922,1923],{"class":35,"line":82},[33,1924,1925],{"class":39},"# ✅ CORRECT — BASH_SOURCE[0] + symlink resolution\n",[33,1927,1928,1930,1932,1934,1936,1938,1940,1942,1944,1946,1948,1951,1953,1955],{"class":35,"line":88},[33,1929,1468],{"class":196},[33,1931,200],{"class":124},[33,1933,203],{"class":196},[33,1935,233],{"class":65},[33,1937,236],{"class":72},[33,1939,239],{"class":114},[33,1941,236],{"class":72},[33,1943,1774],{"class":114},[33,1945,209],{"class":72},[33,1947,212],{"class":196},[33,1949,1950],{"class":72},"[0]}\")\")\"",[33,1952,249],{"class":196},[33,1954,252],{"class":65},[33,1956,218],{"class":196},[33,1958,1959],{"class":35,"line":94},[33,1960,1961],{"class":39},"# realpath resolves all symlinks → \u002Fopt\u002Fapp\u002Fbin\n",[33,1963,1964],{"class":35,"line":100},[33,1965,1966],{"class":39},"# dirname → \u002Fopt\u002Fapp\u002Fbin (the real script's directory)\n",[15,1968,1970],{"id":1969},"sourcing-libraries-with-guards","Sourcing Libraries with Guards",[20,1972,1973],{"language":22},[24,1974,1976],{"className":26,"code":1975,"language":22,"meta":28,"style":28},"# ── lib\u002Futils.sh: a reusable library ──\n# Guard against re-sourcing (prevents redefining functions):\n[[ -n \"${_UTILS_SH_SOURCED:-}\" ]] && return 0\n_UTILS_SH_SOURCED=1\n\n# Library functions:\nlog() { printf '[%s] %s\\n' \"$(date -Iseconds)\" \"$*\" >&2; }\n\ndie() { log \"FATAL: $*\"; exit 1; }\n# ⚠️ die uses `exit` — but if this file is SOURCED, exit closes the caller's shell!\n# In a library, use `return` for \"stop this file\" and let the caller decide:\ndie_lib() { log \"FATAL: $*\"; return 1; }\n# The caller: source utils.sh; die_lib \"error\" || exit 1\n\nrequire() { command -v \"$1\" &>\u002Fdev\u002Fnull || die \"missing: $1\"; }\n\n# ── source the library ──\nsource \"$SCRIPT_DIR\u002Flib\u002Futils.sh\"\nlog \"library loaded\"\n",[30,1977,1978,1983,1988,2012,2021,2025,2030,2058,2062,2084,2089,2094,2117,2122,2126,2160,2164,2169,2179],{"__ignoreMap":28},[33,1979,1980],{"class":35,"line":36},[33,1981,1982],{"class":39},"# ── lib\u002Futils.sh: a reusable library ──\n",[33,1984,1985],{"class":35,"line":43},[33,1986,1987],{"class":39},"# Guard against re-sourcing (prevents redefining functions):\n",[33,1989,1990,1993,1995,1997,2000,2002,2004,2006,2009],{"class":35,"line":49},[33,1991,1992],{"class":196},"[[ ",[33,1994,621],{"class":124},[33,1996,209],{"class":72},[33,1998,1999],{"class":196},"_UTILS_SH_SOURCED",[33,2001,1218],{"class":124},[33,2003,1397],{"class":72},[33,2005,643],{"class":196},[33,2007,2008],{"class":124},"return",[33,2010,2011],{"class":65}," 0\n",[33,2013,2014,2016,2018],{"class":35,"line":56},[33,2015,1999],{"class":196},[33,2017,200],{"class":124},[33,2019,2020],{"class":72},"1\n",[33,2022,2023],{"class":35,"line":62},[33,2024,53],{"emptyLinePlaceholder":52},[33,2026,2027],{"class":35,"line":76},[33,2028,2029],{"class":39},"# Library functions:\n",[33,2031,2032,2034,2036,2038,2040,2042,2044,2046,2048,2050,2052,2054,2056],{"class":35,"line":82},[33,2033,295],{"class":114},[33,2035,377],{"class":196},[33,2037,380],{"class":65},[33,2039,383],{"class":72},[33,2041,236],{"class":72},[33,2043,388],{"class":114},[33,2045,391],{"class":65},[33,2047,394],{"class":72},[33,2049,397],{"class":72},[33,2051,400],{"class":65},[33,2053,403],{"class":72},[33,2055,406],{"class":124},[33,2057,409],{"class":196},[33,2059,2060],{"class":35,"line":88},[33,2061,53],{"emptyLinePlaceholder":52},[33,2063,2064,2066,2068,2070,2072,2074,2076,2078,2080,2082],{"class":35,"line":94},[33,2065,415],{"class":114},[33,2067,377],{"class":196},[33,2069,295],{"class":114},[33,2071,422],{"class":72},[33,2073,400],{"class":65},[33,2075,403],{"class":72},[33,2077,429],{"class":196},[33,2079,432],{"class":65},[33,2081,435],{"class":65},[33,2083,409],{"class":196},[33,2085,2086],{"class":35,"line":100},[33,2087,2088],{"class":39},"# ⚠️ die uses `exit` — but if this file is SOURCED, exit closes the caller's shell!\n",[33,2090,2091],{"class":35,"line":105},[33,2092,2093],{"class":39},"# In a library, use `return` for \"stop this file\" and let the caller decide:\n",[33,2095,2096,2099,2101,2103,2105,2107,2109,2111,2113,2115],{"class":35,"line":111},[33,2097,2098],{"class":114},"die_lib",[33,2100,377],{"class":196},[33,2102,295],{"class":114},[33,2104,422],{"class":72},[33,2106,400],{"class":65},[33,2108,403],{"class":72},[33,2110,429],{"class":196},[33,2112,2008],{"class":124},[33,2114,435],{"class":65},[33,2116,409],{"class":196},[33,2118,2119],{"class":35,"line":140},[33,2120,2121],{"class":39},"# The caller: source utils.sh; die_lib \"error\" || exit 1\n",[33,2123,2124],{"class":35,"line":153},[33,2125,53],{"emptyLinePlaceholder":52},[33,2127,2128,2130,2132,2134,2136,2138,2140,2142,2144,2146,2148,2150,2152,2154,2156,2158],{"class":35,"line":166},[33,2129,443],{"class":114},[33,2131,377],{"class":196},[33,2133,448],{"class":65},[33,2135,451],{"class":65},[33,2137,397],{"class":72},[33,2139,456],{"class":65},[33,2141,403],{"class":72},[33,2143,461],{"class":196},[33,2145,464],{"class":124},[33,2147,467],{"class":196},[33,2149,470],{"class":124},[33,2151,473],{"class":114},[33,2153,476],{"class":72},[33,2155,456],{"class":65},[33,2157,403],{"class":72},[33,2159,409],{"class":196},[33,2161,2162],{"class":35,"line":179},[33,2163,53],{"emptyLinePlaceholder":52},[33,2165,2166],{"class":35,"line":184},[33,2167,2168],{"class":39},"# ── source the library ──\n",[33,2170,2171,2173,2175,2177],{"class":35,"line":190},[33,2172,1833],{"class":65},[33,2174,397],{"class":72},[33,2176,1838],{"class":196},[33,2178,1841],{"class":72},[33,2180,2181,2183],{"class":35,"line":221},[33,2182,295],{"class":114},[33,2184,2185],{"class":72}," \"library loaded\"\n",[15,2187,2189,2191,2192,2195],{"id":2188},"readonly-and-local-immutability-and-scope",[30,2190,193],{}," and ",[30,2193,2194],{},"local",": Immutability and Scope",[20,2197,2198],{"language":22},[24,2199,2201],{"className":26,"code":2200,"language":22,"meta":28,"style":28},"# ── readonly: constants (can't be reassigned) ──\nreadonly PI=3.14159\nreadonly CONFIG_DIR=\"\u002Fetc\u002Fmyapp\"\nreadonly VERSION=$(cat \"$SCRIPT_DIR\u002FVERSION\" 2>\u002Fdev\u002Fnull || echo \"unknown\")\n\n# PI=3   # ✗ bash: PI: readonly variable (fatal with set -u)\n# readonly is global — survives function calls (unlike local)\n\n# ── local: function-scoped variables ──\nprocess() {\n    local count=0      # local to process() — doesn't leak to caller\n    local result=\"\"    # each call gets a fresh copy\n    # ...\n}\n\n# ── readonly + local: constant within a function ──\nparse() {\n    local -r max_retries=3   # local AND readonly (can't reassign within function)\n    for ((i = 0; i \u003C max_retries; i++)); do\n        # ...\n    done\n    # max_retries=5  # ✗ bash: max_retries: readonly variable\n}\n\n# ── Always use local for function variables ──\n# Without local: function variables are GLOBAL — pollute scope, clobber existing vars\n# With local: function-scoped — clean, no side effects on caller\n",[30,2202,2203,2208,2220,2232,2265,2269,2274,2279,2283,2288,2295,2310,2325,2330,2334,2338,2343,2350,2368,2396,2401,2405,2410,2414,2418,2423,2428],{"__ignoreMap":28},[33,2204,2205],{"class":35,"line":36},[33,2206,2207],{"class":39},"# ── readonly: constants (can't be reassigned) ──\n",[33,2209,2210,2212,2215,2217],{"class":35,"line":43},[33,2211,193],{"class":124},[33,2213,2214],{"class":196}," PI",[33,2216,200],{"class":124},[33,2218,2219],{"class":65},"3.14159\n",[33,2221,2222,2224,2227,2229],{"class":35,"line":49},[33,2223,193],{"class":124},[33,2225,2226],{"class":196}," CONFIG_DIR",[33,2228,200],{"class":124},[33,2230,2231],{"class":72},"\"\u002Fetc\u002Fmyapp\"\n",[33,2233,2234,2236,2238,2240,2242,2245,2247,2249,2252,2254,2256,2258,2260,2263],{"class":35,"line":56},[33,2235,193],{"class":124},[33,2237,262],{"class":196},[33,2239,200],{"class":124},[33,2241,203],{"class":196},[33,2243,2244],{"class":114},"cat",[33,2246,397],{"class":72},[33,2248,1838],{"class":196},[33,2250,2251],{"class":72},"\u002FVERSION\"",[33,2253,125],{"class":124},[33,2255,128],{"class":72},[33,2257,131],{"class":124},[33,2259,1103],{"class":65},[33,2261,2262],{"class":72}," \"unknown\"",[33,2264,218],{"class":196},[33,2266,2267],{"class":35,"line":62},[33,2268,53],{"emptyLinePlaceholder":52},[33,2270,2271],{"class":35,"line":76},[33,2272,2273],{"class":39},"# PI=3   # ✗ bash: PI: readonly variable (fatal with set -u)\n",[33,2275,2276],{"class":35,"line":82},[33,2277,2278],{"class":39},"# readonly is global — survives function calls (unlike local)\n",[33,2280,2281],{"class":35,"line":88},[33,2282,53],{"emptyLinePlaceholder":52},[33,2284,2285],{"class":35,"line":94},[33,2286,2287],{"class":39},"# ── local: function-scoped variables ──\n",[33,2289,2290,2293],{"class":35,"line":100},[33,2291,2292],{"class":114},"process",[33,2294,502],{"class":196},[33,2296,2297,2299,2302,2304,2307],{"class":35,"line":105},[33,2298,508],{"class":124},[33,2300,2301],{"class":196}," count",[33,2303,200],{"class":124},[33,2305,2306],{"class":65},"0",[33,2308,2309],{"class":39},"      # local to process() — doesn't leak to caller\n",[33,2311,2312,2314,2317,2319,2322],{"class":35,"line":111},[33,2313,508],{"class":124},[33,2315,2316],{"class":196}," result",[33,2318,200],{"class":124},[33,2320,2321],{"class":72},"\"\"",[33,2323,2324],{"class":39},"    # each call gets a fresh copy\n",[33,2326,2327],{"class":35,"line":140},[33,2328,2329],{"class":39},"    # ...\n",[33,2331,2332],{"class":35,"line":153},[33,2333,576],{"class":196},[33,2335,2336],{"class":35,"line":166},[33,2337,53],{"emptyLinePlaceholder":52},[33,2339,2340],{"class":35,"line":179},[33,2341,2342],{"class":39},"# ── readonly + local: constant within a function ──\n",[33,2344,2345,2348],{"class":35,"line":184},[33,2346,2347],{"class":114},"parse",[33,2349,502],{"class":196},[33,2351,2352,2354,2357,2360,2362,2365],{"class":35,"line":190},[33,2353,508],{"class":124},[33,2355,2356],{"class":65}," -r",[33,2358,2359],{"class":196}," max_retries",[33,2361,200],{"class":124},[33,2363,2364],{"class":65},"3",[33,2366,2367],{"class":39},"   # local AND readonly (can't reassign within function)\n",[33,2369,2370,2373,2376,2378,2380,2383,2386,2389,2392,2394],{"class":35,"line":221},[33,2371,2372],{"class":124},"    for",[33,2374,2375],{"class":196}," ((i ",[33,2377,200],{"class":124},[33,2379,673],{"class":65},[33,2381,2382],{"class":196},"; i ",[33,2384,2385],{"class":124},"\u003C",[33,2387,2388],{"class":196}," max_retries; i",[33,2390,2391],{"class":124},"++",[33,2393,676],{"class":196},[33,2395,1021],{"class":124},[33,2397,2398],{"class":35,"line":257},[33,2399,2400],{"class":39},"        # ...\n",[33,2402,2403],{"class":35,"line":270},[33,2404,1179],{"class":124},[33,2406,2407],{"class":35,"line":311},[33,2408,2409],{"class":39},"    # max_retries=5  # ✗ bash: max_retries: readonly variable\n",[33,2411,2412],{"class":35,"line":316},[33,2413,576],{"class":196},[33,2415,2416],{"class":35,"line":322},[33,2417,53],{"emptyLinePlaceholder":52},[33,2419,2420],{"class":35,"line":334},[33,2421,2422],{"class":39},"# ── Always use local for function variables ──\n",[33,2424,2425],{"class":35,"line":350},[33,2426,2427],{"class":39},"# Without local: function variables are GLOBAL — pollute scope, clobber existing vars\n",[33,2429,2430],{"class":35,"line":361},[33,2431,2432],{"class":39},"# With local: function-scoped — clean, no side effects on caller\n",[15,2434,1859,2436,2439],{"id":2435},"anti-pattern-eval-on-untrusted-input",[30,2437,2438],{},"eval"," on Untrusted Input",[20,2441,2442],{"language":22},[24,2443,2445],{"className":26,"code":2444,"language":22,"meta":28,"style":28},"# ❌ DANGEROUS — eval runs a string as a command (code injection)\nuser_input='$(rm -rf \u002F)'\neval \"echo $user_input\"    # runs `rm -rf \u002F` !!!!\n\n# ❌ Also dangerous — indirect via eval\nvar_name=\"user_input; rm -rf \u002F\"\neval \"echo \\$$var_name\"    # command injection via the variable name!\n\n# ✅ SAFE — indirect expansion ${!var} (no eval)\nvar_name=\"HOME\"\necho \"${!var_name}\"        # \u002Fhome\u002Fuser — expands the variable whose NAME is in var_name\n# ${!var} is safe: it only expands a VARIABLE, doesn't run commands.\n\n# ✅ SAFE — nameref (Bash 4.3+)\nset_value() {\n    local -n _ref=$1   # nameref — safe, no eval\n    _ref=\"$2\"\n}\nset_value my_var \"hello\"\necho \"$my_var\"   # hello\n\n# ✅ SAFE — arrays for data, not code\n# Don't store commands as strings and eval them — store as array elements:\ncmd=(docker run --rm -v \"$PWD:\u002Fapp\" myapp)\n\"${cmd[@]}\"    # executes the array as a command — no eval, no word-splitting issues\n\n# ── When eval is (rarely) needed ──\n# To expand a variable containing a variable name (use ${!var} instead)\n# To build a command dynamically (use arrays instead)\n# To run code from a trusted source (config file you control) — still risky\n",[30,2446,2447,2452,2462,2477,2481,2486,2496,2513,2517,2522,2531,2548,2553,2557,2562,2569,2586,2600,2604,2614,2628,2632,2637,2642,2675,2691,2695,2700,2705,2710],{"__ignoreMap":28},[33,2448,2449],{"class":35,"line":36},[33,2450,2451],{"class":39},"# ❌ DANGEROUS — eval runs a string as a command (code injection)\n",[33,2453,2454,2457,2459],{"class":35,"line":43},[33,2455,2456],{"class":196},"user_input",[33,2458,200],{"class":124},[33,2460,2461],{"class":72},"'$(rm -rf \u002F)'\n",[33,2463,2464,2466,2469,2472,2474],{"class":35,"line":49},[33,2465,2438],{"class":65},[33,2467,2468],{"class":72}," \"echo ",[33,2470,2471],{"class":196},"$user_input",[33,2473,403],{"class":72},[33,2475,2476],{"class":39},"    # runs `rm -rf \u002F` !!!!\n",[33,2478,2479],{"class":35,"line":56},[33,2480,53],{"emptyLinePlaceholder":52},[33,2482,2483],{"class":35,"line":62},[33,2484,2485],{"class":39},"# ❌ Also dangerous — indirect via eval\n",[33,2487,2488,2491,2493],{"class":35,"line":76},[33,2489,2490],{"class":196},"var_name",[33,2492,200],{"class":124},[33,2494,2495],{"class":72},"\"user_input; rm -rf \u002F\"\n",[33,2497,2498,2500,2502,2505,2508,2510],{"class":35,"line":82},[33,2499,2438],{"class":65},[33,2501,2468],{"class":72},[33,2503,2504],{"class":65},"\\$",[33,2506,2507],{"class":196},"$var_name",[33,2509,403],{"class":72},[33,2511,2512],{"class":39},"    # command injection via the variable name!\n",[33,2514,2515],{"class":35,"line":88},[33,2516,53],{"emptyLinePlaceholder":52},[33,2518,2519],{"class":35,"line":94},[33,2520,2521],{"class":39},"# ✅ SAFE — indirect expansion ${!var} (no eval)\n",[33,2523,2524,2526,2528],{"class":35,"line":100},[33,2525,2490],{"class":196},[33,2527,200],{"class":124},[33,2529,2530],{"class":72},"\"HOME\"\n",[33,2532,2533,2536,2538,2541,2543,2545],{"class":35,"line":105},[33,2534,2535],{"class":65},"echo",[33,2537,209],{"class":72},[33,2539,2540],{"class":124},"!",[33,2542,2490],{"class":196},[33,2544,1397],{"class":72},[33,2546,2547],{"class":39},"        # \u002Fhome\u002Fuser — expands the variable whose NAME is in var_name\n",[33,2549,2550],{"class":35,"line":111},[33,2551,2552],{"class":39},"# ${!var} is safe: it only expands a VARIABLE, doesn't run commands.\n",[33,2554,2555],{"class":35,"line":140},[33,2556,53],{"emptyLinePlaceholder":52},[33,2558,2559],{"class":35,"line":153},[33,2560,2561],{"class":39},"# ✅ SAFE — nameref (Bash 4.3+)\n",[33,2563,2564,2567],{"class":35,"line":166},[33,2565,2566],{"class":114},"set_value",[33,2568,502],{"class":196},[33,2570,2571,2573,2576,2579,2581,2583],{"class":35,"line":179},[33,2572,508],{"class":124},[33,2574,2575],{"class":65}," -n",[33,2577,2578],{"class":196}," _ref",[33,2580,200],{"class":124},[33,2582,456],{"class":529},[33,2584,2585],{"class":39},"   # nameref — safe, no eval\n",[33,2587,2588,2591,2593,2595,2598],{"class":35,"line":184},[33,2589,2590],{"class":196},"    _ref",[33,2592,200],{"class":124},[33,2594,403],{"class":72},[33,2596,2597],{"class":65},"$2",[33,2599,570],{"class":72},[33,2601,2602],{"class":35,"line":190},[33,2603,576],{"class":196},[33,2605,2606,2608,2611],{"class":35,"line":221},[33,2607,2566],{"class":114},[33,2609,2610],{"class":72}," my_var",[33,2612,2613],{"class":72}," \"hello\"\n",[33,2615,2616,2618,2620,2623,2625],{"class":35,"line":257},[33,2617,2535],{"class":65},[33,2619,397],{"class":72},[33,2621,2622],{"class":196},"$my_var",[33,2624,403],{"class":72},[33,2626,2627],{"class":39},"   # hello\n",[33,2629,2630],{"class":35,"line":270},[33,2631,53],{"emptyLinePlaceholder":52},[33,2633,2634],{"class":35,"line":311},[33,2635,2636],{"class":39},"# ✅ SAFE — arrays for data, not code\n",[33,2638,2639],{"class":35,"line":316},[33,2640,2641],{"class":39},"# Don't store commands as strings and eval them — store as array elements:\n",[33,2643,2644,2647,2649,2651,2654,2657,2660,2662,2664,2667,2670,2673],{"class":35,"line":322},[33,2645,2646],{"class":196},"cmd",[33,2648,200],{"class":124},[33,2650,1154],{"class":196},[33,2652,2653],{"class":72},"docker",[33,2655,2656],{"class":72}," run",[33,2658,2659],{"class":72}," --rm",[33,2661,451],{"class":72},[33,2663,397],{"class":72},[33,2665,2666],{"class":196},"$PWD",[33,2668,2669],{"class":72},":\u002Fapp\"",[33,2671,2672],{"class":72}," myapp",[33,2674,218],{"class":196},[33,2676,2677,2679,2681,2683,2685,2688],{"class":35,"line":334},[33,2678,280],{"class":114},[33,2680,2646],{"class":196},[33,2682,1383],{"class":114},[33,2684,1386],{"class":124},[33,2686,2687],{"class":114},"]}\"",[33,2689,2690],{"class":39},"    # executes the array as a command — no eval, no word-splitting issues\n",[33,2692,2693],{"class":35,"line":350},[33,2694,53],{"emptyLinePlaceholder":52},[33,2696,2697],{"class":35,"line":361},[33,2698,2699],{"class":39},"# ── When eval is (rarely) needed ──\n",[33,2701,2702],{"class":35,"line":366},[33,2703,2704],{"class":39},"# To expand a variable containing a variable name (use ${!var} instead)\n",[33,2706,2707],{"class":35,"line":372},[33,2708,2709],{"class":39},"# To build a command dynamically (use arrays instead)\n",[33,2711,2712],{"class":35,"line":412},[33,2713,2714],{"class":39},"# To run code from a trusted source (config file you control) — still risky\n",[15,2716,2718],{"id":2717},"idempotent-scripts","Idempotent Scripts",[20,2720,2721],{"language":22},[24,2722,2724],{"className":26,"code":2723,"language":22,"meta":28,"style":28},"# ── Idempotent: safe to run multiple times (same result) ──\n\n# ── Idempotent directory creation ──\nmkdir -p \"$dir\"          # -p: no error if exists, creates parents\n# ❌ mkdir \"$dir\"         # errors if exists (not idempotent)\n\n# ── Idempotent file creation ──\n[[ -e \"$file\" ]] || touch \"$file\"   # create only if doesn't exist\n# Or: touch \"$file\"  (touch is idempotent — updates timestamp if exists)\n\n# ── Idempotent symlink ──\nln -sf \"$target\" \"$link\"   # -f: force — replaces existing link (idempotent)\n\n# ── Idempotent config write (atomic) ──\nwrite_config() {\n    local content=\"$1\"\n    local file=\"$2\"\n    # Write to temp, then atomically rename (crash-safe + idempotent)\n    local tmp=$(mktemp \"$file.XXXXXX\")\n    printf '%s\\n' \"$content\" > \"$tmp\"\n    mv \"$tmp\" \"$file\"   # atomic rename — readers never see partial state\n}\n\n# ── Idempotent database migration ──\nmigrate() {\n    local version_file=\"$DB_DIR\u002F.version\"\n    local current_version=0\n    [[ -f \"$version_file\" ]] && current_version=$(\u003C\"$version_file\")\n\n    if ((current_version \u003C 1)); then\n        # run migration 1\n        echo \"running migration 1...\"\n    fi\n    if ((current_version \u003C 2)); then\n        # run migration 2\n        echo \"running migration 2...\"\n    fi\n    echo \"2\" > \"$version_file\"   # update version marker\n}\n# Running twice: first run does migrations 1 and 2, second run does nothing (already at 2).\n",[30,2725,2726,2731,2735,2740,2757,2762,2766,2771,2802,2807,2811,2816,2841,2845,2850,2857,2872,2887,2892,2915,2940,2960,2964,2968,2973,2980,2997,3009,3040,3044,3059,3064,3072,3076,3091,3096,3103,3107,3126,3130],{"__ignoreMap":28},[33,2727,2728],{"class":35,"line":36},[33,2729,2730],{"class":39},"# ── Idempotent: safe to run multiple times (same result) ──\n",[33,2732,2733],{"class":35,"line":43},[33,2734,53],{"emptyLinePlaceholder":52},[33,2736,2737],{"class":35,"line":49},[33,2738,2739],{"class":39},"# ── Idempotent directory creation ──\n",[33,2741,2742,2745,2748,2750,2752,2754],{"class":35,"line":56},[33,2743,2744],{"class":114},"mkdir",[33,2746,2747],{"class":65}," -p",[33,2749,397],{"class":72},[33,2751,1674],{"class":196},[33,2753,403],{"class":72},[33,2755,2756],{"class":39},"          # -p: no error if exists, creates parents\n",[33,2758,2759],{"class":35,"line":62},[33,2760,2761],{"class":39},"# ❌ mkdir \"$dir\"         # errors if exists (not idempotent)\n",[33,2763,2764],{"class":35,"line":76},[33,2765,53],{"emptyLinePlaceholder":52},[33,2767,2768],{"class":35,"line":82},[33,2769,2770],{"class":39},"# ── Idempotent file creation ──\n",[33,2772,2773,2775,2778,2780,2783,2785,2788,2790,2793,2795,2797,2799],{"class":35,"line":88},[33,2774,1992],{"class":196},[33,2776,2777],{"class":124},"-e",[33,2779,397],{"class":72},[33,2781,2782],{"class":196},"$file",[33,2784,403],{"class":72},[33,2786,2787],{"class":196}," ]] ",[33,2789,470],{"class":124},[33,2791,2792],{"class":114}," touch",[33,2794,397],{"class":72},[33,2796,2782],{"class":196},[33,2798,403],{"class":72},[33,2800,2801],{"class":39},"   # create only if doesn't exist\n",[33,2803,2804],{"class":35,"line":94},[33,2805,2806],{"class":39},"# Or: touch \"$file\"  (touch is idempotent — updates timestamp if exists)\n",[33,2808,2809],{"class":35,"line":100},[33,2810,53],{"emptyLinePlaceholder":52},[33,2812,2813],{"class":35,"line":105},[33,2814,2815],{"class":39},"# ── Idempotent symlink ──\n",[33,2817,2818,2821,2824,2826,2829,2831,2833,2836,2838],{"class":35,"line":111},[33,2819,2820],{"class":114},"ln",[33,2822,2823],{"class":65}," -sf",[33,2825,397],{"class":72},[33,2827,2828],{"class":196},"$target",[33,2830,403],{"class":72},[33,2832,397],{"class":72},[33,2834,2835],{"class":196},"$link",[33,2837,403],{"class":72},[33,2839,2840],{"class":39},"   # -f: force — replaces existing link (idempotent)\n",[33,2842,2843],{"class":35,"line":140},[33,2844,53],{"emptyLinePlaceholder":52},[33,2846,2847],{"class":35,"line":153},[33,2848,2849],{"class":39},"# ── Idempotent config write (atomic) ──\n",[33,2851,2852,2855],{"class":35,"line":166},[33,2853,2854],{"class":114},"write_config",[33,2856,502],{"class":196},[33,2858,2859,2861,2864,2866,2868,2870],{"class":35,"line":179},[33,2860,508],{"class":124},[33,2862,2863],{"class":196}," content",[33,2865,200],{"class":124},[33,2867,403],{"class":72},[33,2869,456],{"class":65},[33,2871,570],{"class":72},[33,2873,2874,2876,2879,2881,2883,2885],{"class":35,"line":184},[33,2875,508],{"class":124},[33,2877,2878],{"class":196}," file",[33,2880,200],{"class":124},[33,2882,403],{"class":72},[33,2884,2597],{"class":65},[33,2886,570],{"class":72},[33,2888,2889],{"class":35,"line":190},[33,2890,2891],{"class":39},"    # Write to temp, then atomically rename (crash-safe + idempotent)\n",[33,2893,2894,2896,2899,2901,2903,2906,2908,2910,2913],{"class":35,"line":221},[33,2895,508],{"class":124},[33,2897,2898],{"class":196}," tmp",[33,2900,200],{"class":124},[33,2902,203],{"class":196},[33,2904,2905],{"class":114},"mktemp",[33,2907,397],{"class":72},[33,2909,2782],{"class":196},[33,2911,2912],{"class":72},".XXXXXX\"",[33,2914,218],{"class":196},[33,2916,2917,2920,2923,2925,2928,2930,2933,2935,2938],{"class":35,"line":257},[33,2918,2919],{"class":65},"    printf",[33,2921,2922],{"class":72}," '%s\\n'",[33,2924,397],{"class":72},[33,2926,2927],{"class":196},"$content",[33,2929,403],{"class":72},[33,2931,2932],{"class":124}," >",[33,2934,397],{"class":72},[33,2936,2937],{"class":196},"$tmp",[33,2939,570],{"class":72},[33,2941,2942,2945,2947,2949,2951,2953,2955,2957],{"class":35,"line":270},[33,2943,2944],{"class":114},"    mv",[33,2946,397],{"class":72},[33,2948,2937],{"class":196},[33,2950,403],{"class":72},[33,2952,397],{"class":72},[33,2954,2782],{"class":196},[33,2956,403],{"class":72},[33,2958,2959],{"class":39},"   # atomic rename — readers never see partial state\n",[33,2961,2962],{"class":35,"line":311},[33,2963,576],{"class":196},[33,2965,2966],{"class":35,"line":316},[33,2967,53],{"emptyLinePlaceholder":52},[33,2969,2970],{"class":35,"line":322},[33,2971,2972],{"class":39},"# ── Idempotent database migration ──\n",[33,2974,2975,2978],{"class":35,"line":334},[33,2976,2977],{"class":114},"migrate",[33,2979,502],{"class":196},[33,2981,2982,2984,2987,2989,2991,2994],{"class":35,"line":350},[33,2983,508],{"class":124},[33,2985,2986],{"class":196}," version_file",[33,2988,200],{"class":124},[33,2990,403],{"class":72},[33,2992,2993],{"class":196},"$DB_DIR",[33,2995,2996],{"class":72},"\u002F.version\"\n",[33,2998,2999,3001,3004,3006],{"class":35,"line":361},[33,3000,508],{"class":124},[33,3002,3003],{"class":196}," current_version",[33,3005,200],{"class":124},[33,3007,3008],{"class":65},"0\n",[33,3010,3011,3013,3016,3018,3021,3023,3026,3028,3030,3032,3034,3036,3038],{"class":35,"line":366},[33,3012,618],{"class":196},[33,3014,3015],{"class":124},"-f",[33,3017,397],{"class":72},[33,3019,3020],{"class":196},"$version_file",[33,3022,403],{"class":72},[33,3024,3025],{"class":196}," ]] && current_version",[33,3027,200],{"class":124},[33,3029,203],{"class":196},[33,3031,2385],{"class":124},[33,3033,403],{"class":72},[33,3035,3020],{"class":196},[33,3037,403],{"class":72},[33,3039,218],{"class":196},[33,3041,3042],{"class":35,"line":372},[33,3043,53],{"emptyLinePlaceholder":52},[33,3045,3046,3048,3051,3053,3055,3057],{"class":35,"line":412},[33,3047,664],{"class":124},[33,3049,3050],{"class":196}," ((current_version ",[33,3052,2385],{"class":124},[33,3054,435],{"class":65},[33,3056,676],{"class":196},[33,3058,679],{"class":124},[33,3060,3061],{"class":35,"line":440},[33,3062,3063],{"class":39},"        # run migration 1\n",[33,3065,3066,3069],{"class":35,"line":485},[33,3067,3068],{"class":65},"        echo",[33,3070,3071],{"class":72}," \"running migration 1...\"\n",[33,3073,3074],{"class":35,"line":490},[33,3075,712],{"class":124},[33,3077,3078,3080,3082,3084,3087,3089],{"class":35,"line":496},[33,3079,664],{"class":124},[33,3081,3050],{"class":196},[33,3083,2385],{"class":124},[33,3085,3086],{"class":65}," 2",[33,3088,676],{"class":196},[33,3090,679],{"class":124},[33,3092,3093],{"class":35,"line":505},[33,3094,3095],{"class":39},"        # run migration 2\n",[33,3097,3098,3100],{"class":35,"line":519},[33,3099,3068],{"class":65},[33,3101,3102],{"class":72}," \"running migration 2...\"\n",[33,3104,3105],{"class":35,"line":533},[33,3106,712],{"class":124},[33,3108,3109,3112,3115,3117,3119,3121,3123],{"class":35,"line":560},[33,3110,3111],{"class":65},"    echo",[33,3113,3114],{"class":72}," \"2\"",[33,3116,2932],{"class":124},[33,3118,397],{"class":72},[33,3120,3020],{"class":196},[33,3122,403],{"class":72},[33,3124,3125],{"class":39},"   # update version marker\n",[33,3127,3128],{"class":35,"line":573},[33,3129,576],{"class":196},[33,3131,3132],{"class":35,"line":579},[33,3133,3134],{"class":39},"# Running twice: first run does migrations 1 and 2, second run does nothing (already at 2).\n",[15,3136,3138],{"id":3137},"output-discipline-stdout-for-data-stderr-for-messages","Output Discipline: stdout for Data, stderr for Messages",[20,3140,3141],{"language":22},[24,3142,3144],{"className":26,"code":3143,"language":22,"meta":28,"style":28},"# ── stdout: machine-parseable data (consumed by other commands) ──\n# ── stderr: human-readable messages (logs, warnings, errors) ──\n\n# ── Correct separation ──\nprocess() {\n    log \"processing...\" >&2    # log to stderr (human)\n    echo \"$result\"              # data to stdout (machine)\n}\n\n# Capture data only:\nresult=$(process)              # captures stdout (data), stderr shows on terminal\n# Capture both:\n{ output=$(process 2>&1); }    # captures stdout AND stderr into output\n\n# ── Anti-pattern: mixing data and messages on stdout ──\n# ❌ BAD — log message pollutes the data output\nprocess() {\n    echo \"processing...\"   # ← goes to stdout — mixed with data!\n    echo \"$result\"         # ← data also on stdout — $() captures both\n}\nresult=$(process)   # \"processing...\\nresult\" — contains the log message!\n\n# ✅ GOOD — messages to stderr, data to stdout\nprocess() {\n    echo \"processing...\" >&2  # stderr — not captured\n    echo \"$result\"            # stdout — only data\n}\nresult=$(process)   # just the data\n\n# ── Progress bar to stderr ──\nfor ((i = 0; i \u003C 100; i++)); do\n    printf '\\r[%-50s] %d%%' \"$(printf '#%.0s' {1..50} | head -c $((i\u002F2)))\" \"$i\" >&2\n    sleep 0.01\ndone\necho \"\" >&2   # newline after progress bar\n",[30,3145,3146,3151,3156,3160,3165,3171,3183,3197,3201,3205,3210,3227,3232,3252,3256,3261,3266,3272,3281,3294,3298,3314,3318,3323,3329,3340,3353,3357,3372,3376,3381,3408,3449,3457,3462],{"__ignoreMap":28},[33,3147,3148],{"class":35,"line":36},[33,3149,3150],{"class":39},"# ── stdout: machine-parseable data (consumed by other commands) ──\n",[33,3152,3153],{"class":35,"line":43},[33,3154,3155],{"class":39},"# ── stderr: human-readable messages (logs, warnings, errors) ──\n",[33,3157,3158],{"class":35,"line":49},[33,3159,53],{"emptyLinePlaceholder":52},[33,3161,3162],{"class":35,"line":56},[33,3163,3164],{"class":39},"# ── Correct separation ──\n",[33,3166,3167,3169],{"class":35,"line":62},[33,3168,2292],{"class":114},[33,3170,502],{"class":196},[33,3172,3173,3175,3178,3180],{"class":35,"line":76},[33,3174,536],{"class":114},[33,3176,3177],{"class":72}," \"processing...\"",[33,3179,406],{"class":124},[33,3181,3182],{"class":39},"    # log to stderr (human)\n",[33,3184,3185,3187,3189,3192,3194],{"class":35,"line":82},[33,3186,3111],{"class":65},[33,3188,397],{"class":72},[33,3190,3191],{"class":196},"$result",[33,3193,403],{"class":72},[33,3195,3196],{"class":39},"              # data to stdout (machine)\n",[33,3198,3199],{"class":35,"line":88},[33,3200,576],{"class":196},[33,3202,3203],{"class":35,"line":94},[33,3204,53],{"emptyLinePlaceholder":52},[33,3206,3207],{"class":35,"line":100},[33,3208,3209],{"class":39},"# Capture data only:\n",[33,3211,3212,3215,3217,3219,3221,3224],{"class":35,"line":105},[33,3213,3214],{"class":196},"result",[33,3216,200],{"class":124},[33,3218,203],{"class":196},[33,3220,2292],{"class":114},[33,3222,3223],{"class":196},")              ",[33,3225,3226],{"class":39},"# captures stdout (data), stderr shows on terminal\n",[33,3228,3229],{"class":35,"line":111},[33,3230,3231],{"class":39},"# Capture both:\n",[33,3233,3234,3237,3239,3241,3243,3246,3249],{"class":35,"line":140},[33,3235,3236],{"class":196},"{ output",[33,3238,200],{"class":124},[33,3240,203],{"class":196},[33,3242,2292],{"class":114},[33,3244,3245],{"class":124}," 2>&1",[33,3247,3248],{"class":196},"); }    ",[33,3250,3251],{"class":39},"# captures stdout AND stderr into output\n",[33,3253,3254],{"class":35,"line":153},[33,3255,53],{"emptyLinePlaceholder":52},[33,3257,3258],{"class":35,"line":166},[33,3259,3260],{"class":39},"# ── Anti-pattern: mixing data and messages on stdout ──\n",[33,3262,3263],{"class":35,"line":179},[33,3264,3265],{"class":39},"# ❌ BAD — log message pollutes the data output\n",[33,3267,3268,3270],{"class":35,"line":184},[33,3269,2292],{"class":114},[33,3271,502],{"class":196},[33,3273,3274,3276,3278],{"class":35,"line":190},[33,3275,3111],{"class":65},[33,3277,3177],{"class":72},[33,3279,3280],{"class":39},"   # ← goes to stdout — mixed with data!\n",[33,3282,3283,3285,3287,3289,3291],{"class":35,"line":221},[33,3284,3111],{"class":65},[33,3286,397],{"class":72},[33,3288,3191],{"class":196},[33,3290,403],{"class":72},[33,3292,3293],{"class":39},"         # ← data also on stdout — $() captures both\n",[33,3295,3296],{"class":35,"line":257},[33,3297,576],{"class":196},[33,3299,3300,3302,3304,3306,3308,3311],{"class":35,"line":270},[33,3301,3214],{"class":196},[33,3303,200],{"class":124},[33,3305,203],{"class":196},[33,3307,2292],{"class":114},[33,3309,3310],{"class":196},")   ",[33,3312,3313],{"class":39},"# \"processing...\\nresult\" — contains the log message!\n",[33,3315,3316],{"class":35,"line":311},[33,3317,53],{"emptyLinePlaceholder":52},[33,3319,3320],{"class":35,"line":316},[33,3321,3322],{"class":39},"# ✅ GOOD — messages to stderr, data to stdout\n",[33,3324,3325,3327],{"class":35,"line":322},[33,3326,2292],{"class":114},[33,3328,502],{"class":196},[33,3330,3331,3333,3335,3337],{"class":35,"line":334},[33,3332,3111],{"class":65},[33,3334,3177],{"class":72},[33,3336,406],{"class":124},[33,3338,3339],{"class":39},"  # stderr — not captured\n",[33,3341,3342,3344,3346,3348,3350],{"class":35,"line":350},[33,3343,3111],{"class":65},[33,3345,397],{"class":72},[33,3347,3191],{"class":196},[33,3349,403],{"class":72},[33,3351,3352],{"class":39},"            # stdout — only data\n",[33,3354,3355],{"class":35,"line":361},[33,3356,576],{"class":196},[33,3358,3359,3361,3363,3365,3367,3369],{"class":35,"line":366},[33,3360,3214],{"class":196},[33,3362,200],{"class":124},[33,3364,203],{"class":196},[33,3366,2292],{"class":114},[33,3368,3310],{"class":196},[33,3370,3371],{"class":39},"# just the data\n",[33,3373,3374],{"class":35,"line":372},[33,3375,53],{"emptyLinePlaceholder":52},[33,3377,3378],{"class":35,"line":412},[33,3379,3380],{"class":39},"# ── Progress bar to stderr ──\n",[33,3382,3383,3386,3388,3390,3392,3394,3396,3399,3402,3404,3406],{"class":35,"line":440},[33,3384,3385],{"class":124},"for",[33,3387,2375],{"class":196},[33,3389,200],{"class":124},[33,3391,673],{"class":65},[33,3393,2382],{"class":196},[33,3395,2385],{"class":124},[33,3397,3398],{"class":65}," 100",[33,3400,3401],{"class":196},"; i",[33,3403,2391],{"class":124},[33,3405,676],{"class":196},[33,3407,1021],{"class":124},[33,3409,3410,3412,3415,3417,3419,3422,3424,3427,3430,3433,3436,3439,3441,3444,3446],{"class":35,"line":485},[33,3411,2919],{"class":65},[33,3413,3414],{"class":72}," '\\r[%-50s] %d%%'",[33,3416,236],{"class":72},[33,3418,380],{"class":65},[33,3420,3421],{"class":72}," '#%.0s' {1..50} ",[33,3423,1046],{"class":124},[33,3425,3426],{"class":114}," head",[33,3428,3429],{"class":65}," -c",[33,3431,3432],{"class":72}," $((",[33,3434,3435],{"class":114},"i\u002F2",[33,3437,3438],{"class":72},")))\"",[33,3440,397],{"class":72},[33,3442,3443],{"class":196},"$i",[33,3445,403],{"class":72},[33,3447,3448],{"class":124}," >&2\n",[33,3450,3451,3454],{"class":35,"line":490},[33,3452,3453],{"class":114},"    sleep",[33,3455,3456],{"class":65}," 0.01\n",[33,3458,3459],{"class":35,"line":496},[33,3460,3461],{"class":124},"done\n",[33,3463,3464,3466,3469,3471],{"class":35,"line":505},[33,3465,2535],{"class":65},[33,3467,3468],{"class":72}," \"\"",[33,3470,406],{"class":124},[33,3472,3473],{"class":39},"   # newline after progress bar\n",[15,3475,3477,3478,3480],{"id":3476},"the-main-function-pattern","The ",[30,3479,959],{}," Function Pattern",[20,3482,3483],{"language":22},[24,3484,3486],{"className":26,"code":3485,"language":22,"meta":28,"style":28},"# ── Put ALL logic in functions, call main at the end ──\n\n# Benefits:\n# 1. Functions are testable (source the script, call functions)\n# 2. No top-level execution (safe if sourced as a library)\n# 3. Clear entry point\n# 4. Variables in functions are local (clean scope)\n\nmain() {\n    parse_args \"$@\"\n    validate\n    do_work\n}\n\nparse_args() {\n    while [[ $# -gt 0 ]]; do\n        case \"$1\" in\n            -v|--verbose) verbose=true; shift ;;\n            *)            args+=(\"$1\"); shift ;;\n        esac\n    done\n}\n\nvalidate() {\n    [[ ${#args[@]} -ge 1 ]] || die \"missing required argument\"\n    require docker\n}\n\ndo_work() {\n    # ... actual work ...\n    :\n}\n\n# ── Entry point (only if executed, not sourced) ──\nif [[ \"${BASH_SOURCE[0]}\" == \"${0}\" ]]; then\n    main \"$@\"\nfi\n# When sourced: BASH_SOURCE[0] != $0, so main doesn't run (acts as a library).\n# When executed: BASH_SOURCE[0] == $0, so main runs.\n",[30,3487,3488,3493,3497,3502,3507,3512,3517,3522,3526,3532,3542,3547,3552,3556,3560,3566,3582,3594,3618,3641,3645,3649,3653,3657,3664,3694,3702,3706,3710,3717,3722,3727,3731,3735,3740,3764,3774,3778,3783],{"__ignoreMap":28},[33,3489,3490],{"class":35,"line":36},[33,3491,3492],{"class":39},"# ── Put ALL logic in functions, call main at the end ──\n",[33,3494,3495],{"class":35,"line":43},[33,3496,53],{"emptyLinePlaceholder":52},[33,3498,3499],{"class":35,"line":49},[33,3500,3501],{"class":39},"# Benefits:\n",[33,3503,3504],{"class":35,"line":56},[33,3505,3506],{"class":39},"# 1. Functions are testable (source the script, call functions)\n",[33,3508,3509],{"class":35,"line":62},[33,3510,3511],{"class":39},"# 2. No top-level execution (safe if sourced as a library)\n",[33,3513,3514],{"class":35,"line":76},[33,3515,3516],{"class":39},"# 3. Clear entry point\n",[33,3518,3519],{"class":35,"line":82},[33,3520,3521],{"class":39},"# 4. Variables in functions are local (clean scope)\n",[33,3523,3524],{"class":35,"line":88},[33,3525,53],{"emptyLinePlaceholder":52},[33,3527,3528,3530],{"class":35,"line":94},[33,3529,959],{"class":114},[33,3531,502],{"class":196},[33,3533,3534,3536,3538,3540],{"class":35,"line":100},[33,3535,967],{"class":114},[33,3537,397],{"class":72},[33,3539,972],{"class":65},[33,3541,570],{"class":72},[33,3543,3544],{"class":35,"line":105},[33,3545,3546],{"class":114},"    validate\n",[33,3548,3549],{"class":35,"line":111},[33,3550,3551],{"class":114},"    do_work\n",[33,3553,3554],{"class":35,"line":140},[33,3555,576],{"class":196},[33,3557,3558],{"class":35,"line":153},[33,3559,53],{"emptyLinePlaceholder":52},[33,3561,3562,3564],{"class":35,"line":166},[33,3563,996],{"class":114},[33,3565,502],{"class":196},[33,3567,3568,3570,3572,3574,3576,3578,3580],{"class":35,"line":179},[33,3569,1004],{"class":124},[33,3571,1007],{"class":72},[33,3573,1010],{"class":65},[33,3575,1013],{"class":65},[33,3577,673],{"class":65},[33,3579,1018],{"class":196},[33,3581,1021],{"class":124},[33,3583,3584,3586,3588,3590,3592],{"class":35,"line":184},[33,3585,1027],{"class":124},[33,3587,397],{"class":72},[33,3589,456],{"class":65},[33,3591,403],{"class":72},[33,3593,1036],{"class":124},[33,3595,3596,3598,3600,3602,3604,3607,3609,3612,3614,3616],{"class":35,"line":190},[33,3597,1070],{"class":1042},[33,3599,1046],{"class":124},[33,3601,1075],{"class":1042},[33,3603,1052],{"class":124},[33,3605,3606],{"class":196}," verbose",[33,3608,200],{"class":124},[33,3610,3611],{"class":72},"true",[33,3613,429],{"class":196},[33,3615,1090],{"class":65},[33,3617,1064],{"class":196},[33,3619,3620,3622,3625,3627,3629,3631,3633,3635,3637,3639],{"class":35,"line":221},[33,3621,1146],{"class":124},[33,3623,3624],{"class":196},"            args",[33,3626,1151],{"class":124},[33,3628,1154],{"class":196},[33,3630,403],{"class":72},[33,3632,456],{"class":65},[33,3634,403],{"class":72},[33,3636,1163],{"class":196},[33,3638,1090],{"class":65},[33,3640,1064],{"class":196},[33,3642,3643],{"class":35,"line":257},[33,3644,1173],{"class":124},[33,3646,3647],{"class":35,"line":270},[33,3648,1179],{"class":124},[33,3650,3651],{"class":35,"line":311},[33,3652,576],{"class":196},[33,3654,3655],{"class":35,"line":316},[33,3656,53],{"emptyLinePlaceholder":52},[33,3658,3659,3662],{"class":35,"line":322},[33,3660,3661],{"class":114},"validate",[33,3663,502],{"class":196},[33,3665,3666,3669,3672,3675,3677,3680,3683,3685,3687,3689,3691],{"class":35,"line":334},[33,3667,3668],{"class":196},"    [[ ${",[33,3670,3671],{"class":124},"#",[33,3673,3674],{"class":196},"args[",[33,3676,1386],{"class":124},[33,3678,3679],{"class":196},"]} ",[33,3681,3682],{"class":124},"-ge",[33,3684,435],{"class":65},[33,3686,2787],{"class":196},[33,3688,470],{"class":124},[33,3690,473],{"class":114},[33,3692,3693],{"class":72}," \"missing required argument\"\n",[33,3695,3696,3699],{"class":35,"line":350},[33,3697,3698],{"class":114},"    require",[33,3700,3701],{"class":72}," docker\n",[33,3703,3704],{"class":35,"line":361},[33,3705,576],{"class":196},[33,3707,3708],{"class":35,"line":366},[33,3709,53],{"emptyLinePlaceholder":52},[33,3711,3712,3715],{"class":35,"line":372},[33,3713,3714],{"class":114},"do_work",[33,3716,502],{"class":196},[33,3718,3719],{"class":35,"line":412},[33,3720,3721],{"class":39},"    # ... actual work ...\n",[33,3723,3724],{"class":35,"line":440},[33,3725,3726],{"class":65},"    :\n",[33,3728,3729],{"class":35,"line":485},[33,3730,576],{"class":196},[33,3732,3733],{"class":35,"line":490},[33,3734,53],{"emptyLinePlaceholder":52},[33,3736,3737],{"class":35,"line":496},[33,3738,3739],{"class":39},"# ── Entry point (only if executed, not sourced) ──\n",[33,3741,3742,3744,3746,3748,3750,3752,3754,3756,3758,3760,3762],{"class":35,"line":505},[33,3743,1421],{"class":124},[33,3745,1424],{"class":196},[33,3747,280],{"class":72},[33,3749,212],{"class":196},[33,3751,215],{"class":72},[33,3753,1433],{"class":124},[33,3755,397],{"class":72},[33,3757,1438],{"class":65},[33,3759,403],{"class":72},[33,3761,1018],{"class":196},[33,3763,679],{"class":124},[33,3765,3766,3768,3770,3772],{"class":35,"line":519},[33,3767,1450],{"class":114},[33,3769,397],{"class":72},[33,3771,972],{"class":65},[33,3773,570],{"class":72},[33,3775,3776],{"class":35,"line":533},[33,3777,1462],{"class":124},[33,3779,3780],{"class":35,"line":560},[33,3781,3782],{"class":39},"# When sourced: BASH_SOURCE[0] != $0, so main doesn't run (acts as a library).\n",[33,3784,3785],{"class":35,"line":573},[33,3786,3787],{"class":39},"# When executed: BASH_SOURCE[0] == $0, so main runs.\n",[15,3789,3791],{"id":3790},"guard-against-sourcing-library-mode","Guard Against Sourcing (Library Mode)",[20,3793,3794],{"language":22},[24,3795,3797],{"className":26,"code":3796,"language":22,"meta":28,"style":28},"# ── Detect if script is being sourced vs executed ──\n# When sourced: BASH_SOURCE[0] != BASH_SOURCE[1] (caller)\n# When executed: BASH_SOURCE[0] == BASH_SOURCE[1] (both are the script)\n# Simpler: (return 0 2>\u002Fdev\u002Fnull) succeeds in a sourced context, fails when executed\n\n(return 0 2>\u002Fdev\u002Fnull) && SOURCED=1 || SOURCED=0\n\nif ((SOURCED)); then\n    # Library mode: don't run main, just define functions\n    :\nelse\n    # Executed: run main\n    main \"$@\"\nfi\n\n# ── Or the standard idiom ──\nif [[ \"${BASH_SOURCE[0]}\" == \"${0}\" ]]; then\n    main \"$@\"\nfi\n",[30,3798,3799,3804,3809,3814,3819,3823,3851,3855,3864,3869,3873,3877,3882,3892,3896,3900,3905,3929,3939],{"__ignoreMap":28},[33,3800,3801],{"class":35,"line":36},[33,3802,3803],{"class":39},"# ── Detect if script is being sourced vs executed ──\n",[33,3805,3806],{"class":35,"line":43},[33,3807,3808],{"class":39},"# When sourced: BASH_SOURCE[0] != BASH_SOURCE[1] (caller)\n",[33,3810,3811],{"class":35,"line":49},[33,3812,3813],{"class":39},"# When executed: BASH_SOURCE[0] == BASH_SOURCE[1] (both are the script)\n",[33,3815,3816],{"class":35,"line":56},[33,3817,3818],{"class":39},"# Simpler: (return 0 2>\u002Fdev\u002Fnull) succeeds in a sourced context, fails when executed\n",[33,3820,3821],{"class":35,"line":62},[33,3822,53],{"emptyLinePlaceholder":52},[33,3824,3825,3827,3829,3831,3833,3835,3838,3840,3842,3844,3847,3849],{"class":35,"line":76},[33,3826,1154],{"class":196},[33,3828,2008],{"class":124},[33,3830,673],{"class":65},[33,3832,125],{"class":124},[33,3834,128],{"class":72},[33,3836,3837],{"class":196},") && SOURCED",[33,3839,200],{"class":124},[33,3841,1085],{"class":72},[33,3843,131],{"class":124},[33,3845,3846],{"class":196}," SOURCED",[33,3848,200],{"class":124},[33,3850,3008],{"class":72},[33,3852,3853],{"class":35,"line":82},[33,3854,53],{"emptyLinePlaceholder":52},[33,3856,3857,3859,3862],{"class":35,"line":88},[33,3858,1421],{"class":124},[33,3860,3861],{"class":196}," ((SOURCED)); ",[33,3863,679],{"class":124},[33,3865,3866],{"class":35,"line":94},[33,3867,3868],{"class":39},"    # Library mode: don't run main, just define functions\n",[33,3870,3871],{"class":35,"line":100},[33,3872,3726],{"class":65},[33,3874,3875],{"class":35,"line":105},[33,3876,1787],{"class":124},[33,3878,3879],{"class":35,"line":111},[33,3880,3881],{"class":39},"    # Executed: run main\n",[33,3883,3884,3886,3888,3890],{"class":35,"line":140},[33,3885,1450],{"class":114},[33,3887,397],{"class":72},[33,3889,972],{"class":65},[33,3891,570],{"class":72},[33,3893,3894],{"class":35,"line":153},[33,3895,1462],{"class":124},[33,3897,3898],{"class":35,"line":166},[33,3899,53],{"emptyLinePlaceholder":52},[33,3901,3902],{"class":35,"line":179},[33,3903,3904],{"class":39},"# ── Or the standard idiom ──\n",[33,3906,3907,3909,3911,3913,3915,3917,3919,3921,3923,3925,3927],{"class":35,"line":184},[33,3908,1421],{"class":124},[33,3910,1424],{"class":196},[33,3912,280],{"class":72},[33,3914,212],{"class":196},[33,3916,215],{"class":72},[33,3918,1433],{"class":124},[33,3920,397],{"class":72},[33,3922,1438],{"class":65},[33,3924,403],{"class":72},[33,3926,1018],{"class":196},[33,3928,679],{"class":124},[33,3930,3931,3933,3935,3937],{"class":35,"line":190},[33,3932,1450],{"class":114},[33,3934,397],{"class":72},[33,3936,972],{"class":65},[33,3938,570],{"class":72},[33,3940,3941],{"class":35,"line":221},[33,3942,1462],{"class":124},[15,3944,3946],{"id":3945},"tips-tricks","💡 Tips & Tricks",[20,3948,3949],{"language":22},[24,3950,3952],{"className":26,"code":3951,"language":22,"meta":28,"style":28},"# ── `printf '%q'` for safe variable quoting ──\n# %q shell-quotes a string (escapes special chars) — safe for re-eval or passing as args\nfile=\"my file with spaces & special\"\nprintf '%q\\n' \"$file\"   # my\\ file\\ with\\ spaces\\ \\&\\ special\n\n# ── Use `declare -p` to serialize variables ──\narr=(\"a\" \"b\" \"c\")\ndeclare -p arr          # declare -a arr=([0]=\"a\" [1]=\"b\" [2]=\"c\")\n# Can be saved and sourced to restore state:\ndeclare -p arr > state.sh  # save\nsource state.sh            # restore\n\n# ── Atomic operations with trap EXIT ──\n# If a script can be interrupted, use trap EXIT for cleanup:\ntrap '[[ -f \"$tmpfile\" ]] && rm -f \"$tmpfile\"' EXIT\ntmpfile=$(mktemp)\n\n# ── Use `flock` for single-instance scripts ──\nexec 9> \u002Ftmp\u002Fmyapp.lock\nflock -n 9 || { echo \"already running\" >&2; exit 1; }\n# Lock held until fd 9 is closed (script exits)\n\n# ── Check for required Bash version ──\nif ((BASH_VERSINFO[0] \u003C 4)); then\n    echo \"Requires Bash 4+ (for associative arrays)\" >&2\n    exit 1\nfi\n",[30,3953,3954,3959,3964,3974,3990,3994,3999,4019,4031,4036,4053,4063,4067,4072,4077,4087,4100,4104,4109,4120,4150,4155,4159,4164,4185,4194,4201],{"__ignoreMap":28},[33,3955,3956],{"class":35,"line":36},[33,3957,3958],{"class":39},"# ── `printf '%q'` for safe variable quoting ──\n",[33,3960,3961],{"class":35,"line":43},[33,3962,3963],{"class":39},"# %q shell-quotes a string (escapes special chars) — safe for re-eval or passing as args\n",[33,3965,3966,3969,3971],{"class":35,"line":49},[33,3967,3968],{"class":196},"file",[33,3970,200],{"class":124},[33,3972,3973],{"class":72},"\"my file with spaces & special\"\n",[33,3975,3976,3978,3981,3983,3985,3987],{"class":35,"line":56},[33,3977,380],{"class":65},[33,3979,3980],{"class":72}," '%q\\n'",[33,3982,397],{"class":72},[33,3984,2782],{"class":196},[33,3986,403],{"class":72},[33,3988,3989],{"class":39},"   # my\\ file\\ with\\ spaces\\ \\&\\ special\n",[33,3991,3992],{"class":35,"line":62},[33,3993,53],{"emptyLinePlaceholder":52},[33,3995,3996],{"class":35,"line":76},[33,3997,3998],{"class":39},"# ── Use `declare -p` to serialize variables ──\n",[33,4000,4001,4004,4006,4008,4011,4014,4017],{"class":35,"line":82},[33,4002,4003],{"class":196},"arr",[33,4005,200],{"class":124},[33,4007,1154],{"class":196},[33,4009,4010],{"class":72},"\"a\"",[33,4012,4013],{"class":72}," \"b\"",[33,4015,4016],{"class":72}," \"c\"",[33,4018,218],{"class":196},[33,4020,4021,4023,4025,4028],{"class":35,"line":88},[33,4022,325],{"class":124},[33,4024,2747],{"class":65},[33,4026,4027],{"class":196}," arr          ",[33,4029,4030],{"class":39},"# declare -a arr=([0]=\"a\" [1]=\"b\" [2]=\"c\")\n",[33,4032,4033],{"class":35,"line":94},[33,4034,4035],{"class":39},"# Can be saved and sourced to restore state:\n",[33,4037,4038,4040,4042,4045,4047,4050],{"class":35,"line":100},[33,4039,325],{"class":124},[33,4041,2747],{"class":65},[33,4043,4044],{"class":196}," arr ",[33,4046,464],{"class":124},[33,4048,4049],{"class":196}," state.sh  ",[33,4051,4052],{"class":39},"# save\n",[33,4054,4055,4057,4060],{"class":35,"line":105},[33,4056,1833],{"class":65},[33,4058,4059],{"class":72}," state.sh",[33,4061,4062],{"class":39},"            # restore\n",[33,4064,4065],{"class":35,"line":111},[33,4066,53],{"emptyLinePlaceholder":52},[33,4068,4069],{"class":35,"line":140},[33,4070,4071],{"class":39},"# ── Atomic operations with trap EXIT ──\n",[33,4073,4074],{"class":35,"line":153},[33,4075,4076],{"class":39},"# If a script can be interrupted, use trap EXIT for cleanup:\n",[33,4078,4079,4081,4084],{"class":35,"line":166},[33,4080,582],{"class":65},[33,4082,4083],{"class":72}," '[[ -f \"$tmpfile\" ]] && rm -f \"$tmpfile\"'",[33,4085,4086],{"class":72}," EXIT\n",[33,4088,4089,4092,4094,4096,4098],{"class":35,"line":179},[33,4090,4091],{"class":196},"tmpfile",[33,4093,200],{"class":124},[33,4095,203],{"class":196},[33,4097,2905],{"class":114},[33,4099,218],{"class":196},[33,4101,4102],{"class":35,"line":184},[33,4103,53],{"emptyLinePlaceholder":52},[33,4105,4106],{"class":35,"line":190},[33,4107,4108],{"class":39},"# ── Use `flock` for single-instance scripts ──\n",[33,4110,4111,4114,4117],{"class":35,"line":221},[33,4112,4113],{"class":65},"exec",[33,4115,4116],{"class":124}," 9>",[33,4118,4119],{"class":72}," \u002Ftmp\u002Fmyapp.lock\n",[33,4121,4122,4125,4127,4130,4132,4135,4137,4140,4142,4144,4146,4148],{"class":35,"line":257},[33,4123,4124],{"class":114},"flock",[33,4126,2575],{"class":65},[33,4128,4129],{"class":65}," 9",[33,4131,131],{"class":124},[33,4133,4134],{"class":196}," { ",[33,4136,2535],{"class":65},[33,4138,4139],{"class":72}," \"already running\"",[33,4141,406],{"class":124},[33,4143,429],{"class":196},[33,4145,432],{"class":65},[33,4147,435],{"class":65},[33,4149,409],{"class":196},[33,4151,4152],{"class":35,"line":270},[33,4153,4154],{"class":39},"# Lock held until fd 9 is closed (script exits)\n",[33,4156,4157],{"class":35,"line":311},[33,4158,53],{"emptyLinePlaceholder":52},[33,4160,4161],{"class":35,"line":316},[33,4162,4163],{"class":39},"# ── Check for required Bash version ──\n",[33,4165,4166,4168,4171,4173,4176,4178,4181,4183],{"class":35,"line":322},[33,4167,1421],{"class":124},[33,4169,4170],{"class":196}," ((BASH_VERSINFO[",[33,4172,2306],{"class":65},[33,4174,4175],{"class":196},"] ",[33,4177,2385],{"class":124},[33,4179,4180],{"class":65}," 4",[33,4182,676],{"class":196},[33,4184,679],{"class":124},[33,4186,4187,4189,4192],{"class":35,"line":334},[33,4188,3111],{"class":65},[33,4190,4191],{"class":72}," \"Requires Bash 4+ (for associative arrays)\"",[33,4193,3448],{"class":124},[33,4195,4196,4198],{"class":35,"line":350},[33,4197,563],{"class":65},[33,4199,4200],{"class":65}," 1\n",[33,4202,4203],{"class":35,"line":361},[33,4204,1462],{"class":124},[15,4206,4208],{"id":4207},"️-edge-cases-gotchas","⚠️ Edge Cases & Gotchas",[20,4210,4211],{"language":22},[24,4212,4214],{"className":26,"code":4213,"language":22,"meta":28,"style":28},"# ── Functions must be defined before use ──\n# Bash reads top-to-bottom — no hoisting. Call functions AFTER defining them.\n# Put all functions at the top, main \"$@\" at the bottom.\n\n# ── Uppercase variable names can clobber env vars ──\n# PATH, HOME, USER, TERM are env vars. Don't use ALLCAPS for regular vars.\n# Use: file_count, output_dir (lower_case for variables)\n# Use: MAX_RETRIES, VERSION (UPPER_CASE for readonly constants)\n\n# ── `set -e` in a sourced library ──\n# `set -e` applies to the current shell. If you source a library with `set -e`,\n# it enables -e in the CALLER's shell (surprising!).\n# Don't put `set -e` in libraries — put it in scripts that are executed.\n\n# ── `return` vs `exit` in a sourced file ──\n# In a sourced file: `exit` closes the caller's shell! Use `return`.\n# In an executed script: `exit` is fine.\n\n# ── `trap` can be overridden ──\n# If you source a file that sets trap EXIT, it overrides your trap EXIT.\n# Always set traps AFTER sourcing libraries.\n\n# ── `readonly` can't be unset ──\n# readonly PI=3.14  → can't unset PI, can't reassign.\n# To \"reset\": use a subshell: ( readonly PI=3; echo $PI )  → PI is gone after subshell.\n\n# ── `eval` is almost always the wrong tool ──\n# For indirect variable: use ${!var}\n# For building commands: use arrays: cmd=(cmd arg1 arg2); \"${cmd[@]}\"\n# For dynamic function calls: declare -F | grep, or call by name: \"$func_name\" arg\n",[30,4215,4216,4221,4226,4231,4235,4240,4245,4250,4255,4259,4264,4269,4274,4279,4283,4288,4293,4298,4302,4307,4312,4317,4321,4326,4331,4336,4340,4345,4350,4355],{"__ignoreMap":28},[33,4217,4218],{"class":35,"line":36},[33,4219,4220],{"class":39},"# ── Functions must be defined before use ──\n",[33,4222,4223],{"class":35,"line":43},[33,4224,4225],{"class":39},"# Bash reads top-to-bottom — no hoisting. Call functions AFTER defining them.\n",[33,4227,4228],{"class":35,"line":49},[33,4229,4230],{"class":39},"# Put all functions at the top, main \"$@\" at the bottom.\n",[33,4232,4233],{"class":35,"line":56},[33,4234,53],{"emptyLinePlaceholder":52},[33,4236,4237],{"class":35,"line":62},[33,4238,4239],{"class":39},"# ── Uppercase variable names can clobber env vars ──\n",[33,4241,4242],{"class":35,"line":76},[33,4243,4244],{"class":39},"# PATH, HOME, USER, TERM are env vars. Don't use ALLCAPS for regular vars.\n",[33,4246,4247],{"class":35,"line":82},[33,4248,4249],{"class":39},"# Use: file_count, output_dir (lower_case for variables)\n",[33,4251,4252],{"class":35,"line":88},[33,4253,4254],{"class":39},"# Use: MAX_RETRIES, VERSION (UPPER_CASE for readonly constants)\n",[33,4256,4257],{"class":35,"line":94},[33,4258,53],{"emptyLinePlaceholder":52},[33,4260,4261],{"class":35,"line":100},[33,4262,4263],{"class":39},"# ── `set -e` in a sourced library ──\n",[33,4265,4266],{"class":35,"line":105},[33,4267,4268],{"class":39},"# `set -e` applies to the current shell. If you source a library with `set -e`,\n",[33,4270,4271],{"class":35,"line":111},[33,4272,4273],{"class":39},"# it enables -e in the CALLER's shell (surprising!).\n",[33,4275,4276],{"class":35,"line":140},[33,4277,4278],{"class":39},"# Don't put `set -e` in libraries — put it in scripts that are executed.\n",[33,4280,4281],{"class":35,"line":153},[33,4282,53],{"emptyLinePlaceholder":52},[33,4284,4285],{"class":35,"line":166},[33,4286,4287],{"class":39},"# ── `return` vs `exit` in a sourced file ──\n",[33,4289,4290],{"class":35,"line":179},[33,4291,4292],{"class":39},"# In a sourced file: `exit` closes the caller's shell! Use `return`.\n",[33,4294,4295],{"class":35,"line":184},[33,4296,4297],{"class":39},"# In an executed script: `exit` is fine.\n",[33,4299,4300],{"class":35,"line":190},[33,4301,53],{"emptyLinePlaceholder":52},[33,4303,4304],{"class":35,"line":221},[33,4305,4306],{"class":39},"# ── `trap` can be overridden ──\n",[33,4308,4309],{"class":35,"line":257},[33,4310,4311],{"class":39},"# If you source a file that sets trap EXIT, it overrides your trap EXIT.\n",[33,4313,4314],{"class":35,"line":270},[33,4315,4316],{"class":39},"# Always set traps AFTER sourcing libraries.\n",[33,4318,4319],{"class":35,"line":311},[33,4320,53],{"emptyLinePlaceholder":52},[33,4322,4323],{"class":35,"line":316},[33,4324,4325],{"class":39},"# ── `readonly` can't be unset ──\n",[33,4327,4328],{"class":35,"line":322},[33,4329,4330],{"class":39},"# readonly PI=3.14  → can't unset PI, can't reassign.\n",[33,4332,4333],{"class":35,"line":334},[33,4334,4335],{"class":39},"# To \"reset\": use a subshell: ( readonly PI=3; echo $PI )  → PI is gone after subshell.\n",[33,4337,4338],{"class":35,"line":350},[33,4339,53],{"emptyLinePlaceholder":52},[33,4341,4342],{"class":35,"line":361},[33,4343,4344],{"class":39},"# ── `eval` is almost always the wrong tool ──\n",[33,4346,4347],{"class":35,"line":366},[33,4348,4349],{"class":39},"# For indirect variable: use ${!var}\n",[33,4351,4352],{"class":35,"line":372},[33,4353,4354],{"class":39},"# For building commands: use arrays: cmd=(cmd arg1 arg2); \"${cmd[@]}\"\n",[33,4356,4357],{"class":35,"line":412},[33,4358,4359],{"class":39},"# For dynamic function calls: declare -F | grep, or call by name: \"$func_name\" arg\n",[15,4361,4363],{"id":4362},"quick-quiz","🧠 Quick Quiz",[4365,4366,4367],"p",{},"Why does this script work when executed but not when sourced?",[20,4369,4370],{"language":22},[24,4371,4373],{"className":26,"code":4372,"language":22,"meta":28,"style":28},"#!\u002Fusr\u002Fbin\u002Fenv bash\nset -euo pipefail\necho \"starting\"\n# ... script logic ...\necho \"done\"\n",[30,4374,4375,4379,4388,4395,4400],{"__ignoreMap":28},[33,4376,4377],{"class":35,"line":36},[33,4378,40],{"class":39},[33,4380,4381,4383,4386],{"class":35,"line":43},[33,4382,66],{"class":65},[33,4384,4385],{"class":65}," -euo",[33,4387,73],{"class":72},[33,4389,4390,4392],{"class":35,"line":49},[33,4391,2535],{"class":65},[33,4393,4394],{"class":72}," \"starting\"\n",[33,4396,4397],{"class":35,"line":56},[33,4398,4399],{"class":39},"# ... script logic ...\n",[33,4401,4402,4404],{"class":35,"line":62},[33,4403,2535],{"class":65},[33,4405,4406],{"class":72}," \"done\"\n",[4408,4409,4410,4414,4430,4445,4467,4470,4479,4537],"details",{},[4411,4412,4413],"summary",{},"Answer",[4365,4415,4416,4417,4421,4422,4425,4426,4429],{},"When ",[4418,4419,4420],"strong",{},"executed"," (",[30,4423,4424],{},".\u002Fscript.sh","): ",[30,4427,4428],{},"set -euo pipefail"," applies to the script's shell. If a command fails, the script exits. The calling terminal is unaffected.",[4365,4431,4416,4432,4421,4435,4425,4438,4440,4441,4444],{},[4418,4433,4434],{},"sourced",[30,4436,4437],{},"source script.sh",[30,4439,4428],{}," applies to the ",[4418,4442,4443],{},"caller's shell"," (your interactive terminal or the script that sourced it). Now:",[4446,4447,4448,4455,4461],"ul",{},[4449,4450,4451,4454],"li",{},[30,4452,4453],{},"set -e",": your terminal exits on any command failure!",[4449,4456,4457,4460],{},[30,4458,4459],{},"set -u",": any unset variable reference in the terminal kills it!",[4449,4462,4463,4466],{},[30,4464,4465],{},"set -o pipefail",": changes pipe behavior for the entire terminal session!",[4365,4468,4469],{},"This is a common surprise: sourcing a \"safe\" script makes the caller's shell \"safe\" too — which can be dangerous (exiting the user's terminal).",[4365,4471,4472,4475,4476,4478],{},[4418,4473,4474],{},"The fix",": libraries should NOT set ",[30,4477,4453],{},". Only executable scripts should. Use the guard:",[20,4480,4481],{"language":22},[24,4482,4484],{"className":26,"code":4483,"language":22,"meta":28,"style":28},"if [[ \"${BASH_SOURCE[0]}\" == \"${0}\" ]]; then\n    set -euo pipefail   # only in executed mode\n    main \"$@\"\nfi\n",[30,4485,4486,4510,4523,4533],{"__ignoreMap":28},[33,4487,4488,4490,4492,4494,4496,4498,4500,4502,4504,4506,4508],{"class":35,"line":36},[33,4489,1421],{"class":124},[33,4491,1424],{"class":196},[33,4493,280],{"class":72},[33,4495,212],{"class":196},[33,4497,215],{"class":72},[33,4499,1433],{"class":124},[33,4501,397],{"class":72},[33,4503,1438],{"class":65},[33,4505,403],{"class":72},[33,4507,1018],{"class":196},[33,4509,679],{"class":124},[33,4511,4512,4515,4517,4520],{"class":35,"line":43},[33,4513,4514],{"class":65},"    set",[33,4516,4385],{"class":65},[33,4518,4519],{"class":72}," pipefail",[33,4521,4522],{"class":39},"   # only in executed mode\n",[33,4524,4525,4527,4529,4531],{"class":35,"line":49},[33,4526,1450],{"class":114},[33,4528,397],{"class":72},[33,4530,972],{"class":65},[33,4532,570],{"class":72},[33,4534,4535],{"class":35,"line":56},[33,4536,1462],{"class":124},[4365,4538,4539,4542,4543,4545],{},[4418,4540,4541],{},"The lesson",": ",[30,4544,4453],{}," (and friends) affect the current shell. When a script is sourced, that's the caller's shell. Guard strict mode behind the executed-vs-sourced check.",[4547,4548,4549],"style",{},"html pre.shiki code .sdCPZ, html code.shiki .sdCPZ{--shiki-default:#6A737D;--shiki-github-dark:#6A737D}html pre.shiki code .snvgF, html code.shiki .snvgF{--shiki-default:#005CC5;--shiki-github-dark:#79B8FF}html pre.shiki code .sJ6F3, html code.shiki .sJ6F3{--shiki-default:#032F62;--shiki-github-dark:#9ECBFF}html pre.shiki code .sIsaT, html code.shiki .sIsaT{--shiki-default:#6F42C1;--shiki-github-dark:#B392F0}html pre.shiki code .svdQ7, html code.shiki .svdQ7{--shiki-default:#D73A49;--shiki-github-dark:#F97583}html pre.shiki code .ssxIu, html code.shiki .ssxIu{--shiki-default:#24292E;--shiki-github-dark:#E1E4E8}html pre.shiki code .sCrzJ, html code.shiki .sCrzJ{--shiki-default:#E36209;--shiki-github-dark:#FFAB70}html pre.shiki code .svAP2, html code.shiki .svAP2{--shiki-default:#032F62;--shiki-github-dark:#DBEDFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html.github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}",{"title":28,"searchDepth":43,"depth":43,"links":4551},[4552,4553,4555,4557,4558,4560,4562,4563,4564,4566,4567,4568,4569],{"id":17,"depth":43,"text":18},{"id":1465,"depth":43,"text":4554},"SCRIPT_DIR: Reliable Script Directory Resolution",{"id":1858,"depth":43,"text":4556},"Anti-Pattern: $0 for Script Directory",{"id":1969,"depth":43,"text":1970},{"id":2188,"depth":43,"text":4559},"readonly and local: Immutability and Scope",{"id":2435,"depth":43,"text":4561},"Anti-Pattern: eval on Untrusted Input",{"id":2717,"depth":43,"text":2718},{"id":3137,"depth":43,"text":3138},{"id":3476,"depth":43,"text":4565},"The main Function Pattern",{"id":3790,"depth":43,"text":3791},{"id":3945,"depth":43,"text":3946},{"id":4207,"depth":43,"text":4208},{"id":4362,"depth":43,"text":4363},"Deep-dive into production Bash script architecture: the main function pattern, SCRIPT_DIR with symlink resolution, library sourcing, readonly\u002Flocal hygiene, die\u002Frequire idioms, eval avoidance, and idempotent design. Code-first reference for senior engineers.","md",{},"\u002Fbash\u002F13-best-practices",{"title":5,"description":4570},"bash\u002F13-best-practices","MyB-e4FcnoXk2sCrM-eV-QwfqhAgl1CRNXdcoeL2Dis",1789924644818]