Skip to content

Instantly share code, notes, and snippets.

@pirate
Last active February 19, 2021 00:10
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pirate/4cd513df511dd1f1dfba90f8df14479d to your computer and use it in GitHub Desktop.
Save pirate/4cd513df511dd1f1dfba90f8df14479d to your computer and use it in GitHub Desktop.
Get all the information you could ever want about stdin, stdout, and stderr file descriptors in Python (e.g. is it a TTY, terminal, pipe, redirection, etc.)
#!/usr/bin/env python3
# Get all the information you could ever want about the STDIN, STDOUT, STDERR file descriptors (e.g. is it a TTY, terminal, pipe, redirection, etc.)
# Works cross-platform on Windows, macOS, Linux, in Docker, and in Docker-Compose by using stat library
#
# Useful for detecting and handling different stdin/stdout redirect scenarios in CLI scripts,
# e.g. is the user piping a file in or are they interactively typing things in?
# is the process output being saved to a file or being printed to a terminal?
# can we ask the user for input or is it a non-interactive masquerading as a TTY?
#
# Further reading:
# - https://stackoverflow.com/questions/13442574/how-do-i-determine-if-sys-stdin-is-redirected-from-a-file-vs-piped-from-another
# - https://stackoverflow.com/questions/33871836/find-out-if-there-is-input-from-a-pipe-or-not-in-python
# - https://stackoverflow.com/questions/30137135/confused-about-docker-t-option-to-allocate-a-pseudo-tty
import os
import sys
import stat
def log_dict_summary(obj: dict):
sys.stderr.write(' '.join(f'{key}={str(val).ljust(6)}' for key, val in obj.items()) + '\n')
def get_fd_info(fd):
FILENO = fd.fileno()
MODE = os.fstat(FILENO).st_mode
IS_PIPE, IS_TERMINAL = stat.S_ISFIFO(MODE), stat.S_ISREG(MODE)
return {
'NAME': fd.name[1:-1],
'FILENO': FILENO,
'MODE': MODE,
'IS_PIPE': IS_PIPE,
'IS_FILE': IS_TERMINAL,
'IS_TERMINAL': not (IS_PIPE or IS_FILE),
'IS_TTY': hasattr(fd, 'isatty') and fd.isatty(),
'IS_LINE_BUFFERED': fd.line_buffering,
'IS_READABLE': fd.readable(),
}
sys.stdout.write('[>&1] this is python stdout\n')
sys.stderr.write('[>&2] this is python stderr\n')
log_dict_summary(get_fd_info(sys.stdin))
log_dict_summary(get_fd_info(sys.stdout))
log_dict_summary(get_fd_info(sys.stderr))
### BASH STDIN/STDOUT/STDERR BEHAVIOR (as tested on macOS in bash)
### BASH w/ stdin
# python3 test_stdin_stdout_stderr.py
# NAME=stdin FILENO=0 IS_TTY=True IS_PIPE=False IS_FILE=False IS_TERMINAL=True IS_LINE_BUFFERED=True IS_READABLE=True
# NAME=stdout FILENO=1 IS_TTY=True IS_PIPE=False IS_FILE=False IS_TERMINAL=True IS_LINE_BUFFERED=True IS_READABLE=False
# NAME=stderr FILENO=2 IS_TTY=True IS_PIPE=False IS_FILE=False IS_TERMINAL=True IS_LINE_BUFFERED=True IS_READABLE=False
# echo test | python3 test_stdin_stdout_stderr.py
# NAME=stdin FILENO=0 IS_TTY=False IS_PIPE=True IS_FILE=False IS_TERMINAL=False IS_LINE_BUFFERED=False IS_READABLE=True
# NAME=stdout FILENO=1 IS_TTY=True IS_PIPE=False IS_FILE=False IS_TERMINAL=True IS_LINE_BUFFERED=True IS_READABLE=False
# NAME=stderr FILENO=2 IS_TTY=True IS_PIPE=False IS_FILE=False IS_TERMINAL=True IS_LINE_BUFFERED=True IS_READABLE=False
# python3 test_stdin_stdout_stderr.py < testin
# NAME=stdin FILENO=0 IS_TTY=False IS_PIPE=False IS_FILE=True IS_TERMINAL=False IS_LINE_BUFFERED=False IS_READABLE=True
# NAME=stdout FILENO=1 IS_TTY=True IS_PIPE=False IS_FILE=False IS_TERMINAL=True IS_LINE_BUFFERED=True IS_READABLE=False
# NAME=stderr FILENO=2 IS_TTY=True IS_PIPE=False IS_FILE=False IS_TERMINAL=True IS_LINE_BUFFERED=True IS_READABLE=False
### BASH w/ stdout
# python3 test_stdin_stdout_stderr.py > testout
# NAME=stdin FILENO=0 IS_TTY=True IS_PIPE=False IS_FILE=False IS_TERMINAL=True IS_LINE_BUFFERED=True IS_READABLE=True
# NAME=stdout FILENO=1 IS_TTY=False IS_PIPE=False IS_FILE=True IS_TERMINAL=False IS_LINE_BUFFERED=False IS_READABLE=False
# NAME=stderr FILENO=2 IS_TTY=True IS_PIPE=False IS_FILE=False IS_TERMINAL=True IS_LINE_BUFFERED=True IS_READABLE=False
# python3 test_stdin_stdout_stderr.py | cat
# NAME=stdin FILENO=0 IS_TTY=True IS_PIPE=False IS_FILE=False IS_TERMINAL=True IS_LINE_BUFFERED=True IS_READABLE=True
# NAME=stdout FILENO=1 IS_TTY=False IS_PIPE=True IS_FILE=False IS_TERMINAL=False IS_LINE_BUFFERED=False IS_READABLE=False
# NAME=stderr FILENO=2 IS_TTY=True IS_PIPE=False IS_FILE=False IS_TERMINAL=True IS_LINE_BUFFERED=True IS_READABLE=False
#### DOCKER-COMPOSE STDIN/STDOUT/STDERR BEHAVIOR (as tested on macOS in bash)
### TTY w/ stdin
# docker-compose run test (X docker sends stderr to terminal stdout)
# NAME=stdin FILENO=0 IS_TTY=True IS_PIPE=False IS_FILE=False IS_TERMINAL=True IS_LINE_BUFFERED=False IS_READABLE=True
# NAME=stdout FILENO=1 IS_TTY=True IS_PIPE=False IS_FILE=False IS_TERMINAL=True IS_LINE_BUFFERED=False IS_READABLE=False
# NAME=stderr FILENO=2 IS_TTY=True IS_PIPE=False IS_FILE=False IS_TERMINAL=True IS_LINE_BUFFERED=False IS_READABLE=False
# docker-compose run test < testin (√ docker sends stderr to terminal stderr!)
# NAME=stdin FILENO=0 IS_TTY=False IS_PIPE=True IS_FILE=False IS_TERMINAL=False IS_LINE_BUFFERED=False IS_READABLE=True
# NAME=stdout FILENO=1 IS_TTY=False IS_PIPE=True IS_FILE=False IS_TERMINAL=False IS_LINE_BUFFERED=False IS_READABLE=False
# NAME=stderr FILENO=2 IS_TTY=False IS_PIPE=True IS_FILE=False IS_TERMINAL=False IS_LINE_BUFFERED=False IS_READABLE=False
# echo test | docker-compose run test (√ docker sends stderr to terminal stderr!)
# NAME=stdin FILENO=0 IS_TTY=False IS_PIPE=True IS_FILE=False IS_TERMINAL=False IS_LINE_BUFFERED=False IS_READABLE=True
# NAME=stdout FILENO=1 IS_TTY=False IS_PIPE=True IS_FILE=False IS_TERMINAL=False IS_LINE_BUFFERED=False IS_READABLE=False
# NAME=stderr FILENO=2 IS_TTY=False IS_PIPE=True IS_FILE=False IS_TERMINAL=False IS_LINE_BUFFERED=False IS_READABLE=False
### TTY w/ stdout
# docker-compose run test > testout (X docker sends stderr to file as well)
# NAME=stdin FILENO=0 IS_TTY=True IS_PIPE=False IS_FILE=False IS_TERMINAL=True IS_LINE_BUFFERED=False IS_READABLE=True
# NAME=stdout FILENO=1 IS_TTY=True IS_PIPE=False IS_FILE=False IS_TERMINAL=True IS_LINE_BUFFERED=False IS_READABLE=False
# NAME=stderr FILENO=2 IS_TTY=True IS_PIPE=False IS_FILE=False IS_TERMINAL=True IS_LINE_BUFFERED=False IS_READABLE=False
# docker-compose run test | cat (X docker sends stderr to pipe as well)
# NAME=stdin FILENO=0 IS_TTY=True IS_PIPE=False IS_FILE=False IS_TERMINAL=True IS_LINE_BUFFERED=False IS_READABLE=True
# NAME=stdout FILENO=1 IS_TTY=True IS_PIPE=False IS_FILE=False IS_TERMINAL=True IS_LINE_BUFFERED=False IS_READABLE=False
# NAME=stderr FILENO=2 IS_TTY=True IS_PIPE=False IS_FILE=False IS_TERMINAL=True IS_LINE_BUFFERED=False IS_READABLE=False
### NON-TTY w/ stdin
# docker-compose run -T test (√ docker sends stderr to terminal stderr)
# NAME=stdin FILENO=0 IS_TTY=False IS_PIPE=True IS_FILE=False IS_TERMINAL=False IS_LINE_BUFFERED=False IS_READABLE=True
# NAME=stdout FILENO=1 IS_TTY=False IS_PIPE=True IS_FILE=False IS_TERMINAL=False IS_LINE_BUFFERED=False IS_READABLE=False
# NAME=stderr FILENO=2 IS_TTY=False IS_PIPE=True IS_FILE=False IS_TERMINAL=False IS_LINE_BUFFERED=False IS_READABLE=False
# docker-compose run -T test < testin (√ docker sends stderr to terminal stderr)
# NAME=stdin FILENO=0 IS_TTY=False IS_PIPE=True IS_FILE=False IS_TERMINAL=False IS_LINE_BUFFERED=False IS_READABLE=True
# NAME=stdout FILENO=1 IS_TTY=False IS_PIPE=True IS_FILE=False IS_TERMINAL=False IS_LINE_BUFFERED=False IS_READABLE=False
# NAME=stderr FILENO=2 IS_TTY=False IS_PIPE=True IS_FILE=False IS_TERMINAL=False IS_LINE_BUFFERED=False IS_READABLE=False
# echo test | docker-compose run -T test (√ docker sends stderr to terminal stderr)
# NAME=stdin FILENO=0 IS_TTY=False IS_PIPE=True IS_FILE=False IS_TERMINAL=False IS_LINE_BUFFERED=False IS_READABLE=True
# NAME=stdout FILENO=1 IS_TTY=False IS_PIPE=True IS_FILE=False IS_TERMINAL=False IS_LINE_BUFFERED=False IS_READABLE=False
# NAME=stderr FILENO=2 IS_TTY=False IS_PIPE=True IS_FILE=False IS_TERMINAL=False IS_LINE_BUFFERED=False IS_READABLE=False
### NON-TTY w/ stdout
# docker-compose run -T test > testout (√ docker sends stderr to terminal stderr)
# NAME=stdin FILENO=0 IS_TTY=False IS_PIPE=True IS_FILE=False IS_TERMINAL=False IS_LINE_BUFFERED=False IS_READABLE=True
# NAME=stdout FILENO=1 IS_TTY=False IS_PIPE=True IS_FILE=False IS_TERMINAL=False IS_LINE_BUFFERED=False IS_READABLE=False
# NAME=stderr FILENO=2 IS_TTY=False IS_PIPE=True IS_FILE=False IS_TERMINAL=False IS_LINE_BUFFERED=False IS_READABLE=False
#### DOCKER STDIN/STDOUT/STDERR BEHAVIOR (tested on macOS in bash)
### TTY w/ stdin
# docker run -it test (X docker sends stderr to terminal stdout)
# NAME=stdin FILENO=0 IS_TTY=True IS_PIPE=False IS_FILE=False IS_TERMINAL=True IS_LINE_BUFFERED=False IS_READABLE=True
# NAME=stdout FILENO=1 IS_TTY=True IS_PIPE=False IS_FILE=False IS_TERMINAL=True IS_LINE_BUFFERED=False IS_READABLE=False
# NAME=stderr FILENO=2 IS_TTY=True IS_PIPE=False IS_FILE=False IS_TERMINAL=True IS_LINE_BUFFERED=False IS_READABLE=False
# docker run -it test < testin (X stdin with TTY not allowed)
# echo test | docker run -it test (X stdin with TTY not allowed)
### TTY w/ stdout
# docker run -it test > testout (X docker sends stderr to file as well)
#
# (same behavior as docker-compose...)
#
# docker run -it test | cat (X docker sends stderr to pipe as well)
#
# (same behavior as docker-compose...)
#
### NON-TTY w/ no stdin
# docker run test (X stdin is ignored, stderr goes to stderr)
# NAME=stdin FILENO=0 IS_TTY=False IS_PIPE=False IS_FILE=False IS_TERMINAL=True IS_LINE_BUFFERED=False IS_READABLE=True
# NAME=stdout FILENO=1 IS_TTY=False IS_PIPE=True IS_FILE=False IS_TERMINAL=False IS_LINE_BUFFERED=False IS_READABLE=False
# NAME=stderr FILENO=2 IS_TTY=False IS_PIPE=True IS_FILE=False IS_TERMINAL=False IS_LINE_BUFFERED=False IS_READABLE=False
# docker run test < testin (X stdin is ignored)
#
#
#
# echo test | docker run test (X stdin is ignored)
#
#
#
### NON-TTY w/ stdin
# docker run test (√ stdin->stdin, stdout->stdout, stderr->stderr)
# NAME=stdin FILENO=0 IS_TTY=False IS_PIPE=False IS_FILE=False IS_TERMINAL=True IS_LINE_BUFFERED=False IS_READABLE=True
# NAME=stdout FILENO=1 IS_TTY=False IS_PIPE=True IS_FILE=False IS_TERMINAL=False IS_LINE_BUFFERED=False IS_READABLE=False
# NAME=stderr FILENO=2 IS_TTY=False IS_PIPE=True IS_FILE=False IS_TERMINAL=False IS_LINE_BUFFERED=False IS_READABLE=False
# docker run -i test < testin (√ file->stdin, stdout->stdout, stderr->stderr)
#
# (same behavior as docker-compose...)
#
# echo test | docker run -i test (√ pipe->stdin, stdout->stdout, stderr->stderr)
#
# (same behavior as docker-compose...)
#
### NON-TTY w/ stdout
# docker run test > testout (√ null->stdin, stdout->file, stderr->stderr)
#
# (same behavior as docker-compose...)
#
# TODO: test behavior over SSH with -T and without
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment