As of 2016-02-26, there will be no more posts for this blog. s/blog/pba/
Showing posts with label stdin. Show all posts

I was writing a small Python script and wanted it to handle three types of inputs:

  1. Standard input by user inputing from keyboard
  2. Standard input by pipeline
  3. File input

I wanted to make my script can do as follow:

% program # Sometimes, implying using standard input but depends on the purpose of program
% program - # Indicates using standard input
% program filename
% prog2 | program # Pipe prog2s standard output to programs standard input

We should all have no problem with file, you just check the argument to see if we need to open a file as input.

The first two input types are not hard to do, we only need to use file.isatty() to identify current state of standard input:

file.isatty()
Return True if the file is connected to a tty(-like) device, else False.

We can decide based on the following:

sys.stdin.isatty()

I spent few hours trying to find the solution for them, I did find one, but it's not portable and not good for me.


The final code is

#!/usr/bin/env python


import os
import select
import signal
import sys
import termios
import time
import tty


def ttywidth():

#f = os.popen('stty size', 'r')
# Should return 'HHH WWW'
#width = int(f.read().split(' ')[1])
f = os.popen('tput cols', 'r')
width = int(f.read())
f.close()
return width


def getch():

return sys.stdin.read(1)


def update_width(signum, frame):

global width

width = ttywidth()
sys.stdout.write(str(width) + '\r\n')

width = ttywidth()

# Use signal to be ackknowledged of window change event
signal.signal(signal.SIGWINCH, update_width)

# Get stdin file descriptor
fd = sys.stdin.fileno()
# Backup, important!
old_settings = termios.tcgetattr(fd)
tty.setraw(sys.stdin.fileno())

p = select.poll()
# Register for data-in
p.register(sys.stdin, select.POLLIN)

while True:
# If do not need the width of terminal, then this catch might not be
# necessary.
try:
# Wait for 1ms, if still not char in, then return.
if p.poll(1):
ch = getch()
if ch == "\x03":
# Ctrl+C
break
if ch == "\x0d":
# Entry key
sys.stdout.write("\033[97;101m" + " " * width + "\r\n\033[39;49m\r\n")
break
except select.error:
# Conflict with signal
# select.error: (4, 'Interrupted system call') on p.poll(1)
pass
# Must restore the setting, or stdin still not echo after exit this program.
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)

For first part of problem, terminal's width (columns), it calls external program, which is what I don't like. There also is a way to use environment variable COLUMNS, but it's not a good way because most of people would not export COLUMNS.

The second part of problem, non-blocking stdin without echoing, it uses select.poll to do non-blocking and tty to do no echoing. In addition, it uses termios to backup and to restore terminal attributes, which is very important. If it doesn't do, then the stdin of shell will be still not echoing after program exits.

Unless you are in console, or you would change terminal's geometry sometimes, therefore signal comes to catch the event SIGWINCH. That will slightly affect select.poll, but should be no harm, you can see the try except for that.

One more thing to note, after setraw(), it needs to use "\r\n"("\x0d\x0a") not just "\n" for a newline (this I don't know the reason).

All I want from the code is when user hits enter, then it prints out a horizontal bar with same width of terminal as seperator.

This code is put in Public Domain. If you know of a portable code, and it does rely on additional library, please share with me. Please also comment on the code, explain some things that I didn't mention above, those must be the things that I didn't know. It should be able to merge the code inside main loop into getch(), make it return None when no data available, I leave you that to finish.

Updated on 5/26: If you don't want to show cursor, you can do:
# Hide cursor
sys.stdout.write('\033[?25l')
# Show cursor
sys.stdout.write('\033[?25h')
If you don't like change original code for "\r\n", you can do
class STDOUT_R:

@staticmethod
def write(s):

s = s.replace('\n', '\r\n')
sys.__stdout__.write(s.encode('utf-8'))

@staticmethod
def flush():

return sys.__stdout__.flush()


class STDERR_R:

@staticmethod
def write(s):

s = s.replace('\n', '\r\n')
sys.__stderr__.write(s.encode('utf-8'))

@staticmethod
def flush():

return sys.__stderr__.flush()