5

Say I have the following:

for i in $@; do
    echo ${i+1}
done

and I run this on shell $ test.sh 3 5 8 4, it outputs 1 1 1 1 why wouldn't ${i+1} work? I am trying to access the next argument for a list of command line arguments.

1
  • What do you mean next argument? Do you mean if gave 3, then you want 4?
    – cuonglm
    Feb 10, 2016 at 6:57

5 Answers 5

11

Each character in shell may have an special meaning.

The code ${i+1} does not mean "add 1 to i".
To find what it means, execute this command:

LESS=+/'\{parameter\:\+word\}' man bash

And read:

${parameter:+word}
Use Alternate Value. If parameter is null or unset, nothing is substituted, otherwise the expansion of word is substituted.

And a little way above:

Omitting the colon results in a test only for a parameter that is unset.

As $i has a value set by the loop for i in $@; the "Alternate Value" is substituted and 1 is printed.

If you want to add 1 to the value of the arguments, do this:

for    i
do     echo "$((i+1))"
done

There is no need for the in "$@" (and get used to quoting all expansions).

$ ./test.sh 3 5 8 4
4
6
9
5

Next argument.

But that is not "the next argument" either. The core issue is with your loop, you are using the value of arguments in a loop, not an index to the arguments. You need to loop over an index i of the arguments, not the value i of each argument. Something like:

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

That will print an index, as this shows:

$ ./test.sh 3 5 8 4
1
2
3
4

Indirection

How do we access the argument at position $i?: With indirection:

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

See the simple ! added ?

Now it runs like this:

$  ./test.sh  3 5 8 4
3
5
8
4

Final solution.

And to print both the present argument and the next, use this:

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

No, there is no simpler way than to calculate the value in the variable $j.


$ ./test.sh 3 5 8 4
3 5
5 8
8 4
4 

That works for text also:

$ ./test.sh sa jwe yqs ldfgt
sa jwe
jwe yqs
yqs ldfgt
ldfgt 
2
  • very well explained. Thanks!
    – Rags
    Feb 8, 2022 at 5:19
  • Very helpful. You taught me. Feb 4 at 1:38
1

In your example code i is the value, not the index.
You'll need the exclamation mark for using a variable value as variable.
I didn't get the (i+1) to work without defining another variable. Maybe someone can give a hint to optimize that.

check () {
  for i in $(seq $#); do
    let j=i+1
    echo "$i: i=${!i} i+1=${!j}"
  done
}

check a b c

1: i=a i+1=b
2: i=b i+1=c
3: i=c i+1=
0

First note that, ${i+1} is an parameter expansion pattern, which means if parameter i is Not unset or null, it will be substituted by the expansion of 1, which of course just results in 1. Hence you are getting all 1's in the output.

You need to use an arithmetic operator, not parameter expansion.

For example:

% check () { for i in "$@"; do echo $((i+1)); done ;} 

% check 2 4 5 6
3
5
6
7
1
  • 1
    hi, I meant to ask how to access the next/previous argument using just i Feb 10, 2016 at 6:05
0

I find myself using shift for the same purpose. Here's a small example taken from here:

#!/bin/bash

# This script can clean up files that were last accessed over 365 days ago.

USAGE="Usage: $0 dir1 dir2 dir3 ... dirN"

if [ "$#" == "0" ]; then
    echo "$USAGE"
    exit 1
fi

while (( "$#" )); do

if [[ $(ls "$1") == "" ]]; then 
    echo "Empty directory, nothing to be done."
  else 
    find "$1" -type f -a -atime +365 -exec rm -i {} \;
fi

shift
0

Expanding on user79743's answer above:

Suppose we want to have a argument that takes a value like --mode delete or --mode list.

When ${!i} == "--mode", we want to read the value for the following index (j or i+1) and retain it for later. Consider that on the next loop iteration that we will end up inspecting i+1 a second time (was j on previous loop, is i on the current loop).

I found that i is mutable, so we can increment it by 1 meaning that the next loop iteration will be i+2, so to speak.

I did want to say that, generally speaking, it is unwise to mutate a value while iterating it. This approach did work for me. I'm sure there are probably several (better) ways to do this.

Without incrementing i. Some indexes are inspected more than once.

# $ ./test.sh --mode delete 3 5 8 4
# [1]: --mode [2]: delete
# [2]: delete # <-- index [2] is inspected twice
# [3]: 3
# [4]: 5
# [5]: 8
# [6]: 4
for (( i=1; i<=$#; i++)); do
  if [[ ${!i} == "--mode" ]]; then
    j=$((i+1))
    MODE=${!j} # retain for later
    echo "[$i]: ${!i} [$j]: ${!j}"
  else
    echo "[$i]: ${!i}"
  fi
done

With incrementing i. Each index is only inspected once.

# $ ./test.sh --mode delete 3 5 8 4
# [1]: --mode [2]: delete
# [3]: 3
# [4]: 5
# [5]: 8
# [6]: 4
for (( i=1; i<=$#; i++)); do
  if [[ ${!i} == "--mode" ]]; then
    j=$((i+1))
    MODE=${!j} # retain for later
    echo "[$i]: ${!i} [$j]: ${!j}"
    i=$i+1 # <-- incrementing the index
  else
    echo "[$i]: ${!i}"
  fi
done

You must log in to answer this question.

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