Why "bash -c" can't receive full list of arguments? - bash

I have next two scripts:
test.sh:
echo "start"
echo $#
echo "end"
run.sh:
echo "parameters from user:"
echo $#
echo "start call test.sh:"
bash -c "./test.sh $#"
Execute above run.sh:
$ ./run.sh 1 2
parameters from user:
1 2
start call test.sh:
start
1
end
You could see although I pass 2 arguments to run.sh, the test.sh just receive the first argument.
But, if I change run.sh to next which just drop bash -c:
echo "parameters from user:"
echo $#
echo "start call test.sh:"
./test.sh $#
The behavior becomes as expected which test.sh receive 2 arguments:
$ ./run.sh 1 2
parameters from user:
1 2
start call test.sh:
start
1 2
end
Question:
For some reason, I have to use bash -c in my full scenario, then could you kindly tell me what's wrong here? How I could fix that?

It is because of the quoting of the arguments is in wrong place. When you run a sequence of commands inside bash -c, think of that as it being a full shell script in itself, and need to pass arguments accordingly. From the bash manual
If Bash is started with the -c option (see Invoking Bash), then $0 is set to the first argument after the string to be executed, if one is present. Otherwise, it is set to the filename used to invoke Bash, as given by argument zero.
But if one notices your command below,
bash -c "./test.sh $#"
when your expectation was to pass the arguments to the test.sh, inside '..', but the $# inside double-quotes expanded pre-maturely, undergoing word-splitting to produce the first argument value only, i.e. value of $1
But even when you have fixed it by using single quotes as below, it still can't work, because remember the contents passed to -c is evaluated in its own shell context and needs arguments passed explicitly,
set -- 1 2
bash -c 'echo $#' # Both the cases still don't work, as the script
bash -c 'echo "$#"' # inside '-c' is still not passed any arguments
To fix, the above, you need an explicit passing of arguments the contents inside -c as below. The _ (underscore) character represents the pathname of the shell invoked to execute the script (in this case bash). More at Bash Variables on the manual
set -- 1 2
bash -c 'printf "[%s]\n" "$#"' _ "$#"
[1]
[2]
So to fix your script, in run.sh, pass the arguments as
bash -c './test.sh "$#"' _ "$#"

Besides the accept one, find another solution just now. If add -x when call the run.sh, I could see next:
$ bash -x ./run.sh 1 2
+ echo 'parameters from user:'
parameters from user:
+ echo 1 2
1 2
+ echo 'start call test.sh:'
start call test.sh:
+ bash -c './test.sh 1' 2
start
1
end
So, it looks bash -c "./test.sh $#" is interpreted as bash -c './test.sh 1' 2.
Inspired from this, I tried to use $* to replace $#, which then just pass all params as a single parameter, then with next it also works well:
run.sh:
echo "parameters from user:"
echo $*
echo "start call test.sh:"
bash -c "./test.sh $*"
Execution:
$ bash -x ./run.sh 1 2
+ echo 'parameters from user:'
parameters from user:
+ echo 1 2
1 2
+ echo 'start call test.sh:'
start call test.sh:
+ bash -c './test.sh 1 2'
start
1 2
end

Related

Why getting blank output when running "/bin/sh" with "-c" option [duplicate]

This question already has answers here:
How to use positional parameters with "bash -c" command?
(2 answers)
Closed 14 days ago.
I am running /bin/sh with -c option as below but getting a blank output:
[root#dockerhost dproj]# /bin/sh -c echo helloworld
[root#dockerhost dproj]#
Why the above command is not printing helloworld?
I have tried to read the man page but not able to understand anything.
From man sh:
-c Read commands from the command_string operand.
Set the value of special parameter 0
(see Section 2.5.2, Special Parameters) from the
value of the command_name operand and the positional
parameters ($1, $2, and so on) in sequence from the
remaining argument operands.
No commands shall be read from the standard input.
/bin/sh -c echo helloworld runs the command echo, which prints a blank line. You meant to type:
/bin/sh -c "echo helloworld"
which runs the command echo helloworld.
The -c option to sh causes the first non-option argument to be treated as a string of commands to run in a new shell. The remaining arguments are used to fill in the that shell's numbered arguments, starting with $0 (the "name" under which the shell process is running.) These arguments are not automatically parsed to any utility executed by that shell.
So if you invoke sh with /bin/sh -c echo helloworld:
the input passed to the shell interpreter sh is simply echo
helloworld becomes $0 in the sh session.
Normally, $0 in a shell invocation should be the name of the shell; that's what it will be set to by default:
bash$ /bin/sh
$ echo $0
/bin/sh
$ exit
bash$ echo $0
bash
Since the command given to the shell by -c is interpreted as though it were input to the shell itself, you can use $n in the command in order to refer to the extra arguments to the shell interpreter. If you want to do that, you need to remember to single-quote the -c option argument so that it's contents are not interpreted by the outer shell. Perhaps studying the following will help:
bash$ /bin/sh -c 'echo $0' fakename # Correctly single-quoted
fakename
bash$ /bin/sh -c "echo $0" fakename # Here, $0 is expanded by the outer shell
bash
bash$ /bin/sh -c 'echo "$#"' sh arg1 arg2 arg3 # $0 is not part of $#
arg1 arg2 arg3

Invalid behavior for arguments in Bash version 4.4 vs version 5.1? [duplicate]

This question already has an answer here:
Running a command with bash -c vs without
(1 answer)
Closed last year.
I am confused with this behavior, I have the following script:
backup.sh
#!/bin/bash -x
set -e
if [[ $# -eq 0 ]] ; then
echo 'No arguments passed'
exit 1
fi
# Get the arguments
for ARGUMENT in "$#"; do
KEY=$(echo $ARGUMENT | cut -f1 -d=)
VALUE=$(echo $ARGUMENT | cut -f2 -d=)
case "$KEY" in
backup_dir) BACKUP_DIR=${VALUE} ;;
postgres_dbs) POSTGRES_DBS=${VALUE} ;;
backup_name) BACKUP_NAME=${VALUE} ;;
postgres_port) POSTGRES_PORT=${VALUE} ;;
postgres_host) POSTGRES_HOST=${VALUE} ;;
*) ;;
esac
done
And I am executing it using:
1.
/bin/bash -c /usr/bin/backup.sh postgres_dbs=grafana,keycloak backup_name=postgres-component-test-20220210.165630 backup_dir=/backups/postgres postgres_port=5432 postgres_host=postgres.default.svc.cluster.local
/usr/bin/backup.sh postgres_dbs=grafana,keycloak backup_name=postgres-component-test-20220210.165630 backup_dir=/backups/postgres postgres_port=5432 postgres_host=postgres.default.svc.cluster.local
But the output is:
+ set -e
+ [[ 0 -eq 0 ]]
+ echo 'No arguments passed'
No arguments passed
+ exit 1
Environment:
# cat /etc/os-release
NAME="Ubuntu"
VERSION="18.04.3 LTS (Bionic Beaver)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 18.04.3 LTS"
VERSION_ID="18.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=bionic
UBUNTU_CODENAME=bionic
Bash version where I can reproduce this issue:
GNU bash, version 4.4.20(1)-release (x86_64-pc-linux-gnu)
However, this is not happening in the Bash version:
GNU bash, version 5.1.8(1)-release (x86_64-apple-darwin20.3.0)
It's not a bug, just a feature!
When you use the bash -c 'code …' style, actually the first CLI argument is passed to the inline code as $0, not $1.
Furthermore, if the 'code …' itself invokes an external script such as ./script.sh, then you should not forget to pass the arguments using the "$#" construct.
So you could just write (as pointed out in the comments):
bash -c './script.sh "$#"' bash "first argument"
Or most succinctly, just like you mention you had already tried:
bash script.sh "first argument"
Additional notes
As your example was not really "minimal" (it had a very long command-line), here is a complete minimal example that you might want to test for debugging purpose:
script.sh
#!/usr/bin/env bash
echo "\$#: $#"
for arg; do printf -- '- %s\n' "$arg"; done
Then you should get a session similar to:
$ chmod a+x script.sh
$ bash -c ./script.sh "arg 1" "arg 2"
$#: 0
$ bash -c './script.sh "$#"' "arg 1" "arg 2"
$#: 1
- arg 2
$ bash -c './script.sh "$#"' bash "arg 1" "arg 2"
$#: 2
- arg 1
- arg 2
$ bash script.sh "arg 1" "arg 2"
$#: 2
- arg 1
- arg 2
$ ./script.sh "arg 1" "arg 2"
$#: 2
- arg 1
- arg 2
You wrote two ways to invoke the script, which boil down to:
bash -c ./script.sh arg1 arg2 arg3
./script.sh arg1 arg2 arg3
The second way is the preferred way to invoke scripts. Running them directly tells Linux to use the interpreter listed in the shebang line. There's no reason I can see for this invocation style to drop arguments.
The first, however, does indeed lose all the arguments. It's because -c doesn't belong there. If you want to invoke an explicit bash shell you should write simply:
bash ./script.sh arg1 arg2 arg3
That will correctly pass all the arguments to the script.
When you add -c it turns ./script.sh from the name of a script into a full blown command line. What's the difference? Well, now that command line is responsible for forwarding its arguments to the script, if that's what it wants to have happen. With -c you need to explicitly pass them on:
bash -c './script.sh "$#"' bash arg1 arg2 arg3
Yuck! It's encased in single quotes, and there's an ugly "$#" in there. It's needed, though. Without "$#" the arguments are simply dropped on the floor.
-c also takes an extra argument, the value for $0. So not only is "$#" needed, you also have to add an extra bash argument to set $0. (bash is a good choice since that's what $0 is normally set to when running a bash script.)

Including $# to pass on all command line arguments when a shell script invokes itself with bash -c

I need a bash script to invoke itself (actually in a different context, inside Docker container) and I'm using a bash -c command to do so. However, I'm struggling with how to pass on all command line variables, even after reading lots of related questions here. This is an example script:
#!/bin/bash
# If not in the right context, invoke script in right context and exit
if [ -z ${NESTED+x} ]; then
NESTED=true bash -c "./test.sh $#"
exit
fi
echo "$1"
echo "$2"
echo "$3"
If I save this as test.sh and call it with ./test.sh 1 2 "3 4" I'd want to see those arguments echo'ed, but only the first one is output.
If I use set -x it shows bash inserts some unexpected quoting so the call becomes NESTED=true bash -c './test2.sh 1' 2 3 4. That explains the output but I haven't been able to figure out the right way to do this.
bash -c should not be used as it cannot handle "3 4" easily:
#!/bin/bash
# If not in the right context, invoke script in right context and exit
if [ -z ${NESTED+x} ]; then
NESTED=true ./test.sh "$#"
exit
fi
echo "$1"
echo "$2"
echo "$3"

Empty Positional Argument when using /bin/bash -c <script>

I'm trying to launch a script with /bin/bash -c with positional arguments but can't figure out the following issue:
Suppose I have test.sh as follows:
#!/bin/bash
echo $0
echo $1
> ./test.sh a
./test.sh
a
> /bin/bash -c ./test.sh a
./test.sh
Why does the second one return an empty position argument for $1? Based on the man page:
-c If the -c option is present, then commands are read from the first non-option argument command_string. If there are arguments after the command_string, the first argument is assigned to $0 and any remaining arguments are assigned to the positional
parameters. The assignment to $0 sets the name of the shell, which is used in warning and error messages.
It seems like "a" should be assigned to $0 at least, which is not what I saw. /bin/bash -c 'echo $0' a works as expected. Thanks!
The string after -c acts like a miniature script, and the arguments after that are passed to it as $0, $1, $2, etc. For example:
$ bash -c 'echo "\$0=$0, \$1=$1, \$2=$2"' zero one two
$0=zero, $1=one, $2=two
(Note: it's important that the mini-script is in single-quotes; without them the references to $0 would be expanded by your interactive shell before they even get passed to the bash -c command.)
In your case, the mini-script runs another script (./test.sh), but doesn't pass on the arguments. If you wanted to pass them on, you'd do something like this:
$ bash -c './test.sh "$1" "$2"' zero one two
./test.sh
one
If the script had bothered to print its $2 here, it would've gotten "two". It doesn't help to pass on $0, because for a real script that's automatically set to the actual command used to run the script.
bash [long-opt] [-abefhkmnptuvxdBCDHP] [-o option] [-O shopt_option]
-c string [argument ...]
-c supposed to be followed by a string, so you may quote ./test.sh a like:
$ /bin/bash -c "./test.sh a"
./test.sh
a
The -c option does not collect all following arguments of the bash command, but just uses the first non-option argument, which in your case is the one immediately following it. I don't see why you want to use -c here. I would write your command as
/bin/bash test.sh a
Since in this case, no PATH search is involved, you can also omit the ./ part. In fact, test.sh doesn't even need to be executable here.

Pass all args to a command called in a new shell using bash -c

I've simplified my example to the following:
file1.sh:
#!/bin/bash
bash -c "./file2.sh $#"
file2.sh:
#!/bin/bash
echo "first $1"
echo "second $2"
I expect that if I call ./file1.sh a b to get:
first a
second b
but instead I get:
first a
second
In other words, my later arguments after the first one are not getting passed through to the command that I'm executing inside a new bash shell. I've tried many variations of removing and moving around the quotation marks in the file1.sh file, but haven't got this to work.
Why is this happening, and how do I get the behavior I want?
(UPDATE - I realize it seems pointless that I'm calling bash -c in this example, my actual file1.sh is a proxy script for a command that gets called locally to run in a docker container so it's actually docker exec -i mycontainer bash -c '')
Change file1.sh to this with different quoting:
#!/bin/bash
bash -c './file2.sh "$#"' - "$#"
- "$#" is passing hyphen to populate $0 and $# is being passed in to populate all other positional parameters in bash -c command line.
You can also make it:
bash -c './file2.sh "$#"' "$0" "$#"
However there is no real need to use bash -c here and you can just use:
./file2.sh "$#"

Resources