Search code examples
arraysbashvariablesdynamic

Bash: Dynamically create variables with the variable name based on a filename


This question comes from a desire to reduce some repetition in a bash script, around checking if we have something in a file and if so, loading the first line of it into a variable of the same name.

At the moment, there are dozens of lines like this for each variable used throughout:

[[ -s './config/foo' ]] && read -r foo < './config/foo' || echo "problem with ./config/foo file"
[[ -s './config/bar' ]] && read -r foo < './config/bar' || echo "problem with ./config/foo file"

But rather than going through way too many lines like this, I thought I could maybe automate this process by using an array (and also later, expand to other tests with this method).

So I started on the code below, but got stuck at wondering if it's possible to create variable names dynamically? I have no idea how to do that, or if it's even possible. I know I can get the name of the file we would want to use for making the variable name by using ${file##*/} to strip off the paths (so ./config/foo becomes foo for example), but how to turn that result into a variable name and then set it to the contents of the first line of a file, like the original script does?

Here's what I have so far, where DYNAMIC_VARIABLE_NAME_HERE would be the name we can get from ${file##*/}:

#!/bin/bash

# check and load config files
required_files=(
  './config/foo'
  './config/bar'
)
for file in "${required_files[@]}"; do
  [[ -s "${file}" ]] && read -r DYNAMIC_VARIABLE_NAME_HERE < "${file}" || failed+=("${file}")
done
if [[ ${#failed[@]} -ne 0 ]]; then
  echo "there is a problem with the following configuration files:"
  for file in "${failed[@]}"; do
    echo "${file}"
  done
fi

# check
echo "foo: ${foo}"
echo "bar: ${bar}"

Output so far

foo:
bar:

Desired output

foo: [first line of ./config/foo file]
bar: [first line of ./config/bar file]

Solution

  • Setup:

    $ head foo bar
    ==> foo <==
    1st line from foo
    2nd line from foo
    
    ==> bar <==
    1st line from bar
    2nd line from bar
    

    If using bash 4.3+ you could use a nameref, eg:

    for fname in foo bar
    do
        declare -n curr_var="${fname}"             # nameref
        read -r curr_var < "${fname}"
    done
    

    This generates:

    $ typeset -p foo bar
    declare -- foo="1st line from foo"
    declare -- bar="1st line from bar"
    

    One problem with this approach is ... how do you keep track of the dynamically generated variable names. In this case we've hardcoded the variable names foo and bar in the code, but what if there were 10 files, and we don't know (programmatically) beforehand what the file/variable names are ... how/where do you keep track of the variable names?


    A different approach using an associative array:

    unset      myvar                               # insure array name not in use
    declare -A myvar                               # define Associative array
    
    for fname in *                                 # for now assume this matches on files "foo" and "bar"
    do
        read -r myvar[$fname] < "${fname}"
    done
    

    This generates:

    $ typeset -p myvar
    declare -A myvar=([bar]="1st line from bar" [foo]="1st line from foo" )
    

    Furthermore, we can get a list of our variable names by perusing the indices of the array, eg:

    for varname in "${!myvar[@]}"                  # loop through array indices
    do
        echo "varname = ${varname} : contents: ${myvar[$varname]}"
    done
    

    This generates:

    varname = bar : contents: 1st line from bar
    varname = foo : contents: 1st line from foo