Bash Script for generating Jekyll Markdown posts
Did I really get bored again?
It seems like my default pattern of being bored on Sundays was broken. This time I was bored on a Saturday. With nothing much to do, and laziness drowning my desire for physical activity, I started browsing through my ever-increasing list of side projects. However, working on a side project felt like a major chore at that moment, so I thought about writing a couple of blog posts, detailing some of my recent activities.
Energized by the idea, I opened up my local Jekyll environment, and was about to create a new post, when I was struck with the frustration of creating a new file yyyy-mm-dd-title-of-the-post.md
inside the _posts
directory.
Not only that, I also had to add front matter to each of the posts, which was different for each post.
---
layout: <layout>
title: "The Title of the post"
date: yyyy-mm-dd hh:mm:ss +TIMEZONE
categories: <category>
---
To do this for each and every post is a frustration that cannot be borne. So, in the spirit of a regular developer, let’s automate this mundane task.
Let’s automate creating a new post
There are many things to automate here, but for simplicity sake, let’s automate the following:
- generation of the post inside
_posts
with the proper filename - injecting front matter with dynamic content
To achieve the above, we must get the dynamic content somehow.
This automation is to be done using a bash script. Since my development environment is Linux (Ubuntu) this will be a good enough method.
The bash script will take in arguments when the script is called form the terminal.
Parsing arguments in Bash script
For the aforementioned requirements, we need category, title, layout as option-arguments. Since this is a bash script, let’s also add help as another category.
Let’s define each of those categories a little.
title
- to be used in front matter, and also used to generate filename
- takes a string as input
eg: "this is a title"
category
- to be used in front matter
- can have values {miscellaneous, programming, poems, songs, stories}
- has a default value as
miscellaneous
- takes a string as input
eg: "programming"
layout
- to be used in front matter- to be used in front matter
- has a default value as
post
- takes string as input
eg: "page"
help
- has no non-option argument
To achieve the above, we shall use [GNU getopts
] library, which is installed by default in Linux.
We shall parse both long-option and short-option arguments.
SHORT=c:l:t:h
LONG=category:,layout:,title:,help
OPTS=$(getopt --name create_post -o $SHORT --long $LONG -- "$@")
eval set -- "$OPTS"
while :
do
case "$1" in
-c | --category )
category="$2"
shift 2
;;
-l | --layout )
layout="$2"
shift 2
;;
-t | --title )
title="$2"
shift 2
;;
-h | --help)
help
exit 2
;;
--)
shift;
break
;;
esac
done
Using the --help
or -h
option will run the function help
which in turn calls other functions.
description
- short description of what this bash script doesusage
- syntax for usageexample_usage
- examples of using the scriptoptions
- the different options allowed in the script
Once the arguments are parsed, we also need to check variables & set defaults.
Parsing non-option arguments
# check title cannot be empty
if [[ -z "$title" ]]; then
echo "--title is non-optional argument"
echo "Check out some example usages."
echo
example_usage
echo
ask_help
echo
exit 1 # exit with error
fi
# check layout & title
# if empty, populate with default value
[ -z "$category" ] && category=$DEFAULT_CATEGORY
[ -z "$layout" ] && layout=$DEFAULT_LAYOUT
Title is a non-optional argument, and if it’s not provided, show some examples, show instruction to ask help, and exit script.
Category, and Layout are set to default values if they are not provided.
Playing with Variables & Parsing
Using the values of the options which are set to corresponding variables, let’s create more variables which will later be used to generate file, as well as to inject dynamic front matter.
Parse title
Title is provided as a string eg: "this is a title"
This can directly be injected into front matter, but for generating the filename we need to parse it further.
echo $title | tr -c '[:alnum:]\~n\r' ' ' | awk '{$1=$1};1' | tr -c '[:alnum:]\n\r' '-' | tr 'A-Z' 'a-z')
The parsing is done in 4 steps:
- substitute all special chars (non-alphanum) with space
- remove start, trailing space + remove multiple-spaces, tabs & replace with single space
- substitute space with - (hyphen)
- convert entire string to lowercase
There’s an interesting bug that was found here.
Since the non-optional arguments, especially title
can be any string, if the string contains !x
where x
can be certain special characters, strings, or a combination of both, this triggers Bash History Expansion, which is an unwanted behaviour here. History expansion will modify the input depending on what value x
has.
This Bash History Expansion cannot be avoided in the script, since the expansion is done in the interactive shell where this script is called from.
To avoid the aforementioned behaviour, there are 2 steps we can take:
- Set the Bash History Expansion to off.
I’ve permanently set it off by adding set +H
to my .bashrc
since I can’t imagine any situation where this Bash History Expansion to be useful.
- Use ‘ (single-quote) instead of “ (double-quote) while entering strings.
This is a lazy alternative, but should be used to avoid errors.
In my case, I couldn’t be bothered to remember to use ‘ (single-quote) every time, so setting off Bash Expansion was the way to go.
Parse date, time, timezone
Date, time, and timezone will be used in filename & front matter.
parsed_date=$(date +"%Y-%m-%d")
parsed_time=$(date +"%T")
timezone=$(date +"%z")
Create filename variable
The filename variable will be used to create the file inside _posts/
directory.
filename="$parsed_date-$parsed_lowercase_title.md"
Create a New Post
Let’s create the file in posts/
directory and inject it with front matter.
Create file in _posts/
First, let’s check if a file with the same filename
exists. If it does, exist, else, let’s create the file.
if [[ -f ./_posts/$filename ]]; then
echo "Error creating file with title='$title'"
echo "File: $filename already exists inside _posts/"
echo "Use a different 'title' to create another file."
echo
exit 1 #exit with error
else
# create the file inside _posts
touch ./_posts/$filename
echo "created $filename inside _posts/"
echo
fi
Inject Front Matter
Each file in _posts expects a front matter:
---
layout: page
title: <category>
permalink: /<category>/
---
Let’s inject the dynamic front-matter from the bash script into the file created above.
# inject front matter into the file
echo "---" >> ./_posts/$filename
echo "layout: $layout" >> ./_posts/$filename
echo "title: \"$title\"" >> ./_posts/$filename
echo "date: $parsed_date $parsed_time $timezone" >> ./_posts/$filename
echo "categories: $category" >> ./_posts/$filename
echo "---" >> ./_posts/$filename
Generate Table of Contents
I also want to add a table of contents for each post, which is autogenerated using the headers (h2, h3, h4)
To do this, multiple options are available, and I chose the default approach in Jekyll provided by Kramdown.
I also want to style the TOC to be a little more appealing, and to achieve that, let’s inject some inline CSS as well.
printf "\n<style type='text/css'>#markdown-toc::before" >> ./_posts/$filename
printf "{content:'Table of Contents';font-weight:700}" >> ./_posts/$filename
printf "#markdown-toc{border:3px solid #aaa;padding:1.5em;" >> ./_posts/$filename
printf "margin-left:0;display:inline-block}</style>\n" >> ./_posts/$filename
printf "\n* TOC\n" >> ./_posts/$filename
printf "{:toc}\n" >> ./_posts/$filename
After, everything above has executed perfectly, it will look like:
Check out the entire series of posts related to Blog Setup