4

I'm having a problem with a shell script (POSIX shell under HP-UX, FWIW). I have a function called print_arg into which I'm passing the name of a parameter as $1. Given the name of the parameter, I then want to print the name and the value of that parameter. However, I keep getting an error. Here's an example of what I'm trying to do:

#!/usr/bin/sh

function print_arg
  {
  # $1 holds the name of the argument to be shown

  arg=$1

  # The following line errors off with
  #   ./test_print.sh[9]: argval=${"$arg"}: The specified substitution is not valid for this command.

  argval=${"$arg"}

  if [[ $argval != '' ]] ; then
    printf "ftp_func: $arg='$argval'\n"
  fi
  }

COMMAND="XYZ"

print_arg "COMMAND"

I've tried re-writing the offending line every way I can think of. I've consulted the local oracles. I've checked the online "BASH Scripting Guide". And I sharpened up the ol' wavy-bladed knife and scrubbed the altar until it gleamed, but then I discovered that our local supply of virgins has been cut down to, like, nothin'. Drat!

Any advice regarding how to get the value of a parameter whose name is passed into a function as a parameter will be received appreciatively.

chepner
  • 497,756
  • 71
  • 530
  • 681

5 Answers5

9

You could use eval, though using direct indirection as suggested by SiegeX is probably nicer if you can use bash.

#!/bin/sh

foo=bar
print_arg () {
    arg=$1
    eval argval=\"\$$arg\"
    echo "$argval"
}
print_arg foo
Community
  • 1
  • 1
Emil Sit
  • 22,894
  • 7
  • 53
  • 75
  • thanks much. I also thought @SiegeX's answer was a nice solution (and many thanks to @SiegeX for offering it up), but unfortunately the ${!foo} construct errored off under the POSIX shell in the same way that my attempts had. Yours worked perfectly under POSIX. Many thanks. – Bob Jarvis - Слава Україні Dec 07 '10 at 19:21
  • 2
    @Bob: There's no need for the command substitution, and in fact it would cause harm (depending on the shell, backslashes in the string and an initial `-` may be interpreted by `echo`, and trailing newlines would be stipped by the command substitution). Even simpler, you could just write `eval argval="\$$1"`. – Gilles 'SO- stop being evil' Dec 07 '10 at 21:58
  • @Gilles: thanks for the additional feedback. I've taken your simpler code and am running with it. :-) – Bob Jarvis - Слава Україні Dec 08 '10 at 15:17
  • Unless you're sure what can be passed to the function, it makes sense to remove from the argument any unnecessary characters: `arg=$(echo "$arg" | tr -d $'\n' | sed -r 's/[^a-zA-Z0-9_]//g'); eval argval=\$$arg`. – x-yuri Jun 29 '22 at 12:28
  • Or even: `arg=$(echo "$arg" | tr -dc 'a-zA-Z0-9_'); eval argval=\$$arg`. – x-yuri Jun 29 '22 at 12:40
8

In bash (but not in other sh implementations), indirection is done by: ${!arg}

Input

foo=bar
bar=baz

echo $foo
echo ${!foo}

Output

bar
baz
Gilles 'SO- stop being evil'
  • 104,111
  • 38
  • 209
  • 254
SiegeX
  • 135,741
  • 24
  • 144
  • 154
  • thanks for your answer. Unfortunately, under the POSIX shell we're stuck with here this construct didn't work. I tagged this question with 'bash' because *most of the time* BASH shell stuff works fine under the POSIX shell. Sadly, not this time. But many thanks anyways. – Bob Jarvis - Слава Україні Dec 07 '10 at 19:23
  • @Bob: There are *many* things that Bash can do that are not possible in a POSIX-only shell. It's better to be clear about your constraints than to imply something that you can't use. – Dennis Williamson Dec 07 '10 at 20:35
1

Even though the answer's already accepted, here's another method for those who need to preserve newlines and special characters like Escape ( \033 ): Storing the variable in base64.

You need: bc, wc, echo, tail, tr, uuencode, uudecode

Example

#!/bin/sh


#====== Definition =======#
varA="a
b
c"
# uuencode the variable
varB="`echo "$varA" | uuencode -m -`"
# Skip the first line of the uuencode output.
varB="`NUM=\`(echo "$varB"|wc -l|tr -d "\n"; echo -1)|bc \`; echo "$varB" | tail -n $NUM)`"


#====== Access =======#

namevar1=varB
namevar2=varA

echo simple eval:
eval "echo \$$namevar2"
echo simple echo: 
echo $varB
echo precise echo: 
echo "$varB"
echo echo of base64
eval "echo \$$namevar1"
echo echo of base64 - with updated newlines
eval "echo \$$namevar1 | tr ' ' '\n'"
echo echo of un-based, using sh instead of eval (but could be made with eval, too)
export $namevar1
sh -c "(echo 'begin-base64 644 -'; echo \$$namevar1 | tr ' ' '\n' )|uudecode"

Result

simple eval:
a b c
simple echo:
YQpiCmMK ====
precise echo:
YQpiCmMK
====
echo of base64
YQpiCmMK ====
echo of base64 - with updated newlines
YQpiCmMK
====
echo of un-based, using sh instead of eval (but could be made with eval, too)
a
b
c

Alternative

You also could use the set command and parse it's output; with that, you don't need to treat the variable in a special way before it's accessed.

u_Ltd.
  • 544
  • 1
  • 4
  • 9
  • Why would you store a value in base64? `sh` can store values with `\033` and other characters. And they're preserved by `eval val=\$$name`. – x-yuri Jun 29 '22 at 13:09
  • There are rare cases where this is needed. All of following must be true: 1. no performance requirements, 2. working with command substitution (``) because normal substitution e.g. in POSIX shell is not sufficient, 3. last newlines matter – u_Ltd. Jun 30 '22 at 17:40
  • And 4. you don't want to use functions. [use case: concatenation] – u_Ltd. Jun 30 '22 at 17:48
  • I must admit I cannot remember when I used base64 encoded variables last time – u_Ltd. Jun 30 '22 at 17:49
  • And I must admit I didn't know that you can assign variables without quoting (`a=$b`). – u_Ltd. Jun 30 '22 at 17:51
  • [Quoting when assigning is incidental anyway] – u_Ltd. Jun 30 '22 at 17:57
  • There're ways to preserve newlines, e.g.: `a=$'\n'; b=\`echo "$a"x\`; b=${b%x}`. And to confirm: `echo -n "$b" | od -c`. This I understand resolves concerns #2 and #3. (Or else I don't understand what means "not sufficient.") Regarding me carelessly mentioning `eval val=\$$name`. That's okay if you're sure that `$name` doesn't contain anything funny (a legitimate variable name, not `$; rm some-file` or something). If not, see [here](https://stackoverflow.com/a/36235741/52499). – x-yuri Jul 02 '22 at 11:57
1

A safer solution with eval:

v=1

valid_var_name='[[:alpha:]_][[:alnum:]_]*$'

print_arg() {
    local arg=$1
    if ! expr "$arg" : "$valid_var_name" >/dev/null; then
        echo "$0: invalid variable name ($arg)" >&2
        exit 1
    fi
    local argval
    eval argval=\$$arg
    echo "$argval"
}

print_arg v
print_arg 'v; echo test'

Inspired by the following answer.

x-yuri
  • 16,722
  • 15
  • 114
  • 161
1

This worked surprisingly well:

#!/bin/sh
foo=bar

print_arg () {
    local line name value
    set | \
    while read line; do
        name=${line%=*} value=${line#*=\'}
        if [ "$name" = "$1" ]; then
            echo ${value%\'}
        fi
    done
}

print_arg foo

It has all the POSIX clunkiness, in Bash would be much sorter, but then again, you won't need it because you have ${!}. This -in case it proves solid- would have the advantage of using only builtins and no eval. If I were to construct this function using an external command, it would have to be sed. Would obviate the need for the read loop and the substitutions. Mind that asking for indirections in POSIX without eval, has to be paid with clunkiness! So don't beat me!

ata
  • 2,045
  • 1
  • 14
  • 19
  • Are you sure that in POSIX every value is enclosed in `'`? Then, what if `foo=some\'value`? – x-yuri Jun 29 '22 at 12:58
  • @x-yuri hmmm you're right, perhaps it's best to just look at the first = character in the line and separate lhs and rhs right there. I'll revise that and edit if it works. – ata Sep 24 '22 at 00:16