Bash – Invoke subshell command with loaded rcfile

bashbashrc

I have some Python code that does: (this is a simplified version)

shell = os.environ['SHELL']
os.environ['PATH'] = ... # some new PATH value
if call(shell, '-c', 'program_that_checks_that_PATH_is_sane'): # hasn't been mangled by the rcfile
    sys.exit('fix your .bashrc|.zshrc|.config/fish')
call(shell) # drops the user in a shell

I foolishly believed that $SHELL -c would've been enough for my purposes, but I recently discovered that the PATH check wasn't working on bash

I tried to look at some of the suggested solutions, but bash is much more brittle than i expected

Basically, I discovered that BASH_ENV, or manually sourcing with bash -c "source ~/.bashrc; do_your_stuff" won't work if some errors can be triggered by the sourced file

These same errors are not a problem if invoking bash interactively (that is, bash -i will ignore errors, and bash -c will silently fail). But bash -i is not a workable solution (I guess that this is because bash -i will "steal" the stdin, thus stopping the invoking python process)

The solution can't also simply be "don't use a broken ~/.bashrc":

  • The code has to be robust in face of poorly configured environments
  • This happens with a default .bashrc, like the one supplied by ubuntu (this is an old version)

The error actually happens (found it by set -xe before sourcing) inside /usr/share/bash-completion/bash_completion at this line: [[ -f /etc/slackware-version ]] && sysvdirs=( /etc/rc.d ) (how can this fail the process of sourcing is beyond me)

After lots of tweaking, I found out how could I detect such errors when sourcing:

set -e && . ~/.bashrc & wait %% ; echo $?

Weirdly, the following will fail instead:

set -e ; . ~/.bashrc & wait %% ; echo $?

Still, this isn't good enough, because neither

bash -c "set -e && . ~/.bashrc & wait %% ; echo \$?"
call(['bash', '-c', 'set -e && . ~/.bashrc & wait %% ; echo $?'])

will print a nonzero exit code. I'd also prefer to avoid to rely on shell specific code like this.

How can I make sure that when invoking bash -c the .bashrc will be correctly loaded?

As an alternative to this whole issue, I'm thinking of munging the .{bash,zsh}rc, to identify problems with the PATH. It's a poor solution, but it'd cover 90% of the cases, and avoid forking extra processes.

import re
r = '^export PATH\s?=\s?([^:]+:)+(\$PATH|\${PATH})'
g = re.match(r, 'export PATH=/usr/bin:$PATH').groups()
any(check_collision(x[:-1]) for x in g[:-1])

edit:

The bash -i flag seems to be the simplest and most promising half-solution, but its behavior is non obvious, some examples to help people understand:

This runs ls, and then subsequently leaves us in the Python prompt, with Python running

python3 -ic "import os; os.system(\"bash -c ls\")"

This stops the Python process

python3 -ic "import os; os.system(\"bash -ic ls\")"

Supplying a different stdin to the child process is not enough:

python3 -ic "import subprocess;f=open('/dev/null');subprocess.call(['bash', '-ic', 'ls'], stdin=f)"

Finally, bash -ic doesn't actually gets additional commands from the stdin, the following 2 commands have the exact same behavior:

 echo echo sed | bash -c "sed 's/[^ ]*$/bash/'"
 echo echo sed | bash -ic "sed 's/[^ ]*$/bash/'"

Best Answer

Don't use the SHELL environment variable in a script. That's the user's preferred interactive shell. You don't know anything about the syntax it supports. It could be tcsh, zsh, fish, rc, …

Don't load .bashrc from a script. The user is likely to put things that rely on bash being interactive and having a terminal; it might hang, or reconfigure your terminal, or hide your script's output, or any number of bad things. When you run bash -c …, .bashrc is not loaded, and that's by design.

If you need to check that PATH is sane, you can do it in Python, you don't need to invoke a shell for that. If by “the code has to be robust in face of poorly configured environments” you mean that your Python script should try to replicate the PATH of interactive shells even if it isn't started from one, then don't: you're far more likely to mess up in strange ways (and in particular to override an explicit setting from the user) than you're likely to help the user.

If your script is dropping the user into an interactive shell for some reason, then do run os.environ['SHELL'], falling back to pwd.getpwuid(os.geteuid()).pw_shell if the environment variable isn't set. This will run an interactive shell provided that your script is running in a terminal, you don't need to attempt to guess -i or other options which may or may not mean what you hope. If your script isn't running in a terminal then it has no business attempting to interact with the user and shouldn't start an interactive shell.

Related Question