Last active
February 19, 2021 00:10
-
-
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.)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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