1
\$\begingroup\$

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?
\$\endgroup\$
3
  • 1
    \$\begingroup\$ Did you consider using 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\$ Commented Jan 22 at 8:15
  • 1
    \$\begingroup\$ @TobySpeight I briefly looked at tsorts 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 of tsort 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\$ Commented Jan 22 at 15:26
  • \$\begingroup\$ I haven’t reviewed your use case in detail, but have you considered using make? \$\endgroup\$
    – tripleee
    Commented Mar 13 at 15:58

0

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.