why single quotes and double quotes works differently in "bash -c"? - linux

While using bash -c option to spawn new shell before running shell script,
I encountered unexpected action.
When I use single quotes, new shell is spawned to run the script.
$ bash -c 'echo pid is $$'
pid is 53465
$ bash -c 'echo pid is $$'
pid is 53466
$ bash -c 'echo pid is $$'
pid is 53477
But double quotes didn't.
$ bash -c "echo pid is $$"
pid is 2426
$ bash -c "echo pid is $$"
pid is 2426
$ bash -c "echo pid is $$"
pid is 2426
I carefully read similar question and bash manual but could not find why.
Anyone knows the reason why?

So when you execute the command
$ command "echo pid is $$"
The double quotes ensure that the command command gets a string passed where all substitutions are done. So assuming that the pid of the interactive shell is 1234. You will get the equivalent
$ command "echo pid is 1234"
When you use single quotes, the command gets passed the string echo pid is $$ where $$ is just like any string. If the command is now bash, $$ has a special meaning
$ bash -c 'echo pid is $$'
So now you get the PID returned of the executed command bash and not of your interactive shell.

Related

bash -c running script in the same process

I ran some tests to see how bash -c spawns subprocesses for the command given to it, and noticed some strange behavior.
This is the content of test.sh:
#!/usr/bin/env fish
echo $fish_pid
readlink /proc/$fish_pid/exe
Here are the experiment results:
$ bash -c 'echo $$ && ./test.sh'
296
296
/usr/bin/fish
$ bash -c 'echo $$ && ./test.sh && echo $$'
403
404
/usr/bin/fish
403
$ bash -c 'echo $$; ./test.sh'
317
318
/usr/bin/fish
$ bash -c 'echo $$; exec ./test.sh'
349
349
/usr/bin/fish
It's almost as if bash is doing tail call optimization by automatically inserting an exec in the first case. What's going on here?
Exactly what you are describing; Bash notices that it can reuse the current process, and so avoids doing a fork() system call and then wait for the subprocess when just an exec() system call is sufficient.
It's not clear why it's not doing that in the third case; that one would seem as clear-cut as the first one, if not even more so.

Get PID of last executed command (NO BACKGROUND)

I want to know the PID of the last executed command.
I saw this a lot:
$ command &
$ pid=$!
But I'm searching for the same thing without running the command in the background.
You can use the following construction in scripts:
PID=`sh -c "echo $$; exec your_command -with-arguments"`
echo $PID

Shell scripting shell inside shell

I would like to connect to different shells (csh, ksh etc.,) and execute command inside each switched shell.
Following is the sample program which reflects my intention:
#!/bin/bash
echo $SHELL
csh
echo $SHELL
exit
ksh
echo $SHELL
exit
Since, i am not well versed with Shell scripting need a pointer on how to achieve this. Any help would be much appreciated.
If you want to execute only one single command, you can use the -c option
csh -c 'echo $SHELL'
ksh -c 'echo $SHELL'
If you want to execute several commands, or even a whole script in a child-shell, you can use the here-document feature of bash and use the -s (read commands from stdin) on the child shells:
#!/bin/bash
echo "this is bash"
csh -s <<- EOF
echo "here go the commands for csh"
echo "and another one..."
EOF
echo "this is bash again"
ksh -s <<- EOF
echo "and now, we're in ksh"
EOF
Note that you can't easily check the shell you are in by echo $SHELL, because the parent shell expands this variable to the text /././bash. If you want to be sure that the child shell works, you should check if a shell-specific syntax is working or not.
It is possible to use the command line options provided by each shell to run a snippet of code.
For example, for bash use the -c option:
bash -c $code
bash -c 'echo hello'
zsh and fish also use the -c option.
Other shells will state the options they use in their man pages.
You need to use the -c command line option if you want to pass commands on bash startup:
#!/bin/bash
# We are in bash already ...
echo $SHELL
csh -c 'echo $SHELL'
ksh -c 'echo $SHELL'
You can pass arbitrary complex scripts to a shell, using the -c option, as in
sh -c 'echo This is the Bourne shell.'
You will save you a lot of headaches related to quotes and variable expansion if you wrap the call in a function reading the script on stdin as:
execute_with_ksh()
{
local script
script=$(cat)
ksh -c "${script}"
}
prepare_complicated_script()
{
# Write shell script on stdout,
# for instance by cat-ting a here-document.
cat <<'EOF'
echo ${SHELL}
EOF
}
prepare_complicated_script | execute_with_ksh
The advantage of this method is that it easy to insert a tee in the pipe or to break the pipe to control the script being passed to the shell.
If you want to execute the script on a remote host through ssh you should consider encode your script in base 64 to transmit it safely to the remote shell.

linux env command does not work as expected?

These 2 lines work as expected:
$ env NEW=hello bash -c "env | grep hello"
NEW=hello
$ env NEW=hello bash -c "echo $PATH"
/bin:/usr/bin
But I don't know why the following does not work (as expected).
$ env NEW=hello bash -c "echo $NEW"
Any suggestion?
$ env NEW=hello bash -c "echo $NEW"
You're using double-quotes on the argument to bash here, so the $NEW in the argument is expanded by your current shell, not by the bash command you're executing. Since $NEW isn't set in your current shell, the command is expanded to bash -c "echo ".
Use single-quotes on the argument to solve this:
$ env NEW=hello bash -c 'echo $NEW'
hello

How to run a pkill when invoking a shell to execute a string of commands?

To automate a system administration task, I wrote down the following line of shell code:
bash -c 'pkill -TERM -f java; true'
The problem is that pkill kills the bash immediately after the pkill command executes, and therefore subsequent commands do not have a chance to execute.
Apart from splitting the them into two lines:
bash -c 'pkill -TERM -f java'
bash -c 'true'
Is there any other workaround?
If you want to kill all java processes, simply drop the -f:
bash -c 'pkill -TERM java; true'
If you really also want to kill non-java processes like mplayer "jungle_gremlins_of_java.avi", the typical "solution" is to rewrite the command so that the pattern doesn't match itself:
bash -c 'pkill -TERM -f "[j]ava"; true'

Resources