Suppose that I want to subdivide a bash
script into small idempotent functions, and I also want that those functions can depend on each other. For example, assume that I want to configure_Y
and configure_Z
, but they both require that I install_X
.
I would like to do something like this to express dependencies between X
, Y
and Z
:
function install_X() {
echo "install X"
}
function configure_Y() {
install_X
echo "configure Y"
}
function configure_Z() {
install_X
echo "configure Z"
}
configure_Y
configure_Z
This has the problem that it would attempt to execute install_X
multiple times. Even if install_X
is idempotent from the point of view of the state of the system (files etc.), it might still log some status messages to the console to indicate whether something was changed or not.
Here is how I tried to avoid it: I defined a helper function ensure
declare -A dfs_status
# Ensures that a given command is executed at most once
function ensure {
local key="$*"
case "${dfs_status[$key]-undiscovered}" in
"started")
echo "Circular dependency detected for command '$key'"
exit 1
;;
"finished")
return
;;
"undiscovered")
dfs_status[$key]="started"
;;
esac
"$@"
dfs_status[$key]="finished"
}
and then reformulated the X
-Y
-Z
example as follows:
function installed() {
local -r pkg="$1"
echo "Checking whether $pkg is already installed or not..."
}
function configured_Y() {
ensure installed 'X'
echo "Checking whether Y is already configured..."
}
function configured_Z() {
ensure installed 'X'
echo "Checking whether Z is already configured..."
}
ensure configured_Y
ensure configured_Z
Here is the full code, with topological sorting example from Cormen/Leiserson:
#!/bin/bash
declare -A dfs_status
# Ensures that a given command is executed at most once
function ensure {
local key="$*"
case "${dfs_status[$key]-undiscovered}" in
"started")
echo "Circular dependency detected for command '$key'"
exit 1
;;
"finished")
return
;;
"undiscovered")
dfs_status[$key]="started"
;;
esac
"$@"
dfs_status[$key]="finished"
}
# This handles the "installation" of leaf nodes, which have
# no further dependencies.
function installed() {
echo "$1"
}
# DAG example from Cormen Leiserson
# (note that the adjacency matrix is transposed compared to the book)
function installed_jacket() {
ensure installed_tie
ensure installed_belt
echo jacket
}
function installed_tie() {
ensure installed 'shirt'
echo tie
}
function installed_belt() {
ensure installed_pants
ensure installed 'shirt'
echo belt
}
function installed_pants() {
ensure installed 'undershorts'
echo pants
}
function installed_shoes() {
ensure installed 'socks'
ensure installed 'undershorts'
ensure installed_pants
echo shoes
}
ensure installed_shoes
ensure installed 'watch'
ensure installed_belt
ensure installed_jacket
My main problem with this approach is that it's the caller who ensures that a function is entered at most once, not the function itself. On the other hand, it spares me from having two enter
/exit
statements in every function to ensure cycle-detection.
- Are there simpler / more idiomatic ways to do this?
- Do you see any reasons why it would break for strange inputs?
- Is all of this complete nonsense, should I bite the bullet and rewrite everything as Ansible/Puppet/Chef plugin, or move to NixOS, or do something else?
tsort
to perform the topological sort? Why did you need to reimplement it? Might be worth commenting the code for future readers so they don't hit the same limitations. \$\endgroup\$tsort
s documentation, but quickly discarded it. It requires that the graph structure is specified explicitly in the input, whereas here, the graph structure is implicit, and computed lazily during the execution of the functions. Mapping the output oftsort
back to function invocations would probably be a mess as well. Finally, "topological sorting" is too trivial of an "algorithm" to require a separate tool: all one has to do is to invoke the functions ensuring that each one is run at most once, and then it sorts out itself. So, the overhead was too much. \$\endgroup\$make
? \$\endgroup\$