1

I'm using Bash 4.

I'm trying out a few experiments with Bash. I want to dynamically assign an array to the value of a variable. If you read the code below, it will be easier to understand.

myFunc =RESULT_PREF 'bar' 'baz'

function myFunc() {
    local params
    parser params "$@"
}


function parser() {
    if [ '=' = "${2::1}" ]; then
        local name="${1}"
        shift 2
        local param_array=( "$@" )

        # In PHP, I'd do:
        # $$name = $param_array

        # The ideal outcome:
        # $params should contain the values 'bar' and 'baz'.
    fi
}

How do I do what I've mentioned in the comments of my code? I can't seem to do this using eval. Even if I could, I've read so many bad things about eval.

Any advice?

  • Are you saying that you want one function to assign to a local variable in another function?  I believe that's impossible.  Also, you should get into the habit of saying "$@" instead of plain $@.  Also, you don't need to quote 'bar' and 'baz', but, as long as you are doing so, you should probably quote =RESULT_PREF, too, to make it clear that you're treating it as just a string parameter. – G-Man Oct 24 '15 at 1:45
1
myFunc(){
    parse "$1" &&                          #parse() is a test
    eval "  shift                          #$1 is a valid, eval-safe name
            local name$1 ${1#=}="'("$@")'  #$1 is expanded before shift
}

parse()
    case ${1#=} in
    ("$1"|""|[0-9]*|*[!_[:alnum:]]*) ! :   #return false for all invalid names
    esac

It does also work without eval given another execution step or two:

myFunc(){
    local -a "name$1[@]" "${1#=}"='("${@:2}")' &&
    [ -z "${1%=*}" ] && printf %s\\n "${!name}"
}

In that case I just allow the local builtin to do all of the validation and assignment at once. Basically, if the local command can assign "${1#=}"=("${@#"$1"}") successfully and "${1%=*}" is null then you know the assignment has taken place successfully. Any syntax error - such as bad shell names - will automatically fail and return with error during the local assignment, and the simple validation [ test ] which follows is all that is necessary to ensure that you don't accidentally do local name=morename=("${@#"$1"}").

The natural upshot to this is that when a bad name is passed in as =$1 the shell will automatically print out a meaningful error message to stderr for you and do all of the error handling automatically with no fuss.

Like this:

myFunc =goodname 1 2 3 'and some     ;more' &&
myFunc =bad-name 1 2 3 'and some     ;more'

1
2
3
and some     ;more
bash: local: `bad-name=("${@:2}")': not a valid identifier

Note that doing functionname(){list;} is almost definitely not what you mean to do.

If you intend to localize the function's traps and similar you need functionname{list;}.

If you intend to share all state but your locally defined variables with the current shell then functionname(){list;} or name()list are equivalent because the function keyword is ignored by the shell (except that some shells which implement it tend to parse the following list incorrectly if it is not contained in curly braces) when name is followed by ().

  • Interesting. I didn't know local could do some sort of validation, as well. parse() will be a function stored in a separate shell file that will be sourced in to any script where a function will need it, so I want to keep most of the grunt work within parse. This is all a part of my little experiment :) I'm still learning the finer points of Bash. Is there any way I can achieve this? – Housni Oct 25 '15 at 22:59
  • sure, i guess, but it will get messy. when you give a function jurisdiction over another namespace it can tend to get messy. it's why i recommend the case statement - it makes more sense to me to use functions as tests and allow their returns to direct my handling of namespaces. theres another way, too, with getopts. – mikeserv Oct 26 '15 at 1:59
3

In bash 4.3+, there is a way to define a variable reference which you can see in help declare:

-n: make NAME a reference to the variable named by its value

Therefore a script like this outputs a line a:

n(){ declare -n g="$1"; g=a; }
n b; printf '%s\n' "$b";

So here's how you should do that in parser:

parser() {
    [ "$1" ] || ! echo 'no name given' >&2 || return 42
    if [[ "$2" == =* ]]; then
        declare -n name="$1"
        shift 2
        name=("$@")
    fi
}

Using eval does work, but it can be unsafe if you expand the rhs too early:

shopt -e extglob # bash >= 3
[[ "$1" == [[:alpha:]_]*([[:word:]]) ]] || return 42 # invalid varname
# mikeserv has another solution for filtering, basically expanding
# the inverse of the expression to basic, portable globs
# Do this:
eval "$1"'=( "$@" )'
eval "$foo=\$bar"
# But not:
eval "$1=(\"$@\")"
eval "$foo=$bar"

I think you will know what to do with eval then. But simply for a higher possibility that you will accept my answer, let me write it in full:

shopt -s extglob
parser() {
    [[ "$1" == [[:alpha:]_]*([[:word:]]) ]] || ! echo 'bad varname' >&2 || return 42
    if [[ "$2" == =* ]]; then
        name="$1"
        shift 2
        eval "$name=(\"\$@\")"
    fi
}
  • Sounds interesting, but that must be a very recent addition.  It's not in bash version 4.2.37, and I can't find it in the online man pages. – G-Man Oct 24 '15 at 2:19
  • @G-Man Oh, yes, it seems to be bash 4.3. However it is in the man page: gnu.org/software/bash/manual/bash.html#Shell-Parameters talks about the nameref. – Arthur2e5 Oct 24 '15 at 18:34
  • @mikeserv Oops, I shouldn't use that !. – Arthur2e5 Oct 24 '15 at 20:02
  • @mikeserv Now it matches [a-zA-Z][a-zA-Z0-9_]*. – Arthur2e5 Oct 24 '15 at 20:03
  • and now you still have to handle the initial =. – mikeserv Oct 24 '15 at 20:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.