637

I have a simple script where the first argument is reserved for the filename, and all other optional arguments should be passed to other parts of the script.

Using Google I found this wiki, but it provided a literal example:

echo "${@: -1}"

I can't get anything else to work, like:

echo "${@:2,1}"

I get "Bad substitution" from the terminal.

What is the problem, and how can I process all but the first argument passed to a bash script?

ahcox
  • 9,349
  • 5
  • 33
  • 38
theta
  • 24,593
  • 37
  • 119
  • 159
  • 37
    To call out for anyone else confused, the wrong shebang was provided causing `"{@:2}"` to not work, which is why the correct answer matches above. – Guvante Oct 22 '15 at 00:06
  • 6
    You just used default shell, which is dash on Ubuntu and many other Linuxes. In dash "${@: -1}" is interpreted as: {parameter:-word} - Use Default Values, and use word if the parameter is not defined or null. So in dash "${@: -1}" results exactly the same as "$@". To use bash just use the following first line in the script file: #!/bin/bash – luart Dec 21 '15 at 06:20

5 Answers5

931

Use this:

echo "${@:2}"

The following syntax:

echo "${*:2}"

would work as well, but is not recommended, because as @Gordon already explained, that using *, it runs all of the arguments together as a single argument with spaces, while @ preserves the breaks between them (even if some of the arguments themselves contain spaces). It doesn't make the difference with echo, but it matters for many other commands.

Community
  • 1
  • 1
Oliver Charlesworth
  • 267,707
  • 33
  • 569
  • 680
  • 9
    I just realised my shebang was bad: `#!/usr/bin/env sh` That's why I had problems. You example works fine, same as above provided, after I removed that shebang – theta Jan 29 '12 at 22:36
  • 134
    Use `"${@:2}"` instead -- using `*` runs all of the arguments together as a single argument with spaces, while `@` preserves the breaks between them (even if some of the arguments themselves contain spaces). The difference isn't noticeable with `echo`, but it matters for many other things. – Gordon Davisson Jan 30 '12 at 05:13
  • 3
    @GordonDavisson The whole point here is to run the arguments together. Were we passing filenames, you would be correct. `echo` is forgiving enough to concatenate them for you; other commands might not be so nice. Don't just use one or the other: learn the difference between `*` and `@`, and when to use each. You should be using them about equally. A good example of when this will be a problem: if `$3` contains a line break (`\n`), it will be replaced with a space, provided you have a default `$IFS` variable. – Zenexer May 10 '13 at 04:15
  • 1
    @Zenexer: As I understand it, the question was how to pass all but the first argument to "to other part of script", with `echo` just used as an example -- in which case they should *not* be run together. In my experience, situations where you want them run together are rare (see [this question for one example](http://stackoverflow.com/questions/3348443/a-confusion-about-array-versus-array-in-the-context-of-a-bash-comple/)), and `"$@"` is almost always what you want. Also, the problem you mention with a line break only occurs if `$@` (or `$*`) isn't in double-quotes. – Gordon Davisson May 10 '13 at 05:43
  • @GordonDavisson Hmm... you're right, now that I think about it; the line break shouldn't be an issue. I must have been thinking of something else. However, I still have to disagree about running them together; I need to use `$*` quite often in my scripts. – Zenexer May 10 '13 at 12:26
  • FWIW, the [Google Shell Style Guide](http://google-styleguide.googlecode.com/svn/trunk/shell.xml?showone=Quoting#Quoting) says that `"$@"` is right almost everytime, and `$*` is wrong almost everytime. There is more information why in the style guide. – Dennis Dec 29 '13 at 02:33
  • What's the name for this syntax? – Addison Jan 11 '19 at 15:43
  • @Addison: It's the "parameter:offset" syntax described [here](https://www.gnu.org/software/bash/manual/bash.html#Shell-Parameter-Expansion). – Oliver Charlesworth Jan 11 '19 at 15:48
  • @theta that is to say, this is Bash syntax not Sh syntax ! – Samie Bencherif Jun 11 '22 at 14:46
  • @theta take a look: `$ sh -c 'echo 1 $1; echo 2 "$@"; echo 3 "${@:2}"; shift ; echo 4 1 $1 ; echo 5 2 "$@" ; echo 6 3 "${@:2}"' -- Мама мыла раму` – Anthony Jul 12 '22 at 13:39
241

If you want a solution that also works in /bin/sh try

first_arg="$1"
shift
echo First argument: "$first_arg"
echo Remaining arguments: "$@"

shift [n] shifts the positional parameters n times. A shift sets the value of $1 to the value of $2, the value of $2 to the value of $3, and so on, decreasing the value of $# by one.

Esteis
  • 4,669
  • 2
  • 29
  • 45
Ben Jackson
  • 90,079
  • 9
  • 98
  • 150
  • 29
    +1: You should probably save $1 into a variable before shifting it away. – glenn jackman Jan 30 '12 at 13:57
  • 3
    Surprisingly `foo=shift` doesn't do what I'd expect. – Keith Smiley Mar 03 '14 at 16:45
  • 1
    I know this is old, but try `foo=$(shift)` – raumaan kidwai Jun 03 '15 at 19:39
  • 12
    @raumaankidwai That doesn't work for 2 reasons: 1) `shift` (in shell) does not have any output. It just discards `$1` and shifts everything down. 2) `$(...)` starts a subshell, which has its own local arguments. It shifts the arguments in the subshell, which does not affect the parent – Ben Jackson Jun 03 '15 at 22:36
  • 2
    Thanks :) Is there any way to do what `somecommand "$1" "${@:2}"` does with this method (i.e. shift "inline") though? – LHeng Dec 11 '19 at 09:19
  • @Anonymous not that I know that works in sh. The normal way is to save the prior arguments and then shift. The good part is that is works in every terminal I've ever checked. bash has something like what you ask, though – DGoiko Dec 23 '20 at 21:17
  • This answer is compatible with shellchecker – Michael Peng May 25 '22 at 23:09
  • Very nice. I am looking for this POSIX compliant approach too. Together with @Anonymous, I wish we can find an one-liner which does the job like `"$1" "${@:2}"`. I try using subshells `$1 (shift; echo "$@";)`, command substitution `$1 $(shift; echo "$@";)` or compand commands `$1 { shift; echo "$@"; }`. But none of them work. – midnite May 31 '22 at 09:27
58

Working in bash 4 or higher version:

#!/bin/bash
echo "$0";         #"bash"
bash --version;    #"GNU bash, version 5.0.3(1)-release (x86_64-pc-linux-gnu)"

In function:

echo $@;              #"p1" "p2" "p3" "p4" "p5"
echo ${@: 0};  #"bash" "p1" "p2" "p3" "p4" "p5"
echo ${@: 1};         #"p1" "p2" "p3" "p4" "p5"
echo ${@: 2};              #"p2" "p3" "p4" "p5"
echo ${@: 2:1};            #"p2"
echo ${@: 2:2};            #"p2" "p3"
echo ${@: -2};                       #"p4" "p5"
echo ${@: -2:1};                     #"p4"

Notice the space between ':' and '-', otherwise it means different:

${var:-word} If var is null or unset,
word is substituted for var. The value of var does not change.

${var:+word} If var is set,
word is substituted for var. The value of var does not change.

Which is described in:Unix / Linux - Shell Substitution

walknotes
  • 860
  • 8
  • 7
  • 1
    Your explanation helped a lot and the examples worked pretty fine when using `bash`. Nevertheless I couldn't use the "minus" elements. It didn't the work for me. Not as expected. Thank you – Geppettvs D'Constanzo Jul 06 '22 at 16:14
4

http://wiki.bash-hackers.org/scripting/posparams

It explains the use of shift (if you want to discard the first N parameters) and then implementing Mass Usage

S Raghav
  • 1,386
  • 1
  • 16
  • 26
OnlineCop
  • 4,019
  • 23
  • 35
3

Came across this looking for something else. While the post looks fairly old, the easiest solution in bash is illustrated below (at least bash 4) using set -- "${@:#}" where # is the starting number of the array element we want to preserve forward:

    #!/bin/bash

    someVar="${1}"
    someOtherVar="${2}"
    set -- "${@:3}"
    input=${@}

    [[ "${input[*],,}" == *"someword"* ]] && someNewVar="trigger"

    echo -e "${someVar}\n${someOtherVar}\n${someNewVar}\n\n${@}"

Basically, the set -- "${@:3}" just pops off the first two elements in the array like perl's shift and preserves all remaining elements including the third. I suspect there's a way to pop off the last elements as well.

Pavman
  • 125
  • 7