191

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'

11 Answers11

150

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.

24

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.

chicks
  • 3,915
  • 10
  • 29
  • 37
ExpoBi
  • 365
19

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
7

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.

6

What you need is to execute a startup script, then proceed with the interactive session.

The default startup script is ~/.bashrc, another script could be given with the --init-file option. If you simply pass an --init-file option, your script would replace the default, rather than augment it.

The solution is to pass, using the <(...) syntax, a temporary script that sources the default ~/.bashrc followed by any other commands:

bash --init-file <(echo ". ~/.bashrc; ls; pwd; ### other commands... ###")
6

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"

1
$ bash --init-file <(echo 'ls; pwd')
$ bash --rcfile <(echo 'ls; pwd')

In case you can't use process substitution:

$ cat script
ls; pwd
$ bash --init-file script

With sh (dash, busybox):

$ ENV=script sh

Or:

$ bash -c 'ls; pwd; exec bash'
$ sh -c 'ls; pwd; exec sh'
x-yuri
  • 2,526
1

I don't have enough reputation here to comment yet, but I think the various solutions with bash's --rcfile (aka --init-file) are the most practical.

For a POSIX-compatible solution, you could also try something like

sh -si < my_init_file

where my_init_file terminates with

exec </dev/tty

which connects stdin to the terminal.

You could even try

sh -i -c'my startup commands; exec </dev/tty'

Note that making the shell interactive (with -i) means that certain errors are treated more leniently.

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.

jago
  • 11
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

I accomplish basically the same thing by just using a script, typically for the purpose of setting environment variables for use in a specific project directory;

$ cat shell.sh
#!/bin/bash
export PATH=$PWD/bin:$PATH
export USERNAME=foo
export PASSWORD=bar
export DB_SERVER=http://localhost:6001
bash

$ echo ${USERNAME:-none}
none

$ ./shell.sh

$ echo $USERNAME
foo

This drops you into an interactive bash shell after all the environment adjustments are made; you can update this script with the relevant other commands you want to run.