[{"data":1,"prerenderedAt":637},["ShallowReactive",2],{"mdc-bm5r1z-key":3,"mdc--dvd8u9-key":16,"mdc--fo1083-key":69,"mdc-7jellz-key":99,"mdc-aw3728-key":222,"mdc-6xky05-key":234,"mdc-5z2uit-key":345,"mdc--tiai0s-key":355,"mdc--gspbdq-key":418,"mdc--6m127m-key":527,"mdc-8bovhe-key":575,"mdc-5yt5e-key":587},{"data":4,"body":5},{},{"type":6,"children":7},"root",[8],{"type":9,"tag":10,"props":11,"children":12},"element","p",{},[13],{"type":14,"value":15},"text","Almost every useful script is a loop. Do this to every file. Retry until it works. Read the list and act on each line. The shell gives you three loop forms, and two of them cover nearly everything.",{"data":17,"body":18},{},{"type":6,"children":19},[20,34,46,64],{"type":9,"tag":21,"props":22,"children":24},"h2",{"id":23},"for-walks-a-list-of-words",[25,32],{"type":9,"tag":26,"props":27,"children":29},"code",{"className":28},[],[30],{"type":14,"value":31},"for",{"type":14,"value":33}," walks a list of words",{"type":9,"tag":35,"props":36,"children":40},"pre",{"className":37,"code":39,"language":14},[38],"language-text","for FILE in *.log\ndo\n  echo \"Compressing $FILE\"\n  gzip \"$FILE\"\ndone\n",[41],{"type":9,"tag":26,"props":42,"children":44},{"__ignoreMap":43},"",[45],{"type":14,"value":39},{"type":9,"tag":10,"props":47,"children":48},{},[49,54,56,62],{"type":9,"tag":26,"props":50,"children":52},{"className":51},[],[53],{"type":14,"value":31},{"type":14,"value":55}," takes a list of ",{"type":9,"tag":57,"props":58,"children":59},"strong",{},[60],{"type":14,"value":61},"words",{"type":14,"value":63}," — not a range, not a collection, just words separated by whitespace — and runs the body once per word with the variable set to it.",{"type":9,"tag":10,"props":65,"children":66},{},[67],{"type":14,"value":68},"That list can come from anywhere the shell can produce words: a glob, a literal list, a variable, the output of a command.",{"data":70,"body":71},{},{"type":6,"children":72},[73,78],{"type":9,"tag":74,"props":75,"children":77},"terminal-teaser",{":lines":76},"[{\"cmd\":\"for X in one two three; do echo \\\"got $X\\\"; done\",\"out\":\"got one\\ngot two\\ngot three\"},{\"cmd\":\"for N in 1 2 3; do echo \\\"$((N * N))\\\"; done\",\"out\":\"1\\n4\\n9\"}]",[],{"type":9,"tag":10,"props":79,"children":80},{},[81,83,89,91,97],{"type":14,"value":82},"The semicolon form is the same loop written on one line — ",{"type":9,"tag":26,"props":84,"children":86},{"className":85},[],[87],{"type":14,"value":88},"do",{"type":14,"value":90}," needs a separator before it exactly as ",{"type":9,"tag":26,"props":92,"children":94},{"className":93},[],[95],{"type":14,"value":96},"then",{"type":14,"value":98}," did.",{"data":100,"body":101},{},{"type":6,"children":102},[103,109,121,130,181,208,213],{"type":9,"tag":21,"props":104,"children":106},{"id":105},"the-empty-glob-trap",[107],{"type":14,"value":108},"The empty-glob trap",{"type":9,"tag":10,"props":110,"children":111},{},[112,114,119],{"type":14,"value":113},"This is the one thing to know about ",{"type":9,"tag":26,"props":115,"children":117},{"className":116},[],[118],{"type":14,"value":31},{"type":14,"value":120}," before you write a real one:",{"type":9,"tag":35,"props":122,"children":125},{"className":123,"code":124,"language":14},[38],"for FILE in *.log\ndo\n  rm \"$FILE\"\ndone\n",[126],{"type":9,"tag":26,"props":127,"children":128},{"__ignoreMap":43},[129],{"type":14,"value":124},{"type":9,"tag":10,"props":131,"children":132},{},[133,135,141,143,148,150,156,158,164,166,172,174,179],{"type":14,"value":134},"In a directory with no ",{"type":9,"tag":26,"props":136,"children":138},{"className":137},[],[139],{"type":14,"value":140},".log",{"type":14,"value":142}," files, a POSIX shell leaves the unmatched pattern alone. The loop runs ",{"type":9,"tag":57,"props":144,"children":145},{},[146],{"type":14,"value":147},"once",{"type":14,"value":149},", with ",{"type":9,"tag":26,"props":151,"children":153},{"className":152},[],[154],{"type":14,"value":155},"FILE",{"type":14,"value":157}," set to the literal string ",{"type":9,"tag":26,"props":159,"children":161},{"className":160},[],[162],{"type":14,"value":163},"*.log",{"type":14,"value":165},", and ",{"type":9,"tag":26,"props":167,"children":169},{"className":168},[],[170],{"type":14,"value":171},"rm",{"type":14,"value":173}," fails complaining about a file named ",{"type":9,"tag":26,"props":175,"children":177},{"className":176},[],[178],{"type":14,"value":163},{"type":14,"value":180},".",{"type":9,"tag":10,"props":182,"children":183},{},[184,186,191,193,198,200,206],{"type":14,"value":185},"With ",{"type":9,"tag":26,"props":187,"children":189},{"className":188},[],[190],{"type":14,"value":171},{"type":14,"value":192}," that's merely noisy. With something that creates or moves files, you get a real file called ",{"type":9,"tag":26,"props":194,"children":196},{"className":195},[],[197],{"type":14,"value":163},{"type":14,"value":199}," on disk, and a ",{"type":9,"tag":26,"props":201,"children":203},{"className":202},[],[204],{"type":14,"value":205},"*",{"type":14,"value":207}," in a filename is a genuinely unpleasant thing to clean up.",{"type":9,"tag":10,"props":209,"children":210},{},[211],{"type":14,"value":212},"The fix is to check:",{"type":9,"tag":35,"props":214,"children":217},{"className":215,"code":216,"language":14},[38],"for FILE in *.log\ndo\n  [ -e \"$FILE\" ] || continue\n  rm \"$FILE\"\ndone\n",[218],{"type":9,"tag":26,"props":219,"children":220},{"__ignoreMap":43},[221],{"type":14,"value":216},{"data":223,"body":224},{},{"type":6,"children":225},[226],{"type":9,"tag":227,"props":228,"children":233},"quiz",{":answer":229,":options":230,"explanation":231,"question":232},"0","[\"Once — it prints the literal text `*.tmp`\",\"Zero times — an unmatched glob produces an empty list\",\"It's an error; the loop doesn't run\"]","An unmatched pattern is passed through unchanged, so the list is one word long. Guard the body with `[ -e \"$F\" ] || continue` whenever the loop does anything more consequential than echo.","A directory contains no `.tmp` files. How many times does `for F in *.tmp; do echo \"$F\"; done` print something?",[],{"data":235,"body":236},{},{"type":6,"children":237},[238,250,269,278,297,310,319],{"type":9,"tag":21,"props":239,"children":241},{"id":240},"while-repeats-until-a-command-fails",[242,248],{"type":9,"tag":26,"props":243,"children":245},{"className":244},[],[246],{"type":14,"value":247},"while",{"type":14,"value":249}," repeats until a command fails",{"type":9,"tag":10,"props":251,"children":252},{},[253,255,260,262,267],{"type":14,"value":254},"Where ",{"type":9,"tag":26,"props":256,"children":258},{"className":257},[],[259],{"type":14,"value":31},{"type":14,"value":261}," walks a known list, ",{"type":9,"tag":26,"props":263,"children":265},{"className":264},[],[266],{"type":14,"value":247},{"type":14,"value":268}," repeats as long as a command keeps succeeding:",{"type":9,"tag":35,"props":270,"children":273},{"className":271,"code":272,"language":14},[38],"COUNT=1\nwhile [ \"$COUNT\" -le 5 ]\ndo\n  echo \"Attempt $COUNT\"\n  COUNT=$((COUNT + 1))\ndone\n",[274],{"type":9,"tag":26,"props":275,"children":276},{"__ignoreMap":43},[277],{"type":14,"value":272},{"type":9,"tag":10,"props":279,"children":280},{},[281,287,289,295],{"type":9,"tag":26,"props":282,"children":284},{"className":283},[],[285],{"type":14,"value":286},"$(( ... ))",{"type":14,"value":288}," is arithmetic expansion — the shell's built-in integer maths. It is the portable way to count; the older ",{"type":9,"tag":26,"props":290,"children":292},{"className":291},[],[293],{"type":14,"value":294},"expr",{"type":14,"value":296}," runs an external program for every single increment.",{"type":9,"tag":10,"props":298,"children":299},{},[300,302,308],{"type":14,"value":301},"The condition is a command, same as with ",{"type":9,"tag":26,"props":303,"children":305},{"className":304},[],[306],{"type":14,"value":307},"if",{"type":14,"value":309},". Which makes retry loops read almost like English:",{"type":9,"tag":35,"props":311,"children":314},{"className":312,"code":313,"language":14},[38],"until curl -sf http://localhost:8080/health\ndo\n  echo \"Waiting for the service...\"\n  sleep 2\ndone\necho \"Up.\"\n",[315],{"type":9,"tag":26,"props":316,"children":317},{"__ignoreMap":43},[318],{"type":14,"value":313},{"type":9,"tag":10,"props":320,"children":321},{},[322,328,330,335,337,343],{"type":9,"tag":26,"props":323,"children":325},{"className":324},[],[326],{"type":14,"value":327},"until",{"type":14,"value":329}," is ",{"type":9,"tag":26,"props":331,"children":333},{"className":332},[],[334],{"type":14,"value":247},{"type":14,"value":336}," with the sense inverted: it loops while the command ",{"type":9,"tag":338,"props":339,"children":340},"em",{},[341],{"type":14,"value":342},"fails",{"type":14,"value":344},". Both exist because sometimes one of them reads better than negating the other.",{"data":346,"body":347},{},{"type":6,"children":348},[349],{"type":9,"tag":227,"props":350,"children":354},{":answer":229,":options":351,"explanation":352,"question":353},"[\"Blocks until the file `/tmp/ready` appears, checking once a second\",\"Deletes `/tmp/ready` after one second\",\"Loops forever, since `until` has no exit condition\"]","`until` repeats while its condition is false. `[ -f ... ]` is false until the file exists, so the loop spins — one second at a time — and falls through the moment it appears. It's the shell's version of waiting on a flag.","What does `until [ -f /tmp/ready ]; do sleep 1; done` do?",[],{"data":356,"body":357},{},{"type":6,"children":358},[359,365,370,379,400],{"type":9,"tag":21,"props":360,"children":362},{"id":361},"reading-a-file-line-by-line",[363],{"type":14,"value":364},"Reading a file line by line",{"type":9,"tag":10,"props":366,"children":367},{},[368],{"type":14,"value":369},"This is the pattern worth committing to memory, because the obvious alternative is wrong:",{"type":9,"tag":35,"props":371,"children":374},{"className":372,"code":373,"language":14},[38],"while read -r LINE\ndo\n  echo \"Line: $LINE\"\ndone \u003C servers.txt\n",[375],{"type":9,"tag":26,"props":376,"children":377},{"__ignoreMap":43},[378],{"type":14,"value":373},{"type":9,"tag":10,"props":380,"children":381},{},[382,384,390,392,398],{"type":14,"value":383},"The redirect goes on ",{"type":9,"tag":26,"props":385,"children":387},{"className":386},[],[388],{"type":14,"value":389},"done",{"type":14,"value":391},", feeding the whole loop from the file. ",{"type":9,"tag":26,"props":393,"children":395},{"className":394},[],[396],{"type":14,"value":397},"read",{"type":14,"value":399}," consumes one line per iteration and returns non-zero at end of file, which is what stops the loop.",{"type":9,"tag":10,"props":401,"children":402},{},[403,409,411,416],{"type":9,"tag":26,"props":404,"children":406},{"className":405},[],[407],{"type":14,"value":408},"-r",{"type":14,"value":410}," stops ",{"type":9,"tag":26,"props":412,"children":414},{"className":413},[],[415],{"type":14,"value":397},{"type":14,"value":417}," from interpreting backslashes in the data. There is essentially never a reason to omit it.",{"data":419,"body":420},{},{"type":6,"children":421},[422],{"type":9,"tag":423,"props":424,"children":426},"deep-dive",{"title":425},"Why not `for LINE in $(cat file)`?",[427,446,455,467,478,498,507],{"type":9,"tag":10,"props":428,"children":429},{},[430,432,437,439,444],{"type":14,"value":431},"Because ",{"type":9,"tag":26,"props":433,"children":435},{"className":434},[],[436],{"type":14,"value":31},{"type":14,"value":438}," splits on ",{"type":9,"tag":57,"props":440,"children":441},{},[442],{"type":14,"value":443},"whitespace",{"type":14,"value":445},", not on newlines. A file like:",{"type":9,"tag":35,"props":447,"children":450},{"className":448,"code":449,"language":14},[38],"web-01 production\nweb-02 staging\n",[451],{"type":9,"tag":26,"props":452,"children":453},{"__ignoreMap":43},[454],{"type":14,"value":449},{"type":9,"tag":10,"props":456,"children":457},{},[458,460,465],{"type":14,"value":459},"gives you four iterations, not two — and any line containing a ",{"type":9,"tag":26,"props":461,"children":463},{"className":462},[],[464],{"type":14,"value":205},{"type":14,"value":466}," gets glob-expanded against the current directory on the way through.",{"type":9,"tag":10,"props":468,"children":469},{},[470,476],{"type":9,"tag":26,"props":471,"children":473},{"className":472},[],[474],{"type":14,"value":475},"while read",{"type":14,"value":477}," splits on newlines, which is what a line-oriented file actually means. It also streams: the file is read a line at a time rather than loaded whole into a command substitution, so it works on a log you couldn't fit in memory.",{"type":9,"tag":10,"props":479,"children":480},{},[481,483,488,490,496],{"type":14,"value":482},"One consequence to know about. The loop body runs in the shell, but if you pipe ",{"type":9,"tag":338,"props":484,"children":485},{},[486],{"type":14,"value":487},"into",{"type":14,"value":489}," the loop — ",{"type":9,"tag":26,"props":491,"children":493},{"className":492},[],[494],{"type":14,"value":495},"cat file | while read -r LINE; do ...; done",{"type":14,"value":497}," — the loop runs in a subshell, and variables it sets are lost when the pipe ends:",{"type":9,"tag":35,"props":499,"children":502},{"className":500,"code":501,"language":14},[38],"COUNT=0\ncat servers.txt | while read -r L; do COUNT=$((COUNT + 1)); done\necho \"$COUNT\"      # 0, on most shells\n",[503],{"type":9,"tag":26,"props":504,"children":505},{"__ignoreMap":43},[506],{"type":14,"value":501},{"type":9,"tag":10,"props":508,"children":509},{},[510,512,518,520,525],{"type":14,"value":511},"Redirect with ",{"type":9,"tag":26,"props":513,"children":515},{"className":514},[],[516],{"type":14,"value":517},"\u003C file",{"type":14,"value":519}," on ",{"type":9,"tag":26,"props":521,"children":523},{"className":522},[],[524],{"type":14,"value":389},{"type":14,"value":526}," instead of piping, and the count survives.",{"data":528,"body":529},{},{"type":6,"children":530},[531,549,566],{"type":9,"tag":21,"props":532,"children":534},{"id":533},"break-and-continue",[535,541,543],{"type":9,"tag":26,"props":536,"children":538},{"className":537},[],[539],{"type":14,"value":540},"break",{"type":14,"value":542}," and ",{"type":9,"tag":26,"props":544,"children":546},{"className":545},[],[547],{"type":14,"value":548},"continue",{"type":9,"tag":10,"props":550,"children":551},{},[552,557,559,564],{"type":9,"tag":26,"props":553,"children":555},{"className":554},[],[556],{"type":14,"value":540},{"type":14,"value":558}," leaves the loop; ",{"type":9,"tag":26,"props":560,"children":562},{"className":561},[],[563],{"type":14,"value":548},{"type":14,"value":565}," skips to the next iteration. Both are what turn a loop into something with a shape:",{"type":9,"tag":35,"props":567,"children":570},{"className":568,"code":569,"language":14},[38],"for HOST in web-01 web-02 web-03\ndo\n  ping -c1 -W1 \"$HOST\" >/dev/null 2>&1 || continue\n  echo \"$HOST is up\"\ndone\n",[571],{"type":9,"tag":26,"props":572,"children":573},{"__ignoreMap":43},[574],{"type":14,"value":569},{"data":576,"body":577},{},{"type":6,"children":578},[579],{"type":9,"tag":580,"props":581,"children":586},"fill-blank",{":answer":582,"hint":583,"placeholder":584,"prompt":585},"[\"for F in *.conf\",\"for F in *.conf; do\",\"for F in ./*.conf\"]","The list is a glob; no quotes around it, or it wouldn't expand.","for F ...","Loop over every `.conf` file in the current directory, storing each name in `F`. Write just the opening line of the loop.",[],{"data":588,"body":589},{},{"type":6,"children":590},[591,597,602,611,624],{"type":9,"tag":21,"props":592,"children":594},{"id":593},"a-loop-that-does-something-real",[595],{"type":14,"value":596},"A loop that does something real",{"type":9,"tag":10,"props":598,"children":599},{},[600],{"type":14,"value":601},"Putting the lesson together — compress yesterday's logs, skipping anything already compressed:",{"type":9,"tag":35,"props":603,"children":606},{"className":604,"code":605,"language":14},[38],"#!/bin/sh\nLOGDIR=/var/log/myapp\n\nfor FILE in \"$LOGDIR\"/*.log\ndo\n  [ -e \"$FILE\" ] || continue          # empty-glob guard\n  [ -s \"$FILE\" ] || continue          # skip empty files\n\n  echo \"Compressing $FILE\"\n  if gzip \"$FILE\"; then\n    echo \"  ok\"\n  else\n    echo \"  FAILED: $FILE\" >&2\n  fi\ndone\n",[607],{"type":9,"tag":26,"props":608,"children":609},{"__ignoreMap":43},[610],{"type":14,"value":605},{"type":9,"tag":10,"props":612,"children":613},{},[614,616,622],{"type":14,"value":615},"Note ",{"type":9,"tag":26,"props":617,"children":619},{"className":618},[],[620],{"type":14,"value":621},">&2",{"type":14,"value":623}," on the failure line — that sends it to standard error, so a human watching sees the problem while a pipeline collecting the normal output stays clean.",{"type":9,"tag":10,"props":625,"children":626},{},[627,629,635],{"type":14,"value":628},"Next up: ",{"type":9,"tag":26,"props":630,"children":632},{"className":631},[],[633],{"type":14,"value":634},"case",{"type":14,"value":636}," — the readable way to branch on a value, and how scripts dispatch on their first argument.",1787908866327]