[{"data":1,"prerenderedAt":586},["ShallowReactive",2],{"mdc--5ojok4-key":3,"mdc--7pqbec-key":24,"mdc-1p0xer-key":106,"mdc--bf86vz-key":118,"mdc-kx98m9-key":218,"mdc-io256w-key":230,"mdc--bxtkn1-key":340,"mdc--9qjn52-key":350,"mdc--7otglr-key":393,"mdc--dmzeko-key":532},{"data":4,"body":5},{},{"type":6,"children":7},"root",[8],{"type":9,"tag":10,"props":11,"children":12},"element","p",{},[13,16,22],{"type":14,"value":15},"text","Once a script passes about fifty lines, the same five commands start appearing in three places. A ",{"type":9,"tag":17,"props":18,"children":19},"strong",{},[20],{"type":14,"value":21},"function",{"type":14,"value":23}," gives that block a name, and turns a wall of commands into something with structure.",{"data":25,"body":26},{},{"type":6,"children":27},[28,35,48,57,62,96],{"type":9,"tag":29,"props":30,"children":32},"h2",{"id":31},"defining-and-calling",[33],{"type":14,"value":34},"Defining and calling",{"type":9,"tag":36,"props":37,"children":41},"pre",{"className":38,"code":40,"language":14},[39],"language-text","#!/bin/sh\n\nlog() {\n  echo \"[$(date +%H:%M:%S)] $1\"\n}\n\nlog \"Starting\"\nlog \"Done\"\n",[42],{"type":9,"tag":43,"props":44,"children":46},"code",{"__ignoreMap":45},"",[47],{"type":14,"value":40},{"type":9,"tag":36,"props":49,"children":52},{"className":50,"code":51,"language":14},[39],"[09:14:22] Starting\n[09:14:22] Done\n",[53],{"type":9,"tag":43,"props":54,"children":55},{"__ignoreMap":45},[56],{"type":14,"value":51},{"type":9,"tag":10,"props":58,"children":59},{},[60],{"type":14,"value":61},"Two things to notice, both of which are the shell being consistent rather than special.",{"type":9,"tag":10,"props":63,"children":64},{},[65,70,72,78,80,86,88,94],{"type":9,"tag":17,"props":66,"children":67},{},[68],{"type":14,"value":69},"A function is called like a command",{"type":14,"value":71},", with no parentheses and no commas — ",{"type":9,"tag":43,"props":73,"children":75},{"className":74},[],[76],{"type":14,"value":77},"log \"Starting\"",{"type":14,"value":79},", not ",{"type":9,"tag":43,"props":81,"children":83},{"className":82},[],[84],{"type":14,"value":85},"log(\"Starting\")",{"type":14,"value":87},". As far as the rest of the script is concerned it ",{"type":9,"tag":89,"props":90,"children":91},"em",{},[92],{"type":14,"value":93},"is",{"type":14,"value":95}," a command, and it shadows an external program of the same name.",{"type":9,"tag":10,"props":97,"children":98},{},[99,104],{"type":9,"tag":17,"props":100,"children":101},{},[102],{"type":14,"value":103},"It must be defined before it is called.",{"type":14,"value":105}," The shell reads a script top to bottom; a function that appears after the line that calls it does not exist yet. Convention is to put them all near the top, after the shebang.",{"data":107,"body":108},{},{"type":6,"children":109},[110],{"type":9,"tag":111,"props":112,"children":117},"quiz",{":answer":113,":options":114,"explanation":115,"question":116},"0","[\"It prints \\\"nope\\\" — a function takes precedence over an external program\",\"The real `/usr/bin/ls` runs; functions cannot shadow commands\",\"The shell reports an ambiguous command error\"]","Lookup order is builtins and functions first, then `PATH`. Handy for stubbing something out in a test script, and a real hazard when a helper accidentally takes the name of a tool the script uses later. `command ls` bypasses the function and runs the external one.","A script defines a function named `ls` that prints \"nope\". What happens on the next line, which runs `ls`?",[],{"data":119,"body":120},{},{"type":6,"children":121},[122,128,171,180,191,196,205],{"type":9,"tag":29,"props":123,"children":125},{"id":124},"arguments-work-exactly-as-they-do-for-scripts",[126],{"type":14,"value":127},"Arguments work exactly as they do for scripts",{"type":9,"tag":10,"props":129,"children":130},{},[131,133,139,141,147,148,154,156,162,164,169],{"type":14,"value":132},"Inside a function, ",{"type":9,"tag":43,"props":134,"children":136},{"className":135},[],[137],{"type":14,"value":138},"$1",{"type":14,"value":140},", ",{"type":9,"tag":43,"props":142,"children":144},{"className":143},[],[145],{"type":14,"value":146},"$2",{"type":14,"value":140},{"type":9,"tag":43,"props":149,"children":151},{"className":150},[],[152],{"type":14,"value":153},"$#",{"type":14,"value":155},", and ",{"type":9,"tag":43,"props":157,"children":159},{"className":158},[],[160],{"type":14,"value":161},"\"$@\"",{"type":14,"value":163}," refer to the ",{"type":9,"tag":17,"props":165,"children":166},{},[167],{"type":14,"value":168},"function's",{"type":14,"value":170}," arguments, not the script's:",{"type":9,"tag":36,"props":172,"children":175},{"className":173,"code":174,"language":14},[39],"greet() {\n  echo \"Hello, $1. You gave me $# argument(s).\"\n}\n\ngreet Ada\nHello, Ada. You gave me 1 argument(s).\n",[176],{"type":9,"tag":43,"props":177,"children":178},{"__ignoreMap":45},[179],{"type":14,"value":174},{"type":9,"tag":10,"props":181,"children":182},{},[183,189],{"type":9,"tag":43,"props":184,"children":186},{"className":185},[],[187],{"type":14,"value":188},"$0",{"type":14,"value":190}," is the exception — it stays the script's name, since a function doesn't have one of its own.",{"type":9,"tag":10,"props":192,"children":193},{},[194],{"type":14,"value":195},"If you need the script's arguments inside a function, pass them through explicitly:",{"type":9,"tag":36,"props":197,"children":200},{"className":198,"code":199,"language":14},[39],"main() {\n  echo \"Script was called with: $@\"\n}\nmain \"$@\"\n",[201],{"type":9,"tag":43,"props":202,"children":203},{"__ignoreMap":45},[204],{"type":14,"value":199},{"type":9,"tag":10,"props":206,"children":207},{},[208,210,216],{"type":14,"value":209},"That last line is a common idiom: define everything as functions, then hand the whole argument list to ",{"type":9,"tag":43,"props":211,"children":213},{"className":212},[],[214],{"type":14,"value":215},"main",{"type":14,"value":217}," at the bottom. It makes the script's entry point obvious and keeps the top-level free of stray logic.",{"data":219,"body":220},{},{"type":6,"children":221},[222],{"type":9,"tag":223,"props":224,"children":229},"fill-blank",{":answer":225,"hint":226,"placeholder":227,"prompt":228},"[\"deploy \\\"$TARGET\\\"\",\"deploy $TARGET\"]","A function is called like any other command — and quote the expansion.","deploy ...","Call a function named `deploy`, passing it the value of the variable `TARGET` as its only argument.",[],{"data":231,"body":232},{},{"type":6,"children":233},[234,240,245,263,272,298,310,319],{"type":9,"tag":29,"props":235,"children":237},{"id":236},"returning-status-not-values",[238],{"type":14,"value":239},"Returning: status, not values",{"type":9,"tag":10,"props":241,"children":242},{},[243],{"type":14,"value":244},"This is where shell functions diverge from every other language you know.",{"type":9,"tag":10,"props":246,"children":247},{},[248,254,256,261],{"type":9,"tag":43,"props":249,"children":251},{"className":250},[],[252],{"type":14,"value":253},"return",{"type":14,"value":255}," sets an ",{"type":9,"tag":17,"props":257,"children":258},{},[259],{"type":14,"value":260},"exit status",{"type":14,"value":262}," — a number from 0 to 255 — not a value:",{"type":9,"tag":36,"props":264,"children":267},{"className":265,"code":266,"language":14},[39],"is_installed() {\n  command -v \"$1\" >/dev/null 2>&1\n}\n\nif is_installed docker; then\n  echo \"docker is available\"\nfi\n",[268],{"type":9,"tag":43,"props":269,"children":270},{"__ignoreMap":45},[271],{"type":14,"value":266},{"type":9,"tag":10,"props":273,"children":274},{},[275,277,282,284,290,292,296],{"type":14,"value":276},"No ",{"type":9,"tag":43,"props":278,"children":280},{"className":279},[],[281],{"type":14,"value":253},{"type":14,"value":283}," needed there at all: a function's status is the status of its last command, and ",{"type":9,"tag":43,"props":285,"children":287},{"className":286},[],[288],{"type":14,"value":289},"command -v",{"type":14,"value":291}," already exits 0 or 1. The function reads as a predicate because it ",{"type":9,"tag":89,"props":293,"children":294},{},[295],{"type":14,"value":93},{"type":14,"value":297}," one.",{"type":9,"tag":10,"props":299,"children":300},{},[301,303,308],{"type":14,"value":302},"To return a ",{"type":9,"tag":17,"props":304,"children":305},{},[306],{"type":14,"value":307},"value",{"type":14,"value":309},", you print it and the caller captures it:",{"type":9,"tag":36,"props":311,"children":314},{"className":312,"code":313,"language":14},[39],"config_path() {\n  if [ -f \"./app.conf\" ]; then\n    echo \"./app.conf\"\n  else\n    echo \"/etc/app.conf\"\n  fi\n}\n\nCONFIG=$(config_path)\n",[315],{"type":9,"tag":43,"props":316,"children":317},{"__ignoreMap":45},[318],{"type":14,"value":313},{"type":9,"tag":10,"props":320,"children":321},{},[322,324,330,332,338],{"type":14,"value":323},"Which has an important consequence: anything a function prints becomes part of its \"return value\" when called this way. A stray progress message inside ",{"type":9,"tag":43,"props":325,"children":327},{"className":326},[],[328],{"type":14,"value":329},"config_path",{"type":14,"value":331}," would end up concatenated into ",{"type":9,"tag":43,"props":333,"children":335},{"className":334},[],[336],{"type":14,"value":337},"$CONFIG",{"type":14,"value":339},".",{"data":341,"body":342},{},{"type":6,"children":343},[344],{"type":9,"tag":111,"props":345,"children":349},{":answer":113,":options":346,"explanation":347,"question":348},"[\"`return` sets an exit status, which command substitution doesn't capture — only printed output is captured\",\"Command substitution only works on external programs\",\"`$TOTAL` is out of scope by the time `return` runs\"]","`return` and `$(...)` are two different channels. The status is in `$?`; the substitution captures standard output. Print the value with `echo` — and note that a total over 255 wouldn't survive `return` anyway, since exit statuses wrap at 256.","A function computes a total and ends with `return $TOTAL`. The caller does `SUM=$(add_up)`. Why is `SUM` empty?",[],{"data":351,"body":352},{},{"type":6,"children":353},[354,360,365,374],{"type":9,"tag":29,"props":355,"children":357},{"id":356},"variables-are-global-by-default",[358],{"type":14,"value":359},"Variables are global by default",{"type":9,"tag":10,"props":361,"children":362},{},[363],{"type":14,"value":364},"Every variable a function sets is visible to the whole script, and every variable the script has set is visible inside it:",{"type":9,"tag":36,"props":366,"children":369},{"className":367,"code":368,"language":14},[39],"COUNT=0\n\nbump() {\n  COUNT=$((COUNT + 1))     # modifies the outer COUNT\n}\n\nbump; bump\necho \"$COUNT\"              # 2\n",[370],{"type":9,"tag":43,"props":371,"children":372},{"__ignoreMap":45},[373],{"type":14,"value":368},{"type":9,"tag":10,"props":375,"children":376},{},[377,379,385,387,392],{"type":14,"value":378},"Sometimes that is exactly what you want — it is how a function accumulates a result without printing it. More often it is an accident waiting to happen, because a helper using ",{"type":9,"tag":43,"props":380,"children":382},{"className":381},[],[383],{"type":14,"value":384},"i",{"type":14,"value":386}," as a loop counter will quietly destroy the caller's ",{"type":9,"tag":43,"props":388,"children":390},{"className":389},[],[391],{"type":14,"value":384},{"type":14,"value":339},{"data":394,"body":395},{},{"type":6,"children":396},[397],{"type":9,"tag":398,"props":399,"children":401},"deep-dive",{"title":400},"`local`, and what to do without it",[402,415,424,444,449,482,508,517,522],{"type":9,"tag":10,"props":403,"children":404},{},[405,407,413],{"type":14,"value":406},"Bash, ksh, dash, and BusyBox ash all support ",{"type":9,"tag":43,"props":408,"children":410},{"className":409},[],[411],{"type":14,"value":412},"local",{"type":14,"value":414},":",{"type":9,"tag":36,"props":416,"children":419},{"className":417,"code":418,"language":14},[39],"process() {\n  local i\n  local tmp\n  for i in 1 2 3; do tmp=\"$i\"; done\n}\n",[420],{"type":9,"tag":43,"props":421,"children":422},{"__ignoreMap":45},[423],{"type":14,"value":418},{"type":9,"tag":10,"props":425,"children":426},{},[427,429,435,437,442],{"type":14,"value":428},"It is not in POSIX. In practice every shell you will meet has it, and using it is the right call for anything shipping to Linux — but it means a script with ",{"type":9,"tag":43,"props":430,"children":432},{"className":431},[],[433],{"type":14,"value":434},"#!/bin/sh",{"type":14,"value":436}," and ",{"type":9,"tag":43,"props":438,"children":440},{"className":439},[],[441],{"type":14,"value":412},{"type":14,"value":443}," in it is making an assumption, and should say so in a comment.",{"type":9,"tag":10,"props":445,"children":446},{},[447],{"type":14,"value":448},"If you genuinely need strict POSIX, the alternatives are:",{"type":9,"tag":10,"props":450,"children":451},{},[452,457,459,465,467,473,474,480],{"type":9,"tag":17,"props":453,"children":454},{},[455],{"type":14,"value":456},"Prefix your names.",{"type":14,"value":458}," A helper called ",{"type":9,"tag":43,"props":460,"children":462},{"className":461},[],[463],{"type":14,"value":464},"retry",{"type":14,"value":466}," uses ",{"type":9,"tag":43,"props":468,"children":470},{"className":469},[],[471],{"type":14,"value":472},"_retry_count",{"type":14,"value":140},{"type":9,"tag":43,"props":475,"children":477},{"className":476},[],[478],{"type":14,"value":479},"_retry_max",{"type":14,"value":481},". Ugly, but it works everywhere and makes the ownership obvious in a stack trace.",{"type":9,"tag":10,"props":483,"children":484},{},[485,490,492,498,500,506],{"type":9,"tag":17,"props":486,"children":487},{},[488],{"type":14,"value":489},"Use a subshell.",{"type":14,"value":491}," Wrap the body in ",{"type":9,"tag":43,"props":493,"children":495},{"className":494},[],[496],{"type":14,"value":497},"( ... )",{"type":14,"value":499}," instead of ",{"type":9,"tag":43,"props":501,"children":503},{"className":502},[],[504],{"type":14,"value":505},"{ ... }",{"type":14,"value":507}," and every variable is contained, because it runs in a child process:",{"type":9,"tag":36,"props":509,"children":512},{"className":510,"code":511,"language":14},[39],"process() (\n  i=1                       # cannot escape this subshell\n  echo \"$i\"\n)\n",[513],{"type":9,"tag":43,"props":514,"children":515},{"__ignoreMap":45},[516],{"type":14,"value":511},{"type":9,"tag":10,"props":518,"children":519},{},[520],{"type":14,"value":521},"The cost is that the function now cannot set anything for the caller at all, and pays a fork on every call.",{"type":9,"tag":10,"props":523,"children":524},{},[525,530],{"type":9,"tag":17,"props":526,"children":527},{},[528],{"type":14,"value":529},"Save and restore.",{"type":14,"value":531}," Verbose enough that nobody does it, but it is the fully portable answer.",{"data":533,"body":534},{},{"type":6,"children":535},[536,542,547,556,581],{"type":9,"tag":29,"props":537,"children":539},{"id":538},"what-functions-are-actually-for",[540],{"type":14,"value":541},"What functions are actually for",{"type":9,"tag":10,"props":543,"children":544},{},[545],{"type":14,"value":546},"The version worth writing is not the clever one — it is the one that makes the script's shape visible:",{"type":9,"tag":36,"props":548,"children":551},{"className":549,"code":550,"language":14},[39],"#!/bin/sh\nset -eu\n\nAPP_DIR=/opt/app\n\nlog()  { echo \"[$(date +%H:%M:%S)] $*\"; }\ndie()  { echo \"ERROR: $*\" >&2; exit 1; }\n\nrequire() {\n  command -v \"$1\" >/dev/null 2>&1 || die \"$1 is not installed\"\n}\n\nbuild() {\n  log \"Building\"\n  cd \"$APP_DIR\" || die \"no such directory: $APP_DIR\"\n  npm run build || die \"build failed\"\n}\n\ndeploy() {\n  log \"Deploying to $1\"\n  rsync -a ./dist/ \"$1:/var/www/site/\" || die \"rsync to $1 failed\"\n}\n\nmain() {\n  [ \"$#\" -ge 1 ] || die \"usage: $(basename \"$0\") HOST...\"\n\n  require npm\n  require rsync\n\n  build\n  for HOST in \"$@\"; do\n    deploy \"$HOST\"\n  done\n  log \"All done\"\n}\n\nmain \"$@\"\n",[552],{"type":9,"tag":43,"props":553,"children":554},{"__ignoreMap":45},[555],{"type":14,"value":550},{"type":9,"tag":10,"props":557,"children":558},{},[559,565,566,572,574,579],{"type":9,"tag":43,"props":560,"children":562},{"className":561},[],[563],{"type":14,"value":564},"log",{"type":14,"value":436},{"type":9,"tag":43,"props":567,"children":569},{"className":568},[],[570],{"type":14,"value":571},"die",{"type":14,"value":573}," are the two functions worth putting in every script you write. ",{"type":9,"tag":43,"props":575,"children":577},{"className":576},[],[578],{"type":14,"value":571},{"type":14,"value":580}," in particular — printing to standard error and exiting in one word — removes the temptation to let a failure slide past because handling it properly would have taken three lines.",{"type":9,"tag":10,"props":582,"children":583},{},[584],{"type":14,"value":585},"Next up: redirection and here-documents — controlling where a script's input and output actually go.",1787908866327]