I needed a way to declare a list of git repositories in a shell script β each with a short name, a local path, and a remote URL β and then iterate over them in both a bootstrap script and a status script. No YAML parser, no jq, no external dependencies. Just Bash.
Here's what I landed on.
The data
REPO_SPECS=(
"website|website/anode-labs.com|https://github.com/mcaserta/anode-labs.com.git"
"bitcrust|product/bitcrust|https://github.com/mcaserta/bitcrust.git"
"presitter|product/presitter|https://github.com/mcaserta/presitter.git"
"stepbruv|product/stepbruv|https://github.com/mcaserta/stepbruv.git"
)
REPO_SPECS is a plain Bash indexed array. Each element is a single string with
three fields separated by |. You could use any delimiter that won't appear in
your data β pipe works well because it's unlikely to show up in file paths or
URLs.
Iterating and splitting
for spec in "${REPO_SPECS[@]}"; do
IFS='|' read -r name dir remote <<< "$spec"
echo "name=$name dir=$dir remote=$remote"
done
Two things are doing the heavy lifting here:
-
"${REPO_SPECS[@]}"expands the array so that each element becomes a separate word in theforloop. The double quotes matter β without them, elements containing spaces would be split further by the shell. -
IFS='|' read -r name dir remote <<< "$spec"splits the current element on|and assigns the three resulting tokens toname,dir, andremote.
The IFS='|' prefix sets the field separator only for the read command β it
doesn't change IFS for the rest of the script. The -r flag prevents
backslash interpretation. The <<< (here-string) feeds the variable's value
into read's standard input.
Running the loop above prints:
name=website dir=website/anode-labs.com remote=https://github.com/mcaserta/anode-labs.com.git
name=bitcrust dir=product/bitcrust remote=https://github.com/mcaserta/bitcrust.git
name=presitter dir=product/presitter remote=https://github.com/mcaserta/presitter.git
name=stepbruv dir=product/stepbruv remote=https://github.com/mcaserta/stepbruv.gitA real example
In my workspace bootstrap script, the pattern looks like this:
source "$SCRIPT_DIR/repos.sh"
for spec in "${REPO_SPECS[@]}"; do
IFS='|' read -r name dir remote <<< "$spec"
target="$WORK_ROOT/$dir"
if [[ -d "$target/.git" ]]; then
echo "[ok] $name already present"
continue
fi
if [[ -d "$target" ]]; then
echo "[skip] $name exists but is not a git repo"
continue
fi
if [[ -z "$remote" ]]; then
echo "[skip] $name has no configured remote"
continue
fi
echo "[clone] $name -> $target"
git clone "$remote" "$target"
done
A status script sources the same repos.sh file and uses the same IFS='|' read
pattern to print branch and dirty-state info for each repo. One data file, two
consumers, zero parsing libraries.
Why not associative arrays?
Bash 4+ has associative arrays, but they only give you one value per key. Here I need three fields per entry and I want to preserve declaration order (associative arrays don't guarantee it). Pipe-delimited strings in an indexed array solve both problems without any ceremony.
Why not a CSV or a config file?
You could absolutely use an external file and parse it with awk or cut. But
for a handful of records that are tightly coupled to the scripts that consume
them, inlining the data in a sourced .sh file keeps things simple. One fewer
file to read, one fewer format to document.
Gotchas
- If a field itself contains
|, this breaks. Pick a delimiter that your data won't contain. <<<(here-strings) are a Bash-ism. This won't work in plain POSIXsh. If you need POSIX compatibility, pipe throughechoinstead:echo "$spec" | IFS='|' read -r name dir remote. But beware: in some shells thereadruns in a subshell and the variables won't survive the pipe. At that point, just use Bash.- If you have more fields than variables, the last variable gets the remainder. Fewer fields than variables and the extras are empty. Both behaviors are usually what you want.
That's it
A Bash array, a pipe character, and IFS β three ingredients, no external
dependencies, and a data format that's readable enough that you can maintain it
by hand without a parser. Sometimes the simplest thing that works is the best
thing that works.