GIST 4 CLIFE!

You can read public gists and create anonymous gists without any authentication using the Github API.

Let’s look at the docs:

So the API call expects JSON encoded data. The filename extension in "file1.txt" is used to detect the type of syntax highlighting. ".txt" will not use syntax highlighting but we could use ".py" instead to set it to Python.

requests

Python comes with urllib2 (Python 2) and urllib.request (Python 3) but the recommended HTTP client library is requests. There is even a GitHub API v3 example in the requests docs.

One could say that this is an example of “sending an API request in Python”.

So let’s give it a try:

>>> import requests
>>>
>>> url = 'https://api.github.com/gists'
>>> r   = requests.post(url, json={'files':{'file1.txt':{'content':'hi'}}})
>>> r.json()['html_url']
'https://gist.github.com/bb3b66a670955d401273a2f4ff8007c2'

As there is the json= “helper” for sending requests which performs a json.dumps() behind the scenes, response objects have the .json() method which performs a json.loads() to turn the JSON string into a Python structure.

json.dumps(…, indent=2)

If you’d like to view the full response you use json.dumps() to pretty-print it:

>>> import json
>>> print(json.dumps(r.json(), indent=2))
...
  "description": null, 
  "truncated": false, 
  "url": "https://api.github.com/gists/bb3b66a670955d401273a2f4ff8007c2", 
  "created_at": "2017-03-21T21:20:20Z", 
  "html_url": "https://gist.github.com/bb3b66a670955d401273a2f4ff8007c2", 
...

We’ve truncated the output but you can try it out for yourself.

os.isatty()

So let’s try to build something we can call from the command-line.

os.isatty(0) is a way to check if your code has been called in a pipeline or if it has had input redirected into it. It will return False in both of these cases. This can be useful when building command-line tools.

$ python -c 'import os; print("Yes" if os.isatty(0) else "No")'
Yes
$ python -c 'import os; print("Yes" if os.isatty(0) else "No")' < filename
No
$ echo moo | python -c 'import os; print("Yes" if os.isatty(0) else "No")' 
No

If os.isatty(0) is True we will process sys.argv for a filename to read. If it is False we will read from sys.stdin to get the data from the pipeline or redirection.

gist.py

gist.py

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
import os, requests, sys url = 'https://api.github.com/gists' ext = 'txt' if os.isatty(0): filename = sys.argv[1] with open(filename) as fh: content = fh.read() if '.' in filename: ext = filename.split('.')[-1] else: content = sys.stdin.read() r = requests.post(url, json={ 'files': { 'file1.' + ext: { 'content': content } } }) print(r.json()['html_url'])

We’ve set the default extension to txt. If we look in sys.argv[1] for a filename and it contains . we assume it has an extension and we split() it out.

$ python gist.py gist.py
https://gist.github.com/9be8e23c234ee3a55abbd1333435524c

Did it work?

It did. It also extracted the filename extension successfully to give us syntax highlighting.

Improvements?

Well we don’t check that we were given a filename and if we were we don’t check if it is valid. len(sys.argv) == 2 would check that we were passed a single filename.

Checking if a file is there before calling open() is a race-condition so it’s usually better to try the open and catch the exception if it is raised.

One may want to use pygments.lexers.guess_text() to try to guess the type of the data if there is no filename (or extension) given. This could allow you to syntax highlight in such cases.

>>> pygments.lexers.guess_lexer('import sys\nprint(\'o hai\')')
<pygments.lexers.PythonLexer>

You could use argparse or click to build the command-line interface.

Finally, if you did not want to put this code into its own file you could create a shell function instead.

gist as a bash function

You have probably encountered aliases before e.g. alias ll='ls -l'

When it comes to complex commands that contain their own quoting though it can become painful:

$ alias gist='python -c '\''print("\"Hello.\"")'\'''
$ gist
"Hello."

Using a function removes the need for the first layer of quoting:

$ unalias gist
$ gist () { python -c 'print("\"Hello.\"")'; }
$ gist
"Hello."

Functions are much more powerful though, they are essentially “inlined” scripts:

$ wrapper () { arg=$1; shift; echo cmd --arg1 "$arg" --arg2 --arg3 "$@"; }
$ wrapper a b c d
cmd --arg1 a --arg2 --arg3 b c d

So Python’s -c option allows to pass code in a string and because we’ve used single quotes to surround our code the simplest thing to do is convert all single quotes within our code to double quotes.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
gist () { python -c ' import os, requests, sys url = "https://api.github.com/gists" ext = "txt" if os.isatty(0): filename = sys.argv[1] with open(filename) as fh: content = fh.read() if "." in filename: ext = filename.split(".")[-1] else: content = sys.stdin.read() r = requests.post(url, json={ "files": { "file1." + ext: { "content": content } } }) print(r.json()["html_url"]) ' "$@" }

"$@" here are all the arguments passed to the function which we send on to Python so they are available in sys.argv

You can use '\'' to “escape” a single quote within surrounding single quotes but I suppose technically you could say that’s not what you’re doing:

$ echo '$single'\''$(quote)'
$single'$(quote)

So what you have here are 3 separate “words”:

  • '$single'
  • \'
  • '$(quote)'

So '\'' means you’re actually closing the first ' then using \' then opening a new '

It is possible to avoid the quoting issues by replacing -c with python - and a heredoc however that introduces another issue with regards to the reading of stdin. It can be remedied by using process substituion a lá python <(cat <<\. but that topic probably warrants its own discussion.