Output to variable with an indirect command (eval)

10,989

Solution 1

So you have a variable CMD whose value is the string $CMD_AIX, and the value of the variable CMD_AIX is the string (o=`host "myhost" `). You want to interpret the value of CMD_AIX as a shell command and execute it.

To execute the value of CMD_AIX as a shell command, you need to construct a shell command that does that. That's what eval is for:

eval "$CMD_AIX"

This command, in turn, is constructed dynamically. So you need to run it with eval:

dispatcher=…
eval "$dispatcher"

Given the way the inner command is constructed, we get

dispatcher="eval \"\$CMD_$OS\""
eval "$dispatcher"

Or cutting one variable out:

eval "eval \"\$CMD_$OS\""

Note that the command (o=`host "myhost" `) is pointless: it assigns the output of host to the variable o in a subshell; the value is not made available outside the subshell. Remove the parentheses. Also, host \"$host\" is weird: you're putting the value of the variable host between double quotes; it doesn't matter since host names don't contain shell special characters, but to use the value of the variable when evaluating the shell snippet, you should leave host unexpanded here.

#!/bin/sh
OS=AIX # should probably be OS=`uname -r`
host=myhost
CMD_AIX="o=\`host \"\$host\"\`"
eval "eval \"\$CMD_$OS\""

If you have ksh93 or bash ≥ 4.0, you can simplify this script by using associative arrays. Instead of stuffing the code for each operating system in a different variable, put them in an associative array.

#!/usr/bin/env bash
typeset -A commands
commands[AIX]="o=\`host \"\$host\"\`"
OS=AIX
host=myhost
if [ -n "${commands[$OS]}" ]; then
  eval "${commands[$OS]}"
else
  echo >&2 "Operating system $OS not supported"
  exit 2
fi

Solution 2

I think the problem with what you're doing there is that it doesn't make any sense. No offense, but I mean that I think you just have the wrong idea about what eval does. It looks like you're trying to use it just to run a command - that's basic shell functionality and doesn't require that the shell attempt to interpret its last expansions as new commands - which is what eval does.

The way you have all of your quotes written in you're protecting anything from being secondly evaluated anyway. Or, rather, you're protecting the first pass completely - and so nothing at all happens until the second pass. The problem with all of that is that you might as well just not do a second evaluation (and a second layer of quotes) at all.

It looks to me like you're attempting to run a certain command based on the value of $OS - maybe...? Well, this is among the most accessible API features in shell.

For example:

OS=AIX
host=myhost
_cmd_AIX() { host "$host"; }
_cmd_WIN() { exit "$((LOSE=1))"; }

You see - you can run those defined functions quoted. Their command name doesn't need any special second interpreter to have significance - not even when it occurs as the result of an expansion. It just needs to be in command position. They will still even accept parameters that way - so you could pass $host as $1 if you wished. You just call it like this:

"_cmd_$OS"

The $OS var will expand to AIX and the resulting command word will be _cmd_AIX - which is a predefined shell function the name of which occurs as a whole and properly delimited shell word in command position - and so it is executed. Just like that. No contortions necessary - and it even comes with its own array.

Redefine $OS at any time to the name of some other valid suffix for other behaviors.

Solution 3

With zsh:

$ echo $CMD
$CMD_AIX
$ echo ${(e)CMD}
(o=`host "myhost" `)
$ echo ${(e)${(e)CMD}}
(o=myhost has address 1.2.3.4)

The (e) expansion flag is to evaluate the expansions in the value of the variable being expanded.

With ksh93:

$ function CMD.get { eval ".sh.value=\"$_\""; }
$ function CMD_AIX.get { eval ".sh.value=\"$_\""; }
$ echo "$CMD_AIX"
(o=myhost has address 1.2.3.4)
$ echo "$CMD"
(o=myhost has address 1.2.3.4)
$ CMD=\$USER
$ echo "$CMD"
stephane

CMD.get is a discipline function that is invoked any time $CMD is expanded typically used to define what that expansion should be to allow variables with dynamic content. Here we define it as returning the evaluation of its content within double quotes (that assumes the value doesn't contain double quotes).

You could also use types:

typeset -T evaluating_variable=(
  function get { eval ".sh.value=\"$_\""; }
)

OS=AIX host=myhost
evaluating_variable CMD_AIX='(o=`host "'$host'"`)'
evaluating_variable CMD=\$CMD_$OS

Now why you'd want to do that in a shell script is another matter.

Share:
10,989

Related videos on Youtube

Luciano
Author by

Luciano

Updated on September 18, 2022

Comments

  • Luciano
    Luciano over 1 year

    What is wrong with this indirect command when run with eval ?

    #!/bin/bash
    OS=AIX
    host=myhost
    
    CMD_AIX="(o=\`host \"$host\" \`)"
    CMD=\$CMD_$OS
    
    echo $CMD
    eval echo $CMD
    
    eval "$CMD"
    

    Ouput:

    $ myscript.sh
    $CMD_AIX
    (o=`host "myhost" `)
    myscript.sh: line 12: (o=`host: command not found
    

    EDIT - NOTES: It's in a very heterogeneous structure with about 40 servers, mixing SCO, AIX and Linux, several versions of each, including variations of 32 and 64 bits. SCO and AIX cannot be updated. Bash and Korn shell are common in all servers but I'm limited to version 3 of bash (in SCO), Ksh has several differences between them, so we're preferring bash, which haven't support to associative arrays in v3 (it cannot be updated and it's outside of my decisions, and these servers will begin to be replaced by AIX 7 and Linux in a few months).

    There are a big system to be maintained which includes some scripts already based in sh and bsh, we won't and cannot reconstruct it now, just tittle maintenance to resist some months until the end of replacement.

    The approach used in these scripts is largely spread in a big solution which generates some scripts, which we cannot change it from scratch.

    The sample code above is just an adopted snippet, it isn't the real code. Please don't consider the logic, just the problem of indirection (eval).

    SOLUTION: I got a solution that worked very fine just chaining the eval, like:

    torun=`eval $CMD`
    output=`eval torun`
    

    This answer and that another one, which worked very fine and answered the question, were oddly down voted a lot.

    • lcd047
      lcd047 about 9 years
      What problem are you trying to solve?
    • Luciano
      Luciano about 9 years
      Like the question snippet of code, I need to capture the ouput of a command which need to be mounted dynamically, so I guess that eval is the best approach to do that.
    • lcd047
      lcd047 about 9 years
      It might be better to describe the problem, rather than the solution. Why does that command need to be "mounted dynamically"? What does that even mean?
    • terdon
      terdon about 9 years
      I'm willing to bet it doesn't need to be built. Have you considered using arrays instead? Could you explain why you are building your command this way?
    • Luciano
      Luciano about 9 years
      Sorry, it ins't a solution, but an approach, which lead me to get trapped by eval indirection, that I'm asking about. The problem or requirement are: Need to mount a command dynamically and run the command like it was run by system() function from C library.
    • Luciano
      Luciano about 9 years
      Guys this was just a 'simplified' snippet of a real script, just to show the eval indirection problem, sorry if I let some aspects of the original scripts which shouldn't and could polute the question, I'll edit it.
    • 0xC0000022L
      0xC0000022L about 9 years
      @Luciano: since you're asking for Bash, you should stick to $(...) for subshells instead of backticks. It will avoid the obvious escaping nightmare you're getting yourself into. Since you run the command locally, what's wrong with o=$(host "$host)"?
    • Luciano
      Luciano almost 9 years
      @0xC0000022L: Yes, of course. However some code are keep near portable as possible, if we need to just copy&paste and run in another shell with minimal rewrite.
    • Nordine Lotfi
      Nordine Lotfi about 3 years
      Those answers where you found your solution are probably downvoted a lot because of the whole eval is evil and the fact that it also call eval two times, one on it's output, which is a bit silly...although it work in this case.
  • Luciano
    Luciano almost 9 years
    Thanks, good explanation. Although it was already fixed, I added notes to the question. I guess this can explain better my situation.