-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Cariad Eccleston
authored
Sep 19, 2021
1 parent
c75ad68
commit 93f7924
Showing
15 changed files
with
291 additions
and
17 deletions.
There are no files selected for viewing
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,11 @@ | ||
{ | ||
"cSpell.words": [ | ||
"naughtty" | ||
"epilog", | ||
"execvpe", | ||
"fcntl", | ||
"ioctl", | ||
"naughtty", | ||
"struct", | ||
"TIOCSWINSZ" | ||
] | ||
} |
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
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1,5 @@ | ||
# naughtty | ||
# naughtty | ||
|
||
`naughtty` is a Python package and CLI tool for executing shell commands in a pseudo-terminal. | ||
|
||
NOTE: This package will probably only work in GNU/Linux due to reliance on pseudo-terminals. I haven't tested it in any other operating system. |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,5 @@ | ||
from naughtty.version import get_version | ||
from naughtty.naughtty import NaughTTY | ||
|
||
__all__ = ["get_version"] | ||
__all__ = [ | ||
"NaughTTY", | ||
] |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,9 @@ | ||
from naughtty import get_version | ||
|
||
|
||
def cli_entry() -> None: | ||
print(f"naughtty v{get_version()}") | ||
from naughtty.cli import make_response | ||
|
||
print(make_response()) | ||
|
||
|
||
if __name__ == "__main__": | ||
|
||
cli_entry() |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
from argparse import ArgumentParser, Namespace | ||
from typing import List, Optional, Tuple | ||
|
||
from naughtty.constants import DEFAULT_CHARACTER_PIXELS, DEFAULT_TERMINAL_SIZE | ||
from naughtty.naughtty import NaughTTY | ||
from naughtty.version import get_version | ||
|
||
|
||
def make_naughtty(ns: Namespace) -> NaughTTY: | ||
"""Makes a `NaughTTY` instance based on the given command line arguments.""" | ||
|
||
character_pixels: Optional[Tuple[int, int]] = None | ||
|
||
if ns.char: | ||
parts = str(ns.char).split(",") | ||
character_pixels = (int(parts[0]), int(parts[1])) | ||
|
||
return NaughTTY( | ||
columns=int(ns.cols) if ns.cols else None, | ||
command=ns.command, | ||
rows=int(ns.rows) if ns.rows else None, | ||
character_pixels=character_pixels, | ||
) | ||
|
||
|
||
def make_response(cli_args: Optional[List[str]] = None) -> str: | ||
"""Makes a response to the given command line arguments.""" | ||
|
||
parser = ArgumentParser( | ||
# We don't want ArgumentParser to pick up on "--help" in the child | ||
# command's arguments: | ||
add_help=False, | ||
description="Executes a shell command in a pseudo-terminal and prints its output to stdout.", | ||
epilog="For example: naughtty pipenv --help > out.txt", | ||
) | ||
|
||
parser.add_argument("command", help="command", nargs="*") | ||
|
||
parser.add_argument( | ||
"--char", | ||
help=f"character size in pixels (default={DEFAULT_CHARACTER_PIXELS[0]},{DEFAULT_CHARACTER_PIXELS[1]})", | ||
metavar="WIDTH,HEIGHT", | ||
) | ||
|
||
parser.add_argument( | ||
"--cols", | ||
help=f"columns (default=system default or {DEFAULT_TERMINAL_SIZE[0]})", | ||
) | ||
|
||
parser.add_argument("--help", action="store_true", help="print this help") | ||
|
||
parser.add_argument( | ||
"--rows", | ||
help=f"rows (default=system default or {DEFAULT_TERMINAL_SIZE[1]})", | ||
) | ||
|
||
parser.add_argument( | ||
"--version", | ||
action="store_true", | ||
help="print the version", | ||
) | ||
|
||
args = parser.parse_args(cli_args) | ||
|
||
if args.version: | ||
return get_version() | ||
|
||
# If we discover "--help" AND a command then that "--help" is intended for | ||
# the command and not us: | ||
if not args.command or (args.help and not args.command): | ||
return parser.format_help() | ||
|
||
n = make_naughtty(args) | ||
n.execute() | ||
return n.output |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
DEFAULT_CHARACTER_PIXELS = (9, 18) | ||
DEFAULT_TERMINAL_SIZE = (80, 24) |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,94 @@ | ||
from fcntl import ioctl | ||
from os import environ, execvpe, read | ||
from pty import CHILD, fork | ||
from shutil import get_terminal_size | ||
from struct import pack | ||
from termios import TIOCSWINSZ | ||
from typing import List, Optional, Tuple | ||
|
||
from naughtty.constants import DEFAULT_CHARACTER_PIXELS, DEFAULT_TERMINAL_SIZE | ||
|
||
|
||
class NaughTTY: | ||
""" | ||
Executes a shell command in a pseudo-terminal. | ||
Arguments: | ||
command: Command with arguments to execute | ||
character_pixels: Pixel width and height of pseudo-terminal characters | ||
(default=naughtty.constants.DEFAULT_CHARACTER_PIXELS) | ||
columns: Pseudo-terminal column count (default=current or | ||
naughtty.constants.DEFAULT_TERMINAL_COLUMNS) | ||
rows: Pseudo-terminal row count (default=current or | ||
naughtty.constants.DEFAULT_TERMINAL_ROWS) | ||
""" | ||
|
||
def __init__( | ||
self, | ||
command: List[str], | ||
character_pixels: Optional[Tuple[int, int]] = None, | ||
columns: Optional[int] = None, | ||
rows: Optional[int] = None, | ||
) -> None: | ||
|
||
current_terminal_size = get_terminal_size(DEFAULT_TERMINAL_SIZE) | ||
|
||
self._child_output = "" | ||
|
||
self.character_pixels = character_pixels or DEFAULT_CHARACTER_PIXELS | ||
self.command = command | ||
self.terminal_size = ( | ||
columns or current_terminal_size.columns, | ||
rows or current_terminal_size.lines, | ||
) | ||
|
||
def execute(self) -> None: | ||
"""Executes the command in a pseudo-terminal.""" | ||
|
||
pid, fd = fork() | ||
|
||
# We intentionally exclude this `if` from coverage reports because the | ||
# case where `pid == CHILD` only ever occurs in the fork and not _this_ | ||
# process, so the watcher doesn't know it gets touched. | ||
if pid != CHILD: # pragma: no cover | ||
# Packed structure documentation: | ||
# https://www.delorie.com/djgpp/doc/libc/libc_495.html | ||
child_terminal_size = pack( | ||
"HHHH", | ||
self.terminal_size[1], | ||
self.terminal_size[0], | ||
self.terminal_size[0] * self.character_pixels[0], | ||
self.terminal_size[1] * self.character_pixels[1], | ||
) | ||
# TIOCSWINSZ = Terminal Input/Output Control Set WINdow SiZe | ||
ioctl(fd, TIOCSWINSZ, child_terminal_size) | ||
|
||
if pid == CHILD: | ||
# The first argument is the name, not the executable. | ||
# | ||
# `execvpe` will terminate the child when its process is complete. | ||
# | ||
# Also note that we intentionally exclude this `if` branch from | ||
# coverage reports because it only ever runs in the fork and not | ||
# _this_ process, so the watcher doesn't know it gets touched. | ||
execvpe(self.command[0], self.command, environ) # pragma: no cover | ||
|
||
child_output = bytes() | ||
|
||
try: | ||
while True: | ||
child_output += read(fd, 1024) | ||
except OSError: | ||
# The child has terminated and there's nothing more to read. | ||
pass | ||
|
||
self._child_output = child_output.decode("UTF-8") | ||
|
||
@property | ||
def output(self) -> str: | ||
"""Gets the execution's output.""" | ||
|
||
return self._child_output |
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
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
from shutil import get_terminal_size | ||
|
||
from ansiscape import green, sequence, yellow | ||
|
||
print(green("Hello, world!")) | ||
|
||
ts = get_terminal_size((-1, -1)) | ||
print(sequence("Terminal width: ", yellow(str(ts[0])))) | ||
print(sequence("Terminal height: ", yellow(str(ts[1])))) |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
from argparse import Namespace | ||
|
||
from naughtty.cli import make_naughtty, make_response | ||
|
||
|
||
def test_make_naughtty__custom() -> None: | ||
naughtty = make_naughtty( | ||
Namespace( | ||
char="5,6", | ||
cols="3", | ||
command=["python", "tests/out-color.py"], | ||
rows="4", | ||
) | ||
) | ||
|
||
assert naughtty.character_pixels == (5, 6) | ||
assert naughtty.command == ["python", "tests/out-color.py"] | ||
assert naughtty.terminal_size == (3, 4) | ||
|
||
|
||
def test_make_naughtty__defaults() -> None: | ||
naughtty = make_naughtty( | ||
Namespace( | ||
char=None, | ||
cols=None, | ||
command=["python", "tests/out-color.py"], | ||
rows=None, | ||
) | ||
) | ||
|
||
assert naughtty.character_pixels == (9, 18) | ||
assert naughtty.command == ["python", "tests/out-color.py"] | ||
assert naughtty.terminal_size == (80, 24) | ||
|
||
|
||
def test_make_response__for_execution() -> None: | ||
response = make_response(["python", "tests/out-color.py"]) | ||
|
||
assert ( | ||
response | ||
== """\x1b[32mHello, world!\x1b[39m\r | ||
Terminal width: \x1b[33m80\x1b[39m\r | ||
Terminal height: \x1b[33m24\x1b[39m\r | ||
""" | ||
) | ||
|
||
|
||
def test_make_response__for_execution__with_help() -> None: | ||
response = make_response(["pipenv", "--help"]) | ||
assert "naughtty" not in response | ||
|
||
|
||
def test_make_response__for_help() -> None: | ||
response = make_response(["--help"]) | ||
assert response.startswith("usage: ") | ||
assert response.endswith("naughtty pipenv --help > out.txt\n") | ||
assert "naughtty" in response | ||
|
||
|
||
def test_make_response__for_version() -> None: | ||
assert make_response(["--version"]) == "-1.-1.-1" |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
from naughtty import NaughTTY | ||
|
||
|
||
def test_naughtty() -> None: | ||
n = NaughTTY(["python", "tests/out-color.py"]) | ||
n.execute() | ||
assert ( | ||
n.output | ||
== """\x1b[32mHello, world!\x1b[39m\r | ||
Terminal width: \x1b[33m80\x1b[39m\r | ||
Terminal height: \x1b[33m24\x1b[39m\r | ||
""" | ||
) |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
from naughtty import get_version | ||
from naughtty.version import get_version | ||
|
||
|
||
def test_get_version() -> None: | ||
|