3

I want to obtain the (zsh) shell prompt in a python script. Simply using

import os
prompt = os.environ['PS1']

seems not to be the right way, because PS1 is usually not forwarded to subprocesses. In the same manner env | grep PS in a shell fails.

So I concluded that I should probably start an interactive shell as subprocess and querry its prompt. From a shell I can just do

zsh -c -i 'echo $PS1'

(must be single quotes, double quotes fail)

I tried to do the same from python(2.7) with subprocess like this:

print subprocess.check_output(['-i','-c',r"'echo $PS1'"],executable="/bin/zsh")

This fails with

subprocess.CalledProcessError: Command '['-i', '-c', "'echo $PS1'"]' returned non-zero exit status 127

I think this is not because of the $PS1 but due to the way how i provide the echo-part as argument, because also echoing bare strings in this way fails.

Trying back and forth various combinations I ended up with

prompt =  subprocess.check_output("""zsh -c -i 'echo $PS1'""",shell=True,executable="/bin/zsh")

which seems to do the job but seems wrong to me as this is starting a shell and in that shell calling yet another shell with -c -i 'echo $PS1'.

What's the correct way of obtaining the shell prompt.

1
  • It seems to me, that the kwarg executable does not, what you think it does. It is used to replace the executable from the given command line. With shell=False it will replace the first element of the argument list, with shell=True it will be used as the shell instead of sh. It is not usually needed. See here for more information.
    – Adaephon
    Jun 13, 2016 at 16:10

2 Answers 2

1
print subprocess.check_output(['zsh','-i','-c','echo $PS1'])

When running zsh from another shell, the quotes around echo $PS1 are necessary so that the whole string is passed as the single argument after -c. They had to be single quotes because in double-quotes, the first shell would have expanded $PS1.

In Python, the whole command is a single string, so it is quoted like any other string. Adding a second set is like typing 'echo $PS1' (with the quotes) into zsh - the shell looks for, and fails to find, an executable named 'echo $PS1'.

3
  • There is an issue with this approach! Try this for example: print subprocess.check_output(['zsh','-i','-c','date']). It causes Python shell to stop.
    – Vombat
    Jun 10, 2016 at 16:27
  • 1
    @coffeMug Fascinating. It seems to be OK for shell built-ins, but if the shell needs to start another process, a stop signal gets sent back up to the Python shell. I'll investigate further.
    – JigglyNaga
    Jun 10, 2016 at 16:52
  • 1
    The unexpected stop is down to two interactive shells handling job-control. It only occurs when using a Python shell (rather than a script) and using -i with -c (unusual but necessary for this problem) and using external executables in the command (not required for this problem).
    – JigglyNaga
    Jun 11, 2016 at 7:44
1

I think the correct way is to call subprocess as follows:

>>> prompt = subprocess.check_output("""echo $PS1""",shell=True,executable="/bin/zsh")

Then you can check the result by >>> prompt Enter or you can use call to see the results directly:

>>> subprocess.call("""echo $PS1""",shell=True,executable="/bin/zsh")

So you don't need call zsh in the command itself.

To communicate interactively Popen can be used:

>>> Popen(["/bin/zsh"], stdout=PIPE).communicate()[0]
SHELL_PROMPT% echo $PS1    
SHELL_PROMPT% exit  # exit to see the result of command   

For more about this see Subprocess

Also notice the warning about using shell=True from above link:

Warning: Executing shell commands that incorporate unsanitized input from an untrusted source makes a program vulnerable to shell injection, a serious security flaw which can result in arbitrary command execution. For this reason, the use of shell=True is strongly discouraged in cases where the command string is constructed from external input:

>>> from subprocess import call
>>> filename = input("What file would you like to display?\n")
What file would you like to display?
non_existent; rm -rf / #
>>> call("cat " + filename, shell=True) # Uh-oh. This will end badly...
shell=False does not suffer from this vulnerability; the above Note may be helpful in getting code using shell=False to work.
3
  • This doesn't start zsh interactively, so the prompt isn't initialized as required. On the machine where I could test zsh, the result was what was set in /etc/profile.
    – JigglyNaga
    Jun 10, 2016 at 16:44
  • Interesting, but the second and third are not what I'm looking for, it should really run in a python script used like python somestuff.py i.e. run noninteractively. The first also doesn't do the job as the -i is essential to set the prompt (at least for most default prompts and the user defined prompts i've seen so far).
    – pseyfert
    Jun 10, 2016 at 17:01
  • @pseyfert there is a way to call zsh interactively but I think for your needs the answer by JigglyNaga fits better.
    – Vombat
    Jun 10, 2016 at 17:20

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .