[{"data":1,"prerenderedAt":672},["ShallowReactive",2],{"mdc-tns6fy-key":3,"mdc--ydzha5-key":21,"mdc-b8dowx-key":133,"mdc-7r394k-key":198,"mdc--3ak3mm-key":210,"mdc-2aszzs-key":298,"mdc-n9w1nn-key":396,"mdc-bqzig-key":406,"mdc--9in53p-key":442,"mdc--a41uwk-key":482,"mdc--tnu00h-key":542,"mdc-qdelpi-key":583,"mdc-gke4bv-key":595},{"data":4,"body":5},{},{"type":6,"children":7},"root",[8,16],{"type":9,"tag":10,"props":11,"children":12},"element","p",{},[13],{"type":14,"value":15},"text","Everything so far was about making a script work. This lesson is about making one you would be willing to run at 3am, on a machine you are not watching, against data you cannot replace.",{"type":9,"tag":10,"props":17,"children":18},{},[19],{"type":14,"value":20},"Very little of it is clever. It is a short list of habits, and the reason to learn them as a list is that each one exists because of a specific way scripts fail.",{"data":22,"body":23},{},{"type":6,"children":24},[25,39,60,72,77,114,123],{"type":9,"tag":26,"props":27,"children":29},"h2",{"id":28},"set-e-stop-at-the-first-failure",[30,37],{"type":9,"tag":31,"props":32,"children":34},"code",{"className":33},[],[35],{"type":14,"value":36},"set -e",{"type":14,"value":38}," — stop at the first failure",{"type":9,"tag":10,"props":40,"children":41},{},[42,44,50,52,58],{"type":14,"value":43},"By default a script carries on after a command fails. The ",{"type":9,"tag":31,"props":45,"children":47},{"className":46},[],[48],{"type":14,"value":49},"cd",{"type":14,"value":51}," fails, and the ",{"type":9,"tag":31,"props":53,"children":55},{"className":54},[],[56],{"type":14,"value":57},"rm -rf ./*",{"type":14,"value":59}," on the next line runs anyway, somewhere else.",{"type":9,"tag":61,"props":62,"children":66},"pre",{"className":63,"code":65,"language":14},[64],"language-text","#!/bin/sh\nset -e\n",[67],{"type":9,"tag":31,"props":68,"children":70},{"__ignoreMap":69},"",[71],{"type":14,"value":65},{"type":9,"tag":10,"props":73,"children":74},{},[75],{"type":14,"value":76},"Now any command that exits non-zero ends the script immediately.",{"type":9,"tag":10,"props":78,"children":79},{},[80,82,88,90,96,98,104,106,112],{"type":14,"value":81},"There are exceptions, and they are deliberate rather than bugs. A command in an ",{"type":9,"tag":31,"props":83,"children":85},{"className":84},[],[86],{"type":14,"value":87},"if",{"type":14,"value":89}," condition, on the left of ",{"type":9,"tag":31,"props":91,"children":93},{"className":92},[],[94],{"type":14,"value":95},"&&",{"type":14,"value":97}," or ",{"type":9,"tag":31,"props":99,"children":101},{"className":100},[],[102],{"type":14,"value":103},"||",{"type":14,"value":105},", or negated with ",{"type":9,"tag":31,"props":107,"children":109},{"className":108},[],[110],{"type":14,"value":111},"!",{"type":14,"value":113}," is allowed to fail — otherwise you could never test anything. So when a failure is expected, say so:",{"type":9,"tag":61,"props":115,"children":118},{"className":116,"code":117,"language":14},[64],"grep -q ERROR app.log || echo \"clean\"     # not fatal: the || handles it\n",[119],{"type":9,"tag":31,"props":120,"children":121},{"__ignoreMap":69},[122],{"type":14,"value":117},{"type":9,"tag":10,"props":124,"children":125},{},[126,131],{"type":9,"tag":31,"props":127,"children":129},{"className":128},[],[130],{"type":14,"value":36},{"type":14,"value":132}," is not a substitute for handling errors — it is the safety net for the ones you didn't think of.",{"data":134,"body":135},{},{"type":6,"children":136},[137,149,158,163,172,184,189],{"type":9,"tag":26,"props":138,"children":140},{"id":139},"set-u-stop-on-an-undefined-variable",[141,147],{"type":9,"tag":31,"props":142,"children":144},{"className":143},[],[145],{"type":14,"value":146},"set -u",{"type":14,"value":148}," — stop on an undefined variable",{"type":9,"tag":61,"props":150,"children":153},{"className":151,"code":152,"language":14},[64],"set -u\n",[154],{"type":9,"tag":31,"props":155,"children":156},{"__ignoreMap":69},[157],{"type":14,"value":152},{"type":9,"tag":10,"props":159,"children":160},{},[161],{"type":14,"value":162},"Without it, a typo'd or unset variable expands to nothing, silently:",{"type":9,"tag":61,"props":164,"children":167},{"className":165,"code":166,"language":14},[64],"rm -rf \"$BUILD_DIR/\"      # BUILD_DIR unset  ->  rm -rf \"/\"\n",[168],{"type":9,"tag":31,"props":169,"children":170},{"__ignoreMap":69},[171],{"type":14,"value":166},{"type":9,"tag":10,"props":173,"children":174},{},[175,177,182],{"type":14,"value":176},"That is not a hypothetical; it is one of the most famous classes of shell disaster there is. With ",{"type":9,"tag":31,"props":178,"children":180},{"className":179},[],[181],{"type":14,"value":146},{"type":14,"value":183},", the script stops and names the variable instead.",{"type":9,"tag":10,"props":185,"children":186},{},[187],{"type":14,"value":188},"The two together, on line two of every script:",{"type":9,"tag":61,"props":190,"children":193},{"className":191,"code":192,"language":14},[64],"#!/bin/sh\nset -eu\n",[194],{"type":9,"tag":31,"props":195,"children":196},{"__ignoreMap":69},[197],{"type":14,"value":192},{"data":199,"body":200},{},{"type":6,"children":201},[202],{"type":9,"tag":203,"props":204,"children":209},"quiz",{":answer":205,":options":206,"explanation":207,"question":208},"0","[\"rsync is asked to copy to a local path `:/var/www/`, and the deploy silently goes nowhere\",\"The script stops with an undefined-variable error\",\"rsync refuses to run without a host\"]","The expansion becomes empty, so the destination is a plausible-looking local path. rsync succeeds, the script reports success, and nothing reaches the server. `set -u` turns this into an immediate, named error.","A deploy script without `set -u` contains `rsync -a ./dist/ \\\"$HOST:/var/www/\\\"`. `HOST` is misspelled at the point it was set. What happens?",[],{"data":211,"body":212},{},{"type":6,"children":213},[214,220,231,240,260,289],{"type":9,"tag":26,"props":215,"children":217},{"id":216},"quote-everything",[218],{"type":14,"value":219},"Quote everything",{"type":9,"tag":10,"props":221,"children":222},{},[223,225],{"type":14,"value":224},"The recurring theme of this course, stated once as a rule: ",{"type":9,"tag":226,"props":227,"children":228},"strong",{},[229],{"type":14,"value":230},"every variable expansion goes in double quotes unless you have a specific reason otherwise.",{"type":9,"tag":61,"props":232,"children":235},{"className":233,"code":234,"language":14},[64],"cp \"$SRC\" \"$DST\"\n[ -f \"$CONFIG\" ]\nfor f in \"$DIR\"/*; do ...\nrm -rf \"${BUILD_DIR:?BUILD_DIR is not set}\"\n",[236],{"type":9,"tag":31,"props":237,"children":238},{"__ignoreMap":69},[239],{"type":14,"value":234},{"type":9,"tag":10,"props":241,"children":242},{},[243,245,251,253,258],{"type":14,"value":244},"That last form is worth knowing on its own. ",{"type":9,"tag":31,"props":246,"children":248},{"className":247},[],[249],{"type":14,"value":250},"${VAR:?message}",{"type":14,"value":252}," expands to the value, or exits with your message if it is unset or empty — a per-variable ",{"type":9,"tag":31,"props":254,"children":256},{"className":255},[],[257],{"type":14,"value":146},{"type":14,"value":259}," for the lines where the consequences are worst.",{"type":9,"tag":10,"props":261,"children":262},{},[263,265,271,273,279,281,287],{"type":14,"value":264},"Related, from the variables lesson: ",{"type":9,"tag":31,"props":266,"children":268},{"className":267},[],[269],{"type":14,"value":270},"${VAR:-default}",{"type":14,"value":272}," supplies a fallback without assigning, ",{"type":9,"tag":31,"props":274,"children":276},{"className":275},[],[277],{"type":14,"value":278},"${VAR:=default}",{"type":14,"value":280}," supplies it ",{"type":9,"tag":282,"props":283,"children":284},"em",{},[285],{"type":14,"value":286},"and",{"type":14,"value":288}," sets the variable.",{"type":9,"tag":61,"props":290,"children":293},{"className":291,"code":292,"language":14},[64],"LOG_LEVEL=\"${LOG_LEVEL:-info}\"       # respect the environment, have an opinion\n",[294],{"type":9,"tag":31,"props":295,"children":296},{"__ignoreMap":69},[297],{"type":14,"value":292},{"data":299,"body":300},{},{"type":6,"children":301},[302,314,326,335,360],{"type":9,"tag":26,"props":303,"children":305},{"id":304},"trap-clean-up-whatever-happens",[306,312],{"type":9,"tag":31,"props":307,"children":309},{"className":308},[],[310],{"type":14,"value":311},"trap",{"type":14,"value":313}," — clean up whatever happens",{"type":9,"tag":10,"props":315,"children":316},{},[317,319,324],{"type":14,"value":318},"A script that creates a temporary file will, sooner or later, be killed before it removes it. ",{"type":9,"tag":31,"props":320,"children":322},{"className":321},[],[323],{"type":14,"value":311},{"type":14,"value":325}," runs a command when the shell receives a signal or exits:",{"type":9,"tag":61,"props":327,"children":330},{"className":328,"code":329,"language":14},[64],"#!/bin/sh\nset -eu\n\nTMPDIR=$(mktemp -d)\ntrap 'rm -rf \"$TMPDIR\"' EXIT\n\n# ... use \"$TMPDIR\" freely ...\n",[331],{"type":9,"tag":31,"props":332,"children":333},{"__ignoreMap":69},[334],{"type":14,"value":329},{"type":9,"tag":10,"props":336,"children":337},{},[338,344,346,351,353,358],{"type":9,"tag":31,"props":339,"children":341},{"className":340},[],[342],{"type":14,"value":343},"EXIT",{"type":14,"value":345}," fires on ",{"type":9,"tag":282,"props":347,"children":348},{},[349],{"type":14,"value":350},"every",{"type":14,"value":352}," exit path — success, failure, ",{"type":9,"tag":31,"props":354,"children":356},{"className":355},[],[357],{"type":14,"value":36},{"type":14,"value":359}," bailing out, a Ctrl-C. One line, and the temporary directory cannot leak.",{"type":9,"tag":10,"props":361,"children":362},{},[363,365,371,373,379,381,387,389,394],{"type":14,"value":364},"Two habits go with it. Use ",{"type":9,"tag":31,"props":366,"children":368},{"className":367},[],[369],{"type":14,"value":370},"mktemp",{"type":14,"value":372}," rather than a fixed path like ",{"type":9,"tag":31,"props":374,"children":376},{"className":375},[],[377],{"type":14,"value":378},"/tmp/build",{"type":14,"value":380},": a predictable name in a world-writable directory is both a collision and a security problem, since another user can create it first as a symlink to something you will then overwrite. And use single quotes in the trap so ",{"type":9,"tag":31,"props":382,"children":384},{"className":383},[],[385],{"type":14,"value":386},"$TMPDIR",{"type":14,"value":388}," is expanded when the trap ",{"type":9,"tag":282,"props":390,"children":391},{},[392],{"type":14,"value":393},"fires",{"type":14,"value":395},", not when it is installed.",{"data":397,"body":398},{},{"type":6,"children":399},[400],{"type":9,"tag":203,"props":401,"children":405},{":answer":205,":options":402,"explanation":403,"question":404},"[\"Single quotes defer the expansion to when the trap runs, so it uses the value at that moment\",\"Double quotes are not valid in a trap argument\",\"It makes no difference here\"]","With double quotes the value is baked in at the moment `trap` is called. Usually identical — but if the script reassigns `TMPDIR` later, the double-quoted version deletes the old directory and leaks the new one. Deferring is the safer default.","Why `trap 'rm -rf \"$TMPDIR\"' EXIT` with single quotes rather than double?",[],{"data":407,"body":408},{},{"type":6,"children":409},[410,416,421,430],{"type":9,"tag":26,"props":411,"children":413},{"id":412},"make-it-re-runnable",[414],{"type":14,"value":415},"Make it re-runnable",{"type":9,"tag":10,"props":417,"children":418},{},[419],{"type":14,"value":420},"A script that only works on a clean machine will be run twice eventually — after a failure halfway through, which is exactly when you least want a second failure mode.",{"type":9,"tag":61,"props":422,"children":425},{"className":423,"code":424,"language":14},[64],"mkdir -p \"$DEST\"                          # not an error if it exists\nln -sfn \"$RELEASE\" /var/www/current       # replaces an existing link\ngrep -q \"^export PATH\" ~/.profile || echo \"export PATH=...\" >> ~/.profile\n",[426],{"type":9,"tag":31,"props":427,"children":428},{"__ignoreMap":69},[429],{"type":14,"value":424},{"type":9,"tag":10,"props":431,"children":432},{},[433,435,440],{"type":14,"value":434},"The last one is the general pattern: ",{"type":9,"tag":226,"props":436,"children":437},{},[438],{"type":14,"value":439},"check, then act",{"type":14,"value":441},". It is the difference between a script you run and one you can run.",{"data":443,"body":444},{},{"type":6,"children":445},[446,452,461],{"type":9,"tag":26,"props":447,"children":449},{"id":448},"say-what-went-wrong-on-the-right-stream",[450],{"type":14,"value":451},"Say what went wrong, on the right stream",{"type":9,"tag":61,"props":453,"children":456},{"className":454,"code":455,"language":14},[64],"die() {\n  echo \"ERROR: $*\" >&2\n  exit 1\n}\n\n[ -f \"$CONFIG\" ] || die \"config not found: $CONFIG\"\n",[457],{"type":9,"tag":31,"props":458,"children":459},{"__ignoreMap":69},[460],{"type":14,"value":455},{"type":9,"tag":10,"props":462,"children":463},{},[464,466,472,474,480],{"type":14,"value":465},"Errors go to stderr, so they stay visible when output is piped or captured. Messages name the actual value — ",{"type":9,"tag":31,"props":467,"children":469},{"className":468},[],[470],{"type":14,"value":471},"config not found: /etc/app.conf",{"type":14,"value":473}," is diagnosable; ",{"type":9,"tag":31,"props":475,"children":477},{"className":476},[],[478],{"type":14,"value":479},"something went wrong",{"type":14,"value":481}," is not.",{"data":483,"body":484},{},{"type":6,"children":485},[486,492,497,506],{"type":9,"tag":26,"props":487,"children":489},{"id":488},"the-skeleton",[490],{"type":14,"value":491},"The skeleton",{"type":9,"tag":10,"props":493,"children":494},{},[495],{"type":14,"value":496},"Putting the whole course together, this is what a script worth trusting looks like:",{"type":9,"tag":61,"props":498,"children":501},{"className":499,"code":500,"language":14},[64],"#!/bin/sh\n#\n# release.sh — build the site and publish it to a host.\n# Usage: release.sh [-v] HOST\n\nset -eu\n\nVERBOSE=0\n\nlog() { echo \"[$(date +%H:%M:%S)] $*\"; }\ndie() { echo \"ERROR: $*\" >&2; exit 1; }\n\nusage() {\n  cat \u003C\u003CEOF\nUsage: $(basename \"$0\") [-v] HOST\n\n  -v    verbose output\n  -h    show this message\nEOF\n}\n\nmain() {\n  while [ \"$#\" -gt 0 ]; do\n    case \"$1\" in\n      -v) VERBOSE=1; shift ;;\n      -h) usage; exit 0 ;;\n      -*) die \"unknown option: $1\" ;;\n      *)  break ;;\n    esac\n  done\n\n  [ \"$#\" -eq 1 ] || { usage >&2; exit 1; }\n  HOST=\"$1\"\n\n  command -v rsync >/dev/null 2>&1 || die \"rsync is not installed\"\n\n  WORK=$(mktemp -d)\n  trap 'rm -rf \"$WORK\"' EXIT\n\n  log \"Building into $WORK\"\n  npm run build --silent -- --out \"$WORK\" || die \"build failed\"\n\n  [ -n \"$(ls -A \"$WORK\")\" ] || die \"build produced nothing\"\n\n  log \"Publishing to $HOST\"\n  rsync -a --delete \"$WORK/\" \"$HOST:/var/www/site/\" || die \"rsync to $HOST failed\"\n\n  log \"Done\"\n}\n\nmain \"$@\"\n",[502],{"type":9,"tag":31,"props":503,"children":504},{"__ignoreMap":69},[505],{"type":14,"value":500},{"type":9,"tag":10,"props":507,"children":508},{},[509,511,517,519,524,526,532,534,540],{"type":14,"value":510},"Nothing in it is advanced. It is ",{"type":9,"tag":31,"props":512,"children":514},{"className":513},[],[515],{"type":14,"value":516},"set -eu",{"type":14,"value":518},", quoted expansions, a ",{"type":9,"tag":31,"props":520,"children":522},{"className":521},[],[523],{"type":14,"value":311},{"type":14,"value":525},", a ",{"type":9,"tag":31,"props":527,"children":529},{"className":528},[],[530],{"type":14,"value":531},"case",{"type":14,"value":533}," loop over the arguments, two helper functions, and one check that the build actually produced something before it was allowed to ",{"type":9,"tag":31,"props":535,"children":537},{"className":536},[],[538],{"type":14,"value":539},"--delete",{"type":14,"value":541}," on a live server.",{"data":543,"body":544},{},{"type":6,"children":545},[546],{"type":9,"tag":547,"props":548,"children":550},"deep-dive",{"title":549},"ShellCheck",[551,564,573,578],{"type":9,"tag":10,"props":552,"children":553},{},[554,556,562],{"type":14,"value":555},"Install ",{"type":9,"tag":31,"props":557,"children":559},{"className":558},[],[560],{"type":14,"value":561},"shellcheck",{"type":14,"value":563}," and run it on everything you write. It is a static analyser for shell, and it is unusually good — most of this lesson is in its rule set:",{"type":9,"tag":61,"props":565,"children":568},{"className":566,"code":567,"language":14},[64],"$ shellcheck release.sh\n\nIn release.sh line 41:\n  rsync -a --delete $WORK/ \"$HOST:/var/www/site/\"\n                    ^-----^ SC2086: Double quote to prevent globbing\n                            and word splitting.\n",[569],{"type":9,"tag":31,"props":570,"children":571},{"__ignoreMap":69},[572],{"type":14,"value":567},{"type":9,"tag":10,"props":574,"children":575},{},[576],{"type":14,"value":577},"Every warning has a code, and every code has a wiki page explaining the failure mode with an example. Reading a dozen of them will teach you more about the shell's edge cases than any tutorial, this one included.",{"type":9,"tag":10,"props":579,"children":580},{},[581],{"type":14,"value":582},"It is also the cheapest possible CI step. One line in a pipeline catches the unquoted expansion before it reaches a production host, which is not a thing you can say about many linters.",{"data":584,"body":585},{},{"type":6,"children":586},[587],{"type":9,"tag":588,"props":589,"children":594},"fill-blank",{":answer":590,"hint":591,"placeholder":592,"prompt":593},"[\"WORK=$(mktemp -d)\",\"WORK=\\\"$(mktemp -d)\\\"\",\"WORK=`mktemp -d`\"]","One command makes the directory and prints its path; capture that.","WORK=...","Create a temporary directory, storing its path in `WORK`.",[],{"data":596,"body":597},{},{"type":6,"children":598},[599,605,625,637,642],{"type":9,"tag":26,"props":600,"children":602},{"id":601},"where-to-go-from-here",[603],{"type":14,"value":604},"Where to go from here",{"type":9,"tag":10,"props":606,"children":607},{},[608,610,616,618,623],{"type":14,"value":609},"You now have the whole language: variables, quoting, exit status, ",{"type":9,"tag":31,"props":611,"children":613},{"className":612},[],[614],{"type":14,"value":615},"test",{"type":14,"value":617},", loops, ",{"type":9,"tag":31,"props":619,"children":621},{"className":620},[],[622],{"type":14,"value":531},{"type":14,"value":624},", positional parameters, command substitution, functions, and redirection. That is genuinely all of it — POSIX shell is a small language, which is why it has outlived almost everything built to replace it.",{"type":9,"tag":10,"props":626,"children":627},{},[628,630,635],{"type":14,"value":629},"What is left is judgement, and most of that is one question: ",{"type":9,"tag":226,"props":631,"children":632},{},[633],{"type":14,"value":634},"is this still a shell script?",{"type":14,"value":636}," The shell is superb at running programs and moving data between them. It is poor at data structures, arithmetic beyond integers, string manipulation, error handling with any nuance, and anything you would want to unit test. When a script starts wanting those things, it is telling you it should be a program in another language.",{"type":9,"tag":10,"props":638,"children":639},{},[640],{"type":14,"value":641},"Somewhere around two hundred lines, or the first time you reach for an associative array, is the usual moment. Until then, the shell is the shortest path from a problem to a running solution, and now you can write one that holds up.",{"type":9,"tag":10,"props":643,"children":644},{},[645,647,652,654,663,665,670],{"type":14,"value":646},"Two things worth doing next: read Steve Parker's ",{"type":9,"tag":282,"props":648,"children":649},{},[650],{"type":14,"value":651},"Shell Scripting Tutorial",{"type":14,"value":653}," at ",{"type":9,"tag":655,"props":656,"children":660},"a",{"href":657,"rel":658},"https://www.shellscript.sh",[659],"nofollow",[661],{"type":14,"value":662},"shellscript.sh",{"type":14,"value":664}," for a second pass over the same ground in a different voice, and run ",{"type":9,"tag":31,"props":666,"children":668},{"className":667},[],[669],{"type":14,"value":561},{"type":14,"value":671}," over the scripts already on your machine. The second one is more educational than it sounds.",1787908866622]