The Hitchhiker's The Hitchhiker's Guide to CLIs in Guide to CLIs - - PowerPoint PPT Presentation

the hitchhiker s the hitchhiker s guide to clis in guide
SMART_READER_LITE
LIVE PREVIEW

The Hitchhiker's The Hitchhiker's Guide to CLIs in Guide to CLIs - - PowerPoint PPT Presentation

The Hitchhiker's The Hitchhiker's Guide to CLIs in Guide to CLIs in Python Python Vinayak Mehta @vortex_ape $ whoami $ whoami https://github.com/vinayak-mehta https://github.com/camelot-dev https://www.recurse.com In the beginning ...


slide-1
SLIDE 1

The Hitchhiker's The Hitchhiker's Guide to CLIs in Guide to CLIs in Python Python

Vinayak Mehta @vortex_ape

slide-2
SLIDE 2

$ whoami $ whoami

https://github.com/vinayak-mehta

slide-3
SLIDE 3

https://github.com/camelot-dev

slide-4
SLIDE 4

https://www.recurse.com

slide-5
SLIDE 5

In the beginning ... In the beginning ...

slide-6
SLIDE 6
slide-7
SLIDE 7
slide-8
SLIDE 8
slide-9
SLIDE 9

https://www.youtube.com/watch?v=n-eFFd5BmpU

slide-10
SLIDE 10
slide-11
SLIDE 11
slide-12
SLIDE 12
slide-13
SLIDE 13
slide-14
SLIDE 14
slide-15
SLIDE 15
slide-16
SLIDE 16

teletype teletype

slide-17
SLIDE 17

(t)ele(ty)pe (t)ele(ty)pe

slide-18
SLIDE 18

tty tty

slide-19
SLIDE 19

shell shell

slide-20
SLIDE 20

keyboard \ \ input \ (terminal)- - - - - - - - - -(process) / / output / display

slide-21
SLIDE 21

keyboard \ \ input \ (terminal)- - -(termios)- - -(process) / / output / display

slide-22
SLIDE 22

$ man termios

slide-23
SLIDE 23

$ stty -a speed 38400 baud; rows 34; columns 166; line = 0; intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D;

  • ignbrk brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr
  • post -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 c

isig icanon iexten echo echoe echok -echonl -noflsh -xcase -

slide-24
SLIDE 24

$ man termios ... ICANON Enable canonical mode (described below). ...

slide-25
SLIDE 25

$ stty -icanon

slide-26
SLIDE 26
slide-27
SLIDE 27

$ man termios ... ONLCR (XSI) Map NL to CR-NL on output. ...

slide-28
SLIDE 28
slide-29
SLIDE 29
slide-30
SLIDE 30

$ stty -onlcr

slide-31
SLIDE 31
slide-32
SLIDE 32

$ man termios ... ECHO Echo input characters. ...

slide-33
SLIDE 33

$ stty -echo

slide-34
SLIDE 34
slide-35
SLIDE 35

$ reset

slide-36
SLIDE 36

import termios

slide-37
SLIDE 37

Signals Signals

slide-38
SLIDE 38

In-band signaling In-band signaling

slide-39
SLIDE 39

Control characters Control characters

slide-40
SLIDE 40

Control characters Control characters

^H backspace ^J newline ^C interrupt the running process ^D end text input or exit the shell

slide-41
SLIDE 41

Escape sequences Escape sequences

slide-42
SLIDE 42

Escape sequences Escape sequences

\u001b[2J: clear screen \u001b[1m: make text bold \u001b[31m: make text red \u001b[{n}A: moves cursor up by n

slide-43
SLIDE 43

Streams Streams

slide-44
SLIDE 44

stdin stdin

slide-45
SLIDE 45

stdout and stderr stdout and stderr

slide-46
SLIDE 46

Redirection Redirection

slide-47
SLIDE 47

$ echo "hello" > file $ echo "world" >> file

slide-48
SLIDE 48

$ echo "hello" | cat hello

slide-49
SLIDE 49
slide-50
SLIDE 50

Command-line interfaces Command-line interfaces

slide-51
SLIDE 51

Command-line interfaces Command-line interfaces

Prompt

slide-52
SLIDE 52

Command-line interfaces Command-line interfaces

Prompt command

slide-53
SLIDE 53

Command-line interfaces Command-line interfaces

Prompt command option1 option2

slide-54
SLIDE 54

Command-line interfaces Command-line interfaces

Prompt command option1 option2 argument1 argument2 <Enter>

slide-55
SLIDE 55

Command-line interfaces Command-line interfaces

Prompt command option1 option2 argument1 argument2 <Enter> Output

slide-56
SLIDE 56

Arguments Arguments

slide-57
SLIDE 57

Arguments Arguments

$ cp src dst

slide-58
SLIDE 58

Options Options

slide-59
SLIDE 59

Options Options

$ cp -r src dst

slide-60
SLIDE 60

Help Help

slide-61
SLIDE 61

Help Help

$ cp --help

slide-62
SLIDE 62

Man pages Man pages

$ man termios

slide-63
SLIDE 63

Standards Standards

slide-64
SLIDE 64

POSIX POSIX

slide-65
SLIDE 65

XDG base directory specification XDG base directory specification

slide-66
SLIDE 66

XDG base directory specification XDG base directory specification

$XDG_CONFIG_HOME=$HOME/.config $XDG_DATA_HOME=$HOME/.local/share $XDG_CACHE_HOME=$HOME/.cache

slide-67
SLIDE 67

CLIs in Python CLIs in Python

slide-68
SLIDE 68

smol-pip smol-pip

slide-69
SLIDE 69

$ smol-pip install --upgrade package_name

slide-70
SLIDE 70

Standard library Standard library

slide-71
SLIDE 71

sys sys

slide-72
SLIDE 72

sys.argv sys.argv

slide-73
SLIDE 73

getopt getopt

slide-74
SLIDE 74

import sys help = "Pip Installs Packages." if __name__ == "__main__": arguments = sys.argv

slide-75
SLIDE 75

import sys help = "Pip Installs Packages." if __name__ == "__main__": arguments = sys.argv if arguments[1] in ["-h", "--help"]: print(help)

slide-76
SLIDE 76

import sys help = "Pip Installs Packages." if __name__ == "__main__": arguments = sys.argv if arguments[1] in ["-h", "--help"]: print(help) elif arguments[1] in ["-v", "--version"]: print("0.1.0")

slide-77
SLIDE 77

import sys help = "Pip Installs Packages." if __name__ == "__main__": arguments = sys.argv ... else: print(arguments) # ['smol-pip', 'install', '--upgrade', 'Click'] if arguments[1] == "install": # dispatch to install / upgrade code else: raise ValueError("Unknown subcommand!")

slide-78
SLIDE 78
  • ptparse
  • ptparse
slide-79
SLIDE 79

PEP 389 PEP 389

slide-80
SLIDE 80

argparse argparse

slide-81
SLIDE 81

argparse argparse

  • pf
  • file

+f +rgb /f /file

slide-82
SLIDE 82

argparse argparse

pip install pip freeze pip search

slide-83
SLIDE 83

import argparse parser = argparse.ArgumentParser( description="Pip Installs Packages." )

slide-84
SLIDE 84

import argparse parser = argparse.ArgumentParser( description="Pip Installs Packages." ) parser.add_argument( "-v", "--version", action="version", version="0.1.0" )

slide-85
SLIDE 85

subparsers = parser.add_subparsers(dest="subparser_name") install = subparsers.add_parser("install")

slide-86
SLIDE 86

subparsers = parser.add_subparsers(dest="subparser_name") install = subparsers.add_parser("install") install.add_argument( "-u", "--upgrade", action="store_true", help="Upgrade package to the newest available version.", ) install.add_argument("package_name")

slide-87
SLIDE 87

if __name__ == "__main__": arguments = parser.parse_args() print(arguments) # Namespace(package_name='Click', upgrade=True)

slide-88
SLIDE 88

if __name__ == "__main__": arguments = parser.parse_args() print(arguments) # Namespace(package_name='Click', upgrade=True) if arguments.subparser_name == "install": # dispatch to install / upgrade code else: raise ValueError("Unknown subcommand!")

slide-89
SLIDE 89

$ smol-pip --help usage: smol-pip [-h] [-v] {install} ... Pip Installs Packages. positional arguments: {install}

  • ptional arguments:
  • h, --help show this help message and exit
  • v, --version show program's version number and exit
slide-90
SLIDE 90

Python Package Index Python Package Index

slide-91
SLIDE 91

docopt docopt

slide-92
SLIDE 92

help = """Pip Installs Packages. Usage: smol-pip install PACKAGE_NAME smol-pip install --upgrade PACKAGE_NAME Options:

  • h --help Show this screen.
  • -version Show version.

"""

slide-93
SLIDE 93

from docopt import docopt if __name__ == "__main__": arguments = docopt(help, version="0.1.0") print(arguments) # {'--upgrade': True, # 'PACKAGE_NAME': 'Click', # 'install': True}

slide-94
SLIDE 94

from docopt import docopt if __name__ == "__main__": arguments = docopt(help, version="0.1.0") print(arguments) # {'--upgrade': True, # 'PACKAGE_NAME': 'Click', # 'install': True} if arguments["install"]: # dispatch to install / upgrade code else: raise ValueError("Unknown subcommand!")

slide-95
SLIDE 95
slide-96
SLIDE 96

click click

slide-97
SLIDE 97

import click def cli(*args, **kwargs): """Pip Installs Packages.""" pass

slide-98
SLIDE 98

import click @click.group("pip") def cli(*args, **kwargs): """Pip Installs Packages.""" pass

slide-99
SLIDE 99

import click @click.group("pip") @click.version_option("0.1.0") def cli(*args, **kwargs): """Pip Installs Packages.""" pass

slide-100
SLIDE 100

def install(*args, **kwargs): """Install packages.""" # install / upgrade package_name

slide-101
SLIDE 101

@cli.command("install") def install(*args, **kwargs): """Install packages.""" # install / upgrade package_name

slide-102
SLIDE 102

@cli.command("install") @click.option( "-u", "--upgrade", is_flag=True, help="Upgrade package to the newest available version.", ) def install(*args, **kwargs): """Install packages.""" # install / upgrade package_name

slide-103
SLIDE 103

@cli.command("install") @click.option( "-u", "--upgrade", is_flag=True, help="Upgrade package to the newest available version.", ) @click.argument("package_name") def install(*args, **kwargs): """Install packages.""" # install / upgrade package_name

slide-104
SLIDE 104

if __name__ == "__main__": cli()

slide-105
SLIDE 105

@cli.command("install") @click.option( "-u", "--upgrade", is_flag=True, help="Upgrade package to the newest available version.", ) @click.argument("package_name") def install(*args, **kwargs): """Install packages.""" print(kwargs) # {'upgrade': True, 'package_name': 'Click'} # install / upgrade package_name

slide-106
SLIDE 106

$ smol-pip --help Usage: smol-pip [OPTIONS] COMMAND [ARGS]... Pip Installs Packages. Options:

  • -version Show the version and exit.
  • -help Show this message and exit.

Commands: install Install packages.

slide-107
SLIDE 107

click click

slide-108
SLIDE 108

smol-git smol-git

slide-109
SLIDE 109

$ smol-git --help Usage: smol-git [OPTIONS] COMMAND [ARGS]... smol-git - the stupid content tracker Options:

  • -version Show the version and exit.
  • -help Show this message and exit.

Commands: clone Clone a repository into a new directory. commit Record changes to the repository. config Get and set repository or global options. log Show commit logs.

slide-110
SLIDE 110

import click @click.group("smol-git") @click.version_option("0.1.0") def cli(*args, **kwargs): """smol-git - the stupid content tracker""" pass

slide-111
SLIDE 111

Progress bars Progress bars

slide-112
SLIDE 112

@cli.command() @click.argument("src") @click.argument("dest", required=False) def clone(src, dest): ... with click.progressbar(files) as _files: for file in _files: # download file

slide-113
SLIDE 113
slide-114
SLIDE 114

Application folders Application folders

slide-115
SLIDE 115

@cli.command() @click.argument("key") @click.argument("value") def config(key, value): app_dir = click.get_app_dir("smol_git") if not os.path.exists(app_dir):

  • s.makedirs(app_dir)

cfg = os.path.join(app_dir, "config") # set repository or global options

slide-116
SLIDE 116
slide-117
SLIDE 117

Paged output Paged output

slide-118
SLIDE 118

@cli.command() def log(): ... click.echo_via_pager(log_string)

slide-119
SLIDE 119
slide-120
SLIDE 120

Colored text Colored text

slide-121
SLIDE 121

@cli.command() def status(): ... for file in files: file_status = "new file" if file.added else "modifie status += click.style( f"\t{file_status}: {file.name}\n", fg="green", bold=True ) click.echo(status_string)

slide-122
SLIDE 122
slide-123
SLIDE 123

Launching editors Launching editors

slide-124
SLIDE 124

@cli.command() @click.option("-m", "--message", help="The commit message.") def commit(*args, **kwargs): if kwargs["message"] is None: commit_message = click.edit() else: commit_message = kwargs["message"] # commit changes

slide-125
SLIDE 125
slide-126
SLIDE 126

User prompts User prompts

slide-127
SLIDE 127

@cli.command() @click.argument("repository") @click.argument("branch") def push(repository, branch): username = click.prompt("Username for 'https://github.co password = click.prompt( f"Password for 'https://{username}@github.com'", hide_input=True ) # push changes

slide-128
SLIDE 128
slide-129
SLIDE 129

$ smol-git --help Usage: smol-git [OPTIONS] COMMAND [ARGS]... smol-git - the stupid content tracker Options:

  • -version Show the version and exit.
  • -help Show this message and exit.

Commands: clone Clone a repository into a new directory. commit Record changes to the repository. config Get and set repository or global options. log Show commit logs.

slide-130
SLIDE 130

Testing click code Testing click code

slide-131
SLIDE 131

from click.testing import CliRunner from smol_git.cli import cli def test_git_log(): runner = CliRunner() result = runner.invoke(cli, ['log']) assert result.exit_code == 0 assert result.output == expected_output_log

slide-132
SLIDE 132

https://click.palletsprojects.com https://click.palletsprojects.com

slide-133
SLIDE 133

Packaging the CLI Packaging the CLI

slide-134
SLIDE 134

. ├── setup.py └── smol_git ├── cli.py ├── __init__.py ├── utils.py └── __version__.py

slide-135
SLIDE 135

from setuptools import setup setup( ... name="smol-git", entry_points={ "console_scripts": [ "smol-git = smol_git.cli:cli" ] }, ... )

slide-136
SLIDE 136

Pushing to PyPI Pushing to PyPI

slide-137
SLIDE 137

$ python setup.py sdist bdist_wheel $ twine upload dist/*

slide-138
SLIDE 138

User experience User experience

slide-139
SLIDE 139

Unix philosophy Unix philosophy

Write programs that do one thing and do it well. Write programs to work together. Write programs to handle text streams, because that is a universal interface.

slide-140
SLIDE 140

Make features discoverable Make features discoverable

Persistent history History search Auto-completion

slide-141
SLIDE 141

prompt-toolkit prompt-toolkit

slide-142
SLIDE 142

Resources Resources

Talk slides: by Brandon Rhodes - North Bay Python 2017 by Thomas Ballinger - PyCon 2015 by Dave Forgac - PyCon 2019 by Amjith Ramanujam - PyCon 2017 https://vinayak.io/talks The TTY demystified What is the exact difference between a terminal, a shell, a tty and a console? Keynote Terminal whispering Writing Command Line Applications that Click Fish shell design document Awesome CLI Tools https://github.com/vinayak-mehta/smol-git

slide-143
SLIDE 143

@vortex_ape @vortex_ape vinayak.io vinayak.io #talk-guide-to-clis #talk-guide-to-clis