💾 Archived View for thrig.me › blog › 2023 › 05 › 20 › guarded-commands.gmi captured on 2024-07-09 at 01:04:12. Gemini links have been rewritten to link to archived content
⬅️ Previous capture (2023-11-14)
-=-=-=-=-=-=-
Guarded commands have a pretty math-y definition, courtesy Dijkstra.
A perhaps simplified view is to have a test that is applied both before and after some bit of code. This, too, might be confusing, so an example might help. Let's say you have one of those terrible scripts that does stuff:
#!/bin/sh mkdir blah cd blah echo foo >> foo mkdir whatever cd whatever echo bar >> bar
mkdir, by the way, didn't use to be atomic, which meant early on in unix you could end up with a half-created directory. They changed the system to make mkdir atomic.
Anyways, the code is terrible; the mkdir and chdir calls are not checked for failure. Guarded commands may not care whether a command fails. Rather, there is a test associated with each command; if the test passes then the command is not run, and if the test fails after the command is run, the script halts or otherwise signals an error. A benefit of all this testing is that a script that fails somewhere can be restarted without issue (besides the CPU cost of doing various tests). If the above script fails and then is re-run, "foo" or "bar" might be appended multiple times, which might be really bad. Someone might have to manually clean things up, which will take time and effort, or may have to wipe the system and retry from a clean slate, which also takes time and effort. Instead, one could use what I'm calling guarded commands. This may differ from what Dijkstra cooked up.
And since the sh script was getting a bit too long for my taste, we'll switch to a different scripting language.
#!/usr/bin/env tclsh8.6 # # guard.tcl - example implementation of "guarded commands" # on OpenBSD: # doas pkg_add tcl tcllib package require Tcl 8.6 package require defer # run the test: if okay, return. otherwise, run the action, and run # the test again, failing if that test fails proc guard {test action} { if { [uplevel 1 $test]} return else {uplevel 1 $action} if {![uplevel 1 $test]} {return -code 1 "after test failed"} } proc append-line {file string} { set fh [open $file a] puts $fh $string close $fh } proc has-line {file string} { try { set fh [open $file] } on error {} { return 0 } defer::defer close $fh while {[gets $fh line] >= 0} { if {[string match $string $line]} {return 1} } return 0 } # run commands in a directory, creating the directory if need be proc with-directory {dir body} { set oldwd [pwd] defer::defer cd $oldwd guard {file isdirectory $dir} {file mkdir $dir} cd $dir uplevel 1 $body } proc with-file-line {file string} { guard {has-line $file $string} {append-line $file $string} } with-directory blah { with-file-line foo foo with-directory whatever { with-file-line bar bar } }
This is longer, though much of this code could be hidden off in a library, reducing an actual script to an include line and the few relevant guarded calls.
At some point you may want a database for better atomicity and transactions, though that may not be possible for various reasons. Configuration management (e.g. Ansible and suchlike) can also be used to ensure that a system is in a particular state (directories exist, configuration files are aright, etc). Internally Ansible may perform guarded command like checks to ensure that some condition is met. Still, there may be use for guarded command in standalone scripts, especially where a script may need to be run multiple times towards some goal.
Downsides of guarded commands include that the tests are run twice; it may be more efficient to check the return code of e.g. a mkdir call and from that decide what to do. Or, the test might be expensive, especially if 10,000 files are being wrangled... from the web! each over TLS! mostly containing the same thing! No, JavaScript would never stoop to such lows!! In that case it might be more efficient to unpack a tarball into a directory tree, and check if tar failed.
tags #sh #tcl