9

I am trying to assign the output of a command to a variable without the command thinking that it is being piped. The reason for this is that the command in question gives unformatted text as output if it is being piped, but it gives color formatted text if it is being run from the terminal. I need to get this color formatted text.

So far I've tried a few things. I've tried Popen like so:

output = subprocess.Popen(command, stdout=subprocess.PIPE)
output = output.communicate()[0]
output = output.decode()
print(output)

This will let me print the output, but it gives me the unformatted output that I get when the command is piped. That makes sense, as I'm piping it here in the Python code. But I am curious if there is a way to assign the output of this command, directly to a variable, without the command running the piped version of itself.

I have also tried the following version that relies on check_output instead:

output = subprocess.check_output(command)
output = output.decode()
print(output)

And again I get the same unformatted output that the command returns when the command is piped.

Is there a way to get the formatted output, the output the command would normally give from the terminal, when it is not being piped?

nullified
  • 95
  • 1
  • 1
  • 5
  • I think the "coloring" is a function of the shell/tty. The colors are not part of the output of command. So you won't be able to retrieve anything else but this unformatted output. – GhostCat Mar 08 '15 at 09:36
  • 1
    Hi EddyG, sometimes that's true, but you can send color formatting code to the terminal yourself. That's the case in this instance. I double checked the C source of the command itself and it does indeed send the color formatting code. So it's not just the tty. – nullified Mar 08 '15 at 09:51
  • related: [Last unbuffered line can't be read](http://stackoverflow.com/q/25923901/4279) – jfs Mar 11 '15 at 14:34

4 Answers4

8

Using pexpect:

2.py:

import sys

if sys.stdout.isatty():
    print('hello')
else:
    print('goodbye')

subprocess:

import subprocess

p = subprocess.Popen(
    ['python3.4', '2.py'],
    stdout=subprocess.PIPE
)

print(p.stdout.read())

--output:--
goodbye

pexpect:

import pexpect

child = pexpect.spawn('python3.4 2.py')

child.expect(pexpect.EOF)
print(child.before)  #Print all the output before the expectation.

--output:--
hello

Here it is with grep --colour=auto:

import subprocess

p = subprocess.Popen(
    ['grep', '--colour=auto', 'hello', 'data.txt'],
    stdout=subprocess.PIPE
)

print(p.stdout.read())

import pexpect

child = pexpect.spawn('grep --colour=auto hello data.txt')
child.expect(pexpect.EOF)
print(child.before)

--output:--
b'hello world\n'
b'\x1b[01;31mhello\x1b[00m world\r\n'
7stud
  • 46,922
  • 14
  • 101
  • 127
  • 1
    upvote. As far as I can tell, it is the only answer with a code example that actually works. – jfs Mar 08 '15 at 10:23
  • 1
    you could use [`pexpect.runu()`](http://pexpect.readthedocs.org/en/latest/api/pexpect.html?highlight=runu#pexpect.runu) if all you need is to get output as a string. – jfs Mar 08 '15 at 10:34
  • @J.F. Sebastian, Nice. I just read the docs. Would have saved some fumbling with that EOF constant. – 7stud Mar 08 '15 at 10:38
  • for subprocess-based code, you could use check_output with universal_newlines=True – jfs Mar 08 '15 at 10:40
  • I was successful in my aims with this code. Thank you 7stud, and everyone else, for your valuable feedback, especially J. F. Sebastian and Antti Haapala. I have another issue now that is specific to pexpect.spawn, but I wil make another thread. This should work so long as my collaborator doesn't made the additional pexpect dependency. Thanks! – nullified Mar 08 '15 at 16:56
4

Yes, you can use the pty module.

>>> import subprocess
>>> p = subprocess.Popen(["ls", "--color=auto"], stdout=subprocess.PIPE)
>>> p.communicate()[0]
# Output does not appear in colour

With pty:

import subprocess
import pty
import os

master, slave = pty.openpty()
p = subprocess.Popen(["ls", "--color=auto"], stdout=slave)
p.communicate()
print(os.read(master, 100)) # Print 100 bytes
# Prints with colour formatting info

Note from the docs:

Because pseudo-terminal handling is highly platform dependent, there is code to do it only for Linux. (The Linux code is supposed to work on other platforms, but hasn’t been tested yet.)

A less than beautiful way of reading the whole output to the end in one go:

def num_bytes_readable(fd):
    import array
    import fcntl
    import termios
    buf = array.array('i', [0])
    if fcntl.ioctl(fd, termios.FIONREAD, buf, 1) == -1:
        raise Exception("We really should have had data")
    return buf[0]

print(os.read(master, num_bytes_readable(master)))

Edit: nicer way of getting the content at once thanks to @Antti Haapala:

os.close(slave)
f = os.fdopen(master)
print(f.read())

Edit: people are right to point out that this will deadlock if the process generates a large output, so @Antti Haapala's answer is better.

Andrew Magee
  • 6,506
  • 4
  • 35
  • 58
  • You could use `os.fdopen` to open the master descriptor as a python file – Antti Haapala -- Слава Україні Mar 08 '15 at 10:02
  • yes, `pty` may hoodwink the command into thinking that it is run interactively in a terminal (though if the program provides `--color` option such as `ls`; you could just pipe the output) 1. you should mention that `pty` is for Linux (it may also work on other *nices). You don't need to call `p.communicate()` if you use `stdout=slave` -- `p.wait()` is enough. You should probably read from `master` while the command is still running otherwise the program may hang forever after it fills up the corresponding output buffers, [code example](http://stackoverflow.com/a/20509641/4279) <- `pexpect` – jfs Mar 08 '15 at 10:04
  • Closing `slave` file descriptor before reading the output from `master` may lead to an I/O error (it shouldn't but it does). I had to [put `os.close(slave_fd)` after the `while 1` that reads the output to make the code work](http://stackoverflow.com/questions/12419198/python-subprocess-readlines-hangs/12471855#12471855). Also, `f.read()` might hang forever -- try it (I'm not completly sure it was long time ago). – jfs Mar 08 '15 at 10:19
  • Hmm interesting. I was playing around with the `os.fdopen` suggestion but the `read` call hanged if I didn't close the slave first. It seems to work fine.. do you know what would make it fail? – Andrew Magee Mar 08 '15 at 10:20
  • it is because the master never meets "EOF" while the slave is held open. – Antti Haapala -- Слава Україні Mar 08 '15 at 10:28
  • Yeah I guessed as much. – Andrew Magee Mar 08 '15 at 10:33
  • Have you tried your code? What is your system? `os.close(slave)` before reading the data may lead to `errno.EIO error` as mentioned in the comment in [the code I've linked above](http://stackoverflow.com/questions/12419198/python-subprocess-readlines-hangs/12471855#12471855). – jfs Mar 08 '15 at 22:00
  • Yes I've tried it and I can't get it to fail, that's why I was wondering if you knew the situation that could cause it to fail. – Andrew Magee Mar 08 '15 at 22:01
  • [@AnttiHaapala says](http://stackoverflow.com/questions/28924910/python-3-4-3-subprocess-popen-get-output-of-command-without-piping/28925318?noredirect=1#comment46120917_28925318) that EIO is raised each time. – jfs Mar 08 '15 at 22:34
  • Yeah, for EOF, which he catches. His solution still closes the slave before reading from the master. – Andrew Magee Mar 08 '15 at 22:38
  • I've tried some code and here're good news: if you remove `p.communicate()` call and put `p.wait()` after `f.read()` as I've suggested in [the first comment](http://stackoverflow.com/questions/28924910/python-3-4-3-subprocess-popen-get-output-of-command-without-piping/28925318#comment46106793_28925133) then it also supports large output. I can see EIO with `io.open()` on Python 2 and with `os.read()` on both Python 2 and 3 on Linux. – jfs Mar 09 '15 at 04:10
  • I've managed to get EIO exception even with `os.fdopen()` on Python 2 (added a delay in the child process). – jfs Mar 11 '15 at 15:19
  • Using pty, it gives : b'\x1b[0m\x1b[01;32mpty_eg1.py\x1b[0m\r\n' – BhishanPoudel Sep 30 '16 at 00:01
4

A working polyglot example (works the same for Python 2 and Python 3), using pty.

import subprocess
import pty
import os
import sys

master, slave = pty.openpty()
# direct stderr also to the pty!
process = subprocess.Popen(
    ['ls', '-al', '--color=auto'],
    stdout=slave,
    stderr=subprocess.STDOUT
)

# close the slave descriptor! otherwise we will
# hang forever waiting for input
os.close(slave)

def reader(fd):
    try:
        while True:
            buffer = os.read(fd, 1024)
            if not buffer:
                return

            yield buffer

    # Unfortunately with a pty, an 
    # IOError will be thrown at EOF
    # On Python 2, OSError will be thrown instead.
    except (IOError, OSError) as e:
        pass

# read chunks (yields bytes)
for i in reader(master):
    # and write them to stdout file descriptor
    os.write(1, b'<chunk>' + i + b'</chunk>')
  • 1. it shouldn't work on Python 3, try `os.write(1, b'a')` instead. 2. On Linux if you close `slave` before the data is read, an exception may be raised. 3. Use `EnvironmentError` to catch both `IOError`, `OSError` (they are all the same on Python 3). – jfs Mar 08 '15 at 21:57
  • fixed the 1; as for 2, the exception handling is there to catch the exception; and 3, I put them explicitly in the tuple so that one understands that the error is not specifically IOError **or** OSError. – Antti Haapala -- Слава Україні Mar 08 '15 at 22:01
  • do you assert that the exception is *always* raised and all data is always read (assuming no other errors)? I don't understand the point in writing `(IOError, OSError)` instead of `EnvironmentError`. – jfs Mar 08 '15 at 22:10
  • Fixed for the case of getting EOF as an empty string (for me the EOF always results in EIO). – Antti Haapala -- Слава Україні Mar 08 '15 at 22:22
  • Python 2 raises OSError on EOF here. OSError is IOError (literally) on Python 3. What is the point of using both `(IOError, OSError)`? You should probably [reraise non-EIO errors here](http://stackoverflow.com/a/12471855/4279). – jfs Mar 12 '15 at 12:52
1

Many programs automatically turn off colour printing codes when they detect they are not connected directly to a terminal. Many programs will have a flag so you can force colour output. You could add this flag to your process call. For example:

grep "search term" inputfile.txt 
# prints colour to the terminal in most OSes

grep "search term" inputfile.txt | less
# output goes to less rather than terminal, so colour is turned off 

grep "search term" inputfile.txt --color | less
# forces colour output even when not connected to terminal 

Be warned though. The actual colour output is done by the terminal. The terminal interprets special character espace codes and changes the text colour and background color accordingly. Without the terminal to interpret the colour codes you will just see the text in black with these escape codes interspersed throughout.

Dunes
  • 37,291
  • 7
  • 81
  • 97
  • Thanks Dunes, this is on the right track, but it is not quite enough. The app for this command in this case has a configuration file. Users can set the color to never, auto, or always. So I can't force the color myself, I have to try to just get the exact output that the un-piped command would give. If I force the color, then I'll override the users configuration if they happen to have never. – nullified Mar 08 '15 at 09:53
  • @nullified: if you want to emulate `script` command, you could try [`pty.spawn()` or `pexpect`](http://stackoverflow.com/a/25945031/4279) – jfs Mar 08 '15 at 10:13