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:

no 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 :)