[{"data":1,"prerenderedAt":837},["ShallowReactive",2],{"mdc--pxjvmw-key":3,"mdc-r0t0s4-key":33,"mdc-z11413-key":132,"mdc-1ecd72-key":198,"mdc--en73sl-key":207,"mdc-lq78d1-key":219,"mdc--4z1m85-key":312,"mdc--xzs501-key":512,"mdc-clgknv-key":522,"mdc--wxpbd1-key":651,"mdc-ovpkvj-key":820},{"data":4,"body":5},{},{"type":6,"children":7},"root",[8],{"type":9,"tag":10,"props":11,"children":12},"element","p",{},[13,16,23,25,31],{"type":14,"value":15},"text","Two things that belong together: ",{"type":9,"tag":17,"props":18,"children":20},"code",{"className":19},[],[21],{"type":14,"value":22},"case",{"type":14,"value":24},", the shell's way of branching on a value, and the ",{"type":9,"tag":26,"props":27,"children":28},"strong",{},[29],{"type":14,"value":30},"positional parameters",{"type":14,"value":32}," — how a script gets hold of what it was called with. Put them side by side and you have the shape of nearly every command-line tool ever written in shell.",{"data":34,"body":35},{},{"type":6,"children":36},[37,55,67,112],{"type":9,"tag":38,"props":39,"children":41},"h2",{"id":40},"case-beats-a-stack-of-elif",[42,47,49],{"type":9,"tag":17,"props":43,"children":45},{"className":44},[],[46],{"type":14,"value":22},{"type":14,"value":48}," beats a stack of ",{"type":9,"tag":17,"props":50,"children":52},{"className":51},[],[53],{"type":14,"value":54},"elif",{"type":9,"tag":56,"props":57,"children":61},"pre",{"className":58,"code":60,"language":14},[59],"language-text","case \"$1\" in\n  start)\n    echo \"Starting\"\n    ;;\n  stop)\n    echo \"Stopping\"\n    ;;\n  restart)\n    echo \"Restarting\"\n    ;;\n  *)\n    echo \"Usage: $0 {start|stop|restart}\" >&2\n    exit 1\n    ;;\nesac\n",[62],{"type":9,"tag":17,"props":63,"children":65},{"__ignoreMap":64},"",[66],{"type":14,"value":60},{"type":9,"tag":10,"props":68,"children":69},{},[70,72,78,80,86,88,94,96,102,104,110],{"type":14,"value":71},"The parts: ",{"type":9,"tag":17,"props":73,"children":75},{"className":74},[],[76],{"type":14,"value":77},"case VALUE in",{"type":14,"value":79},", then one or more ",{"type":9,"tag":17,"props":81,"children":83},{"className":82},[],[84],{"type":14,"value":85},"PATTERN)",{"type":14,"value":87}," branches, each ending in ",{"type":9,"tag":17,"props":89,"children":91},{"className":90},[],[92],{"type":14,"value":93},";;",{"type":14,"value":95},", closed by ",{"type":9,"tag":17,"props":97,"children":99},{"className":98},[],[100],{"type":14,"value":101},"esac",{"type":14,"value":103},". Matching is top to bottom, first match wins, and ",{"type":9,"tag":17,"props":105,"children":107},{"className":106},[],[108],{"type":14,"value":109},"*",{"type":14,"value":111}," at the end is the catch-all.",{"type":9,"tag":10,"props":113,"children":114},{},[115,117,122,124,130],{"type":14,"value":116},"The ",{"type":9,"tag":17,"props":118,"children":120},{"className":119},[],[121],{"type":14,"value":93},{"type":14,"value":123}," is not optional and not a typo. A single ",{"type":9,"tag":17,"props":125,"children":127},{"className":126},[],[128],{"type":14,"value":129},";",{"type":14,"value":131}," would just be a command separator inside the branch — the doubled form is what says \"this branch is over\".",{"data":133,"body":134},{},{"type":6,"children":135},[136,142,154,163],{"type":9,"tag":38,"props":137,"children":139},{"id":138},"the-patterns-are-globs-not-regular-expressions",[140],{"type":14,"value":141},"The patterns are globs, not regular expressions",{"type":9,"tag":10,"props":143,"children":144},{},[145,147,152],{"type":14,"value":146},"This is the part that makes ",{"type":9,"tag":17,"props":148,"children":150},{"className":149},[],[151],{"type":14,"value":22},{"type":14,"value":153}," more useful than it first looks. The patterns are the same wildcards as filenames:",{"type":9,"tag":56,"props":155,"children":158},{"className":156,"code":157,"language":14},[59],"case \"$FILE\" in\n  *.tar.gz|*.tgz)  tar -xzf \"$FILE\" ;;\n  *.tar.bz2)       tar -xjf \"$FILE\" ;;\n  *.zip)           unzip \"$FILE\" ;;\n  *.gz)            gunzip \"$FILE\" ;;\n  *)               echo \"Don't know how to extract $FILE\" >&2; exit 1 ;;\nesac\n",[159],{"type":9,"tag":17,"props":160,"children":161},{"__ignoreMap":64},[162],{"type":14,"value":157},{"type":9,"tag":10,"props":164,"children":165},{},[166,172,174,180,182,188,190,196],{"type":9,"tag":17,"props":167,"children":169},{"className":168},[],[170],{"type":14,"value":171},"|",{"type":14,"value":173}," separates alternatives within one branch. ",{"type":9,"tag":17,"props":175,"children":177},{"className":176},[],[178],{"type":14,"value":179},"?",{"type":14,"value":181},", ",{"type":9,"tag":17,"props":183,"children":185},{"className":184},[],[186],{"type":14,"value":187},"[a-z]",{"type":14,"value":189},", and ",{"type":9,"tag":17,"props":191,"children":193},{"className":192},[],[194],{"type":14,"value":195},"[!...]",{"type":14,"value":197}," all work as they do in globbing.",{"data":199,"body":200},{},{"type":6,"children":201},[202],{"type":9,"tag":203,"props":204,"children":206},"terminal-teaser",{":lines":205},"[{\"cmd\":\"./extract.sh site.tar.gz\",\"out\":\"Extracting as gzipped tar\"},{\"cmd\":\"./extract.sh photos.zip\",\"out\":\"Extracting as zip\"},{\"cmd\":\"./extract.sh notes.txt\",\"out\":\"Don't know how to extract notes.txt\"}]",[],{"data":208,"body":209},{},{"type":6,"children":210},[211],{"type":9,"tag":212,"props":213,"children":218},"quiz",{":answer":214,":options":215,"explanation":216,"question":217},"0","[\"Anything starting with an upper- or lower-case Y — \\\"y\\\", \\\"Yes\\\", \\\"yeah\\\"\",\"Only the exact strings \\\"Y\\\" and \\\"y\\\"\",\"Only \\\"Y*\\\" and \\\"y*\\\" literally\"]","`[Yy]` matches one character from the set, and `*` matches whatever follows, including nothing. It's the standard way to accept a yes/no answer without caring how the user capitalised it.","In `case \"$X\" in [Yy]*) echo yes ;; esac`, which values of `X` print \"yes\"?",[],{"data":220,"body":221},{},{"type":6,"children":222},[223,248,253,262,271],{"type":9,"tag":38,"props":224,"children":226},{"id":225},"_1-through-9-and",[227,233,235,241,242],{"type":9,"tag":17,"props":228,"children":230},{"className":229},[],[231],{"type":14,"value":232},"$1",{"type":14,"value":234}," through ",{"type":9,"tag":17,"props":236,"children":238},{"className":237},[],[239],{"type":14,"value":240},"$9",{"type":14,"value":189},{"type":9,"tag":17,"props":243,"children":245},{"className":244},[],[246],{"type":14,"value":247},"$#",{"type":9,"tag":10,"props":249,"children":250},{},[251],{"type":14,"value":252},"A script's arguments arrive as numbered variables:",{"type":9,"tag":56,"props":254,"children":257},{"className":255,"code":256,"language":14},[59],"#!/bin/sh\necho \"Called as:      $0\"\necho \"First argument: $1\"\necho \"Second:         $2\"\necho \"How many:       $#\"\necho \"All of them:    $@\"\n",[258],{"type":9,"tag":17,"props":259,"children":260},{"__ignoreMap":64},[261],{"type":14,"value":256},{"type":9,"tag":56,"props":263,"children":266},{"className":264,"code":265,"language":14},[59],"$ ./args.sh hello world\nCalled as:      ./args.sh\nFirst argument: hello\nSecond:         world\nHow many:       2\nAll of them:    hello world\n",[267],{"type":9,"tag":17,"props":268,"children":269},{"__ignoreMap":64},[270],{"type":14,"value":265},{"type":9,"tag":10,"props":272,"children":273},{},[274,280,282,288,290,295,297,303,305,310],{"type":9,"tag":17,"props":275,"children":277},{"className":276},[],[278],{"type":14,"value":279},"$0",{"type":14,"value":281}," is the script's own name as it was invoked — a path, if you ran it as one. ",{"type":9,"tag":17,"props":283,"children":285},{"className":284},[],[286],{"type":14,"value":287},"basename \"$0\"",{"type":14,"value":289}," trims it down for usage messages. Note that ",{"type":9,"tag":17,"props":291,"children":293},{"className":292},[],[294],{"type":14,"value":279},{"type":14,"value":296}," is ",{"type":9,"tag":298,"props":299,"children":300},"em",{},[301],{"type":14,"value":302},"not",{"type":14,"value":304}," counted by ",{"type":9,"tag":17,"props":306,"children":308},{"className":307},[],[309],{"type":14,"value":247},{"type":14,"value":311},".",{"data":313,"body":314},{},{"type":6,"children":315},[316,336,341,354,472,503],{"type":9,"tag":38,"props":317,"children":319},{"id":318},"and-are-not-the-same",[320,326,328,334],{"type":9,"tag":17,"props":321,"children":323},{"className":322},[],[324],{"type":14,"value":325},"\"$@\"",{"type":14,"value":327}," and ",{"type":9,"tag":17,"props":329,"children":331},{"className":330},[],[332],{"type":14,"value":333},"\"$*\"",{"type":14,"value":335}," are not the same",{"type":9,"tag":10,"props":337,"children":338},{},[339],{"type":14,"value":340},"Both expand to all the arguments. Only one of them is safe.",{"type":9,"tag":10,"props":342,"children":343},{},[344,346,352],{"type":14,"value":345},"Given a script called with ",{"type":9,"tag":17,"props":347,"children":349},{"className":348},[],[350],{"type":14,"value":351},"./s.sh \"my file.txt\" other.txt",{"type":14,"value":353},":",{"type":9,"tag":355,"props":356,"children":357},"table",{},[358,377],{"type":9,"tag":359,"props":360,"children":361},"thead",{},[362],{"type":9,"tag":363,"props":364,"children":365},"tr",{},[366,372],{"type":9,"tag":367,"props":368,"children":369},"th",{},[370],{"type":14,"value":371},"Form",{"type":9,"tag":367,"props":373,"children":374},{},[375],{"type":14,"value":376},"Becomes",{"type":9,"tag":378,"props":379,"children":380},"tbody",{},[381,412,434],{"type":9,"tag":363,"props":382,"children":383},{},[384,393],{"type":9,"tag":385,"props":386,"children":387},"td",{},[388],{"type":9,"tag":17,"props":389,"children":391},{"className":390},[],[392],{"type":14,"value":325},{"type":9,"tag":385,"props":394,"children":395},{},[396,402,404,410],{"type":9,"tag":17,"props":397,"children":399},{"className":398},[],[400],{"type":14,"value":401},"\"my file.txt\"",{"type":14,"value":403}," ",{"type":9,"tag":17,"props":405,"children":407},{"className":406},[],[408],{"type":14,"value":409},"\"other.txt\"",{"type":14,"value":411}," — two arguments, boundaries kept",{"type":9,"tag":363,"props":413,"children":414},{},[415,423],{"type":9,"tag":385,"props":416,"children":417},{},[418],{"type":9,"tag":17,"props":419,"children":421},{"className":420},[],[422],{"type":14,"value":333},{"type":9,"tag":385,"props":424,"children":425},{},[426,432],{"type":9,"tag":17,"props":427,"children":429},{"className":428},[],[430],{"type":14,"value":431},"\"my file.txt other.txt\"",{"type":14,"value":433}," — one argument, joined by a space",{"type":9,"tag":363,"props":435,"children":436},{},[437,448],{"type":9,"tag":385,"props":438,"children":439},{},[440,446],{"type":9,"tag":17,"props":441,"children":443},{"className":442},[],[444],{"type":14,"value":445},"$@",{"type":14,"value":447}," unquoted",{"type":9,"tag":385,"props":449,"children":450},{},[451,457,458,464,465,470],{"type":9,"tag":17,"props":452,"children":454},{"className":453},[],[455],{"type":14,"value":456},"\"my\"",{"type":14,"value":403},{"type":9,"tag":17,"props":459,"children":461},{"className":460},[],[462],{"type":14,"value":463},"\"file.txt\"",{"type":14,"value":403},{"type":9,"tag":17,"props":466,"children":468},{"className":467},[],[469],{"type":14,"value":409},{"type":14,"value":471}," — three, split apart",{"type":9,"tag":10,"props":473,"children":474},{},[475,487,489,494,496,501],{"type":9,"tag":26,"props":476,"children":477},{},[478,480,485],{"type":14,"value":479},"Use ",{"type":9,"tag":17,"props":481,"children":483},{"className":482},[],[484],{"type":14,"value":325},{"type":14,"value":486},", with the quotes, always.",{"type":14,"value":488}," It is the only form that passes your arguments through to another command unchanged. ",{"type":9,"tag":17,"props":490,"children":492},{"className":491},[],[493],{"type":14,"value":333},{"type":14,"value":495}," has one legitimate use — joining the arguments into a single string for a log message — and unquoted ",{"type":9,"tag":17,"props":497,"children":499},{"className":498},[],[500],{"type":14,"value":445},{"type":14,"value":502}," has none.",{"type":9,"tag":56,"props":504,"children":507},{"className":505,"code":506,"language":14},[59],"exec docker run \"$@\"        # correct: arguments forwarded intact\nexec docker run $@          # broken by the first filename with a space\n",[508],{"type":9,"tag":17,"props":509,"children":510},{"__ignoreMap":64},[511],{"type":14,"value":506},{"data":513,"body":514},{},{"type":6,"children":515},[516],{"type":9,"tag":212,"props":517,"children":521},{":answer":214,":options":518,"explanation":519,"question":520},"[\"Four separate arguments — the path is split at the space\",\"The command exactly as typed\",\"An error, because unquoted `$@` is invalid syntax\"]","Unquoted `$@` is subject to word splitting like any other expansion, so `/my dir` arrives as two arguments. `\"$@\"` preserves the boundaries the caller intended. This is the standard bug in hand-written wrapper scripts.","A wrapper script ends with `ssh \"$HOST\" $@`. A user runs it with an argument `ls -l /my dir`. What does the remote host receive?",[],{"data":523,"body":524},{},{"type":6,"children":525},[526,535,587,596,601,610],{"type":9,"tag":38,"props":527,"children":529},{"id":528},"shift",[530],{"type":9,"tag":17,"props":531,"children":533},{"className":532},[],[534],{"type":14,"value":528},{"type":9,"tag":10,"props":536,"children":537},{},[538,543,544,549,551,557,559,564,566,571,573,578,580,585],{"type":9,"tag":17,"props":539,"children":541},{"className":540},[],[542],{"type":14,"value":232},{"type":14,"value":234},{"type":9,"tag":17,"props":545,"children":547},{"className":546},[],[548],{"type":14,"value":240},{"type":14,"value":550}," run out, and the tenth argument is not ",{"type":9,"tag":17,"props":552,"children":554},{"className":553},[],[555],{"type":14,"value":556},"$10",{"type":14,"value":558}," in a POSIX shell — that parses as ",{"type":9,"tag":17,"props":560,"children":562},{"className":561},[],[563],{"type":14,"value":232},{"type":14,"value":565}," followed by a literal ",{"type":9,"tag":17,"props":567,"children":569},{"className":568},[],[570],{"type":14,"value":214},{"type":14,"value":572},". ",{"type":9,"tag":17,"props":574,"children":576},{"className":575},[],[577],{"type":14,"value":528},{"type":14,"value":579}," is the answer: it discards ",{"type":9,"tag":17,"props":581,"children":583},{"className":582},[],[584],{"type":14,"value":232},{"type":14,"value":586}," and slides everything down one.",{"type":9,"tag":56,"props":588,"children":591},{"className":589,"code":590,"language":14},[59],"#!/bin/sh\nwhile [ \"$#\" -gt 0 ]\ndo\n  echo \"Handling: $1\"\n  shift\ndone\n",[592],{"type":9,"tag":17,"props":593,"children":594},{"__ignoreMap":64},[595],{"type":14,"value":590},{"type":9,"tag":10,"props":597,"children":598},{},[599],{"type":14,"value":600},"This handles any number of arguments, and it is also the basis of option parsing:",{"type":9,"tag":56,"props":602,"children":605},{"className":603,"code":604,"language":14},[59],"#!/bin/sh\nVERBOSE=0\nOUTPUT=\"\"\n\nwhile [ \"$#\" -gt 0 ]\ndo\n  case \"$1\" in\n    -v|--verbose)\n      VERBOSE=1\n      shift\n      ;;\n    -o|--output)\n      OUTPUT=\"$2\"\n      shift 2\n      ;;\n    -h|--help)\n      echo \"Usage: $(basename \"$0\") [-v] [-o FILE] INPUT...\"\n      exit 0\n      ;;\n    -*)\n      echo \"Unknown option: $1\" >&2\n      exit 1\n      ;;\n    *)\n      break          # first non-option argument: stop parsing\n      ;;\n  esac\ndone\n\necho \"verbose=$VERBOSE output=$OUTPUT remaining: $@\"\n",[606],{"type":9,"tag":17,"props":607,"children":608},{"__ignoreMap":64},[609],{"type":14,"value":604},{"type":9,"tag":10,"props":611,"children":612},{},[613,615,620,621,626,628,634,636,642,644,649],{"type":14,"value":614},"That is ",{"type":9,"tag":17,"props":616,"children":618},{"className":617},[],[619],{"type":14,"value":22},{"type":14,"value":327},{"type":9,"tag":17,"props":622,"children":624},{"className":623},[],[625],{"type":14,"value":528},{"type":14,"value":627}," doing the whole job, and it is roughly what every hand-rolled CLI in shell looks like. ",{"type":9,"tag":17,"props":629,"children":631},{"className":630},[],[632],{"type":14,"value":633},"shift 2",{"type":14,"value":635}," drops an option and its value together; ",{"type":9,"tag":17,"props":637,"children":639},{"className":638},[],[640],{"type":14,"value":641},"break",{"type":14,"value":643}," on the first non-option leaves the file arguments sitting in ",{"type":9,"tag":17,"props":645,"children":647},{"className":646},[],[648],{"type":14,"value":325},{"type":14,"value":650}," for the rest of the script.",{"data":652,"body":653},{},{"type":6,"children":654},[655],{"type":9,"tag":656,"props":657,"children":659},"deep-dive",{"title":658},"`getopts`, and when to reach for it",[660,665,674,722,743,786],{"type":9,"tag":10,"props":661,"children":662},{},[663],{"type":14,"value":664},"The shell has a builtin for this, and for short options it is tidier:",{"type":9,"tag":56,"props":666,"children":669},{"className":667,"code":668,"language":14},[59],"while getopts \"vo:h\" OPT\ndo\n  case \"$OPT\" in\n    v) VERBOSE=1 ;;\n    o) OUTPUT=\"$OPTARG\" ;;\n    h) usage; exit 0 ;;\n    *) usage; exit 1 ;;\n  esac\ndone\nshift $((OPTIND - 1))\n",[670],{"type":9,"tag":17,"props":671,"children":672},{"__ignoreMap":64},[673],{"type":14,"value":668},{"type":9,"tag":10,"props":675,"children":676},{},[677,679,685,687,692,694,700,701,707,709,714,716,721],{"type":14,"value":678},"The string ",{"type":9,"tag":17,"props":680,"children":682},{"className":681},[],[683],{"type":14,"value":684},"\"vo:h\"",{"type":14,"value":686}," declares the valid letters; a trailing ",{"type":9,"tag":17,"props":688,"children":690},{"className":689},[],[691],{"type":14,"value":353},{"type":14,"value":693}," means that option takes a value, which lands in ",{"type":9,"tag":17,"props":695,"children":697},{"className":696},[],[698],{"type":14,"value":699},"$OPTARG",{"type":14,"value":572},{"type":9,"tag":17,"props":702,"children":704},{"className":703},[],[705],{"type":14,"value":706},"$OPTIND",{"type":14,"value":708}," tracks how far it got, so the ",{"type":9,"tag":17,"props":710,"children":712},{"className":711},[],[713],{"type":14,"value":528},{"type":14,"value":715}," afterwards leaves the non-option arguments in ",{"type":9,"tag":17,"props":717,"children":719},{"className":718},[],[720],{"type":14,"value":325},{"type":14,"value":311},{"type":9,"tag":10,"props":723,"children":724},{},[725,727,733,735,741],{"type":14,"value":726},"It also handles bundling — ",{"type":9,"tag":17,"props":728,"children":730},{"className":729},[],[731],{"type":14,"value":732},"-vh",{"type":14,"value":734}," is understood as ",{"type":9,"tag":17,"props":736,"children":738},{"className":737},[],[739],{"type":14,"value":740},"-v -h",{"type":14,"value":742}," — which the hand-written loop above does not.",{"type":9,"tag":10,"props":744,"children":745},{},[746,748,752,754,760,762,768,770,776,778,784],{"type":14,"value":747},"What it does ",{"type":9,"tag":26,"props":749,"children":750},{},[751],{"type":14,"value":302},{"type":14,"value":753}," do is long options. POSIX ",{"type":9,"tag":17,"props":755,"children":757},{"className":756},[],[758],{"type":14,"value":759},"getopts",{"type":14,"value":761}," knows nothing about ",{"type":9,"tag":17,"props":763,"children":765},{"className":764},[],[766],{"type":14,"value":767},"--verbose",{"type":14,"value":769},". GNU ",{"type":9,"tag":17,"props":771,"children":773},{"className":772},[],[774],{"type":14,"value":775},"getopt",{"type":14,"value":777}," (no ",{"type":9,"tag":17,"props":779,"children":781},{"className":780},[],[782],{"type":14,"value":783},"s",{"type":14,"value":785},") does, but it is a separate program with incompatible BSD and GNU versions, which is exactly the portability problem you were avoiding.",{"type":9,"tag":10,"props":787,"children":788},{},[789,791,796,798,803,805,810,812,818],{"type":14,"value":790},"So: ",{"type":9,"tag":17,"props":792,"children":794},{"className":793},[],[795],{"type":14,"value":759},{"type":14,"value":797}," when short options are enough, the ",{"type":9,"tag":17,"props":799,"children":801},{"className":800},[],[802],{"type":14,"value":22},{"type":14,"value":804},"/",{"type":9,"tag":17,"props":806,"children":808},{"className":807},[],[809],{"type":14,"value":528},{"type":14,"value":811}," loop when you want ",{"type":9,"tag":17,"props":813,"children":815},{"className":814},[],[816],{"type":14,"value":817},"--long-form",{"type":14,"value":819}," names and are willing to write ten more lines.",{"data":821,"body":822},{},{"type":6,"children":823},[824,832],{"type":9,"tag":825,"props":826,"children":831},"fill-blank",{":answer":827,"hint":828,"placeholder":829,"prompt":830},"[\"echo $#\",\"echo \\\"$#\\\"\"]","One special parameter holds the count.","echo ...","Inside a script, print the number of arguments it was called with.",[],{"type":9,"tag":10,"props":833,"children":834},{},[835],{"type":14,"value":836},"Next up: command substitution — capturing the output of one command as the input to another, and the pitfalls hiding in the whitespace.",1787908866327]