Terminal Programming with Python series 1: Automation and pty(4)

Introduction

Any command-line UNIX interface may be automated.

This article will demonstrate the use of pseudo-terminals, which cause programs to believe they are attached to a terminal, even when they are not!

At first, fooling programs into beleiving they are attached to a terminal may not seem useful, but it is used in a wide variety of software solutions. This programming technique is indespensible in automation and testing fields.

The case of color ls(1) The command ls -G displays files with colors on OSX and FreeBSD only when standard input is attached to a terminal. When using the subprocess module, we will not see any of these qualities: import subprocess print(subprocess.check_output(['ls', '-G', '/dev'])) With an explicit -G parameter, the output of this program is still colorless. This quick example shows that some programs behave differently when attached to a terminal.

Interactive Furthermore, some programs are only interactive when attached to a terminal. The python executable is an example of this. When we run python directly from a terminal, we receive an interactive REPL: $ python Python 3.5.0 (default, Oct 28 2015, 21:00:27) [GCC 4.2.1 Compatible Apple LLVM 7.0.0 (clang-700.1.76)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> print(4+4) 8 >>> exit() If we run these commands by piping them to standard input, it will not display such decorators, demonstrated here using the standard shell: $ printf 'print(2+2)

exit()' | python 4 And strangely enough, executing Python from Python, using the subprocess module demonstrates the same output: import subprocess, sys python = subprocess.Popen( sys.executable, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE) print(python.communicate(input=b"print(2+2)

exit()")) (b'4

', b'') With a keyboard attached, a terminal may be expected to provide input at any non-determinate future time. Programs such as python test whether any of the standard file descriptors (stdin, stdout, stderr) are attached to a terminal to conditionally offer this behaviour. We can reproduce this conditional check of isatty(3) easily from shell: $ python -c 'import sys,os;print(os.isatty(sys.stdin.fileno()))' True $ echo | python -c 'import sys,os;print(os.isatty(sys.stdin.fileno()))' False As stdin is piped, this fails the test for isatty(3) test.