13

I am a bit stuck. My task is to print the arguments to my script in reverse order except the third and fourth.

What I have is this code:

#!/bin/bash

i=$#
for arg in "$@"
do
    case $i
    in
        3) ;;
        4) ;;
        *) eval echo "$i. Parameter: \$$i";;
    esac
    i=`expr $i - 1`
done

As I hate eval (greetings to PHP), I am looking for a solution without it but I am not able to find one.

How can I define the position of the argument dynamically?

PS: No its not a homework, I am learning shell for an exam so I try to solve old exams.

7 Answers7

14

eval is the only portable way to access a positional parameter by its dynamically-chosen position. Your script would be clearer if you explicitly looped on the index rather than the values (which you aren't using). Note that you don't need expr unless you want your script to run in antique Bourne shells; $((…)) arithmetic is in POSIX. Limit the use of eval to the smallest possible fragment; for example, don't use eval echo, assign the value to a temporary variable.

i=$#
while [ "$i" -gt 0 ]; do
  if [ "$i" -ne 3 ] && [ "$i" -ne 2 ]; then
    eval "value=\${$i}"
    echo "Parameter $i is $value"
  fi
  i=$((i-1))
done

In bash, you can use ${!i} to mean the value of the parameter whose name is $i. This works when $i is either a named parameter or a number (denoting a positional parameter). While you're at it, you can make use of other bash convenience features.

for ((i=$#; i>0; i--)); do
  if ((i != 3 && i != 4)); then
    echo "Parameter $i is ${!i}"
  fi
done
8

I keep a script reverse on my path that does this:

#!/bin/sh

if [ "$#" -gt 0 ]; then
    arg=$1
    shift
    reverse "$@"
    printf '%s\n' "$arg"
fi

Example usage:

$ reverse a b c '*' '-n'
-n
*
c
b
a

You can also use a function instead of a dedicated script.

  • Note that that one (contrary to the other answers posted so far) would also work in the Bourne shell (not that there's any reason to use that shell nowadays) – Stéphane Chazelas Jun 12 '15 at 14:43
  • @StéphaneChazelas: Why quote $#?  Can it be anything other than a non-negative integer? – G-Man Says 'Reinstate Monica' Jun 12 '15 at 17:05
  • 2
    @G-Man, leaving a variable unquoted in list context is invoking the split+glob operator. There's not reason why you'd want to invoke it here. That has nothing to do with the content of the variable. See also http://unix.stackexchange.com/a/171347 towards the end. Also note that some shells (dash, posh for instance) still inherit IFS from the environment. – Stéphane Chazelas Jun 12 '15 at 18:44
  • I found that, on Android, I had to declare local arg=$1 – starfry Sep 04 '16 at 09:50
3

Assuming the positional parameters don't contain newline characters:

[ "$#" -gt 0 ] && printf '%s\n' "$@" | #print in order
sed '3,4 d' |                          #skip 3 and 4
tac                                    #reverse (try tail -r on
                                       #non-GNU systems).

Test:

set 1 2 3 4 5 6
printf '%s\n' "$@" | 
sed '3,4 d' |
tac

Test output:

6
5
2
1
Petr Skocik
  • 28,816
2

This is a correct and non-dangerous use of eval. You fully control the content that you are evaling.

If it still gives you bad feelings, then if you don't care about portability, you can use Bash's ${!i} indirection syntax.

Shawn J. Goff
  • 46,081
1

With zsh:

$ set a 'foo bar' c '' '*'
$ printf '<%s>\n' "${(Oa)@}"
<*>
<>
<c>
<foo bar>
<a>

Oa is a parameter expansion flag to sort the array elements upon expansion in reverse array indices.

To exclude 3 and 4:

$ printf '<%s>\n' "${(Oa)@[5,-1]}" "${(Oa)@[1,2]}"
<*>
<foo bar>
<a>
0

With basically any shell:

printf '{ PS4=\${$(($#-$x))}; } 2>&3; 2>&1\n%.0s' |
x=LINENO+1 sh -sx "$@" 3>/dev/null

And you don't need to use subshells. For example:

set -x a b c
{ last= PS4=\${last:=\${$#}}; set +x; } 2>/dev/null
echo "$last"

...prints...

c

And here is a shell function which can set a shell alias for you that will print the arguments either forward or backward:

tofro() case $1 in (*[!0-9]*|'') ! :;;(*) set "$1"
        until   [ "$1" -eq "$(($#-1))" ]    &&
                shift && alias args=":; printf \
            \"%.\$((\$??\${#*}:0))s%.\$((!\$??\${#*}:0))s\n\" $* "
        do      [ "$#" -gt 1 ] &&
                set "$@ \"\${$#}\" " '"${'"$((1+$1-$#))"'}"' ||
                set "$1" '"$1" "${'"$1"'}"'
        done;   esac

It doesn't attempt to store the literal values for any arguments, but rather it puts a string like this in the args alias:

:;printf    "%.$(($??${#*}:0))s%.$((!$??${#*}:0))s\n" \
            "$1" "${3}" "${2}"  "${2}" "${3}"  "${1}"

...and so stores only references to the parameters backwards and forwards. It will store up to a count as given it as an argument. And so the above alias was generated like:

tofro 3

printf's behavior is affected based on the return value of the previous command - which is always : the null command, and so usually true. printf will skip half of its arguments each time it prints - which will, by default, get the arguments printed out from smallest numbered to largest. However, if you just do:

! args

...it prints them in reverse.

Because the alias does not store any literal values, its value remains static while the actual args might change, but it will still reference as many as it might. For example:

set one two three
tofro 3
args; ! args
shift; args; ! args

...which prints...

one
two
three
three
two
one
two
three


three
two

But resetting the alias can be done like:

tofro 2
args; ! args

...and so it prints...

two
three
three
two
mikeserv
  • 58,310
-3
declare -a argv=( "$@" )
for (( i=$((${#argv[@]}-1)) ; i>=0 ; i=$(($i-1)) )) ; do
        echo "${argv[$i]}"
        # use "${argv[$i]}" in here...
done