compound command with bash -c in tmux vs screen - bash

I need to run a command with bash -c within a tmux session, from within a shell script. In contrast to screen, tmux seems to require to quote the entire command, which leads to problems as bash -c also requires quoting for correct functioning with more complex command strings.
In the following I'm trying to demonstrate the behavior with a minimal example. What I'm trying to achieve involves more complex commands than ls of course. Also for my purpose it is necessary to expand the CMD variable, as it is built in the script before.
A minimal script for screen:
#!/bin/bash
set -x
CMD="ls -l; sleep 5"
screen -d -m bash -c "$CMD"
When executing this script you get (stdout due to -x)
+ CMD='ls -l; sleep 5'
+ screen -d -m bash -c 'ls -l; sleep 5'
The sleep command is for having time to attach to the screen session and see what happens. When attaching to the screen session after executing the above script one sees that the output of the ls command is in long list format, i.e. the command is executed properly.
In tmux it seems one has to quote the command to get it executed in the new session. I use the following script:
#!/bin/bash
set -x
CMD="ls -l; sleep 5"
tmux new -d "bash -c $CMD"
The stdout is
+ CMD='ls -l; sleep 5'
+ tmux new -d 'bash -c ls -l; sleep 5'
As one can see, the cmd sequence for bash -c is not quoted properly anymore. When attaching to the created tmux session one can see, that this results in ls being executed without the long list option recognized.
What can I do to get the proper quoting (i.e. single quotes around the expanded string) for the $CMD string passed to bash -c?
Update
Escaping, as Eric Renouf suggested, with \"$CMD\" produces
tmux new -d 'bash -c "ls -l; sleep 5"'
and escaping with '$CMD' produces
tmux new -d 'bash -c '\''ls -l; sleep 5'\'''
Both works for the provided minimal example, but is still not exactly what screen produces and does not work in my case.
Here are the exact call I'm making (see here for all the gory details):
$SCREEN -S "scalaris_$NODE_NAME" -d -m bash -x -f +B -c "$START_CMD; sleep 365d"
which produces (output of -x)
/usr/bin/screen -S scalaris_first#pvs-pc07.zib.de -d -m bash -x -f +B -c '"/usr/bin/erl" -setcookie "chocolate chip cookie" -pa /home/jvf/code/scalaris/contrib/yaws/ebin -pa /home/jvf/code/scalaris/contrib/log4erl/ebin -pa /home/jvf/code/scalaris/ebin -sasl sasl_error_logger false -yaws embedded true -scalaris log_path "\"/home/jvf/code/scalaris/log/first#pvs-pc07.zib.de\"" -scalaris docroot "\"/home/jvf/code/scalaris/docroot\"" -scalaris config "\"/home/jvf/code/scalaris/bin/scalaris.cfg\"" -scalaris local_config "\"/home/jvf/code/scalaris/bin/scalaris.local.cfg\"" -connect_all false -hidden -name first#pvs-pc07.zib.de -scalaris start_type first -scalaris port 14195 -scalaris yaws_port 8000 -scalaris join_at_list '\''[0]'\'' -scalaris start_mgmt_server true -scalaris nodes_per_vm "1" -s scalaris +sbt db +swt low +sbwt short'
I think the solutions suggested so far do not work because of the use of double quotes within the command, but I'm not 100% positive about that. How can I reproduce the exact quoting screen produces (single quotes around the complete command passed to bash -c) with tmux?

I'm trusting that you need to do this for tmux, but to get the extra quotes you want, you could just escape them in the outer quote like:
tmux new -d "bash -c \"$CMD\""
or you could put those in single quotes in most cases like
tmux new -d "bash -c '$CMD'"
since the $CMD will be expanded by the outer quotes

Related

call a bash function as root but gives 'unexpected end of file'

Why does the last example throw an error but the others work? Bash is invoked in any case.
#!/bin/bash
function hello {
echo "Hello! user=$USER, uid=$UID, home=$HOME";
}
# Test that it works.
hello
# ok
bash -c "$(declare -f hello); hello"
# ok
sudo su $USER bash -c "$(declare -f hello); hello"
# error: bash: -c: line 1: syntax error: unexpected end of file
sudo -i -u $USER bash -c "$(declare -f hello); hello"
It fail because of the -i or --login switch:
It seems like when debugging with -x
$ set -x
$ sudo -i -u $USER bash -c "$(declare -f hello); hello"
++ declare -f hello
+ sudo -i -u lea bash -c 'hello ()
{
echo "Hello! user=$USER, uid=$UID, home=$HOME"
}; hello'
Now if doing it manually it cause the same error:
sudo -i -u lea bash -c 'hello ()
{
echo "Hello! user=$USER, uid=$UID, home=$HOME"
}; hello'
Now lets just do a simple tiny change that make it work:
sudo -i -u lea bash -c 'hello ()
{
echo "Hello! user=$USER, uid=$UID, home=$HOME";}; hello'
The reason is that sudo -i runs everything like an interactive shell. And when doing so, every newline character from the declare -f hello is internally turned into space. The curly-brace code block need a semi-colon before the closing curly-brace when on the same line, which declare -f funcname does not provide since it expands the function source with closing curly brace at a new line.
Now lets make this behaviour very straightforward:
$ sudo bash -c 'echo hello
echo world'
hello
world
It executes both echo statements because they are separated by a newline.
but:
$ sudo -i bash -c 'echo hello
echo world'
hello echo world
It executes the first echo statement that prints everything as arguments because the newline has been replaced by a space.
It is the same code in all examples, so it should be ok.
Yes, "$(declare -f hello); hello" is always the same string. But it is processed differently by sudo su and sudo -i as found out by Lea Gris .
sudo -i quotes its arguments before passing them to bash. This quoting process seems to be undocumented and very poor. To see what was actually executed, you can print the argument passed to bash -c inside your ~/.bash_profile/:
Content of ~/.bash_profile
cat <<EOF
# is executed as
$BASH_EXECUTION_STRING
# resulting in output
EOF
Some examples of sudo -i's terrible and inconsistent quoting
Linebreaks are replaced by line continuations
$ sudo -u $USER -i echo '1
2'
# is executed as
echo 1\
2
# resulting in output
12
Quotes are escaped as literals
$ sudo -u $USER -i echo \'single\' \"double\"
# is executed as
echo \'single\' \"double\"
# resulting in output
'single' "double"
But $ is not quoted
$ sudo -u $USER -i echo \$var
# is executed as
echo $var
# resulting in output
Some side notes:
There might be a misunderstanding in your usage of su.
sudo su $USER bash -c "some command"
does not execute bash -c "echo 1; echo 2". The -c ... is interpreted by su and passed as -c ... to $USER's default shell. Afterwards, the remaining arguments are passed to that shell as well. The executed command is
defaultShellOfUSER -c "some command" bash
You probably wanted to write
sudo su -s bash -c "some command" "$USER"
Interactive shells behave differently
su just executes the command specified by -c. But sudo -i starts a login shell, in your case that login shell seems to be bash (this is not necessarily the case, see section above).
An interactive bash session behaves different from bash -c "..." or bash script.sh. An interactive bash sources files like .profile, .bash_profile, and enables history expansion, aliases, and so on. For a full list see the section Interactive Shell Behavior in bash's manual.

Bash: Execute command WITH ARGUMENTS in new terminal [duplicate]

This question already has answers here:
how do i start commands in new terminals in BASH script
(2 answers)
Closed 20 days ago.
So i want to open a new terminal in bash and execute a command with arguments.
As long as I only take something like ls as command it works fine, but when I take something like route -n , so a command with arguments, it doesnt work.
The code:
gnome-terminal --window-with-profile=Bash -e whoami #WORKS
gnome-terminal --window-with-profile=Bash -e route -n #DOESNT WORK
I already tried putting "" around the command and all that but it still doesnt work
You can start a new terminal with a command using the following:
gnome-terminal --window-with-profile=Bash -- \
bash -c "<command>"
To continue the terminal with the normal bash profile, add exec bash:
gnome-terminal --window-with-profile=Bash -- \
bash -c "<command>; exec bash"
Here's how to create a Here document and pass it as the command:
cmd="$(printf '%s\n' 'wc -w <<-EOF
First line of Here document.
Second line.
The output of this command will be '15'.
EOF' 'exec bash')"
xterm -e bash -c "${cmd}"
To open a new terminal and run an initial command with a script, add the following in a script:
nohup xterm -e bash -c "$(printf '%s\nexec bash' "$*")" &>/dev/null &
When $* is quoted, it expands the arguments to a single word, with each separated by the first character of IFS. nohup and &>/dev/null & are used only to allow the terminal to run in the background.
Try this:
gnome-terminal --window-with-profile=Bash -e 'bash -c "route -n; read"'
The final read prevents the window from closing after execution of the previous commands. It will close when you press a key.
If you want to experience headaches, you can try with more quote nesting:
gnome-terminal --window-with-profile=Bash \
-e 'bash -c "route -n; read -p '"'Press a key...'"'"'
(In the following examples there is no final read. Let’s suppose we fixed that in the profile.)
If you want to print an empty line and enjoy multi-level escaping too:
gnome-terminal --window-with-profile=Bash \
-e 'bash -c "printf \\\\n; route -n"'
The same, with another quoting style:
gnome-terminal --window-with-profile=Bash \
-e 'bash -c '\''printf "\n"; route -n'\'
Variables are expanded in double quotes, not single quotes, so if you want them expanded you need to ensure that the outermost quotes are double:
command='printf "\n"; route -n'
gnome-terminal --window-with-profile=Bash \
-e "bash -c '$command'"
Quoting can become really complex. When you need something more advanced that a simple couple of commands, it is advisable to write an independent shell script with all the readable, parametrized code you need, save it somewhere, say /home/user/bin/mycommand, and then invoke it simply as
gnome-terminal --window-with-profile=Bash -e /home/user/bin/mycommand

Use Bash with script text from stdin and options from command line

I want to use /bin/bash (possibly /bin/sh) with the option -f passed to, and handled by, the script.
Precisely,
while getopts f OPT
do
case $OPT in
"f" ) readonly FLG_F="TRUE"
esac
done
if [ $FLG_F ]; then
rm -rf $KIBRARY_DIR
fi
and when these lines are in a file http://hoge.com/hoge.sh,
I can do this, for instance,
wget http://hoge.com/hoge.sh
/bin/bash hoge.sh -f
but not
/bin/bash -f hoge.sh
I know the reason but I want to do like this,
wget -O - http://hoge.com/hoge.sh | /bin/bash
with -f option for hoge.sh not for /bin/bash
Are there any good ways to do this?
/bin/bash <(wget -O - http://hoge.com/hoge.sh) -f
worked. but this is only for bash users, right?
Using bash you can do
wget -O - http://hoge.com/hoge.sh | /bin/bash -s -- -f
as with -s commands are read from the standard input. This option allows the positional parameters to be set too.
It should work with other POSIX shells too.

script doesn't see arg in '$ ssh bash script arg'

I'd like to see both commands print hello
$ bash -l -c "/bin/echo hello"
hello
$ ssh example_host bash -l -c /bin/echo hello
$
How can hello be passed as a parameter in the ssh command?
The bash -l -c is needed, so login shell startup scripts are executed.
Getting ssh to start a login shell would solve the problem too.
When you pass extra args after -c, they're put into the argv of the shell while that command is executing. You can see that like so:
bash -l -c '/bin/echo "$0" "$#"' hello world
...so, those arguments aren't put on the command line of echo (unless you go out of your way to make it so), but instead are put on the command line of the shell which you're telling to run echo with no arguments.
That is to say: When you run
bash -l -c /bin/echo hello
...that's the equivalent of this:
(exec -a hello bash -c /bin/echo)
...which puts hello into $0 of a bash which runs only /bin/echo. Since running /bin/echo doesn't look at $0, of course it's not going to print hello.
Now, because executing things via ssh means you're going through two steps of shell expansion, it adds some extra complexity. Fortunately, you can have the shell handle that for you automatically, like so:
printf -v cmd_str '%q ' bash -l -c '/bin/echo "$0" "$#"' hello world
ssh remote_host "$cmd_str"
This tells bash (printf %q is a bash extension, not available in POSIX printf) to quote your command such that it expands to itself when processed by a shell, then feeds the result into ssh.
All that said -- treating $0 as a regular parameter is bad practice, and generally shouldn't be done absent a specific and compelling reason. The Right Thing is more like the following:
printf -v cmd '%q ' /bin/echo hello world # your command
printf -v cmd '%q ' bash -l -c "$cmd" # your command, in a login shell
ssh remotehost "$cmd" # your command, in a login shell, in ssh

How to invoke bash, run commands inside the new shell, and then give control back to user?

This must either be really simple or really complex, but I couldn't find anything about it... I am trying to open a new bash instance, then run a few commands inside it, and give the control back to the user inside that same instance.
I tried:
$ bash -lic "some_command"
but this executes some_command inside the new instance, then closes it. I want it to stay open.
One more detail which might affect answers: if I can get this to work I will use it in my .bashrc as alias(es), so bonus points for an alias implementation!
bash --rcfile <(echo '. ~/.bashrc; some_command')
dispenses the creation of temporary files. Question on other sites:
https://serverfault.com/questions/368054/run-an-interactive-bash-subshell-with-initial-commands-without-returning-to-the
https://unix.stackexchange.com/questions/123103/how-to-keep-bash-running-after-command-execution
This is a late answer, but I had the exact same problem and Google sent me to this page, so for completeness here is how I got around the problem.
As far as I can tell, bash does not have an option to do what the original poster wanted to do. The -c option will always return after the commands have been executed.
Broken solution: The simplest and obvious attempt around this is:
bash -c 'XXXX ; bash'
This partly works (albeit with an extra sub-shell layer). However, the problem is that while a sub-shell will inherit the exported environment variables, aliases and functions are not inherited. So this might work for some things but isn't a general solution.
Better: The way around this is to dynamically create a startup file and call bash with this new initialization file, making sure that your new init file calls your regular ~/.bashrc if necessary.
# Create a temporary file
TMPFILE=$(mktemp)
# Add stuff to the temporary file
echo "source ~/.bashrc" > $TMPFILE
echo "<other commands>" >> $TMPFILE
echo "rm -f $TMPFILE" >> $TMPFILE
# Start the new bash shell
bash --rcfile $TMPFILE
The nice thing is that the temporary init file will delete itself as soon as it is used, reducing the risk that it is not cleaned up correctly.
Note: I'm not sure if /etc/bashrc is usually called as part of a normal non-login shell. If so you might want to source /etc/bashrc as well as your ~/.bashrc.
You can pass --rcfile to Bash to cause it to read a file of your choice. This file will be read instead of your .bashrc. (If that's a problem, source ~/.bashrc from the other script.)
Edit: So a function to start a new shell with the stuff from ~/.more.sh would look something like:
more() { bash --rcfile ~/.more.sh ; }
... and in .more.sh you would have the commands you want to execute when the shell starts. (I suppose it would be elegant to avoid a separate startup file -- you cannot use standard input because then the shell will not be interactive, but you could create a startup file from a here document in a temporary location, then read it.)
bash -c '<some command> ; exec /bin/bash'
will avoid additional shell sublayer
You can get the functionality you want by sourcing the script instead of running it. eg:
$cat script
cmd1
cmd2
$ . script
$ at this point cmd1 and cmd2 have been run inside this shell
Append to ~/.bashrc a section like this:
if [ "$subshell" = 'true' ]
then
# commands to execute only on a subshell
date
fi
alias sub='subshell=true bash'
Then you can start the subshell with sub.
The accepted answer is really helpful! Just to add that process substitution (i.e., <(COMMAND)) is not supported in some shells (e.g., dash).
In my case, I was trying to create a custom action (basically a one-line shell script) in Thunar file manager to start a shell and activate the selected Python virtual environment. My first attempt was:
urxvt -e bash --rcfile <(echo ". $HOME/.bashrc; . %f/bin/activate;")
where %f is the path to the virtual environment handled by Thunar.
I got an error (by running Thunar from command line):
/bin/sh: 1: Syntax error: "(" unexpected
Then I realized that my sh (essentially dash) does not support process substitution.
My solution was to invoke bash at the top level to interpret the process substitution, at the expense of an extra level of shell:
bash -c 'urxvt -e bash --rcfile <(echo "source $HOME/.bashrc; source %f/bin/activate;")'
Alternatively, I tried to use here-document for dash but with no success. Something like:
echo -e " <<EOF\n. $HOME/.bashrc; . %f/bin/activate;\nEOF\n" | xargs -0 urxvt -e bash --rcfile
P.S.: I do not have enough reputation to post comments, moderators please feel free to move it to comments or remove it if not helpful with this question.
With accordance with the answer by daveraja, here is a bash script which will solve the purpose.
Consider a situation if you are using C-shell and you want to execute a command
without leaving the C-shell context/window as follows,
Command to be executed: Search exact word 'Testing' in current directory recursively only in *.h, *.c files
grep -nrs --color -w --include="*.{h,c}" Testing ./
Solution 1: Enter into bash from C-shell and execute the command
bash
grep -nrs --color -w --include="*.{h,c}" Testing ./
exit
Solution 2: Write the intended command into a text file and execute it using bash
echo 'grep -nrs --color -w --include="*.{h,c}" Testing ./' > tmp_file.txt
bash tmp_file.txt
Solution 3: Run command on the same line using bash
bash -c 'grep -nrs --color -w --include="*.{h,c}" Testing ./'
Solution 4: Create a sciprt (one-time) and use it for all future commands
alias ebash './execute_command_on_bash.sh'
ebash grep -nrs --color -w --include="*.{h,c}" Testing ./
The script is as follows,
#!/bin/bash
# =========================================================================
# References:
# https://stackoverflow.com/a/13343457/5409274
# https://stackoverflow.com/a/26733366/5409274
# https://stackoverflow.com/a/2853811/5409274
# https://stackoverflow.com/a/2853811/5409274
# https://www.linuxquestions.org/questions/other-%2Anix-55/how-can-i-run-a-command-on-another-shell-without-changing-the-current-shell-794580/
# https://www.tldp.org/LDP/abs/html/internalvariables.html
# https://stackoverflow.com/a/4277753/5409274
# =========================================================================
# Enable following line to see the script commands
# getting printing along with their execution. This will help for debugging.
#set -o verbose
E_BADARGS=85
if [ ! -n "$1" ]
then
echo "Usage: `basename $0` grep -nrs --color -w --include=\"*.{h,c}\" Testing ."
echo "Usage: `basename $0` find . -name \"*.txt\""
exit $E_BADARGS
fi
# Create a temporary file
TMPFILE=$(mktemp)
# Add stuff to the temporary file
#echo "echo Hello World...." >> $TMPFILE
#initialize the variable that will contain the whole argument string
argList=""
#iterate on each argument
for arg in "$#"
do
#if an argument contains a white space, enclose it in double quotes and append to the list
#otherwise simply append the argument to the list
if echo $arg | grep -q " "; then
argList="$argList \"$arg\""
else
argList="$argList $arg"
fi
done
#remove a possible trailing space at the beginning of the list
argList=$(echo $argList | sed 's/^ *//')
# Echoing the command to be executed to tmp file
echo "$argList" >> $TMPFILE
# Note: This should be your last command
# Important last command which deletes the tmp file
last_command="rm -f $TMPFILE"
echo "$last_command" >> $TMPFILE
#echo "---------------------------------------------"
#echo "TMPFILE is $TMPFILE as follows"
#cat $TMPFILE
#echo "---------------------------------------------"
check_for_last_line=$(tail -n 1 $TMPFILE | grep -o "$last_command")
#echo $check_for_last_line
#if tail -n 1 $TMPFILE | grep -o "$last_command"
if [ "$check_for_last_line" == "$last_command" ]
then
#echo "Okay..."
bash $TMPFILE
exit 0
else
echo "Something is wrong"
echo "Last command in your tmp file should be removing itself"
echo "Aborting the process"
exit 1
fi
$ bash --init-file <(echo 'some_command')
$ bash --rcfile <(echo 'some_command')
In case you can't or don't want to use process substitution:
$ cat script
some_command
$ bash --init-file script
Another way:
$ bash -c 'some_command; exec bash'
$ sh -c 'some_command; exec sh'
sh-only way (dash, busybox):
$ ENV=script sh
Here is yet another (working) variant:
This opens a new gnome terminal, then in the new terminal it runs bash. The user's rc file is read first, then a command ls -la is sent for execution to the new shell before it turns interactive.
The last echo adds an extra newline that is needed to finish execution.
gnome-terminal -- bash -c 'bash --rcfile <( cat ~/.bashrc; echo ls -la ; echo)'
I also find it useful sometimes to decorate the terminal, e.g. with colorfor better orientation.
gnome-terminal --profile green -- bash -c 'bash --rcfile <( cat ~/.bashrc; echo ls -la ; echo)'

Resources