Ruby Command-Line Argument Processor

17 Sep 2020

Without looking at any source code, I wanted to try to put together a readable, testable command-line argument processor.

class CommandUtil
  def self.flag_args
    ARGV.select { |f| f =~ /^\-\-\w+$/ }
  end

  def self.flag_strings
    flag_args.map { |f| f.scan(/\w+/)[0] }
  end

  def self.flags
    flag_strings.map(&:to_sym)
  end

  def self.each_flag
    flags.each { |i| yield i }
  end

  def self.with_flags(*args)
    yield if args.any? { |f| flags.include?(f.to_sym) }
  end

  def self.flag_hash
    flags.inject({}) { |hash, f| hash[f] = true; hash }
  end

  def self.flag_values
    key_value_strings = ARGV.map { |arg| arg.scan(/\w+\=\w+/) }.flatten
    key_value_array = key_value_strings.map { |str| str.split("=") }
    key_value_array.map { |i| [i[0].to_sym, i[1]] }
  end

  def self.flag_value_hash
    Hash[*(flag_values.flatten)]
  end

  def self.each_flag_value
    flag_value_hash.each { |key,value| yield key, value }
  end

  def self.options
    flag_hash.merge(flag_value_hash)
  end
end
COLORS = ["red", "green", "blue"]

custom_opts = {}

CommandUtil.with_flags(*COLORS) do
  custom_opts[:color_mode] = true
end

full_opts = CommandUtil.options.merge(custom_opts)

puts full_opts
$ ruby test.rb --red --format=html
{:red=>true, :format=>"html", :color_mode=>true}

Let's add some tests.

describe CommandUtil do
  before(:each) do
    stub_const("ARGV", ["--red", "--blue", "--format=html"])
  end

  describe ".flags" do
    before do
      @flags = CommandUtil.flags
    end

    it "includes all flags" do
      expect(@flags).to include(:red)
      expect(@flags).to include(:blue)
    end

    it "does not include option flags" do
      expect(@flags).not_to include(:format)
    end
  end

  describe ".flag_strings" do
    before do
      @flags = CommandUtil.flag_strings
    end

    it "includes all flag strings" do
      expect(@flags).to include("red")
      expect(@flags).to include("blue")
    end
  end

  describe ".with_flags" do
    before do
      ARGV.replace ["--red", "--blue", "--format=html"]
    end

    it "executes the block if one of the symbol arguments is in the flags" do
      CommandUtil.with_flags(:red, :green) do
        @color_mode = true
      end

      expect(@color_mode).to be(true)
    end

    it "executes the block if one of the string arguments is in the flags" do
      CommandUtil.with_flags("red", "green") do
        @color_mode = true
      end

      expect(@color_mode).to be(true)
    end

    it "does not execute the block if none of the arguments are in the flags" do
      CommandUtil.with_flags(:white, :black) do
        @color_mode = true
      end

      expect(@color_mode).to be(nil)
    end
  end

  describe ".flag_hash" do
    before do
      @flag_hash = CommandUtil.flag_hash
    end

    it "includes all flag keys" do
      expect(@flag_hash[:red]).to be(true)
      expect(@flag_hash[:blue]).to be(true)
    end

    it "does not include option flags" do
      expect(@flag_hash[:format]).to be(nil)
    end
  end

  describe ".flag_value_hash" do
    before do
      @flag_value_hash = CommandUtil.flag_value_hash
    end

    it "includes all flag keys" do
      expect(@flag_value_hash[:format]).to eq("html")
    end

    it "does not include flags" do
      expect(@flag_value_hash[:red]).to be(nil)
    end
  end

  describe ".options" do
    before do
      @options = CommandUtil.options
    end

    it "includes all flag keys" do
      expect(@options[:red]).to be(true)
      expect(@options[:blue]).to be(true)
      expect(@options[:format]).to eq("html")
    end
  end
end