Dynamic variable names in Bash

后端 未结 14 1984
清酒与你
清酒与你 2020-11-21 05:38

I am confused about a bash script.

I have the following code:

function grep_search() {
    magic_way_to_define_magic_variable_$1=`ls | tail -1`
    e         


        
相关标签:
14条回答
  • 2020-11-21 06:10

    Wow, most of the syntax is horrible! Here is one solution with some simpler syntax if you need to indirectly reference arrays:

    #!/bin/bash
    
    foo_1=(fff ffffd) ;
    foo_2=(ggg ccc) ;
    
    for i in 1 2 ;
    do
        eval mine=( \${foo_$i[@]} ) ;
        echo ${mine[@]}" " ;
    done ;
    

    For simpler use cases I recommend the syntax described in the Advanced Bash-Scripting Guide.

    0 讨论(0)
  • 2020-11-21 06:11

    Beyond associative arrays, there are several ways of achieving dynamic variables in Bash. Note that all these techniques present risks, which are discussed at the end of this answer.

    In the following examples I will assume that i=37 and that you want to alias the variable named var_37 whose initial value is lolilol.

    Method 1. Using a “pointer” variable

    You can simply store the name of the variable in an indirection variable, not unlike a C pointer. Bash then has a syntax for reading the aliased variable: ${!name} expands to the value of the variable whose name is the value of the variable name. You can think of it as a two-stage expansion: ${!name} expands to $var_37, which expands to lolilol.

    name="var_$i"
    echo "$name"         # outputs “var_37”
    echo "${!name}"      # outputs “lolilol”
    echo "${!name%lol}"  # outputs “loli”
    # etc.
    

    Unfortunately, there is no counterpart syntax for modifying the aliased variable. Instead, you can achieve assignment with one of the following tricks.

    1a. Assigning with eval

    eval is evil, but is also the simplest and most portable way of achieving our goal. You have to carefully escape the right-hand side of the assignment, as it will be evaluated twice. An easy and systematic way of doing this is to evaluate the right-hand side beforehand (or to use printf %q).

    And you should check manually that the left-hand side is a valid variable name, or a name with index (what if it was evil_code # ?). By contrast, all other methods below enforce it automatically.

    # check that name is a valid variable name:
    # note: this code does not support variable_name[index]
    shopt -s globasciiranges
    [[ "$name" == [a-zA-Z_]*([a-zA-Z_0-9]) ]] || exit
    
    value='babibab'
    eval "$name"='$value'  # carefully escape the right-hand side!
    echo "$var_37"  # outputs “babibab”
    

    Downsides:

    • does not check the validity of the variable name.
    • eval is evil.
    • eval is evil.
    • eval is evil.

    1b. Assigning with read

    The read builtin lets you assign values to a variable of which you give the name, a fact which can be exploited in conjunction with here-strings:

    IFS= read -r -d '' "$name" <<< 'babibab'
    echo "$var_37"  # outputs “babibab\n”
    

    The IFS part and the option -r make sure that the value is assigned as-is, while the option -d '' allows to assign multi-line values. Because of this last option, the command returns with an non-zero exit code.

    Note that, since we are using a here-string, a newline character is appended to the value.

    Downsides:

    • somewhat obscure;
    • returns with a non-zero exit code;
    • appends a newline to the value.

    1c. Assigning with printf

    Since Bash 3.1 (released 2005), the printf builtin can also assign its result to a variable whose name is given. By contrast with the previous solutions, it just works, no extra effort is needed to escape things, to prevent splitting and so on.

    printf -v "$name" '%s' 'babibab'
    echo "$var_37"  # outputs “babibab”
    

    Downsides:

    • Less portable (but, well).

    Method 2. Using a “reference” variable

    Since Bash 4.3 (released 2014), the declare builtin has an option -n for creating a variable which is a “name reference” to another variable, much like C++ references. Just as in Method 1, the reference stores the name of the aliased variable, but each time the reference is accessed (either for reading or assigning), Bash automatically resolves the indirection.

    In addition, Bash has a special and very confusing syntax for getting the value of the reference itself, judge by yourself: ${!ref}.

    declare -n ref="var_$i"
    echo "${!ref}"  # outputs “var_37”
    echo "$ref"     # outputs “lolilol”
    ref='babibab'
    echo "$var_37"  # outputs “babibab”
    

    This does not avoid the pitfalls explained below, but at least it makes the syntax straightforward.

    Downsides:

    • Not portable.

    Risks

    All these aliasing techniques present several risks. The first one is executing arbitrary code each time you resolve the indirection (either for reading or for assigning). Indeed, instead of a scalar variable name, like var_37, you may as well alias an array subscript, like arr[42]. But Bash evaluates the contents of the square brackets each time it is needed, so aliasing arr[$(do_evil)] will have unexpected effects… As a consequence, only use these techniques when you control the provenance of the alias.

    function guillemots() {
      declare -n var="$1"
      var="«${var}»"
    }
    
    arr=( aaa bbb ccc )
    guillemots 'arr[1]'  # modifies the second cell of the array, as expected
    guillemots 'arr[$(date>>date.out)1]'  # writes twice into date.out
                # (once when expanding var, once when assigning to it)
    

    The second risk is creating a cyclic alias. As Bash variables are identified by their name and not by their scope, you may inadvertently create an alias to itself (while thinking it would alias a variable from an enclosing scope). This may happen in particular when using common variable names (like var). As a consequence, only use these techniques when you control the name of the aliased variable.

    function guillemots() {
      # var is intended to be local to the function,
      # aliasing a variable which comes from outside
      declare -n var="$1"
      var="«${var}»"
    }
    
    var='lolilol'
    guillemots var  # Bash warnings: “var: circular name reference”
    echo "$var"     # outputs anything!
    

    Source:

    • BashFaq/006: How can I use variable variables (indirect variables, pointers, references) or associative arrays?
    • BashFAQ/048: eval command and security issues
    0 讨论(0)
  • 2020-11-21 06:13

    I've been looking for better way of doing it recently. Associative array sounded like overkill for me. Look what I found:

    suffix=bzz
    declare prefix_$suffix=mystr
    

    ...and then...

    varname=prefix_$suffix
    echo ${!varname}
    
    0 讨论(0)
  • 2020-11-21 06:13

    Example below returns value of $name_of_var

    var=name_of_var
    echo $(eval echo "\$$var")
    
    0 讨论(0)
  • 2020-11-21 06:14

    Use an associative array, with command names as keys.

    # Requires bash 4, though
    declare -A magic_variable=()
    
    function grep_search() {
        magic_variable[$1]=$( ls | tail -1 )
        echo ${magic_variable[$1]}
    }
    

    If you can't use associative arrays (e.g., you must support bash 3), you can use declare to create dynamic variable names:

    declare "magic_variable_$1=$(ls | tail -1)"
    

    and use indirect parameter expansion to access the value.

    var="magic_variable_$1"
    echo "${!var}"
    

    See BashFAQ: Indirection - Evaluating indirect/reference variables.

    0 讨论(0)
  • 2020-11-21 06:15

    for varname=$prefix_suffix format, just use:

    varname=${prefix}_suffix
    
    0 讨论(0)
提交回复
热议问题