Helping myself write more in the new year

Happy New Year! I’m implementing a Rulin’1 this year to write something every day, and publish it here. It’s something I did for part of last year, but then I decided to change up my website, I got busy, and I fell way off. So here I am, renewed in my quest to post up an essay, poem, recipe, review, or something else every single day this year.

To help myself be more writerly, I decided to write out a script to automate the basic task of beginning a new blog post. I’ve done this before, but new blog, new script, so I began again2. I decided to try writing it in Python to get practice.3

The dark times

My usual workflow is this:

  1. I type vim ~/acdw.net/posts/2019-01-03-the-title.essay in my shell.

  2. I manually type in the YAML frontmatter:

    ---
    title: The title
    date: 2019-01-03
    tags: something, bs, whatever
    ---
  3. I write up my post.

    Usually in writing up my post, I decide I want to change my title. So I change the title in the YAML block … but wait! The filename is wrong now too!

  4. So I have to

    • :wq from vim
    • run mv ~/acdw.net/posts/2019-01-03-the-title.essay ~/acdw.net/posts/2019-01-03-the-new-title.essay
    • vim ~/acdw.net/posts/2019-01-03-the-new-title.essay
    • oh my god.
  5. Rinse, repeat for who knows how many times, and for every post.

This is untenable.

Beginning again

I decided to implement a draft system in Hakyll4, so I can put posts I’m working on in a drafts/ subfolder of my site directory and they won’t be published until they’re ready. But I thought, I can do one better – let’s work on a temporary file!

Python has a library for that5, called tempfile. You can create a temporary file using tempfile.mkstemp that sticks around for a while, so I used that. It can also add the suffix ‘.md’, so vim knows it’s a markdown file, and a prefix so the user can know what they’re working on at a glance.

I just pop that into a function, and I’m good to go.

def new_post(group, output_dir):
    thandle, tname = tempfile.mkstemp(
        suffix='.md', prefix=f'acdw-{group}-', text=True)

Frontmatter

Hakyll uses YAML frontmatter to define metadata about each post6, so my next step is to get that frontmatter in there automatically when I start the script. I just use a constant defined at the top of my script7:

FRONTMATTER = """\
---
title:{space}
date: {date}
tags:{space}
---


""".format(
    space=' ', date=date.today())

and enter it with a quick

with open(thandle, mode='w') as f:
    f.write(FRONTMATTER)

Editing

Okay, now comes the hard part: actually writing the post. I just use a subprocess.run for that, passing the $EDITOR variable from the environment (with a sane default, of course):

subprocess.run([os.getenv("EDITOR", EDITOR), tname, '+'], check=True)

Saving

Remember, I’ve done all this as a tempfile. Now I need to actually save the file in the drafts/ folder. For that, I’ll need the date of the post, the slug, and the group, which is basically my version of a category. I just pass the group in as a parameter to new_post, so that’s taken care of. The date and slug need to be pulled from the YAML frontmatter of the file.

I used the re library instead of PyYAML, because pulling in a whole YAML dependency is silly for two fields and because PyYAML complained about multiple documents when I used the two ---s to delineate the metadata8.

So I find the title and date by searching for their definitions in the file:

title = re.search(r"^title:\s*(.*)$", metadata, re.MULTILINE)
date = re.search(r"^date:\s*(.*)$", metadata, re.MULTILINE)

and then do a couple of sanity checks:

if title is not None:
    title = title.group(1)
else:
    title = ""

if date is not None:
    date = date.group(1)
else:
    date = date.today()

To generate the slug from the title, I just use my handy-dandy slugify function:

def slugify(title):
    words = [str.lower(word) for word in re.split(r"\W+", title) if word != ""]
    return '-'.join(words)

And then I write the file and remove the tempfile:

fname = date + "-" + slug + "." + group

with open(output_dir + fname, 'w+') as f:
    f.write(contents)

os.remove(tname)

Quibbles

There are still a few problems. The first is how to tell my script what group the post should be in. I usually know what group I’m going to write in, but I’m not so sure where it’ll go from there, so that makes sense to pass as a script argument. Thus:

if __name__ == "__main__":
    try:
        group = sys.argv[1]
    except IndexError:
        print("Usage: acdw <group>")

    new_post(group, DRAFT_DIR)

The second problem is that, sometimes I realize I jumped the gun. Like today, at first I was like, I’ll write a poem, then I decided to write this essay about writing essays. So I had to rm ~/acdw.net/drafts/2019-01-03-some-poem.poem so I didn’t clog up my drafts directory, which was very frustrating.

The solution is just to split the file’s contents along the ---s, then see if the body section is empty. If it is, you can bail:

    _, metadata, body = contents.split('---')
    if body.strip() == '':
        print("acdw: Post empty.")
        return None

Packaging

So my script is all done, but it’s a real pain to use. I set it up in a virtualenv, so if I want to use it I have to source the virtualenv and then run it with python and then deactivate the virtualenv. Luckily, we have bash for that! I wrote up a quick bash script that does all that for me and passes all the arguments to my python script and put it in my $PATH. You can see it below, after my python script.

Future thoughts

Even though my script is good enough to use today, I still want to add some features:

I’ll work on these features later, though; too many times I’ve let tinkering get in the way of writing!

Appendix

The script

#!/usr/bin/env python3

import os
import re
import subprocess
import sys
import tempfile
from datetime import date

EDITOR = "editor"
DRAFT_DIR = os.getenv("HOME") + "/acdw.net/drafts/"
FRONTMATTER = """\
---
title:{space}
date: {date}
tags:{space}
---


""".format(
    space=' ', date=date.today())


def slugify(title):
    """Turn a title into a slug."""
    words = [str.lower(word) for word in re.split(r"\W+", title) if word != ""]
    return '-'.join(words)


def new_post(group, output_dir):
    # Make a new tempfile
    thandle, tname = tempfile.mkstemp(
        suffix='.md', prefix=f'acdw-{group}-', text=True)

    # Populate it with YAML frontmatter
    with open(thandle, mode='w') as f:
        f.write(FRONTMATTER)

    # Edit the file in user's EDITOR
    subprocess.run([os.getenv("EDITOR", EDITOR), tname, '+'], check=True)

    # Let's see the new file
    with open(tname, mode='r') as f:
        contents = f.read()

    _, metadata, body = contents.split('---')
    # If nothing has been written, bail
    if body.strip() == '':
        print("acdw: Post empty.")
        return None

    # Get metadata for filename
    title = re.search(r"^title:\s*(.*)$", metadata, re.MULTILINE)
    if title is not None:
        title = title.group(1)
    else:
        title = ""

    slug = slugify(title)

    date = re.search(r"^date:\s*(.*)$", metadata, re.MULTILINE)
    if date is not None:
        date = date.group(1)
    else:
        date = date.today()

    # Write the file to the directory
    fname = date + "-" + slug + "." + group

    with open(output_dir + fname, 'w+') as f:
        f.write(contents)

    # Delete tmp file
    os.remove(tname)

    print("acdw:", fname + " saved.")


if __name__ == "__main__":
    try:
        group = sys.argv[1]
    except IndexError:
        print("Usage: acdw <group>")

    new_post(group, DRAFT_DIR)

The wrapper

#!/bin/bash
# create a new draft/post in acdw.net

source $HOME/.virtualenvs/acdw/bin/activate

python $HOME/dev/python/acdw/acdw.py "$@"

  1. Blog post on this to come, I hope.↩︎

  2. Plus, I don’t really know where I put the dang original script.↩︎

  3. To jump to the script itself, click here.↩︎

  4. I did something very similar to Jorge Israel Peña’s method as outlined in his blog post.↩︎

  5. Is this a meme?↩︎

  6. It looks something like the YAML above.↩︎

  7. I use the {space} variable because I have Vim automatically automatically truncate end-of-line spaces on a save.↩︎

  8. You’re supposed to use --- for the beginning and ... for the end, but seriously, who cares? Hakyll doesn’t, I don’t, so there.↩︎

  9. I’m not sure whether this is a better command for this script or for my Hakyll site script. This requires more thought.↩︎