81

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:)

John
  • 17,011
user40780
  • 1,941

4 Answers4

97

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
  • 3
    An alternate (and ugly) bash possibility: indirect variables: for ((i=2; i<=$#; i++)); do something with "${!i}"; done – glenn jackman Aug 27 '15 at 20:24
  • I am more familiar with this version, since I am more familiar with c++ :) – user40780 Aug 27 '15 at 20:26
  • @Stéphane Chazelas - could you please explain why $@ without quotes makes no sense and should not be used? – Martin Vegter Jul 25 '21 at 05: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. – Jeff Learman Jan 28 '22 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
dr_
  • 29,602
John
  • 17,011
  • could you explain in more detail how do I achieve that in the for loop? Thank you. – user40780 Aug 27 '15 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 '15 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 '15 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. – Jeff Learman Jan 28 '22 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.

MvG
  • 4,411