Take the 2-minute tour ×
Unix & Linux Stack Exchange is a question and answer site for users of Linux, FreeBSD and other Un*x-like operating systems. It's 100% free, no registration required.

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.

share|improve this question

7 Answers 7

up vote 12 down vote accepted

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
share|improve this answer
    
I can't use arg as they are ordered correctly and not in reverse. To the usage of expr, I am limited to use the standard only. –  WarrenFaith Sep 25 '11 at 16:07
1  
@WarrenFaith If your script starts with #!/bin/bash, you can use ${!i} and (()). If you want to stick to standard sh, these aren't available, but $((…)) is. –  Gilles Sep 25 '11 at 16:10
    
Ok, I think I can work with this :) –  WarrenFaith Sep 25 '11 at 16:11
    
Note that the antique Bourne shells wouldn't be able to access positional parameters beyond $9 anyway. –  Stéphane Chazelas Jun 12 at 14:41
1  
eval is not the only portable way as Ryan has shown. –  Stéphane Chazelas Jun 12 at 15:04

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
share|improve this answer

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
share|improve this answer
    
Thanks for the edit, @StéphaneChazelas. –  PSkocik Jun 12 at 15:03

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>
share|improve this answer

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.

share|improve this answer
    
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 at 14:43
    
@StéphaneChazelas: Why quote $#?  Can it be anything other than a non-negative integer? –  G-Man Jun 12 at 17:05
    
@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 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 at 18:44
declare -a argv=( "$@" )
for (( i=$((${#argv[@]}-1)) ; i>=0 ; i=$(($i-1)) )) ; do
        echo "${argv[$i]}"
        # use "${argv[$i]}" in here...
done
share|improve this answer
2  
no explanation at all, nice. Downvote from me. Explain what your code does and I might change my mind. –  WarrenFaith Jun 12 at 11:22
2  
Can be simplified to for ((i = $# - 1; i >= 0; i--)) –  Stéphane Chazelas Jun 12 at 11:33
    
Am I hallucinating?  I thought I saw words in the question that said "print the arguments ... except the third and fourth". –  G-Man Jun 12 at 12:18
1  
@G-Man, the title says "print arguments in reverse" which this is answering without using eval. Excluding 3 and 4 from that is trivial. I don't think the downvotes are justified. It is also self-explanatory (though as I said could be simplified a great deal). –  Stéphane Chazelas Jun 12 at 15:02

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.

share|improve this answer
    
so ${!i} isn't part of the standard syntax? –  WarrenFaith Sep 25 '11 at 16:05
    
No, it's a Bashism. Here are the POSIX parameter expansions: pubs.opengroup.org/onlinepubs/009695399/utilities/… –  Shawn J. Goff Sep 25 '11 at 16:18

Your Answer

 
discard

By posting your answer, you agree to the privacy policy and terms of service.

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