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:
I type
vim ~/acdw.net/posts/2019-01-03-the-title.essay
in my shell.I manually type in the YAML frontmatter:
--- title: The title date: 2019-01-03 tags: something, bs, whatever ---
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!
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.
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):
= tempfile.mkstemp(
thandle, tname ='.md', prefix=f'acdw-{group}-', text=True) suffix
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(
=' ', date=date.today()) space
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):
"EDITOR", EDITOR), tname, '+'], check=True) subprocess.run([os.getenv(
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:
= re.search(r"^title:\s*(.*)$", metadata, re.MULTILINE)
title = re.search(r"^date:\s*(.*)$", metadata, re.MULTILINE) date
and then do a couple of sanity checks:
if title is not None:
= title.group(1)
title else:
= ""
title
if date is not None:
= date.group(1)
date else:
= date.today() date
To generate the slug from the title, I just use my handy-dandy slugify function:
def slugify(title):
= [str.lower(word) for word in re.split(r"\W+", title) if word != ""]
words return '-'.join(words)
And then I write the file and remove the tempfile:
= date + "-" + slug + "." + group
fname
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:
= sys.argv[1]
group 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:
= contents.split('---')
_, metadata, body 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:
- Check if you’re already working on something today, and ask if you’d like to continue on that or start something new
- Maybe a
draft
command that allows you to choose a draft to work on - A
publish
command to move a draft into theposts/
folder9 - A kind of search function so I can quit and come back to a post without finding it in my drafts folder
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 "$@"
Blog post on this to come, I hope.↩︎
Plus, I don’t really know where I put the dang original script.↩︎
I did something very similar to Jorge Israel Peña’s method as outlined in his blog post.↩︎
Is this a meme?↩︎
I use the {space} variable because I have Vim automatically automatically truncate end-of-line spaces on a save.↩︎
You’re supposed to use
---
for the beginning and...
for the end, but seriously, who cares? Hakyll doesn’t, I don’t, so there.↩︎I’m not sure whether this is a better command for this script or for my Hakyll site script. This requires more thought.↩︎