3

I am trying to capture the output of a command that I am running with subprocess.Popen and outputting to an xterm terminal that is embedded in a tkinter built application. I want the output of the command to go to the terminal and be captured so I can analyze the information.

In another case where I just run the command with subprocess.Popen and don't pass the output to the terminal. I can get the output by including stdout=subprocess.PIPE, stderr=subprocess.STDOUT

proc_output = subprocess.Popen('ls', stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
print(str(proc_output.stdout.read()))

I am guessing that the way I have it coded I can send the output to the terminal or to a variable, but not both.

For my code example, once the terminal opens I have to manually type tty in the terminal and then put the output in the tty entry field. After that I can run commands from the cmd_entry and send the output to the terminal .

This code is a slightly modified version of the answer from here.

from tkinter import *
import subprocess

root = Tk()
termf = Frame(root, height=360, width=625)
termf.pack(fill=BOTH, expand=YES)
wid = termf.winfo_id()

f=Frame(root)
Label(f,text="/dev/pts/").pack(side=LEFT)
tty_index = Entry(f, width=3)
tty_index.insert(0, "1")
tty_index.pack(side=LEFT)
Label(f,text="Command:").pack(side=LEFT)
cmd_entry = Entry(f)
cmd_entry.insert(0, "ls -l")
cmd_entry.pack(side=LEFT, fill=X, expand=1)

def send_entry_to_terminal(*args):
    tty="/dev/pts/%s" % tty_index.get()
    proc = subprocess.Popen(f'{cmd_entry.get()} <{tty} >{tty} 2> {tty}',
                            shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
    print(str(proc.stdout.read())
    # cmd(f'{e.get()} <{tty} >{tty} 2> {tty}')

cmd_entry.bind("<Return>", send_entry_to_terminal)
b = Button(f,text="Send", command=send_entry_to_terminal)
b.pack(side=LEFT)
f.pack(fill=X, expand=1)

# cmd(f'xterm -into {wid} -geometry 100x27 -sb -e "tty; sh" &')
subprocess.Popen(f'xterm -into {wid} -geometry 100x27 -sb &', shell=True)

root.mainloop()

What I want to do is have the output of any command ran with subprocess to print to the terminal and be saved in a variable for later processing. Yet right now I can only get one or the other.

John Kugelman
  • 349,597
  • 67
  • 533
  • 578
  • 1
    The top example is code where I was able to easily collect the output and store it in proc_output because it is not routed to the xterm window. From my understanding, xterm doesn't actualy run commands, it just shows the output of `subprocess.Popen(f'{cmd_entry.get()} <{tty} >{tty} 2> {tty}'...` – epic.jordan Apr 25 '19 at 01:49
  • The goal is for the application to run commands using subprocess and send the output to the terminal and store the output. For simplicity, the "application" is replaced with the command entry and Send button. – epic.jordan Apr 25 '19 at 01:55
  • Oh, sorry—I misread and thought you were writing commands to the terminal established by `xterm` (which may not be possible anyway). – Davis Herring Apr 25 '19 at 02:20

1 Answers1

1

Start by converting the shell redirections into native Popen capabilities:

with open(tty,'r+b',0) as f:
  proc=subprocess.Popen(cmd_entry.get(),shell=True,
                        stdin=f,stdout=f,stderr=f)
if proc.wait(): warn_user(…)

Then it should be fairly obvious how to retain a copy of the output:

with open(tty,'r+b',0) as f:
  proc=subprocess.Popen(cmd_entry.get(),shell=True,
                        stdin=f,stdout=subprocess.PIPE,stderr=subprocess.STDOUT)
  out=[]
  for l in proc.stdout:
    f.write(l)
    out.append(l)
  out=b"".join(out)
  if proc.wait(): warn_user(…)

Note that the process will then have a pipe rather than a (pseudo)terminal for its output, which may change its behavior (e.g., ls prints in a single column, and git doesn’t use a pager). If you want to preserve the terminal behavior, you have to open your own pseudoterminal and relay output written to it (which may then include various control sequences) to the one created by xterm.

You might also want to make the process be the foreground process group of the (xterm) terminal so that it can receive keyboard signals; this involves tcsetpgrp and is somewhat complicated.

Davis Herring
  • 36,443
  • 4
  • 48
  • 76
  • Shouldn't tty be f for `stdin=tty stdout=tty stderr=tty` and `tty.write(l)` with or without these changes I get the error: _with open(tty, 'r+b') as f: io.UnsupportedOperation: File or stream is not seekable_ It could be my implementation. – epic.jordan Apr 25 '19 at 03:43
  • @epic.jordan: Sorry, yes, of course `f`; edited. The `r+b` failure seems to be a Python 3 bug: there’s no reason it should do a seek. I put in a workaround that shouldn’t cause too much trouble, but you could also just open the terminal twice, once for reading and once for writing. – Davis Herring Apr 25 '19 at 04:15
  • I did a little research and found by changing the mode to `wb+` and adding `buffering=0` then there is no more error. Also `.join()` needs a string. I opted to use `out = '' out += str(l)` I tried to edit the answer, but I think maybe I'm not supposed to do that... :-) Thanks a lot for the solution! – epic.jordan Apr 25 '19 at 04:48
  • ok, one last comment / question. I guess the `open` mode makes no difference. `r+b` and `wb+` have identical results. Do you know why this is? – epic.jordan Apr 25 '19 at 04:54
  • @epic.jordan: `join` does work with [byte strings](https://docs.python.org/3/library/stdtypes.html#bytes.join) and can be much more efficient than `+=` (although CPython sometimes is clever with the latter). Calling `str(bytes)` in Python 3 will add garbage delimiters. `w+b` means create/truncate and allow reading, but truncation only affects regular files. – Davis Herring Apr 25 '19 at 06:24
  • ahh, I see you added the 'b' in the `out=b"".join(out)`. In this case I am passing a command to sprocess that executes quickly, but in the real use case I am using it to run other programs that can take hours to days. How can I output to the terminal while it is running instead of after it completes? – epic.jordan Apr 27 '19 at 00:22
  • @epic.jordan: The code above does write each line immediately, and `f` is unbuffered. But the subprocess is writing to a pipe and will thus be fully buffered by default. Opening your own pseudoterminal is [one way to fix that](https://stackoverflow.com/q/3465619/8586227). – Davis Herring Apr 27 '19 at 00:29