Awful experience when add sub-command specified options use OptionParser

Following is the real usage in my project.

I have to run OptionParser.parse twice.

  • the first time run on line 36, only for get the correct sub-command in line 49
  • the second time run on line 52, line 54-56, add logic for parse opts of sub-commands

Maybe this is the only expected approach? I feel like it could be simpler,any idea?

It’s common to want to build a command-line tool where subcommands share common options.

My approach is to define a macro and expand the shared options inside each subcommand. After macro expansion the code essentially becomes boilerplate.

The approach is simple enough that I imagine you’ve probably already thought of it. You can easily handle cases where the number of subcommands grows or where the shared parts start to diverge.

The unfortunate part is that OptionParser in Crystal’s standard library is difficult to modify due to historical constraints. Just last month, a proposal to add short option bundling — a basic feature — was reverted because it breaks backward compatibility.

Building command-line tools is my main use case for Crystal, so I hope there’s a way to work around this.

1 Like

My solution is to maintain a hash mapping, {} of Symbol => Proc(OptionParser, CLI, Nil), symbol is the sub-command string, add extra opt rule for sub-command in the Proc, if user input this sub-command, then call the Proc with opts object as arg.

The approach is simple enough that I imagine you’ve probably already thought of it

Actually, I didn’t come up with the idea of doing it this way,:smile:, I thought if follow your instructions, we still need a separate OptionParser to process it and find the correct sub-command, right?

My approach is a naive extension of the official reference, requiring only a single parse call:

macro on_common
  opt.on("-a", "--aa", "Common option A") do
  end

  opt.on("-b", "--bb", "Common option B") do
  end
end

OptionParser.parse do |opt|
  opt.on("sub1", "This is subcommand 1") do
    opt.on("-x", "--xx", "Extra option for sub1") do
    end
    on_common
  end

  opt.on("sub2", "This is subcommand 2") do
    opt.on("-y", "--yy", "Extra option for sub2") do
    end
    on_common
  end
end

This is just one example. I prefer to define all command-line options in a single file, so this approach works well for me. Note that this naive approach has its own drawbacks, and I am not claiming it is always the best solution.