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/'"
bashrc
won't be enough either since i) the relevant lines might not start withexport
and, more importantly, ii) PATH can also be set in other files such as/etc/profile
, ~/.profile,
/etc/bash.bashrc,
~/.bash.login` etc.PATH
and checks) leaves them in the shell, where they'll execute commands. The checks won't be executed in every shell/OS, and not all shells are POSIX (e.g. fish, powershell), so to try to keep the shell-invoking code as similar as possible to the existing code, the check is just done through the exit code of thisprogram_that_checks_that_PATH_is_sane