- How to pass all arguments passed to my Bash script to a function of mine? [duplicate]
- 7 Answers 7
- Passing some arguments:
- Things you probably don’t want to do:
- How to pass all arguments of a function along to another command?
- How to pass all arguments to a script called by /bin/bash
- 1 Answer 1
- tl;dr
- Analysis
- Towards the solution
How to pass all arguments passed to my Bash script to a function of mine? [duplicate]
Let’s say I have a function abc() that will handle the logic related to analyzing the arguments passed to my script. How can I pass all arguments my Bash script has received to abc() ? The number of arguments is variable, so I can’t just hard-code the arguments passed like this:
Possible duplicate of Propagate all arguments in a bash shell script. (This question was actually posted before the one linked here. But the one in the link has more detailed answers and a more informative title and may be best as the reference question)
7 Answers 7
The $@ variable expands to all command-line parameters separated by spaces. Here is an example.
When using $@ , you should (almost) always put it in double-quotes to avoid misparsing of arguments containing spaces or wildcards (see below). This works for multiple arguments. It is also portable to all POSIX-compliant shells.
It is also worth noting that $0 (generally the script’s name or path) is not in $@ .
The Bash Reference Manual Special Parameters Section says that $@ expands to the positional parameters starting from one. When the expansion occurs within double quotes, each parameter expands to a separate word. That is «$@» is equivalent to «$1» «$2» «$3». .
Passing some arguments:
If you want to pass all but the first arguments, you can first use shift to «consume» the first argument and then pass «$@» to pass the remaining arguments to another command. In Bash (and zsh and ksh, but not in plain POSIX shells like dash), you can do this without messing with the argument list using a variant of array slicing: «$» will get you the arguments starting with «$3» . «$» will get you up to four arguments starting at «$3» (i.e. «$3» «$4» «$5» «$6» ), if that many arguments were passed.
Things you probably don’t want to do:
«$*» gives all of the arguments stuck together into a single string (separated by spaces, or whatever the first character of $IFS is). This looses the distinction between spaces within arguments and the spaces between arguments, so is generally a bad idea. Although it might be ok for printing the arguments, e.g. echo «$*» , provided you don’t care about preserving the space within/between distinction.
Assigning the arguments to a regular variable (as in args=»$@» ) mashes all the arguments together like «$*» does. If you want to store the arguments in a variable, use an array with args=(«$@») (the parentheses make it an array), and then reference them as e.g. «$» etc. Note that in Bash and ksh, array indexes start at 0, so $1 will be in args[0] , etc. zsh, on the other hand, starts array indexes at 1, so $1 will be in args[1] . And more basic shells like dash don’t have arrays at all.
Leaving off the double-quotes, with either $@ or $* , will try to split each argument up into separate words (based on whitespace or whatever’s in $IFS ), and also try to expand anything that looks like a filename wildcard into a list of matching filenames. This can have really weird effects, and should almost always be avoided. (Except in zsh, where this expansion doesn’t take place by default.)
How to pass all arguments of a function along to another command?
so I’m able to do program «-p hello_world -tSu» . Is there any way to run the program and custom flags without using the quotation marks? if I do just program -p hello_world -tSu it’ll only use the -p flag and everything after the space will be ignored.
«. is there anyway to run the program and custom flags without using the quotation marks?» Yeah, install zsh! The reason why you should have quotes in bash and in some other shells is for historical reasons, which you can look up. Basically, bash will «split» variables whenever they are not quoted. Consider the following code: foo() < echo "1: $1"; echo "2: $2"; >; var=»a b»; foo $var , whereby bash «splits» the string in var into two arguments to foo (namely, «a» and «b»). (Zsh does away with this legacy behaviour, so quotes are rarely needed.)
@Zorawar, and how would Zsh and the fact that it does not split help here? If you did foo() < ls $1; >and foo «-l foo.txt» in Zsh, it wouldn’t work exactly because of the lack of splitting, and foo -l foo.txt also wouldn’t pass all the arguments along. Regardless of the shell, you still need $@ to pass all the arguments.
@ilkkachu OP: «Is there any way to run the program and custom flags without using the quotation marks.» My tongue-in-cheek answer was essentially, no, not unless you use a shell that does this. The reason why bash does not do this is because of word splitting, which I thought the OP would like to know in order to appreciate why this is so. Yes, zsh does not split on raw strings, but we are talking about the behaviour of parameters, not strings. I don’t see why $@ joined the discussion, though.
@ilkkachu I appreciate that, but I am not, and did not, address that. The answers below have covered that sufficiently well. I just added my comment as supplementary information about why quotes in general are very much recommended in bash, which was my reading between the lines of why the OP asked about quotation marks since, on the surface, why doesn’t $1 include all the other arguments shows a certain lack of understanding of how and why quotes are needed in shell code.
How to pass all arguments to a script called by /bin/bash
I have a script that I want it to receive a number of variable arguments and pass them to the execution of another script, but I’m only being able to pass the first argument set by the first call. My script ( nodetool_improved ) looks like this:
sudo su cassandra -s /bin/bash -c "/usr/local/cassandra/bin/nodetool -u cassandra -pwf /opt/apps/cassandra/resources/security/jmxremote.password $@"
Then /usr/local/cassandra/bin/nodetool receives status , and the output looks OK. But if I call my script like:
$ nodetool_improved help status
Then the output instead of showing the help for the status option, just shows the help for all options, which is effectively the same as calling:
$ /usr/local/cassandra/bin/nodetool help
Some things I’ve tried and which don’t work:
sudo su cassandra -s /bin/bash -c "/usr/local/cassandra/bin/nodetool -u cassandra -pwf /opt/apps/cassandra/resources/security/jmxremote.password -- '$@'"
status': -c: line 0: unexpected EOF while looking for matching `'' status': -c: line 1: syntax error: unexpected end of file
sudo su cassandra -s /bin/bash -c '/usr/local/cassandra/bin/nodetool -u cassandra -pwf /opt/apps/cassandra/resources/security/jmxremote.password "$@"' -- "$@"
Only the status gets picked up. The output is the same as if I called it like:
/usr/local/cassandra/bin/nodetool status
I think I managed to simplify the solution. The previous one was somewhat flawed in the subject of — . See the revision history to learn what was added.
1 Answer 1
tl;dr
sudo su cassandra -s /bin/bash -c \ 'exec /usr/local/cassandra/bin/nodetool \ -u cassandra \ -pwf /opt/apps/cassandra/resources/security/jmxremote.password \ "$@"' \ -- my-inner-shell -- "$@"
sudo su cassandra -s /usr/local/cassandra/bin/nodetool -- \ -u cassandra \ -pwf /opt/apps/cassandra/resources/security/jmxremote.password \ "$@"
Analysis
At first let’s explicitly state that in your commands sudo su runs su and passes all the remaining arguments to it verbatim. Then su cassandra -s /bin/bash runs /bin/bash and passes all the remaining arguments to it verbatim. These steps are irrelevant to your issue. The important thing is what the inner Bash gets.
sudo su cassandra -s /bin/bash -c "/usr/local/cassandra/bin/nodetool -u cassandra -pwf /opt/apps/cassandra/resources/security/jmxremote.password $@"
is very flawed. After sudo and su do their thing, bash runs as if user cassandra did:
# flawed /bin/bash -c "something $@"
The immediate cause of your problem is the fact that double quoted $@ gets expanded by the outer shell before sudo (and su and the inner bash ) even starts. Double-quoted $@ expands to (possibly) separate arguments. The first of them gets concatenated with the string (if any) preceding $@ in double-quotes; the last of them gets concatenated with the string (if any) following $@ in double-quotes. When you pass help and status , it’s as if user cassandra run:
/bin/bash -c "something help" "status"
and only something help is the code. That’s why status was ignored.
The version that uses «something ‘$@'» gets $@ expanded by the outer shell as well (the outer quotes matter) and results in a command like:
/bin/bash -c "something 'help" "status'"
which is even worse because something ‘help interpreted as code is syntactically invalid; there is unmatched single-quote there, hence unexpected EOF .
In general allowing some outer tool to expand anything in a string that becomes code for the inner tool is safe only if you totally control the outcome of the expansion. If it’s not known in advance nor sanitized (which may be hard) then you’ll have code injection vulnerability. E.g. when you run this
and the $variable happens to expand to ; rm -f important_file then you will effectively run
bash -c "echo; rm -f important_file"
You cannot fix this by quoting. E.g. this
will miserably fail if $variable expands to ‘; rm -f important_file’ (where single-quotes belong to the variable).
Your code was flawed not only because $@ could expand to multiple arguments which resulted in all but one being ignored. The argument that was not ignored was able to inject code.
This is a general problem, not only in a shell+shell scenario. The outer tool can e.g. be find .
Towards the solution
The right thing to do is to pass positional parameters of the outer shell as positional parameters (not code) to the inner shell. This try of yours:
sudo su cassandra -s /bin/bash -c '/usr/local/cassandra/bin/nodetool -u cassandra -pwf /opt/apps/cassandra/resources/security/jmxremote.password "$@"' -- "$@"
is almost the right thing.
This is as if user cassandra run:
In the subject of quoting and avoiding code injection this is very good. The code is single-quoted, so the outer shell does not expand the first $@ . In the code interpreted by the inner shell $@ is double-quoted as it should be. This part is fine.
The problem is in — «$@» . Bash follows this convention where — option denotes end of options. It’s good to use it in case something that looks like an option appears from the expansion of the second $@ (performed by the outer shell). The first non-option argument following -c ‘shell code’ becomes $0 in the inner shell. What follows becomes $1 , $2 etc. In your case these come from the expansion of the second $@ , but the outer $1 becomes the inner $0 , the outer $2 becomes the inner $1 etc. That’s why when you passed help and status , help «disappeared».
The solution is to provide a «dummy» argument that will become the inner $0 . This way the outer $1 will become the inner $1 etc. Such «dummy» argument is not really a dummy, it has its purpose. Please read What is the second sh in sh -c ‘some shell code’ sh ?
So the snippet should be like:
/bin/bash -c 'something "$@"' -- some-name "$@"
Now you want to add sudo su … in front. I tested sudo : in general it supports — , but after what looks like a command (e.g. su in sudo su … ) it neither needs nor interprets — . It’s different with su itself. At first I thought su uses everything after -s to build a command. Then I thought it uses everything after -c as arguments. In any of these cases — wouldn’t be needed and I expected su to behave somewhat like sudo .
But no. In my Debian 10 this command:
su kamil A -c B -s /bin/echo C -c D E -- -c F G
works as if user kamil run:
The option-argument to -s can be any executable. su searches for options for itself until it encounters — , no matter if beyond -s or -c . The latest -c before — «wins». Then the tool passes its own option-argument found after -c to the executable along with a leading -c argument, followed by any arguments it didn’t recognize as options or option-arguments to itself.
This means you need — destined for su to actually pass — to the shell. You want something like:
sudo su cassandra -s /bin/bash -c 'something "$@"' -- -- some-name "$@"
I’ve encountered su that in some (not all) circumstances «consumed» — — instead of — . It was probably a weird bug. Separating the two double-dashes with another argument (if possible) seemed to be a workaround. So this may be little better:
sudo su cassandra -s /bin/bash -c 'something "$@"' -- some-name -- "$@"
And because the inner shell is used only to run a single command, then maybe his:
sudo su cassandra -s /bin/bash -c 'exec something "$@"' -- some-name -- "$@"
But since -s accepts any executable and you can pass any argument to it (even without it supporting -c ), you can get rid of /bin/bash completely:
sudo su cassandra -s something -- "$@"
The two solutions in the beginning of the answer are adaptations of the above two «templates».