De computer vertellen wat hij moet doen

Voorbeelden van commando's:

  • mocha
  • git
  • brew install
  • npm install
  • rm

Op fundamenteel niveau was ik niet in staat om mijn computer iets te laten doen waar logica aan te pas kwam of waarbij meerdere programma's aan elkaar geknoopt moesten worden, zoals:

  • Eén ding doen en daarna iets anders.
  • Een foutmelding loggen als een commando mislukt.
  • Een programma starten zodra de computer opstart.
  • Twee commando's tegelijkertijd uitvoeren.
  • Door alle bestanden in een map lopen en voor elk bestand een actie uitvoeren.
  • De output van het ene commando gebruiken als input voor een ander commando.

In theorie had ik NodeJS kunnen gebruiken om dit soort programma's te schrijven. In de praktijk heb ik dat nooit gedaan. Dit kwam enerzijds door mijn mindset: ik was niet gewend om de programma's die ik in de command-line gebruikte te zien als zaken die ik kon beheersen. Anderzijds was er een gebrek aan vaardigheid: ik kende de programma's die ik gebruikte niet goed genoeg om ze te integreren in een script dat ik zelf had geschreven.

Ik bleef echter proberen de shell te leren, en langzaam bereikte ik het punt waarop ik mijn weg kon vinden in scripts met basislogica, zoals deze:

set +e
npm install
status_code=$?
if [[ "$status_code" != "0" ]]; then
  echo "Something went wrong with your npm install. Check your ~/.npmrc to make sure it's authed to our registry."
  exit 1
fi
set -e

Afhankelijk van je ervaring met shell-scripting ben je nu wellicht verbijsterd. Sorry. (Als je nog niet uit je ogen bloedt, is hier de reden waarom dat zou moeten: zie voetnoot 1).

Hoewel de commando's en scripts die ik schreef gebrekkig waren, was het voor mij transformatief; ik had plotseling het vermogen om mijn computer te vertellen bestaande programma's aan elkaar te knopen om mijn doelen te bereiken. Het voelde een beetje als de verandering die optrad toen ik in eerste instantie leerde programmeren.

In de loop der jaren ben ik langzaam meer shell-tools gaan leren en de syntax van de shell gaan begrijpen. Vaak leerde ik een nieuw hulpmiddel of patroon, gevolgd door een moment van pijn wanneer ik besefde hoeveel makkelijker een vorig probleem opgelost had kunnen worden als ik niet een hamer had gebruikt voor een probleem waar een boormachine voor nodig was.

In die tijd heb ik samengewerkt met veel (ongelooflijk sterke!) engineers die minder tijd hebben besteed aan het leren van de shell en in plaats daarvan vertrouwden op GUI's voor taken zoals het draaien van tests, het beheren van git, communiceren met databases en dagelijkse taken. Vertrouwen op een GUI werkt prima totdat je iets wilt bereiken waar de GUI niet voor is ontworpen, en ik denk dat het makkelijk is om in een mindset te glippen waarin je beperkt bent tot wat de GUI kan. Ik heb bekwame engineers veel moeite zien doen omdat ze niet wisten hoe ze zaken moesten aanpakken zoals het gebruik van while om een commando te blijven uitvoeren, of het schrijven van een for-loop om dezelfde operatie op elk bestand in een map uit te voeren.

Ik denk dat diezelfde focus op de GUI ook een probleem kan zijn bij het bewerken en onderhouden van shell-scripts. Ik durf te wedden dat de meeste bedrijven een aanzienlijke hoeveelheid essentiële logica hebben voor het bouwen, deployen, valideren en testen van code in talen als Bash of Zsh. Als je niet comfortabel bent met de taal waarin je tooling is gebouwd, kun je deze niet gemakkelijk lezen of verbeteren. Je kunt misschien de remote servers waarop je code draait vertellen hoe ze zich moeten gedragen, maar niet de computer waarop je werkt vertellen hoe hij zaken als linting en tests parallel moet uitvoeren – dat is jammer!

Waarom shell-scripts?

Laten we een korte zijsprong maken: waarom worden zoveel van deze scripts geschreven in talen die niet de hoofdtaal van het team zijn? Ik denk dat het vaak ergonomischer is om een script te schrijven dat commando's aan elkaar knoopt in een taal die specifiek is ontworpen om dat makkelijk te maken.

Neem een super simpel voorbeeld van het draaien van een test tot deze mislukt: while pnpm exec mocha ./pathToFile.test.ts; do true; done

Er zijn overduidelijke punten van kritiek op deze syntax, maar laten we kijken naar hoe dit eruitziet in NodeJS:

const { execSync } = require("child_process");

while (true) {
  try {
    execSync(`pnpm exec mocha ./pathToFile.test.ts`, { stdio: "inherit" });
  } catch (err) {
    console.error("failed", err);
    break;
  }
}

Er zitten hier veel hobbels en valkuilen aan, en ik persoonlijk vind de shell makkelijker! Ik hoef me geen zorgen te maken over het maken van een bestand, het importeren van child_process, of het instellen van { stdio: "inherit" } om output te zien. En dit is een vrij simpel voorbeeld dat niet eens meerdere programma's via een pipe aan elkaar knoopt, output vastlegt of een tijdelijk bestand gebruikt!

💡 Dit betekent niet dat je jezelf moet berusten in het schrijven in Bash, Zsh of een soortgelijke taal! Voor teams die JavaScript goed kennen, is zx een tool die ik met plezier heb gebruikt. Ik denk dat het deze scripts erg ergonomisch maakt om te schrijven en te onderhouden.

Ruby en Python zijn beide makkelijker dan NodeJS, maar ik denk dat er genoeg talen zijn die nog meer ceremonie vereisen om een kort scriptje als dit te schrijven.

Ik beweer zeker niet dat shell-scripts altijd makkelijker zijn voor build-scripts! Wanneer problemen simpel genoeg zijn dat je slechts twee programma's aan elkaar knoopt, voelt een tool als Bash of Zsh erg ergonomisch. Zodra je complexere logica en datatypes wilt, zul je een taal willen kiezen die het makkelijk maakt om die complexere logica en datatypes te representeren (en te testen!).

Ik vermoed dat veel engineers die klagen over build-scripts in een shell-taal, nooit echt hebben geprobeerd ze om te zetten naar een andere taal. Naast de (potentieel) ingewikkelder syntax, is een groot deel van het "leren schrijven van shell-scripts" eigenlijk niet syntactisch. Als je een script omzet dat commando's aan elkaar knoopt, maar je weet niet hoe de commando's die je koppelt zich gedragen, is het resulterende script waarschijnlijk net zo ondoorgrondelijk.

De gereedschapskist

Kennis van de shell – het vermogen om een computer dingen te laten doen – hangt enorm af van de kennis van de programma's die de taken uitvoeren die je wilt volbrengen! Ik zou zeggen dat kennis van de shell voor 20% uit syntax bestaat en voor 80% uit het hebben van een goede gereedschapskist:

  • Als je fzf kent, kun je snel utilities bouwen met interactief fuzzy-searching. (Voorbeeld: git checkout $(git branch --sort=-committerdate | fzf) laat je een branch kiezen via fuzzy-search).
  • Als je tldr of eg kent, kun je gebruiksvoorbeelden ophalen voor elk ander commando in deze lijst.
  • Als je rsync kent, kun je gewijzigde bestanden kopiëren naar een snellere remote server om iets zwaars en traags uit te voeren.
  • Als je xargs kent, kun je commando's incrementeel opbouwen en werk paralleliseren.
  • Als je sed -i of ast-grep kent, kun je snel gecompliceerde patronen herschrijven in een hele reeks bestanden.
  • Als je direnv kent, kun je ervoor zorgen dat de juiste omgevingsvariabelen zijn ingesteld voor iedereen die commando's uitvoert in een codebase.
  • Als je duckdb kent, kun je SQL schrijven om CSV- en JSON-bestanden lokaal te bevragen als onderdeel van een groter script.
  • Als je gh kent, kun je scripts bouwen om je PR's te controleren en nieuwe PR's te openen vanuit de cli.
  • Als je ngrok kent, kun je snel een lokale poort beschikbaar stellen om iets te testen op een andere machine.

Elk extra programma dat je leert, breidt je mogelijkheden verder uit, omdat elk nieuw hulpmiddel kan worden gebruikt in combinatie met elk ander hulpmiddel dat je al kent.

Ik kan niet genoeg benadrukken dat ik het verste verwijderd ben van een shell-scripting expert, en dat ik slecht ben vergeleken met mensen die het door en door kennen [^2], maar ik heb nog steeds veel waarde gehaald uit het kennen van genoeg shell-syntax om programma's aan elkaar te knopen en het kennen van genoeg programma's die ik daadwerkelijk aan elkaar wil knopen.

Het is geweldig om je computer te kunnen vertellen wat hij moet doen!

***

[^1]: Dit script was geschreven met goede bedoelingen, maar was overdreven gecompliceerd omdat ik niet wist dat de hoofdmodus van if het accepteren van commando's is. [[ (of test) is slechts een speciaal commando. Dit betekent dat onze if-check simpelweg ! npm install kan zijn. Met deze aanpak is het niet nodig om set +e te gebruiken om commando's te laten mislukken (het gebruik van set +e en daarna weer set -e is bedoeld om te voorkomen dat het volledige shell-script faalt omdat één commando mislukte – zie Unofficial Bash Strict Mode). Het betekent ook dat we de exitcode niet hoeven vast te leggen met $?. ``bash if ! npm install; then echo "Something went wrong with your npm install. Check your ~/.npmrc to make sure it's authed to our registry." exit 1 fi ``

[^2]: Uit een HN-commentaar op een vorig blogbericht: "Dit bericht […] betekent dat ik me niet langer afvraag waarom 99% van de shell-scripts die ik tegenkom er onbekwaam uitzien."