My current blogging setup consists of me publishing a post shortly after I've finished typing and proofreading. While this is a straightforward and easy process, it also makes it easy for me to procrastinate on posts for days on end.

As a way to introduce some form of accountability, I thought having the option to schedule a post in advance might be a good idea.

My current publishing setup is a bash script, that builds the site files and deploys the build to Netlify. I started finding ways to automate the execution of this script at a later date.

A cron job might be the first thing comes to mind, however I would like to introduce another tool that is just as useful.

Say hello to at

A cron job will run more than once at the schedule set by you - the same time every hour/day/week/month/year. However, I was looking for something more flexible.

Maybe I feel like posting one article in one week, but two the next week. One week I may post on a Monday, the other week I may feel like posting on a Friday. This is where at comes in, a command line tool to execute a set of commands once, at the specified date and time.

It is installed by default on both Linux and Mac, however, it requires additional steps to work on Mac.

Setup at on Mac

atrun, the utility that executes at jobs, is disabled by default on Mac. Enable it with the following command:

$ sudo launchctl load -w /System/Library/LaunchDaemons/com.apple.atrun.plist

Additionally, atrun requires disk access to execute commands:

  1. Go to System Preferences > Security and Privacy > Privacy
  2. Scroll to the Full Disk Access section and click on the + symbol.
  3. Press Cmd+Shift+G and enter /usr/libexec/atrun.
  4. Select atrun, after which it should appear in the list.

Creating the script

The script takes the following arguments:

I maintain all draft posts in the _drafts directory, so the script adds the directory to the filename. Additionally, I use sed and cut to get the title of the post, which I'll use later on.

cd ~/website
post="_drafts/$1"
title="$(sed '3q;d' $post | cut -c 8-)"

The draft posts are undated, so this script adds the date and time to the post. Since the user input can vary in format, I use date to convert it to a standard format – YYYY-MM-DD for the date and HH:MM:SS +00:00 for the time.

pubdate=$(date -d "$2" +%F)
pubtime=$(date -d "$2" "+%T %z")

at takes the set of commands in multiple ways. For this script, I'm using a heredoc, that sends multiple commands at once.

at $2 <<- EOF

at is supplied with the time entered by the user, followed by the heredoc syntax.

Any command entered after this statement is sent to at, till it encounters the delimiter, EOF. The hyphen is added to remove any tabs at the start of the line, which may be there if the heredoc is indented for readability.

The first command is to add the post date to the post's front matter, which I've done using sed. The line number can vary depending on how your front matter is formatted.

sed -i "7 i date: $pubdate $pubtime" $post

The post date is also added to the filename, to match Jekyll's naming convention for posts. Along with the filename change, the post is moved to the _posts directory, from where it will be published.

mv $post _posts/$pubdate-$1

Lastly, I add the commands from my current publishing script.

rm -rf _site
git add .
git commit -m "$3"
bundle exec jekyll build
netlify deploy --prod --dir=_site

I could add the delimiter, EOF at the end of the script and call it a day. But then I started to think of additional features to add.

How would I know if the build was successful? Apart from checking the site after the scheduled time, was there a way I could receive a notification?

Implementing notifications

I initially thought that this would be a very complex process. I was totally wrong, as both Linux and Mac have command line tools that make creating a notification easy peezy lemon squeezy (well, almost, as Mac seems to require extra steps once again)

Linux

Notifications can be created using notify-send. Add the following line followed by the delimiter and you're good to go!

notify-send "Published! The post, $title, is now live."
EOF

The first string is the title of the notification, and the second string is the message below the title.

Mac

Creating a notification is very simple thanks to osascript:

osascript -e \
'display notification "The post, $title is now live." with title "Published!"'

Getting it to work with at is a whole different story.

When I tried testing with a sample notification, there was no output. Turns out, at sends all output, success or failure to the local mail account, which can be accessed through the mail command line tool.

Here's the output from one of the mails:

2023-07-06 01:59:25.338 osascript[17789:286727] NSNotificationCenter connection invalid
2023-07-06 01:59:25.338 osascript[17789:286727] Connection to notification center invalid. ServerConnectionFailure: 1 invalidated: 0
2023-07-06 01:59:25.338 osascript[17789:286727] Connection to notification center invalid. ServerConnectionFailure: 1 invalidated: 0

After some searching, I figured the problem, but had trouble finding a solution for it. The issue is with at executing in a different namespace, one that does not have access to create notifications. A notification can be created in the user namespace, which is why I was able to create one easily from the command line.1

I happened to find the solution from a GitHub issue, to use a tool called reattach-to-user-namespace.

$ brew install reattach-to-user-namespace
$ reattach-to-user-namespace
fatal: usage: reattach-to-user-namespace [-l] <program> [args...]

    Reattach to the per-user bootstrap namespace in its "Background"
    session then exec the program with args. If "-l" is given,
    rewrite the program's argv[0] so that it starts with a '-'.

Using the mentioned format, I added the following line to the script, followed by the delimiter to complete the heredoc.

reattach-to-user-namespace osascript -e \
'display notification "The post, $title is now live." with title "Published!"'
EOF

Testing the script

I've implemented the script as a shell function, schedule. I've additionally added tab completion for the filename, which works with the zsh shell. However, these commands would work just as fine as a shell script.

Using this post as an example, I ran schedule to publish this post at 4pm UTC on July 22nd.

$ schedule schedule-jekyll.md "Jul 22 4pm" "new post: schedule posts in jekyll"
job 1 at Thu Jul 22 16:00:00 2023

Note: Your computer needs to be up and running at the specified date and time for at to work. So set the date and time accordingly.

You can access the job's script by typing using the -c option with the job number from the resulting output. It includes the default set of environment variables followed by the commands entered from the script.

$ at -c job 1
#!/bin/sh
# atrun uid=501 gid=20
...

cd /Users/piya/website || {
     echo 'Execution directory inaccessible' >&2
     exit 1
}
OLDPWD=/Users/piya; export OLDPWD
sed -i "7 i date: 2023-07-22 16:00:00 +0000" _drafts/schedule-jekyll.md
mv _drafts/schedule-jekyll.md _posts/2023-07-22-schedule-jekyll.md
rm -rf _site
git add .
git commit -m "new post: schedule posts in jekyll"
bundle exec jekyll build
netlify deploy --prod --dir=_site
reattach-to-user-namespace osascript -e \
'display notification "The post, Schedule Posts in Jekyll, is now live." with title "Published!"'

To delete a job before its executed, type atrm job <num>. You can also schedule multiple posts, and view the job queue using atq.

If you're seeing this post, that means the script worked :)

Notes

  1. The closest thing to documentation I could find about this is this article for a different tool.