2

I have the following program that wraps top in a pseudo terminal and prints it back to the real terminal.

import os
import pty
import subprocess
import sys
import time

import select

stdout_master_fd, stdout_slave_fd = pty.openpty()
stderr_master_fd, stderr_slave_fd = pty.openpty()

p = subprocess.Popen(
    "top",
    shell=True,
    stdout=stdout_slave_fd,
    stderr=stderr_slave_fd,
    close_fds=True
)

stdout_parts = []
while p.poll() is None:
    rlist, _, _ = select.select([stdout_master_fd, stderr_master_fd], [], [])
    for f in rlist:
        output = os.read(f, 1000)  # This is used because it doesn't block
        sys.stdout.write(output.decode("utf-8"))
        sys.stdout.flush()
    time.sleep(0.01)

This works well control sequences are handled as expected. However, the subprocess is not using the full dimensions of the real terminal.

For comparison, running the above program:

pty

And running top directly:

direct

I didn't find any api of the pty library to suggest dimensions could be provided.

The dimensions I get in practice for the pseudo terminal are height of 24 lines and width of 80 columns, I'm assuming it might be hardcoded somewhere.

Mugen
  • 8,301
  • 10
  • 62
  • 140
  • Possible duplicate of https://stackoverflow.com/questions/5161552/python-curses-handling-window-terminal-resize – tripleee Nov 30 '21 at 10:04

3 Answers3

1

Reading on Emulate a number of columns for a program in the terminal I found the following working solution, at least on my environment (OSX and xterm)

echo LINES=$LINES COLUMNS=$COLUMNS TERM=$TERM

which comes to LINES=40 COLUMNS=203 TERM=xterm-256color in my shell. Then setting the following in the script gives the expected output:

p = subprocess.Popen(
    "top",
    shell=True,
    stdout=stdout_slave_fd,
    stderr=stderr_slave_fd,
    close_fds=True,
    env={
        "LINES": "40",
        "COLUMNS": "203",
        "TERM": "xterm-256color"
    }
)
Mugen
  • 8,301
  • 10
  • 62
  • 140
1

@Mugen's answer pointed me in the right direction but did not quite work, here is what worked for me personally :

import os
import subprocess

my_env = os.environ.copy()
my_env["LINES"] = "40"
my_env["COLUMNS"] = "203"
result = subprocess.Popen(
    cmd, 
    stdout= subprocess.PIPE,
    env=my_env
    ).communicate()[0]

So I had to first get my entire environment variable with os library and then add the elements I needed to it.

leas
  • 329
  • 8
  • 17
1

The solutions provided by @leas and @Mugen did not work for me, but I eventually stumbled upon ptyprocess Python module, which allows you to provide terminal dimensions when spawning a process.

For context, I am trying to use a Python script to run a PowerShell 7 script and capture the PowerShell script's output. The host OS is Ubuntu Linux 22.04.

My code looks something like this:

from ptyprocess import PtyProcessUnicode

# Run the PowerShell script
script_run_cmd = 'pwsh -file script.ps1 param1 param2'
p = PtyProcessUnicode.spawn(script_run_cmd.split(), dimensions=(24,130))

# Get all script output 
script_output = []
while True:
    try:
        script_output.append(p.readline().rstrip())
    except EOFError:
        break

# Not sure if this is necessary
p.close()

I feel like there should be a class method to get all the output, but I couldn't find one and the above code works well for me.

Bob Loblaw
  • 63
  • 5