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 '16 at 16:10
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.
    – coffeMug
    Jun 10 '16 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 '16 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 '16 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 '16 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 '16 at 17:01
  • @pseyfert there is a way to call zsh interactively but I think for your needs the answer by JigglyNaga fits better.
    – coffeMug
    Jun 10 '16 at 17:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.