78

I need to write a shell script that runs in this way:

./myscript arg1 arg2_1 arg2_2 arg2_3 ....... arg2_#

there is a for loop inside script

for i in $@

However, as I know, $@ includes $1 up to $($#-1). But for my program $1 is distinctly different from $2 $3 $4 etc. I would like to loop from $2 to the end... How do i achieve this? Thank you:)

4 Answers 4

93

First, note that $@ without quotes makes no sense and should not be used. $@ should only be used quoted ("$@") and in list contexts.

for i in "$@" qualifies as a list context, but here, to loop over the positional parameters, the canonical, most portable and simpler form is:

for i
do something with "$i"
done

Now, to loop over the elements starting from the second one, the canonical and most portable way is to use shift:

first_arg=$1
shift # short for shift 1
for i
do something with "$i"
done

After shift, what used to be $1 has been removed from the list (but we've saved it in $first_arg) and what used to be in $2 is now in $1. The positional parameters have been shifted 1 position to the left (use shift 2 to shift by 2...). So basically, our loop is looping from what used to be the second argument to the last.

With bash (and zsh and ksh93, but that's it), an alternative is to do:

for i in "${@:2}"
do something with "$i"
done

But note that it's not standard sh syntax so should not be used in a script that starts with #! /bin/sh -.

In zsh or yash, you can also do:

for i in "${@[3,-3]}"
do something with "$i"
done

to loop from the 3rd to the 3rd last argument.

In zsh, $@ is also known as the $argv array. So to pop elements from the beginning or end of the arrays, you can also do:

argv[1,3]=() # remove the first 3 elements
argv[-3,-1]=()

(shift can also be written 1=() in zsh)

In bash, you can only assign to the $@ elements with the set builtin, so to pop 3 elements off the end, that would be something like:

set -- "${@:1:$#-3}"

And to loop from the 3rd to the 3rd last:

for i in "${@:3:$#-5}"
do something with "$i"
done

POSIXly, to pop the last 3 elements of "$@", you'd need to use a loop:

n=$(($# - 3))
for arg do
  [ "$n" -gt 0 ] && set -- "$@" "$arg"
  shift
  n=$((n - 1))
done
4
  • 3
    An alternate (and ugly) bash possibility: indirect variables: for ((i=2; i<=$#; i++)); do something with "${!i}"; done Aug 27, 2015 at 20:24
  • I am more familiar with this version, since I am more familiar with c++ :)
    – user40780
    Aug 27, 2015 at 20:26
  • @Stéphane Chazelas - could you please explain why $@ without quotes makes no sense and should not be used? Jul 25, 2021 at 5:29
  • @Stéphane Chazelas - because "$@" means quote every string in $*, so for cmd "a = b" c d, inside cmd, "$@" would have three elements, a = b, b, and c, whereas $@ would be no different than $*, producing 5 elements: a, =, b, c, and d. Jan 28, 2022 at 16:56
19

I think you want the shift builtin. It renames $2 to $1, $3 to $2, etc.

Like this:

shift
for i in "$@"; do
    echo $i
done
4
  • could you explain in more detail how do I achieve that in the for loop? Thank you.
    – user40780
    Aug 27, 2015 at 19:51
  • 1
    You don't - you use it before entering the for loop, then you just loop through $@ normally. After the shift call, $@ should be arg2_1 arg2_2 arg2_3...
    – John
    Aug 27, 2015 at 19:54
  • However, I will have one more question: Suppose I want to loop from $1 until $($#-2) (i.e. arg_1 until arg_2_#-1, except arg_2_#)... What should I do?
    – user40780
    Aug 27, 2015 at 19:57
  • @user40780 - that is answered in the selected answer. Instead of using shift, use "${@:1:$#-2}" . To understand better why this works, study "bash arrays", and note that $@ is a special case. Where you'd use ${A[@]:1:$#-2} for an array named A, for $@ you omit the [@] part. Jan 28, 2022 at 17:10
4

There's always the caveman approach:

first=1
for i
do
        if [ "$first" ]
        then
                first=
                continue
        fi
        something with "$i"
done

This leaves $@ intact (in case you want to use it later), and simply loops over every argument, but doesn't process the first one.

3

In bash you can also write that loop with explicit indexing:

for ((i=2; i<=$#; ++i)); do
  process "${!i}"
done

This iterates over all arguments from the second one to the last one. If you want to exclude the last argument instead, simply make this

for ((i=1; i<=$#-1; ++i)); do
  process "${!i}"
done

and if you only want to take every other argument, write it as

for ((i=1; i<=$#; i+=2)); do
  process "${!i}"
done

The story behind this is the arithmetic version of the for builtin, combined with the argument-count $# and the variable indirection ${…}.

One nice application is that you can use this to decide, inside the loop, whether a given option will take the argument which follows it as a value. If it does, increment i (e.g. writing : $((++i))) to consume the next value and skip it during iteration.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .