As I understand and read it, $# turns into "arg1" "arg2" "arg3".
But it seems to have some weird behavior in some cases when quoted.
Test 1, $#
$ function somefunc { sh -c "echo $#"; }
$ somefunc 1 2 3
> + somefunc 1 2 3
> + sh -c 'echo 1' 2 3
> 1
The expected output is 1 2 3, the output I get is 1. Now I see that, for some reason, it seems to only pass echo 1 as command, but why does "echo "1" "2" "3"" turn into it? I would maybe have expected echo because the " before echo may get closed by the " before 1.
Test 2, $*
$ function somefunc { sh -c "echo $*"; }
$ somefunc 1 2 3
> + somefunc 1 2 3
> + sh -c 'echo 1 2 3'
> 1 2 3
This is pretty obvious and works, just to make sure. After all $* passes 1 2 3, not "1" "2" "3".
Test 3, $# and $*
$ function somefunc { sh -c "echo $# $*"; }
$ somefunc 1 2 3
> + somefunc 1 2 3
> + sh -c 'echo 1 2 3 1 2 3'
> 1 2 3 1 2 3
This seems to work again weirdly enough. But I don't get how "echo "1" "2" "3" 1 2 3" turns into 'echo 1 2 3 1 2 3' and doesn't follow the "pattern" that only "echo $#"did.
Test 4, $# and a string
$ function somefunc { sh -c "echo $# hi"; }
$ somefunc 1 2 3
> + somefunc 1 2 3
> + sh -c 'echo 1' 2 '3 hi'
> 1
This again follows the pattern of "echo $#", even thought, as I see it, it is pretty much the same as "echo $# $*" since $* turns into a string after all.
What also puzzles me is that it turned into 'echo 1' 2 '3 hi', the first test would have suggested 'echo 1' 2 3 hi (without the two ' surrounding 3 hi)
Test 5, $# and $var
$ var="hi"
> + var=hi
$ function somefunc { sh -c "echo $# $var"; }
$ somefunc 1 2 3
> + somefunc 1 2 3
> + sh -c 'echo 1 2 3 hi'
> 1 2 3 hi
This works again. So $# seems to work if another variable follows it.
The tests are also possible with su user -c or bash -c instead of sh -c, so I suppose also every other command that executes the next given argument.
I now seem to have gotten some kind of behavior out of it,
but I do still not understand this behavior.
What am I missing here?
The sh language spec has this to say about $#:
Expands to the positional parameters, starting from one. When the expansion occurs within double-quotes, and where field splitting (see Field Splitting ) is performed, each positional parameter shall expand as a separate field, with the provision that the expansion of the first parameter shall still be joined with the beginning part of the original word (assuming that the expanded parameter was embedded within a word), and the expansion of the last parameter shall still be joined with the last part of the original word. If there are no positional parameters, the expansion of '#' shall generate zero fields, even when '#' is double-quoted.
This means that the string "echo $# $*" where the positional parameters are 1, 2, 3 should expand to "echo 1" "2" "3 1 2 3", so that your test 3 should output the string 1. The fact that bash incorrectly expands the string to "echo 1 2 3 1 2 3" and outputs 1 2 3 1 2 3 is indicative of a bug in bash.
The usual technique for dealing with this (yet another) oddity of sh grammar rules is to only use $# in double quotes when it is a distinct word. In other words, it's okay to write "$#", but don't put $# in double quotes unless it is the only thing inside the double quotes.
Using #BrianSwift's hint, everything makes perfectly sense, except for Test3 (here, it is not the case that we get three arguments with the first having a prefix of 'echo ' and the third having a suffix of ' $*'. Instead, the result is a single argument).
But that may probably be explained with the fact that both $# and $* are "special" variables for which special code in the shell exists. I assume the existence of $* just overrides the special behaviour noticed by #BrianSwift, because $* is evaluated later.
Try swapping the order: somefunc() { sh -x "echo $* $#"; }. You will get the split-with-prefix-and-suffix behaviour again which seems to support my above assumption.
So to conclude, everything can be explained with a reference to the manual except for Test3 whose behaviour is probably a bit "underdocumented".
As I understand and read it, $# turns into "arg1" "arg2" "arg3".
No, "$#" turns into "arg1" "arg2" "arg3". The quotes matter.
Related
I have a script where the user can add as many arguments as he would like (numbers).
The script will sum all the numbers beside the last number - The last number (argument) is the number that I need to divide by
For example:
./test.sh 2 2 6 5
This will sum the first 3 numbers (2+2+6) and divide the answer by 5 (the last argument)
How can I use the last argument? Echo ????
How can I move loop the first arguments besides the last one – I would like that all 3 arguments will be added to an array and I can loop it
Please note that the number of arguments can be changed
How can I use the last argument? Echo ????
Granting $# > 0, you can use "${!#}".
How can I move loop the first arguments besides the last one – I would
like that all 3 arguments will be added to an array and I can loop it
Again granting $# > 0, you can refer to "${#:1:$# - 1}".
Read the Arrays section in the bash manual to know how to properly expand arrays.
I also recommend learning how quoting works and knowing the dangers of unwanted word splitting and globbing.
Shortly (with bashisms)
As this question is tagged integer-arithmetic and bash:
Here is a small and efficient script:
#!/bin/bash
addVals=${*: 1 : $# - 1}
declare -i intResult=" ( ${addVals// /+} ) / ${#: -1} "
echo $intResult
But there's no loop...
Long answer
How can I use the last argument? Echo ????
You could make your tries in command line:
set -- 2 2 6 5
Then
echo $#
2 2 6 5
echo ${*: 3}
6 5
echo ${*: -1}
5
echo ${*: 1 : -1}
bash: -1: substring expression < 0
echo $#
4
echo ${*: 1 : $# -1}
2 2 6
Ok, then
someVar=${*: 1 : $# -1}
echo ${someVar// /:SpaceReplacment:}
2:SpaceReplacment:2:SpaceReplacment:6
so
declare -i result
result=" ( ${someVar// /+} ) / ${*: -1} "
echo $result
2
How can I move loop the first arguments besides the last one – I would like that all 3 arguments will be added to an array and I can loop it
Still forward, under command line...
someArray=("${#: 1: $# -1 }")
Use declare -p to show $someArray's content:
declare -p someArray
declare -a someArray=([0]="2" [1]="2" [2]="6")
Then
declare -i mySum=0
for i in "${someArray[#]}";do
mySum+=i
done
echo $mySum
10
echo $(( mySum / ${*: -1} ))
2
Please note that the number of arguments can be changed
Please note:
Using double quotes allow processing of strings containing spaces:
set -- foo bar 'foo bar baz'
echo ${2}
bar
echo ${*: $# }
foo bar baz
Difference betweeen use of "$#" (array to array) and "$*" (array to string)
set -- foo bar 'foo bar' 'foo bar baz'
If I take 3 first elements:
someArray=("${#: 1: $# -1 }")
declare -p someArray
declare -a someArray=([0]="foo" [1]="bar" [2]="foo bar")
But
someArray=("${*: 1: $# -1 }")
declare -p someArray
declare -a someArray=([0]="foo bar foo bar")
There are about a thousand ways of doing this. As you would like to make use of integer arithmetic, you can do the following in bash
A short semi-cryptic version would be:
IFS=+
echo $(( ( ${*} - ${#:-1} ) / ${#:-1} ))
Here we make use of the difference between "${*}" and "${#}" to perform the sum by setting IFS=+ (See What is the difference between "$#" and "$*" in Bash?)
A long classic approach would be:
for i in "$#"; do ((s+=i)); done
echo $(( (s-${#:-1})/${#:-1} ))
It's easier to sum all terms and subtract the last term afterwards
In shell scripts, what is the difference between $# and $*?
Which one is the preferred way to get the script arguments?
Are there differences between the different shell interpreters about this?
From here:
$# behaves like $* except that when quoted the arguments are broken up properly if there are spaces in them.
Take this script for example (taken from the linked answer):
for var in "$#"
do
echo "$var"
done
Gives this:
$ sh test.sh 1 2 '3 4'
1
2
3 4
Now change "$#" to $*:
for var in $*
do
echo "$var"
done
And you get this:
$ sh test.sh 1 2 '3 4'
1
2
3
4
(Answer found by using Google)
A key difference from my POV is that "$#" preserves the original number
of arguments. It's the only form that does. For that reason it is
very handy for passing args around with the script.
For example, if file my_script contains:
#!/bin/bash
main()
{
echo 'MAIN sees ' $# ' args'
}
main $*
main $#
main "$*"
main "$#"
### end ###
and I run it like this:
my_script 'a b c' d e
I will get this output:
MAIN sees 5 args
MAIN sees 5 args
MAIN sees 1 args
MAIN sees 3 args
With $# each parameter is a quoted string. Otherwise it behaves the same.
See: http://tldp.org/LDP/abs/html/internalvariables.html#APPREF
I've got a program that I want to call by passing parameters from a shell variable. Throughout this question, I am going to assume that it is given by
#!/bin/sh
echo $#
i.e. that it prints out the number of arguments that are passed to it. Let's call it count-args.
I call my program like this:
X="arg1 arg2"
count-args $X
This works quite well. But now one of my arguments has a whitespace in it and I can't find a way to escape it, e.g. the following things do not work:
X="Hello\ World"
X="Hello\\ World"
X="'Hello World'"
In all of the cases, my program count-args prints out 2. I want to find a way so I can pass the string Hello World and that it returns 1 instead. How?
Just for clarification: I do not want to pass all parameters as a single string, e.g.
X="Hello World"
count-args $X
should print out 2. I want a way to pass parameters that contain whitespaces.
Use an array to store multiple, space-containing arguments.
$ args=("first one" "second one")
$ count-args "${args[#]}"
2
This can be solved with xargs. By replacing
count-args $X
with
echo $X | xargs count-args
I can use backslashes to escape whitespaces in $X, e.g.
X="Hello\\ World"
echo $X | xargs count-args
prints out 1 and
X="Hello World"
echo $X | xargs count-args
prints out 2.
count-args "$X"
The quotes ensure in bash, that the whole content of variable X is passed as a single parameter.
Your Counting script:
$ cat ./params.sh
#!/bin/sh
echo $#
For completeness here is what happens with various arguments:
$ ./params.sh
0
$ ./params.sh 1 2
2
$ ./params.sh
0
$ ./params.sh 1
1
$ ./params.sh 1 2
2
$ ./params.sh "1 2"
1
And here is what you get with variables:
$ XYZ="1 2" sh -c './params.sh $XYZ'
2
$ XYZ="1 2" sh -c './params.sh "$XYZ"'
1
Taking this a bit further:
$ cat params-printer.sh
#!/bin/sh
echo "Count: $#"
echo "1 : '$1'"
echo "2 : '$2'"
We get:
$ XYZ="1 2" sh -c './params-printer.sh "$XYZ"'
Count: 1
1 : '1 2'
2 : ''
This looks like what you wanted to do.
Now: If you have a script you cannot control and neither can you control the way the script is invoked. Then there is very little you can do to prevent a variable with spaces turning into multiple arguments.
There are quite a few questions around this on StackOverflow which indicate that you need the ability to control how the command is invoked else there is little you can do.
Passing arguments with spaces between (bash) script
Passing a string with spaces as a function argument in bash
Passing arguments to a command in Bash script with spaces
And wow! this has been asked so many times before:
How to pass argument with spaces to a shell script function
In bash $# contains all the arguments used to call the script but I am looking for a solution to remove the first one
./wrapper.sh foo bar baz ...:
#!/bin/bash
# call `cmd` with bar baz ... (withouyt foo one)
I just want to call cmd bar baz ...
You can use shift to shift the argument array. For instance, the following code:
#!/bin/bash
echo $#
shift
echo $#
produces, when called with 1 2 3 prints 1 2 3 and then 2 3:
$ ./example.sh 1 2 3
1 2 3
2 3
shift removes arguments from $#.
shift [n]
Shift positional parameters.
Rename the positional parameters $N+1,$N+2 ... to $1,$2 ... If N is
not given, it is assumed to be 1.
Exit Status:
Returns success unless N is negative or greater than $#.
Environment-variable-expansion! Is a very portable solution.
Remove the first argument: with $#
${##"$1"}
Remove the first argument: with $*
${*#"$1"}
Remove the first and second argument: with $#
${##"$1$2"}
Both $# or $* will work because the result of expansion is a string.
links:
Remove a fixed prefix/suffix from a string in Bash
http://www.tldp.org/LDP/abs/html/abs-guide.html#ARGLIST
Variable expansion is portable because it is defined under gnu core-utils
Search for "Environment variable expansion" at this link:
https://www.gnu.org/software/coreutils/manual/html_node/
In shell scripts, what is the difference between $# and $*?
Which one is the preferred way to get the script arguments?
Are there differences between the different shell interpreters about this?
From here:
$# behaves like $* except that when quoted the arguments are broken up properly if there are spaces in them.
Take this script for example (taken from the linked answer):
for var in "$#"
do
echo "$var"
done
Gives this:
$ sh test.sh 1 2 '3 4'
1
2
3 4
Now change "$#" to $*:
for var in $*
do
echo "$var"
done
And you get this:
$ sh test.sh 1 2 '3 4'
1
2
3
4
(Answer found by using Google)
A key difference from my POV is that "$#" preserves the original number
of arguments. It's the only form that does. For that reason it is
very handy for passing args around with the script.
For example, if file my_script contains:
#!/bin/bash
main()
{
echo 'MAIN sees ' $# ' args'
}
main $*
main $#
main "$*"
main "$#"
### end ###
and I run it like this:
my_script 'a b c' d e
I will get this output:
MAIN sees 5 args
MAIN sees 5 args
MAIN sees 1 args
MAIN sees 3 args
With $# each parameter is a quoted string. Otherwise it behaves the same.
See: http://tldp.org/LDP/abs/html/internalvariables.html#APPREF