Easy command line interfaces in C

Remo Dentato - Sep 21 '23 - - Dev Community

Overview

I already introduced the VRG library describing the support it provides for writing variadic functions in C. Here I'll discuss the support it provides to create command line interfaces for C programs.

The code is freely available (under the MIT Licence) library is available on Github.

Command-line interfaces (CLIs) have always been a fundamental aspect of software development, especially in C programming, where simplicity and control are highly valued. Traditionally, CLI handling in C has been a tedious chore, often filled with repetitive, error-prone code. Existing solutions like the GNU getopt library provide a robust feature set but can be somewhat complex to integrate and use.

Simplicity is Key

One of the standout features of VRG is its simplicity. It offers a clean and intuitive API that reduces boilerplate code and lets you focus on what really matters—your application logic. The VRG library abstracts away the cumbersome parts of CLI handling and provides you with an easy-to-use interface that's both developer-friendly and efficient.

For example, it eliminates the need to write (and maintain!) a separate usage() function with the description of arguments and flags. Also, it offers an intuitive way to mark optional arguments.

Why Choose VRG Over GNU Getopt?

GNU getopt is certainly a robust library, but its API can be daunting for beginners or even intermediate programmers. With getopt, you often find yourself managing multiple global variables, configuring long and short options separately, and worrying about the sequencing of options and arguments.

VRG, on the other hand, provides a more intuitive and straightforward approach to CLI parsing. With a clean, high-level abstraction, you can define flags, their types, and the actions to perform when they are encountered—all enclosed within a single vrgcli() function. It simplifies the entire process and does away with the complexities involved in using something like getopt.

Getting Started with VRG

Using VRG is as simple as including the header file and defining VRGMAIN in one of your source files:

#define VRGMAIN
#include "vrg.h"
Enter fullscreen mode Exit fullscreen mode

After that, you can directly jump into defining your CLI options and arguments using vrgcli() and vrgarg() functions. Here's a quick example to demonstrate its usage:

vrgcli("MyProgram v1.0 (c) 2023 by Me") {
  vrgarg("-h\tDisplay help") {
    vrgusage();
  }
  vrgarg("-f [filename]\tSpecify an optional filename") {
    printf("Filename: %s\n", vrgarg);
  }
}
Enter fullscreen mode Exit fullscreen mode

How Does It Work?

The vrgcli() function acts as a container for all your CLI options and arguments. Each vrgarg() block within vrgcli() defines a specific flag or argument, along with the code to execute when that flag or argument is encountered.

The flag and its description are separated by a tab character \t. The part before the tab is the actual flag specification (-f [filename]), and the part after the tab is the description that will be displayed when you call vrgusage().

Inside the vrgarg() block, you have access to a char pointer vrgarg that points to the argument's value, making it easy to handle the parsed data.

Show Me the Code

Here's a complete example that demonstrates the simplicity and efficiency of VRG:

#define VRGMAIN
#include "vrg.h"

int main(int argc, char *argv[]) {
  vrgcli("My Awesome CLI Program v1.0") {
    vrgarg("-h\tDisplay help") {
      vrgusage();
    }
    vrgarg("-f [filename]\tSpecify an optional filename") {
      printf("Filename: %s\n", vrgarg);
    }
    vrgarg("input\tInput file") {
      printf("Processing input file: %s\n", vrgarg);
    }
    vrgarg() {
      vrgusage("Unexpected argument: '%s'\n", vrgarg);
    }
  }
  return 0;
}
Enter fullscreen mode Exit fullscreen mode

As you can see, VRG allows you to set up a fully functional CLI with just a few lines of code!

The usage() function is automatically generated and mandatory arguments are checked:

$ gcc -o myprog myprog.c 
$ ./myprog
Error: missing 'input' argument
USAGE:
  myprog [OPTIONS] input ...
  My Awesome CLI Program v1.0

ARGUMENTS:
  input         Input file

OPTIONS:
  -h            Display help
  -f [filename] Specify an optional filename
Enter fullscreen mode Exit fullscreen mode

Flags and positional arguments can be intermixed according the user's preference so that the two lines below are equivalent:

  myprog -f pluto pippo
  myprog pippo -f pluto
Enter fullscreen mode Exit fullscreen mode

As added flexibility, the user can specify flags arguments together with the flag itself or as a separate argument (-f pluto is fully equivalent to -fpluto).

Flags can be specified in a long or short format. For example:

  myprog -f pluto pippo
  myprog --file=pluto pippo
Enter fullscreen mode Exit fullscreen mode

Definining them in this way:

   vrgarg("-f, --file filename\tThe file to consider") {
     // ...
   }
Enter fullscreen mode Exit fullscreen mode

Custom Validators

One of the powerful features of VRG is the ability to add custom validation functions. This functionality allows you to set specific rules for the values that command-line arguments can take, offering an additional layer of robustness to your application. What's more, these validation functions can also take additional parameters for more nuanced control.

Quick Example: File Readability Check

Imagine you need to ensure that an argument specifies a file that exists and is readable. You can define a custom validator function like this:

// Check if the specified file exists and is readable
int isfile(char *arg) {
  if (arg == NULL || *arg == '\0') return 0;
  FILE *f = fopen(arg, "rb");
  if (f == NULL) return 0;
  fclose(f);
  return 1;
}
Enter fullscreen mode Exit fullscreen mode

And then you can use it in your vrgarg() function:

vrgarg("-f file\tSpecify the input file", isfile) {
  // Your code here, safely reading the file
}
Enter fullscreen mode Exit fullscreen mode

This ensures that an error will be thrown if the user attempts to specify a file that does not exist or is not readable, thereby enhancing the resilience and user-friendliness of your CLI.

Incorporating custom validation logic is just another way VRG offers you more control while maintaining its simplicity.

Speaking Your Audience's Language

I'm convinced that CLI should be in English whenever possible. This would enlarge the number of potential users since English is the "de facto" standard in many technical environment.

However, it may be appropriate sometimes to localize the CLI to enhance the user experience. If you're catering to a non-English speaking audience, localized error messages can make your software more user-friendly and reduce potential user friction.

For instance, if your target audience is primarily Japanese, VRG allows you to redefine internal message strings for a Japanese interface:

// re-define the internal messages for Japanese
// Note: These translations are machine-generated.

#define VRG_STR_USAGE        "使い方"
#define VRG_STR_ERROR        "エラー"
#define VRG_STR_INVALID      "%T '%N' に対する無効な値 '%V'"
#define VRG_STR_INV_OPTION   "オプション"
#define VRG_STR_INV_ARGUMENT "引数"
#define VRG_STR_NO_ARGOPT    "オプション '%.*s' の引数が不足しています"
#define VRG_STR_NO_ARGUMENT  "引数 '%.*s' が不足しています"
#define VRG_STR_OPTIONS      "オプション"
#define VRG_STR_ARGUMENTS    "引数"

#include "vrg.h"
Enter fullscreen mode Exit fullscreen mode

With these redefinitions, and the proper vrgarg()s, the CLI's help output might resemble:

使い方:
  vrgtest4 [オプション] モデル [出力ファイル名] ...
  バージョン: 1.3RC
  vrgli 関数 の デモ

引数:
  モデル                          モデル の ファイル 名
  [出力ファイル名]                 生成するファイル

オプション:
  -h, --ヘルプ                     この ヘルプ を表示
  -n, --数-光線 数                 光線 の 数 (正 の 整数)
  -t, --なぞる [はい/いいえ]        輪郭をなぞる (ブーリアン)
  --練習                           
  -r                               再びなぞる
Enter fullscreen mode Exit fullscreen mode

Apart from internationalization, this customization feature is invaluable when the default error messages are not detailed enough for your specific use case. Tailoring these strings allows developers to provide feedback that is more contextually relevant, helping users understand exactly what went wrong and potentially how to rectify it.

VRG's capability to redefine error messages and usage strings ensures that developers can craft CLI experiences that are intuitive, relevant, and culturally appropriate. Whether you're prioritizing internationalization or domain-specific feedback, VRG equips you with the tools to communicate effectively with your users.

Conclusion

VRG offers a fresh and straightforward approach to CLI parsing in C. With its clean API and simple syntax, you can define complex command-line interfaces without diving into the intricacies of traditional parsing libraries like GNU getopt. If you're a C developer looking for a simpler and more efficient way to handle CLIs, give VRG a try. It might just be the tool you never knew you needed.

. . . . . . . . . . . . . . . . . . . . . .
Terabox Video Player