67

I want to run a bash subshell, (1) run a few commands, (2) and then remain in that subshell to do as I please. I can do each of these individually:

  1. Run command using -c flag:

    $> bash -c "ls; pwd; <other commands...>"
    

    however, it immediately returns to the "super" shell after the commands are executed. I can also just run an interactive subshell:

  2. Start new bash process:

    $> bash
    

    and it won't exit the subshell until I say so explicitly... but I can't run any initial commands. The closest solution I've found is:

    $> bash -c "ls; pwd; <other commands>; exec bash"
    

    which works, but not the way I wanted to, as it runs the given commands in one subshell, and then opens a separate one for interaction.

I want to do this on a single line. Once I exit the subshell, I should return back to the regular "super"shell without incident. There must be a way~~

NB: What I am not asking...

  1. not asking where to get a hold of the bash man page
  2. not asking how to read initializing commands from a file... I know how to do this, it's not the solution I'm looking for
  3. not interested in using tmux or gnu screen
  4. not interested in giving context to this. I.e., the question is meant to be general, and not for any specific purpose
  5. if possible, I want to avoid using workarounds that sort of accomplish what I want, but in a "dirty" way. I just want to do this on a single line. In particular, I don't want to do something like xterm -e 'ls'
  • I can imagine an Expect solution, but it's hardly the one-liner you want. In what way is the exec bash solution unsuitable for you? – glenn jackman Mar 9 '12 at 16:21
  • @glennjackman sorry, I'm not familiar with the jargon. What is an "Expect solution"? Also, the exec bash solution involves two separate subshells. I want one continuous subshell. – SABBATINI Luca Mar 9 '12 at 19:18
  • 2
    The beauty of exec is that it replaces the first subshell with the second, so you're only left 1 shell below the parent. If your initialization commands set environment variables, they will exist in the exec'ed shell. – glenn jackman Mar 9 '12 at 19:45
  • 1
  • 1
    And the problem with exec is that you lose anything that's not passed down to subshells via the environment, such as non-exported variables, functions, aliases, ... – Curt J. Sampson Sep 15 '18 at 3:49
62

This can be easily done with temporary named pipes:

bash --init-file <(echo "ls; pwd")

Credit for this answer goes to the comment from Lie Ryan. I found this really useful, and it's less noticeable in the comments, so I thought it should be its own answer.

  • 8
    This presumably means that $HOME/.bashrc is not executed though. It would have to be included from the temporary named pipe. – Hubro Aug 4 '15 at 21:47
  • 8
    To clarify, something like this: bash --init-file <(echo ". \"$HOME/.bashrc\"; ls; pwd") – Hubro Aug 4 '15 at 22:02
  • 4
    This is so gross but it does work. I can't believe bash doesn't support this directly. – Pat Niemeyer Feb 24 '17 at 16:07
  • @Hubro, what is the purpose of the . in the command? Without it, I get a "permission denied" on my .bashrc, but I don't understand why, and how the . fixes it. – Gus Mar 12 '17 at 2:01
  • 2
    @Gus, the . is a synonym for the source command: ss64.com/bash/source.html. – Jonathan Potter Mar 12 '17 at 3:18
9

You can do this in a roundabout way with a temp file, although it will take two lines:

echo "ls; pwd" > initfile
bash --init-file initfile
  • For a nice effect you can make the temp file remove itself by including rm $BASH_SOURCE in it. – Eduardo Ivanec Mar 9 '12 at 17:05
  • Eduardo, thank you. That's a nice solution, but... are you saying that this can't be done without having to fiddle with file I/O. There's obvious reasons why I would prefer to keep this as a self contained command, because the moment files come into the mix I'll have to start worrying about how to make random temp-files and then, as you mentioned, deleting them. It just requires so much more effort this way if I want to be rigorous. Hence the desire for a more minimalistic, elegant solution. – SABBATINI Luca Mar 9 '12 at 17:20
  • 1
    @SABBATINILuca: I'm not saying anything like that. This is just a way, and mktemp does solve the temp file issue as @cjc pointed out. Bash could support reading the init commands from stdin, but as far as I can tell it doesn't. Specyfing - as init file and piping them half works, but Bash then exits (probably because it detected the pipeline). The elegant solution, IMHO, is to use exec. – Eduardo Ivanec Mar 9 '12 at 17:50
  • 1
    Doesn't this also override your normal bash initialisation ? @SABBATINILuca what are you trying to achieve with this why do you need to spawn a shell auto run some commands and then keep that shell open? – Iain Mar 9 '12 at 17:58
  • 10
    This is an old question, but Bash can create temporary named pipes by using the following syntax: bash --init-file <(echo "ls; pwd"). – Lie Ryan Feb 13 '14 at 14:59
3

Try this instead:

$> bash -c "ls;pwd;other commands;$SHELL"

$SHELL It makes the shell open in interactive mode, waiting for a close with exit.

  • 5
    FYI, this opens a new shell afterwards, so if any of the commands affect the current shell's state (e.g. sourcing a file) it might not work as expected – ThiefMaster Apr 8 '17 at 14:47
2

The "Expect solution" I was referring to is programming a bash shell with the Expect programming language:

#!/usr/bin/env expect
set init_commands [lindex $argv 0]
set bash_prompt {\$ $}              ;# adjust to suit your own prompt
spawn bash
expect -re $bash_prompt {send -- "$init_commands\r"}
interact
puts "exiting subshell"

You'd run that like: ./subshell.exp "ls; pwd"

  • I guess this would have the advantage of registering the commands in the history too, am I wrong? Also I'm curious if bashrc/profile is executed in this case? – muhuk Nov 7 '15 at 10:49
  • Confirmed that this allow you to put commands in the history, which the other solutions don't. This is great for starting a process in a .screenrc file -- if you exit the started process, you don't close the screen window. – Dan Sandberg Oct 29 '18 at 13:56
1

If sudo -E bash does not work, I use the following, which has met my expectations so far:

sudo HOME=$HOME bash --rcfile $HOME/.bashrc

I set HOME=$HOME because I want my new session to have HOME set to my user's HOME, rather than root's HOME, which happens by default on some systems.

0

less elegant than --init-file, but perhaps more instrumentable:

fn(){
    echo 'hello from exported function'
}

while read -a commands
do
    eval ${commands[@]}
done
0

Why not use native subshells?

$ ( ls; pwd; exec $BASH; )
bar     foo     howdy
/tmp/hello/
bash-4.4$ 
bash-4.4$ exit
$

Enclosing commands with parentheses makes bash spawn a subprocess to run these commands, so you can, for example, alter the environment without affecting parent shell. This is basically more readable equivalent to the bash -c "ls; pwd; exec $BASH".

If that still looks verbose, there are two options. One is to have this snippet as a function:

$ run() { ( eval "$@"; exec $BASH; ) }
$ run 'ls; pwd;'
bar     foo     howdy
/tmp/hello/
bash-4.4$ exit
$ run 'ls;' 'pwd;'
bar     foo     howdy
/tmp/hello/
bash-4.4$ exit
$

Another is to make exec $BASH shorter:

$ R() { exec $BASH; }
$ ( ls; pwd; R )
bar     foo     howdy
/tmp/hello/
bash-4.4$ exit
$

I personally like R approach more, as there is no need to play with escaping strings.

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.