Revisiting the command line: simple options

Sometimes, you don't want a full-fledged standardized way of accessing options.

I wrote a quick utility to do a local LT lookup based on the previously created library, withthe aim of generating citations which could be put into documents I was writing in emacs. It has several command line parameters but only two options (produce italic formatting in HTML or LaTeX; select between bibliographic and footnote formatting). The rest of the parameters are just: the first value is the author's last name, any subsequent parameters are words from the title. The options may be anywhere.

The original command-line LT application used the CommandLineOptions class from my general utilities. That class isn't really meant for this sort of command line, though. We don't want to walk a set of positional arguments, because we don't have a defined order. The author name might be argument 1, 2, or 3. If there are options they might be at the end (does not affect the order otherwise), at the beginning, or in the middle.

Now, this is an arbitrary decision. I could have made -a required for the author name and -t<word>:<word>:... for the title values, but that struck me as in the end less simple at the user end.

What I have allows:

./citegetter Plauger Purpose
P.J. Plauger, <i>Programming on Purpose: Essays on Software Design</i> (Prentice Hall PTR 1997)

with a very, very simple default argument form.

So instead of using a model designed for selecting options, instead I decided just to iterate in a way which doesn't care at all about the actual offsets:

LtLibrary::StreamFormattingType type = LtLibrary::StreamFormattingType::FootnoteHtml;
std::string author;
std::vector<std::string> keys;
bool bibliographicForm = false;

std::ranges::for_each(std::ranges::iota_view{ 1, argc }, [&](const int arg) {
if (argv[arg][0] == '-')
{
if (argv[arg][1] == 'L')
type = LtLibrary::StreamFormattingType::FootnoteLatex;
else if (argv[arg][1] == 'B')
bibliographicForm = true;
}
else if (author.empty())
author = argv[arg];
else
keys.emplace_back(argv[arg]);
});


The use of the iota_view iteration allows the positional logic to operate regardless of order, only based on whether the author value has already been filled. We visit each parameter once and convert to a C++ string only when necessary. (Why use an iota_view rather than for (int i = 1; i != argc; ++i)? Because an iota_view is a slightly purer statement about intent and slightly less concerned with the mechanics.)

What would this look like with the Options class?

LtLibrary::StreamFormattingType type = LtLibrary::StreamFormattingType::FootnoteHtml;
std::string author;
std::vector<std::string> keys;

JSBUtil::CommandLineOptions options(argc, argv);
if (options.hasSingleOption("L"))
type = LtLibrary::StreamFormattingType::FootnoteLatex;
bool bibliographicForm = options.hasSingleOption("B");
std::map<int, std::string> args;
options.getPositionalArgs(args);
std::ranges::for_each(args | std::views::values, [&](const auto &inVal) {
if (author.empty())
author = inVal;
else
keys.push_back(inVal);
});


The values view recreates the iteration ignoring the actual positions of the values.

This is two lines shorter than the version which merely parses the argv list, primarily because the declaration of bibliographicForm can be integrated with the assignment, but it is not inherently simpler or clearer. It is very slightly more expensive (proportionally, considerably more expensive, but this is a small snippet of code used at startup) both in cycles and in memory: the iota_view loop iterates over a generated series of integers, where the loop in the second one iterates over a map which was copied from another map internal to the options object, filtered again through a view reducing that to a stream of strings, and the keys stored have less overhead associated with them on the way to storage in the vector.

Comments

Popular posts from this blog

Boundaries

Overview

Considerations on an Optimization