Jekyll Post Generator in Ruby
A while ago, I blogged a quick console application to create a blog post outline, given a set of command line options. That was in C# and has been working fine for quite a while. But since the underlying technology stack that this blog is built on is jekyll and ruby, I thought it might be a good learning exercise for me to (loosely) port it to ruby.
As code goes, I don't consider it my best work but it helped me explore command line parsing, file and folder manipulation and content generation, all things I'm very comfortable with in the .Net space but had no clue previously in ruby.
require 'optparse'
require 'fileutils'
options = {}
option_parser = OptionParser.new do | opts |
opts.banner = "Usage: post.rb <post title> [options]"
options[:tags] = ' '
opts.on( '-t', '--tags TAGS', 'Comma delimited list of TAGS that apply to this post' ) do | tags |
options[:tags] = tags
end
options[:image] = ''
opts.on( '-i', '--img IMAGE', 'Path to image to include in post' ) do | image_path |
options[:image] = image_path
end
options[:date] = Time.now.strftime("%Y-%m-%d")
opts.on( '-d', '--date DATE', 'DATE of post' ) do | date |
options[:date] = date
end
options[:extension] = '.markdown'
opts.on( '-x', '--ext', 'file extension - defaults to markdown' ) do | x |
options[:extension] = x
end
options[:layout] = 'post'
opts.on( '-l', '--layout LAYOUT', 'Layout to use' ) do | l |
options[:layout] = l
end
options[:publish] = false
opts.on( '-p', '--publish', 'Mark for immediate publish' ) do
options[:publish] = true
end
opts.on( '-h', '--help', 'Display this screen' ) do
puts opts
exit
end
end
option_parser.parse!
if ARGV.empty?
puts option_parser
exit(-1)
end
# First argument must be the title of the post
title = ARGV[0]
# Use the post title to name the file and optional image file
safe_title = title.downcase.strip.gsub(' ', '-')
post_filename = options[:date] + '-' + safe_title + options[:extension]
# If there is a post subfolder, create the post file
# there rather than in the current directory.
current_folder = Dir.pwd
posts_subfolder = '_posts'
path_to_post_folder = File.join(current_folder, posts_subfolder)
path_to_post = Dir.exists?(path_to_post_folder) ? File.join(path_to_post_folder, post_filename) : File.join(current_folder, post_filename)
puts "Creating post in #{path_to_post} "
# Optional path to an image
# if set, we copy the image into the img folder
# and add a link to it in the body of the file
add_image_link = false
# check if image supplied - copy it and put a reference to it in the post
if File.exists?(options[:image])
add_image_link = true
image_post_subfolder = 'img/posts'
path_to_image_posts_folder = File.join(current_folder, image_post_subfolder)
new_post_image_folder = File.join(path_to_image_posts_folder, safe_title)
# create directory for this post
Dir.mkdir(new_post_image_folder) unless Dir.exists?(new_post_image_folder)
# get file name only
# create new path and copy the file
image_filename = File.split(options[:image])[1]
FileUtils.cp(options[:image], new_post_image_folder)
puts image_filename
end
# Generate the post content
yaml = {
'layout' => "#{options[:layout]}",
'title' => title,
'published' => "#{options[:publish]} ",
'tags' => "[ #{options[:tags]} ]"
}
yaml_delimiter = '---'
File.open(path_to_post, "w") do | file |
# Front matter
file.puts yaml_delimiter
yaml.each do | key, value |
file.puts key + ': ' + value + "\n"
end
file.puts yaml_delimiter
# Main content
file.puts
file.puts '# ' + title
file.puts
if add_image_link
file.puts 'Imported this image: '
file.puts "![image](/#{image_post_subfolder}/#{safe_title}/#{image_filename})"
end
end
File System
The main part of the code that I thought would give me most trouble turned out to be the easiest. The file system API seemed familiar enough from .Net to let me get something working very quickly. Once that was done, I spent a bit more time tidying it up and trying to make it a bit more idiomatic and I found the File.open closure a very nice feature.
Command Line Options
Running the tool with no arguments, prompts with the available options:
and here's a typical use:
ruby post.rb "Jekyll Post Generator in Ruby" -t "jekyll, ruby, meta, snippets"
The options code follows a similar pattern to .Net libraries like NDesk but adding options and sensible defaults ended up taking more code than the main work of the script.
Ruby vs .Net
Allowing for differences in the number of options available it appears that the ruby and the .Net versions are not too far away from one another in terms of lines of code.
Maybe there is a "ruby" way to approach this problem that would have reduced the line count and have increased readability (and it certainly doesn't satisfy Sandy Metz's squint test ).
Having said that, I think I got a lot of value from the exercise and it's probably best that I quit while I'm ahead :)