Posix requires the -i
option to a shell to initiate the shell in "interactive mode". The precise definition of interactive mode varies from shell to shell -- obviously zsh
and csh
aren't going to try to interpret commands in .bashrc
-- but using the -i
flag should Do The Right Thing with all reasonable shells.
Normally, you would pass the argument by calling subprocess.call
(or some Popen
variant) with a list:
subprocess.call(['bash', '-i'])
Of course, that won't respect the user's shell preference. You should be able to get that from the SHELL
environment variable:
subprocess.call([os.getenv('SHELL'), '-i'])
In order to get the shell to execute a particular command line, you need to use the -c
command-line option, which is also Posix-standard so it should work on all shells:
subprocess.call([os.getenv('SHELL'), '-i', '-c', command_to_run])
This will work fine in a number of cases, but it can fail if the shell decides to exec
the last (or only) command in command_to_run
(see this answer on http://unix.stackexchange.com for some details.) and you subsequently attempt to invoke another shell to execute another command.
Consider, for example, the simple python program:
import subprocess
subprocess.call(['/bin/bash', '-i', '-c', 'ls'])
subprocess.call(['/bin/bash', '-i', '-c', 'echo second']);
The first bash
process is started. Since it is an interactive shell, it creates a new process group and attaches the terminal to that process group. It then examines the command to run, determine that it is a simple command which runs an external utility, and consequently that it can exec
the command. So it does that, replacing itself with the ls
utility, which is now the leader of the terminal process group. When the ls
terminates, the terminal process group becomes empty, but the terminal is still attached to it. So when the second bash process is started, it tries to create a new process group and attach the terminal to that process group, but that's not possible because the terminal is in a kind of limbo. According to the Posix standard (Base Definitions, §11.1.2):
When there is no longer any process whose process ID or process group ID matches the foreground process group ID, the terminal shall have no foreground process group. It is unspecified whether the terminal has a foreground process group when there is a process whose process ID matches the foreground process group ID, but whose process group ID does not. No actions defined in POSIX.1-2008, other than allocation of a controlling terminal or a successful call to tcsetpgrp()
, shall cause a process group to become the foreground process group of the terminal.
With bash, this only happens if the string passed as the value of the -c
argument is a simple command, so there is a simple workaround: make sure that the string is not a simple command. For example,
subprocess.call([os.getenv('SHELL'), '-i', '-c', ':;' + command_to_run])
which prepends a no-op to the command, making it a compound command. However, that won't work with other shells which are more aggressive in the tail-call optimization. So the general solution needs to follow the path suggested by Posix, also taking care of the description of the tcsetpgrp
system call:
Attempts to use tcsetpgrp()
from a process which is a member of a background process group on a fildes associated with its controlling terminal shall cause the process group to be sent a SIGTTOU
signal. If the calling thread is blocking SIGTTOU
signals or the process is ignoring SIGTTOU
signals, the process shall be allowed to perform the operation, and no signal is sent.
Since the default action on a SIGTTOU
signal is to stop the process, we need to ignore or block the signal. So we end up with the following:
!/usr/bin/python
import signal
import subprocess
import os
# Ignore SIGTTOU
signal.signal(signal.SIGTTOU, signal.SIG_IGN)
def run_command_in_shell(cmd):
# Run the command
subprocess.call([os.getenv('SHELL'), '-i', '-c', cmd])
# Retrieve the terminal
os.tcsetpgrp(0,os.getpgrp())
run_command_in_shell('ls')
run_command_in_shell('ls')