このBlogは移転しました。今後は aish.dev を御覧ください。

Pythonで学ぶ「詳解 UNIXプログラミング」(その11) 第11章 端末入出力

11.1 割り込み文字を無効にし、ファイルの終わりの文字を変更する

import os, sys, termios

if not os.isatty(0):
    sys.exit("standard input is not a terminal device")

vdisable = os.fpathconf(sys.stdin.fileno(), "PC_VDISABLE")
if vdisable < 0:
    sys.exit("_POSIX_VDISABLE not in effect")

term = termios.tcgetattr(sys.stdin.fileno())    # fetch tty state
cc = term[6]
cc[termios.VINTR] = vdisable    # disable INTR character
cc[termios.VEOF] = 2            # EOF is Control-B

termios.tcsetattr(sys.stdin.fileno(), termios.TCSAFLUSH, term)

11.2 tcgetattrの例

import os, sys, termios

term = termios.tcgetattr(sys.stdin.fileno())
size = term[2] & termios.CSIZE

if size == termios.CS5: print "5 bits/byte"
elif size == termios.CS6: print "6 bits/byte"
elif size == termios.CS7: print "7 bits/byte"
elif size == termios.CS8: print "8 bits/byte"
else: print "unknown bits/byte"

term[2] &= ~termios.CSIZE    # zero out the bits
term[2] |= termios.CS8       # set 8 bits/byte

termios.tcsetattr(sys.stdin.fileno(), termios.TCSANOW, term)

11.3 POSIX.1のctermidの実装

_ctermid_name = ''

def ctermid():
    return "/dev/tty"

11.4 POSIX.1のisattyの実装

import termios, sys
def isatty(fd):
    try:
        termios.tcgetattr(fd)
    except termios.error:
        return False
    else:
        return True    # return True if no error

11.5 isatty関数のテスト

print "fd 0:", "tty" if isatty(0) else "not a tty"
print "fd 1:", "tty" if isatty(1) else "not a tty"
print "fd 2:", "tty" if isatty(2) else "not a tty"

11.6 POSIX.1のttyname関数の実装

import termios, sys, os, stat

DEV = "/dev"     # device directory
def ttyname(fd): 
    if not os.isatty(fd):
        return
    fdstat = os.fstat(fd)
    if not stat.S_ISCHR(fdstat.st_mode):
        return

    for fname in os.listdir(DEV):
        pathname = os.path.join(DEV, fname)
        devstat = os.stat(pathname)
        if fdstat.st_ino == devstat.st_ino and fdstat.st_dev == devstat.st_dev:
            # found a match
            return pathname

11.7 ttyname関数のテスト

print "fd 0:", ttyname(0) if isatty(0) else "not a tty"
print "fd 1:", ttyname(1) if isatty(1) else "not a tty"
print "fd 2:", ttyname(2) if isatty(2) else "not a tty"

このttynameの実装はLinuxでは当てはまらないらしく、サンプルを実行すると

fd 0: /dev/stderr
fd 1: /dev/stderr
fd 2: /dev/stderr

となってしまう。本来は

>>> os.ttyname(0)
'/dev/pts/1'

が正しい。

11.8 getpass関数の実装

import sys, os, signal, termios, copy
from ctypes import *
from posixsignal import *

def getpass(prompt):
    fp = open(os.ctermid(), "r+", 0)

    sig = sigset_t()
    sigsave = sigset_t()

    # block SIGINT & SIGTSTP, save signal mask
    sigemptyset(byref(sig))
    sigaddset(byref(sig), signal.SIGINT)
    sigaddset(byref(sig), signal.SIGSTOP)
    sigprocmask(SIG_BLOCK, byref(sig), byref(sigsave))

    term = termios.tcgetattr(fp.fileno())    # save tty state
    termsave = copy.deepcopy(term)           # structure copy

    term[3] &= ~(termios.ECHO | termios.ECHOE | termios.ECHOK | termios.ECHONL)
    termios.tcsetattr(fp.fileno(), termios.TCSAFLUSH, term)

    sys.stdout.write(prompt)
    
    passwd = sys.stdin.readline().rstrip("\n")
    
    # restore tty state
    termios.tcsetattr(fp.fileno(), termios.TCSAFLUSH, termsave)
    # restore signal mask
    sigprocmask(SIG_BLOCK, byref(sigsave), None)
    # done with /dev/tty
    fp.close()
    
    return passwd

このサンプルでは、10章で作成したposixsignalを使用している。

11.9 getpass関数を呼ぶ

passwd = getpasswd("Enter password:")
passwdlen = len(passwd)

# now we use password

del passwd
trash = "\0"+passwdlen

パスワードは用が済んだら'\0'で上書きするべき、と書かれているが、Pythonの文字列は変更不能なので、上書きはできない。ここでは、気休めに明示的なdelでパスワードを削除し、その直後にパスワードと同じ長さの文字列オブジェクトを作成してみた。こうすることで、パスワードで使用したメモリ領域と同じ場所に'\0'が書き込まれることが期待できる…かもしれない。上手く行く保証はないので、あくまで気休めだ。

11.10 端末モードをローまたはcbreakに設定

import copy, termios

save_termios = None
ttysavefd = -1
RESET, RAW, CBREAK = range(0, 3)
ttystate = RESET

def tty_cbreak(fd):    # put terminal into a cbreak mode
    global save_termios, ttystate, ttysavefd
    
    save_termios = termios.tcgetattr(fd)
    
    buf = copy.deepcopy(save_termios)    # structure copy
    buf[3] &= ~(termios.ECHO|termios.ICANON) # echo off, canonical mode off
    buf[6][termios.VMIN] = 1    Case B: 1 byte at a time, no timer
    buf[6][termios.VTIME] = 0
    
    termios.tcsetattr(fd, termios.TCSAFLUSH, buf)
    
    ttystate = CBREAK
    ttysavefd = fd

def tty_raw(fd):    # put terminal into a raw mode
    global save_termios, ttystate, ttysavefd
    
    save_termios = termios.tcgetattr(fd)
    
    buf = copy.deepcopy(save_termios)
    # echo off, canonical mode off, extended input processing off, signal chars off
    buf[3] &= ~(termios.ECHO|termios.ICANON|termios.IEXTEN|termios.ISIG)

    # no SIGINTon BREAK, CR-to NL off, input parity check off, don't strip 8th bit on input,
    # output flow control off
    buf[0] &= ~(termios.BRKINT|termios.ICRNL|termios.INPCK|termios.ISTRIP|termios.IXON)

    # clear size bits, parity checking off
    buf[3] &= ~(termios.CSIZE|termios.PARENB)

    # set 8 bits/char
    buf[3] |= termios.CS8

    # output processing off
    buf[1] &= ~(termios.OPOST)

    # Case B: 1 byte at a time, no timer
    buf[6][termios.VMIN] = 1
    buf[6][termios.VTIME] = 0
    
    termios.tcsetattr(fd, termios.TCSAFLUSH, buf)
    
    ttystate = RAW
    ttysavefd = fd

def tty_reset(fd):    # restore terminal's mode
    global ttystate
    if ttystate in (CBREAK, RAW):
        termios.tcsetattr(fd, termios.TCSAFLUSH, save_termios)
        ttystate = RESET

def tty_exit():      # can be set up by atexit(tty_exit)
    if ttysavefd >= 0:
        tty_reset(ttysavefd)

def tty_termios():    # let caller see original tty state
    return save_termios

11.11 ローモードとcbreakモードのテスト

import signal, sys, os

def sig_catch(signo, frame):
    print "signal caught"
    tty_reset(sys.stdin.fileno())
    sys.exit(0)

# catch signals
signal.signal(signal.SIGINT, sig_catch)
signal.signal(signal.SIGQUIT, sig_catch)
signal.signal(signal.SIGTERM, sig_catch)

fileno = sys.stdin.fileno() 
tty_raw(fileno)
print "Enter raw mode characters, terminate with DELETE"
while True:
    c = os.read(fileno, 1)
    if ord(c) == 0177:
        break
    print hex(ord(c))

tty_reset(fileno)

tty_cbreak(fileno)
print "Enter cbreak mode characters, terminate with SIGINT"
while True:
    c = os.read(fileno, 1)
    print hex(ord(c))

tty_reset(fileno)

11.12 ウィンドウサイズを表示する

import sys, os, signal, fcntl, termios, tty, struct

def pr_winsize(fd):
    ret = fcntl.ioctl(fd, tty.TIOCGWINSZ, struct.pack('hhhh', 0, 0, 0, 0))
    height, width = struct.unpack('hhhh', ret)[:2]
    print "{0} rows, {1} columns".format(height, width)

def sig_winch(signo, frame):
    print "SIGWINCH received"
    pr_winsize(fileno)
    
fileno = sys.stdin.fileno()
if not os.isatty(fileno):
    sys.exit(1)

pr_winsize(fileno)    # print initial size
signal.signal(signal.SIGWINCH, sig_winch)

# sleep forever
while True:
    signal.pause()