一步步了解Argp

本文详细介绍了如何使用GNU Argp库进行命令行参数处理。从创建第一个Argp程序开始,逐步展示了如何添加短选项、长选项、可选参数、选项别名、回调函数、命令行顺序处理、隐藏选项、全局变量、参数分组和封装公共选项到库。还演示了如何处理命令行参数,如何创建隐藏选项,以及如何混合库中的选项。最后,通过一个完整的示例展示了如何使用Argp制作一个莫尔斯电码转换程序,该程序具有交互模式和非交互模式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

译者序

之前整理了过C语言中命令行参数处理-getopt_long。最近看代码的时候遇到Argp|Argp,遂简单翻译下《Step-by-Step into Argp》。

本书地址:http://nongnu.askapache.com/argpbook/step-by-step-into-argp.pdf

书中的示例代码:http://svn.savannah.nongnu.org/viewvc/argpbook/trunk/


序言

本节描述了本书的许可(licensing)以及其相关内容。

本书根据 GNU 自由文档许可证 1.31 版或任何更高版本的条款获得许可。 本书中各种示例的源代码在 GNU 通用公共许可证第 3 版或您选择的任何更高版本下获得许可。 这些许可证鼓励重新分发本书; 所以请随时与朋友分享这本书。


介绍

欢迎来到学习 Argp 的循序渐进之旅,这是 GNU 系统自己的命令行解析工具。 本书涵盖了 Argp 的所有方面; 从解析简单的命令行选项到将库中的选项集混合到您自己的程序中。 本书的节奏放松,侧重于初学者程序员通过示例进行增量学习。

Argp(发音为 Argh-Pee)于 1995 年由 Miles Bader 为 GNU 项目创建。 后来它被合并到 GNU C 标准库中; 计算机上所有 C 程序默认使用的标准库。从一开始 Argp 就表明它是一种将命令行处理合并到程序中的简单方法,因此程序的外观和行为都以标准方式进行。 Argp 也可在非 GNU 系统(如 FreeBSD、Microsoft Windows)上作为外部库使用。

本书重点介绍使用 Argp 的简单命令行程序,这些程序处理命令行选项和参数。 这些程序在 shell(有时称为终端)中运行。 这些程序属于一种称为 CLI(command-line interface,命令行界面)、TUI(text user
interface,文本用户界面)或 KUI(keyboard user interface,键盘用户界面)的类型。 GNU 系统有数以千计的程序属于这一类型,本书将帮助您创建自己的基于命令行的程序。

本书适用于已经对 C 语言有所了解并希望编写更复杂的命令行程序的程序员,这些程序的外观和行为都符合标准

让我们开始并体验使用 Argp 进行命令行处理的乐趣。


本书中的约定

术语“GNU System”是指可从 Red Hat 和 Ubuntu 等供应商处获得的流行的 GNU+Linux 系统。 GNU 项目始于 1984 年,旨在创建一个完全免费的操作系统。

术语“option”通常用作“command-line option”的缩写形式。

当 C 代码在书中展示时,它采用以下形式:

#include <stdio.h>
int main (int argc, char **argv){
	printf (“hello world\n”);
}

所有代码示例均根据 GNU 标准缩进。 它们看起来好像是用缩进程序缩进的。

必须在 shell 中输入的命令如下所示:

$ make step1

当命令或函数的名称,或命令行片段出现在句子中间时,它们像this或者that


你需要什么

本节描述了执行本书中的步骤所需的内容。

需要以下程序:

  • gcc
  • make
  • binutils (ar, ranlib)
  • GNU Readline library

还需要一个编辑器来编写您的程序。如果您熟悉它,我们鼓励您使用 GNU Emacs。 否则需要另一个基于文本的编辑器,如 vim 或 nano。

此外,需要使用网络浏览器从 http://svn.sv.nongnu.org/viewvc/trunk/?root=argpbook 下载示例程序。 从 PDF 剪切和粘贴示例程序可能很烦人,因为没有保留缩进。 希望这些示例程序能给读者带来方便。


命令行参数与选项

  • 命令行选项以及它们在 GNU 系统上的外观和行为。
  • 命令行参数。
  • 具有参数的命令行选项

命令行选项

本节解释什么是命令行选项以及它们在 GNU 系统上的外观和行为。

命令行选项是一种指示程序以特定方式运行的便捷方式。 一些系统称命令行选项为switches,因为它们使您的程序切换行为。 就像火车在铁轨上行驶,铁路道岔使火车驶向新的方向。命令行选项的出现是为了让您的程序朝着新的方向运行。

程序有不同的选项,因为它们可以做不同的事情,但这些选项在重要方面是相同的。 命令行选项具有标准的外观和行为方式。

有两种选项——长选项和短选项。Argp 支持这两种类型。

短选项有一个连字符后跟一个字母。 例如:rm 命令中的“-f”,或 wc 命令中的“-c”。 短选项可以组合成一组。 因此,“ls -la”等同于“ls -l -a”。

长选项有两个连字符,后跟一个没有空格的单词。 例如:“–foo”。 长选项可以非常具有描述性,如 ls 长选项所示:“–dereference-command-line-symlink-to-dir”。 长选项不能组合在一起,它们通常有相应的短选项。

您不需要键入整个长选项名称以使程序理解它:您只需要键入足够长的选项名称以在同一程序中的其他长选项中唯一标识它。 为了说明此功能,请尝试运行“ls --hel”而不是“ls --help”。

为了使长选项更易于使用,一些shells(如 Bash shell)具有允许使用table来补充完成长选项。

用户期望你的程序支持一些选项,如–help,–version。默认情况下,Argp 为您的程序提供了一个 --help 选项。

命令行选项对于程序的操作总是可选的——这就是它们被称为选项的原因。

通过运行带有 --help 作为选项的“sum”程序,可以找到 --help 的真实示例。

# Text 1: A program's customary --help display.
$ sum --help
Usage: sum [OPTION]... [FILE]...
Print checksum and block counts for each FILE.

    -r 		   use BSD sum algorithm, use 1K blocks
    -s, --sysv use System V sum algorithm, use 512 bytes blocks
        --help display this help and exit
        --version output version information and exit
    
With no FILE, or when FILE is -, read standard input.

Report sum bugs to bug-coreutils@gnu.org
GNU coreutils home page: <http://www.gnu.org/software/coreutils/>
General help using GNU software: <http://www.gnu.org/gethelp/>
Report sum translation bugs to <http://translationproject.org/team/>

这是 --help 选项的常见外观。 当用户看到这种帮助时,他们会觉得很舒服,因为他们已经看到了许多其他程序中的通用布局。 帮助格式总是以一个 usage 行开始,然后是程序功能的简短描述,然后是每个程序选项的功能的简短描述。

方括号和点-点-点对您来说可能很陌生:它们的灵感来自 BNF。 方括号表示该术语是可选的,点-点-点表示该术语是可重复的。 用法行告诉我们 sum 程序有很多选项或者根本没有,还有很多文件,或者根本没有文件。

该程序提供标准的 --help 和 --version 选项以及特定于程序行为的其他选项。 我们可以看到 -s 是一个短选项,它等价于长选项 --sysv 。 还显示了一些额外信息,用于获取有关网络上 sum 程序的帮助。

命令行选项有多种形状和大小,本书稍后将向您展示 Argp 如何帮助您处理它们,以便您的程序以与命令行选项相关的标准方式显示和运行。


命令行参数

本节解释什么是命令行参数。

大多数命令行程序都接受参数和选项。 这些参数可以是文件名,如“cp”命令中所示。

# Text 2: Providing an option and two arguments to the cp command.
$ cp -v foo bar
`foo'->`bar'
$

在其他程序中,命令行参数可以是用户名、IP 地址、数字或您能想到的任何其他内容。 参数可以是强制性的,比如在“cp”命令中;也可以是完全可选的,比如在“ls”命令中。 如果程序没有获得正确数量的命令行参数,它将拒绝运行。 稍后我们将看到 Argp 如何让您(程序员)收集提供给您的程序的参数。

有时,一个参数看起来就像一个选项。 例如,也许您有一个名为“–foo”的目录并且您想删除它。 运行“rmdir --foo”将导致 rmdir 程序报告它没有 -foo 选项。 因为参数与长选项的形式无法区分,所以我们需要给程序一个额外的提示,我们为它提供一个参数而不是一个选项。 为了提供该提示,我们使用“–”表示此命令行上没有更多选项。 以下是如何使用该功能的说明:

# Text 3: Providing arguments that are indistinguishable from options.
$ mkdir --foo
mkdir: unrecognized option '--foo'
Try `mkdir --help' for more information.
$ mkdir -- --foo
$ ls
--foo
$ rmdir --foo
rmdir: unrecognized option '--foo'
Try `rmdir --help' for more information.
$ rmdir -- --foo

当参数中出现难以和选项区分的内容时,使用“–”是标准方法。默认情况下,Argp 向您的程序提供此功能。

在本书的后面,您将看到如何在支持 Argp 的程序中处理命令行参数。


选项的参数

本节介绍具有参数的命令行选项。

像程序一样,选项也可以带参数。 属于选项的参数可以像属于程序的参数一样多种多样。 一个选项可以有一个强制参数、一个可选参数或根本没有参数。本节是关于具有强制或可选参数的选项。

带有参数的选项的一个实际示例是在“fold”命令中。 我们来看看 fold 程序的 --help 显示了什么:

# Text 4: Help that depicts an option that has a mandatory argument.
$ fold --help
Usage: fold [OPTION]... [FILE]...
Wrap input lines in each FILE (standard input by default), writing to
standard output.

Mandatory arguments to long options are mandatory for short options too.
-b, --bytes count bytes rather than columns
-s, --spaces break at spaces
-w, --width=WIDTH use WIDTH columns instead of 80
    --help display this help and exit
    --version output version information and exit
    
Report fold bugs to bug-coreutils@gnu.org
GNU coreutils home page: <http://www.gnu.org/software/coreutils/>
General help using GNU software: <http://www.gnu.org/gethelp/>
Report fold translation bugs to <http://translationproject.org/team/>

在这里我们看到 --width 选项需要一个数字作为参数。 提供不带参数的 --width 选项是错误的,如果这样做,程序将无法运行。 您可以通过多种不同方式为 --width 选项提供参数:

# Text 5: Correct ways of providing mandatory arguments to options.
$ echo "hello there" | fold -w3
hel
lo
the
re
$ echo "hello there" | fold -w 4
hell
o th
ere
$ echo "hello there" | fold --width 5
hello
 ther
e
$ echo "hello there" | fold --width=6
hello
there

有几种无效的方法可以为选项提供强制参数。
例如:这些方法不起作用:

# Text 6: Incorrect ways of providing mandatory arguments to options.
$ fold --width5
fold: unrecognized option '--width5'
Try `fold --help' for more information.
$ fold -w=5
fold: invalid number of columns: `=5'

带有可选参数的短选项是一个值得注意的例子。 它们在野外很难找到; 但是在 GNU Make 的 -j 选项中存在一个:

# Text 7: A short option with an optional argument.
-j [N], --jobs[=N] Allow N jobs at once; infinite jobs with no arg.
-k, --keep-going Keep going when some targets can't be made.

这个 -j 选项(以及所有其他带有可选参数的短选项)值得注意的方面是它可以与 -k 组合在一起,但当 -k 跟在 -j 之后时则不能! 因此,“make -kj”工作正常,但“make -jk”不起作用,因为“k”被指定为作业数,这是无效的作业数。 我们没有任何提示可以让程序检测这种情况。 与短选项不同,带有可选参数的长选项需要等号将选项名称与参数值分开。

在本书的后面,我们将了解 Argp 如何让您创建和处理具有强制或可选参数的选项,以及它如何在默认情况下进行一些有用的错误检查。


第零步:你的第一个Argp程序

本节将向您展示如何编写、编译和运行最简单的 Argp 程序。

让我们开始吧。 将以下 C 代码剪切并粘贴到名为 step0.c 的文件中:

// Text 8: step0.c the simplest Argp-enabled program.
#include <stdio.h>
#include <argp.h>
int main (int argc, char **argv) {
	return argp_parse (0, argc, argv, 0, 0, 0);
}

Argp 工具使用 argp_parse 函数来启动解析。 现在我们传入 argc 和 argv,以便 Argp 可以了解传入的命令行。 其他参数启用其他功能,将在后面的部分中介绍。 我们包含了“argp.h”文件,以便我们的程序了解argp-ish的所有内容。

通过在 shell 中键入以下内容来编译程序:

# Text 9: Compiling the step0 program.
$ make step0
cc step0.c -o step0

现在您可以运行您的第一个启用 Argp 的程序。 让我们来看看你的新程序可以做什么!

# Text 10: Running the step0 program to see the help.
$ ./step0 --help
Usage: step0 [OPTION...]
-?, --help Give this help list
--usage Give a short usage message

极好的! 只需很少的努力,我们的 step0 程序就具有其他命令行程序的标准外观和感觉。 我们免费获得一个 --help 选项,还有另一个名为 --usage 的选项。 让我们尝试使用 --usage 选项运行程序:

# Text 11: Running the step0 program to see the usage.
$ ./step0 --usage
Usage: step0 [-?] [--help] [--usage]

使用选项显示了一个非常简短的帮助显示,这对于快速提醒程序具有哪些选项非常有用。 让我们看看我们的新程序还能做什么:

# Text 12: Having fun with the step0 program.
$ ./step0 foo
step0: Too many arguments
Try `step0 --help' or `step0 --usage' for more information.
$ ./step0 --us
Usage: step0 [-?] [--help] [--usage]
$ ./step0 --foo
./step0: unrecognized option '--foo'
Try `step0 --help' or `step0 --usage' for more information.
$ ./step0 --usage --help
Usage: step0 [-?] [--help] [--usage]
$ ./step0 --
$

此示例演示了有关 Argp 的以下内容:

  1. 它为我们的程序做一些自动的错误报告。 默认情况下,它根本不允许任何参数。
  2. 我们可以输入“–us”而不是“–usage”,它会产生相同的效果。
  3. 它为我们的程序不支持的选项做一些自动错误报告。
  4. 它在解析–usage(和–help)后退出。
  5. 它遵循在命令行上表示选项结尾的“–”约定。

这是一个无用的程序,但它比您预期的要多得多,因为它启用了 Argp。 为什么这个程序叫做step0? C 程序员通常必须从零开始计数,因此本书中的各个步骤(程序)都采用了约定。 在下一步中,我们将自己的选项添加到


第一步:添加一个短选项

在本节中,将演示 Argp语法中最常用的元素。 我们介绍了 argp 结构、argp_option 结构和解析器函数。

让我们在我们的程序中添加一个 -d 选项,它将在屏幕上显示一个句点。将以下 C 代码剪切并粘贴到您的编辑器中,并将其另存为 step1.c。

#include <stdio.h>
#include <argp.h>

static int parse_opt (int key, char *arg, struct argp_state *state){
  switch (key){
        case 'd': printf (".\n"); break;
    }
  return 0;
}

int main (int argc, char **argv){
    
    struct argp_option options[] = {
        { 0, 'd', 0, 0, "Show a dot on the screen"},
        { 0 }
    };

    struct argp argp = { options, parse_opt };
    return argp_parse (&argp, argc, argv, 0, 0, 0);
}

Argp 中最重要的数据类型是 struct argp 数据类型。 在这里我们可以看到它包含:

  1. 我们所有的选项(现在只有一个选项)
  2. 指向 Argp 将调用的函数的指针,以帮助解析我们的 -d 选项 (parse_opt)。

然后我们 struct argp 的地址作为第一个参数传递给 argp_parse 函数,让 Argp 知道我们闪亮的新 -d 选项。

我们的选项数组包含一个条目,后跟一个空的终止记录。 描述我们的 -d 选项的记录上有很多零(每个零表示该字段未使用),现在我们只使用第二个字段和第五个字段。 “d”表示它是我们正在创建的“-d”短选项,第 5 个字段包含要在 --help 中显示的选项的描述。 struct argp_option 的其他字段做其他事情,将在后面的部分中介绍。

parse_opt 函数可能会让初学者感到震惊,因为它没有可见的调用。 该功能似乎在与任何其他功能无关的空间中闲逛。 我们可以看到 parse_opt 函数名被赋予了 struct argp,但在程序中的任何地方都没有明显地调用它。 那是因为 Argp 正在代表我们调用我们的 parse_opt 函数,因为它处理我们传递给 argp_parseargcargv。 如果您要查看 argp 源,您会看到它反复调用 parse_opt ,直到完成对命令行的处理Argp不仅为我们的 -d 选项调用 parse_opt,它还为命令行上的每个选项和参数调用 parse_opt

回调函数是 C 程序中经常使用的机制。 我们的 parse_opt 函数是 Argp 的回调函数。 Argp 决定它的回调函数的签名(function signature,是函数的声明信息,包括参数、返回值、调用约定之类)是什么,作为程序员,我们努力实现一个带有该签名的函数。 在这种情况下, parse_opt 函数签名具有三个参数:

  1. 选项的唯一键,可以是短选项“d”。 存在其他非字符键,我们将在后面的部分中演示它们。
  2. 选项参数的值,为字符串格式。 我们的"d"选项不带参数,所以我们在回调函数中忽略这个参数。
  3. 在我们反复迭代回调函数时保持 Argp 状态的变量。 虽然在本程序中没有用到,但在以后的程序中会用到。

最后,当一切正常时回调函数返回零,非零将停止 Argp 并导致 argp_parse 函数返回该非零错误值。

当函数名被赋值给struct argp时,它实际上是将指针赋值给函数在内存中的存在位置。 这类似于数组的名称是指向数组第一个元素的指针。

让我们编译这个程序:

# Text 14: Compiling the step1 program.
$ make step1
cc step1.c -o step1

现在在 shell 中运行它以查看帮助:

# Text 15: Running the step1 program to see the help.
$ ./step1 --help
Usage: step1 [OPTION...]
Usage: step1 [OPTION...]

  -d                         Show a dot on the screen
  -?, --help                 Give this help list
      --usage                Give a short usage message
$

我们再来看下-d选项是否工作了:

Text 16: Having fun with the step1 program.
$ ./step1 -d
.
$ ./step1 -ddd -d
.
.
.
.
$

万岁! 有用。

现实生活中的例子:尽管这是一个非常简单的程序,但它绝非无用。没有参数的短选项出现在您系统上的无数程序中! 查看“rm”命令中的“-f”选项。 或者查看“ls”命令的“-l”选项。 它无处不在,现在您了解了如何使您自己的程序提供与 Argp 类似的选项的基础知识

在下一步中,我们将向 -d 选项添加一个强制参数。


第二步:短选项接受参数

在本节中,将描述 argp_option 结构的参数成员,并演示回调函数的 arg 参数。

让我们为我们的**-d 选项添加一个参数**,这样它就可以产生许多点而不是一个。 将以下 C 代码剪切并粘贴到您的编辑器中,并保存一个名为 step2.c 的文件。

#include <stdio.h>
#include <argp.h>

static int parse_opt (int key, char *arg, struct argp_state *state){
    switch (key){
        case 'd': {
            unsigned int i;
            for (i = 0; i < atoi (arg); i++)
                printf ("."); 
            printf ("\n");
            break;
        }
    }
  return 0;
}

int main (int argc, char **argv) {
    struct argp_option options[] = {
        { 0, 'd', "NUM", 0, "Show some dots on the screen"},
        { 0 }
    };

    struct argp argp = { options, parse_opt, 0, 0 };
    return argp_parse (&argp, argc, argv, 0, 0, 0);
}

我们现在使用结构 argp_option 中的第三个字段。 它表示此 -d 选项有一个参数,并且该参数在 --help 和 --usage 显示中应称为“NUM”。 如果我们将此值改回 0(或者 NULL),-d 选项将停止接受强制参数。

parse_opt 回调函数中,我们使用了 arg参数。 它以字符串的形式出现在我们(程序员)面前,并且 arg的值保证不会为 NULL,因为 Argp 不会让 -d 在没有参数的情况下被调用。 更好的程序会对 arg 的内容进行更多的有效性检查(恶意用户可以向我们的 -d 选项提供随机文本),但出于说明目的,将省略有效性检查。

arg 变量指向我们没有分配的内存,试图释放它是一个非常糟糕的主意。 您可以期望 arg 值指向的内存在程序执行期间保持可访问。 arg 参数指向的内存在回调函数完成后不会消失。 在我们的例子中,我们只需要在执行回调函数时访问 arg 的值。

让我们试一试我们的新程序。 编译它,显示程序的帮助,并尝试新的和改进的 -d 选项:

# Text 18: Compiling step2.c and take it for a spin.
$ make step2
cc step2.c -o step2

$ ./step2 --help
Usage: step2 [OPTION...]

    -d NUM Show some dots on the screen
    -?, --help Give this help list
    	--usage Give a short usage message

$ ./step2 -d1 -d2 -d 3
.
..
...

$ ./step2 -d
./step2: option requires an argument -- 'd'
Try `step2 --help' or `step2 --usage' for more information.

$ ./step2 --usage
Usage: step2 [-?] [-d NUM] [--help] [--usage]
$

我们可以看到 Argp 正在为我们做一些错误检查,并且它会自动构建帮助和使用显示以包含新的实际示例:短选项的强制参数经常出现在您系统的许多程序中。 看看“head”程序的“-n”选项(该选项需要显示在文件中的最上面的行数。)我们已经看到了“fold”程序的“-w”选项。 Argp 使得向您的程序添加带有强制参数的简短选项变得微不足道。

在下一步中,我们将使我们的程序接受一个长选项。


第三步:短选项等价的长选项

在本节中,argp_option 结构的第一个字段用于使程序接受长选项。

让我们让 -d 选项有一个长选项,称为 --dot。 将以下 C 代码剪切并粘贴到您的编辑器中,并保存一个名为 step3.c 的文件。

// Text 19: step3.c : an Argp-enabled program with a long option that has a mandatory argument.

#include <stdio.h>
#include <stdlib.h>
#include <argp.h>

static int parse_opt (int key, char *arg, struct argp_state *state)
{
    switch (key){
        case 'd': {
            unsigned int i;
            for (i = 0; i < atoi (arg); i++)
                printf ("."); 
            printf ("\n");
            break;
        }
    }
  return 0;
}

int 
main (int argc, char **argv)
{
    struct argp_option options[] = 
        {
        { "dot", 'd', "NUM", 0, "Show some dots on the screen"},
        { 0 }
    };

    struct argp argp = { options, parse_opt, 0, 0 };
    return argp_parse (&argp, argc, argv, 0, 0, 0);
}

通过这一小改动,我们的程序现在接受 --dot long 选项。struct argp_option 的第一个字段控制长选项的名称。

如果在选项名称中添加空格(或换行符、制表符或不可打印字符),将会发生不好的事情,所以不要这样做。

让我们试试看:

# Text 20: Compiling and running the step3 program.
➜ ./step3 --help 
Usage: step3 [OPTION...]

  -d, --dot=NUM              Show some dots on the screen
  -?, --help                 Give this help list
      --usage                Give a short usage message

Mandatory or optional arguments to long options are also mandatory or optional
for any corresponding short options.
➜  ./step3 --dot 1 --dot=2 -d3                                       
.
..
...
➜  ./step3 --dot3             
./step3: unrecognized option '--dot3'
Try `step3 --help' or `step3 --usage' for more information.
➜  ./step3 --dot 
./step3: option '--dot' requires an argument
Try `step3 --help' or `step3 --usage' for more information.
➜  ./step3 --do 12
............
➜  ./step3 --usage
Usage: step3 [-?] [-d NUM] [--dot=NUM] [--help] [--usage]

这里我们可以看到帮助显示现在包含了 --dot 选项,它以长选项的标准方式处理。 我们可以看到 Argp 免费提供了一些长选项错误检查(无需程序员做任何事情)。 我们还可以看到我们的程序接受了 --do long 选项。这是用户如何使用长选项走捷径的演示。--do 选项有效,因为没有其他以“do”开头的长选项。 如果有两个以“do”开头的选项,并且我们尝试使用 -do 快捷方式,程序将拒绝运行并向您显示有关选项名称不明确的错误。 这一切都发生在 Argp 中,您不必在你的程序中做任何其他的事情。

帮助显示底部有一个相当冗长的通知。 通知的意思是“NUM”也是短 -d 选项的参数。 Argp 将通知放在那里是因为以这种方式格式化的帮助会给某些人留下 -d 不接受参数的印象。 有经验的用户发现冗长的通知是多余的,您可以通过在 shell 的 ARGP_HELP_FMT 环境变量中添加文本“no-dup-args-note”来关闭它:

Text 21: Turning off the long options/short options argument notice in the bash shell.

$ export ARGP_HELP_FMT="no-dup-args-note"
$ ./step3 --help
Usage: step3 [OPTION...]
    
    -d, --dot=NUM Show some dots on the screen
    -?, --help Give this help list
    	--usage Give a short usage message

尽管对于有经验的用户禁用此通知会很好,但此通知对新用户仍然有用。 这就是 Argp 默认开启的原因,并将此通知翻译成无数不同的口语。

实际示例:“head”程序中的 --lines 选项是一个长选项,相当于 -n 短选项。 通过阅读长选项列表更容易理解程序,并且它们更容易记住。 最重要的是,长选项使用起来更直观。 现在,您也可以制作自己的描述性长选项,这些选项具有短选项等效项。

在下一步中,我们将使我们的程序接受带有可选参数的长选项。


第四步:可选参数

在本节中,我们将使用 argp_option 结构的第 4 个字段使选项接受可选参数。

让我们将 –dot 选项的 NUM 参数设为可选。 将以下 C 代码剪切并粘贴到您的编辑器中,并保存一个名为 step4.c 的文件。

#include <stdio.h>
#include <stdlib.h>
#include <argp.h>

static int parse_opt (int key, char *arg, struct argp_state *state)
{
    switch (key){
        case 'd': {
            unsigned int i;
            unsigned int dots = 0;
            if (arg == NULL)
                dots = 1;
            else
                dots = atoi (arg);
            for (i = 0; i < dots; i++)
                printf ("."); 
            printf ("\n");
            break;
	    }
    }
  return 0;
}

int main (int argc, char **argv){
    struct argp_option options[] = {
        { "dot", 'd', "NUM", OPTION_ARG_OPTIONAL, "Show some dots on the screen"},
        { 0 }
    };
    
    struct argp argp = { options, parse_opt, 0, 0 };
    return argp_parse (&argp, argc, argv, 0, 0, 0);
}

struct argp_option 的第四个字段是一组改变我们选项工作方式的标志。 OPTION_ARG_OPTIONAL 标志只是此按位字段接受的众多标志之一。 其他标志将在后面的部分中显示。

我们的回调函数 parse_optarg 参数现在可以作为 NULL 出现在我们面前,这意味着 -d--dot 选项是在没有参数的命令行上指定的。 当 arg 以非 NULL 的形式出现在我们面前时,它与上一步操作相同。

让我们以 step4 程序为例:

// Text 23: Comping and running the step4 program.

$ make step4
cc step4.c -o step4

$ ./step4 --help
Usage: step4 [OPTION...]
-d, --dot[=NUM] Show some dots on the screen
-?, --help Give this help list
--usage Give a short usage message

$ ./step4 --usage
Usage: step4 [-?] [-d[NUM]] [--dot[=NUM]] [--help] [--usage]

$ ./step4 -d --dot=3 --dot
.
...
.
$ ./step4 --dot 3   # notice <-----
.

$ ./step4 -dd
$

在这里我们可以看到帮助和用法显示现在显示了我们的 --dot 选项的可选参数。 我们还可以看到该程序可以接受 --dot 选项的参数,并且它在没有参数的情况下也能正常运行。 最后两个案例不会像您预期的那样运行。你能弄清楚为什么吗?

  1. 在第一种情况下,似乎无法识别“3”的参数,并且只显示一个点。 为什么会这样? 带有可选参数的长选项需要在选项和参数值之间使用 = 符号。 “3”最终成为程序的参数,但 Argp 不会抱怨这种情况,因为如果我们有回调函数,我们应该处理参数。 程序的参数将在后面的部分中处理。

  2. 在第二种情况下,短选项似乎与另一个短选项成功组合在一起。 毕竟,没有给出错误信息。 带有可选参数的短选项不能组合在一起。 第二个“d”实际上是给 -d 选项的参数。 对“atoi”的调用将“d”转换为 0,这会导致例程根本不显示点,然后是换行符。

实际示例:如果您想查看带有可选参数的长选项,请尝试查看“cp”程序的“–backup[=CONTROL]”选项。 让您的选项接受可选参数很容易。

练习:你能写一个程序,使用 strtoul 而不是 atoi 来检查 arg 字符串的有效性吗?

在下一步中,将演示选项别名。


第五步:选项别名

在本节中,将演示 argp_option 结构的第 4 个字段的另一个选项标志:OPTION_ALIAS标志

这是 step5 程序。 你知道该做什么!

#include <stdio.h>
#include <stdlib.h>
#include <argp.h>

static int parse_opt (int key, char *arg, struct argp_state *state){
  switch (key){
        case 'd': {
            unsigned int i;
            unsigned int dots = 0;
            if (arg == NULL)
                dots = 1;
            else
                dots = atoi (arg);
            for (i = 0; i < dots; i++)
                printf ("."); 
            printf ("\n");
            break;
        }
    }
  return 0;
}

int main (int argc, char **argv){

    struct argp_option options[] = {
        { "dot", 'd', "NUM", OPTION_ARG_OPTIONAL, "Show some dots on the screen"},
        { "period", 0, "FOO", OPTION_ALIAS, "Bar" },
        { 0 }
    };

    struct argp argp = { options, parse_opt, 0, 0 };
    return argp_parse (&argp, argc, argv, 0, 0, 0);
}

我们在我们的程序中添加了一行,它将为我们提供一个长选项--period,相当于--dotOPTION_ALIAS标志使选项继承前一个选项的所有字段,但长选项名称(第一个字段)和键(第二个字段)除外。 您可以拥有任意数量的别名。 其他一些字段已填充为虚拟值说明它们实际上被 Argp 忽略了。

让我们看看我们新的 --period 选项是如何工作的。

Text 25: Compiling and running the step5 program.
$ make step5
cc step5.c -o step5
$ ./step5 --help
Usage: step5 [OPTION...]
-d, --dot[=NUM], --period[=NUM]
Show some dots on the screen
-?, --help Give this help list
--usage Give a short usage message
$ ./step5 --usage
Usage: step5 [-?] [-d[NUM]] [--dot[=NUM]] [--period[=FOO]] [--help]
[--usage]
$ ./step5 --period
.
$ ./step5 --period=4
....

我们可以看到“FOO”和“Bar”字符串被正确忽略并且没有出现在帮助显示中。 新的 --period long 选项出现在 --dot 选项旁边,因为它完全等同于它。 Argp完成了安排帮助和使用显示以适应我们的新选项别名的繁重工作,这样您就不必这样做了。


第六步:回调函数

在本节中,我们将从回调函数中调用回调函数,以便我们可以实现一个全新的选项。

让我们创建一个新的长选项 --ellipsis,在屏幕上显示三个点。 它在功能上等同于 --dot=3,我们将从 parse_opt 中调用 parse_opt 来实现它。

#include <stdio.h>
#include <stdlib.h>
#include <argp.h>

static int parse_opt (int key, char *arg, struct argp_state *state){
    switch (key){
        case 'd': {
            unsigned int i;
            for (i = 0; i < atoi (arg); i++)
                printf ("."); 
            printf ("\n");
            break;
        }
        case 777:
            return parse_opt ('d', "3", state);
        }
    return 0;
}

int main (int argc, char **argv){

    struct argp_option options[] = {
        { "dot", 'd', "NUM", 0, "Show some dots on the screen"},
        { "ellipsis", 777, 0, 0, "Show an ellipsis on the screen"},
        { 0 }
    };

    struct argp argp = { options, parse_opt, 0, 0 };
    return argp_parse (&argp, argc, argv, 0, 0, 0);
}

我们可以看到我们新的 --ellipsis 选项不带参数,而且它有一个非常奇怪的键“777”。 struct argp_option 的第二个字段(关键字段)是特殊的,因为 Argp 会自动检测它是否是要在短选项中使用的可见字符。 777 不是可打印的字符,这意味着 --ellipsis 选项将是一个长选项,没有等效的短选项

我们还可以看到 parse_opt 函数是从内部调用的。这是执行其他选项的最简单方法。 因为我们希望省略号选项在屏幕上显示三个点,所以我们使用 ‘d’ 键调用 parse_opt 来激活 --dot选项,并将 3 作为字符串作为 --dot 选项的参数传递。 我们还传递状态变量,只是因为它是回调函数的必需参数。

让我们运行该程序,看看它是如何运行的。

# Text 27: Compiling and running the step6 program.
$ make step6
cc step6.c -o step6

$ ./step6 --help
Usage: step6 [OPTION...]
    -d, --dot=NUM Show some dots on the screen
    	--ellipsis Show an ellipsis on the screen
    -?, --help Give this help list
    	--usage Give a short usage message
    	
$ ./step6 --usage
Usage: step6 [-?] [-d NUM] [--dot=NUM] [--ellipsis] [--help] [--usage]

$ ./step6 --ellipsis
...
$ ./step6 --dot 3
...
$

我们无法将这个新的 --ellipsis 选项设为别名,因为它没有参数。 相反,我们创建了一个新选项并让它执行我们旧的 --dot 选项,参数为 3。帮助和用法显示显示了新选项,我们可以看到新选项正常工作。 我们还可以看到ellipsis选项没有等价的短选项。

实际示例:这种“选项等效性”在命令行程序中相当频繁地使用。 如果您查看“cp”程序中的 --archive 选项,您会发现它等效于以下选项:“-dR –preserve-all”。 现在轮到你知道在您自己的程序中创建选项等效项是多么容易!

在下一步中,我们将处理程序参数。


第七步:每个参数处理前操作&所有参数处理后操作&向回调函数传地址&命令行顺序处理

在本节中,我们将演示回调函数中的 ARGP_KEY_ARGARGP_KEY_END 键,并且我们还将说明 argp_parse 的第 6 个参数的使用,argp_parse 是用户提供的回调函数数据挂钩。

让我们为我们的程序添加对一到四个参数的支持。 这里的想法是如果没有得到正确数量的参数,程序将报告错误。

#include <stdio.h>
#include <stdlib.h>
#include <argp.h>

static int parse_opt (int key, char *arg, struct argp_state *state)
{
    int *arg_count = state->input;
    switch (key){
            case 'd': {
                unsigned int i;
                for (i = 0; i < atoi (arg); i++)
                    printf ("."); 
            }
            break;
            case 777:
                return parse_opt ('d', "3", state);
            case ARGP_KEY_ARG:{ // 处理每个参数之前,进行的处理
                (*arg_count)--;
                if (*arg_count >= 0)
                    printf (" %s", arg);
            }    
            break;
            case ARGP_KEY_END:{ // 所有参数处理完之后,进行的处理
                printf ("\n");
                if (*arg_count >= 4)
                    argp_failure (state, 1, 0, "too few arguments");
                else if (*arg_count < 0)
                    argp_failure (state, 1, 0, "too many arguments");
            }
            break;
        }
    return 0;
}


int  main (int argc, char **argv){
    
    struct argp_option options[] = {
        { "dot", 'd', "NUM", 0, "Show some dots on the screen"},
        { "ellipsis", 777, 0, 0, "Show an ellipsis on the screen"},
        { 0 }
    };

    int arg_count = 4;
    struct argp argp = { options, parse_opt, "WORD [WORD [WORD [WORD]]]"};
    return argp_parse (&argp, argc, argv, ARGP_IN_ORDER, 0, &arg_count);
}

在这里我们可以看到我们使用了两个特殊的 ARGP_KEY 键。 每当遇到程序的参数时,Argp 都会传入 ARGP_KEY_ARG,并将 arg 参数设置为指向该参数。 当 Argp 处理完最后一个参数将 ARGP_KEY_END 传递给我们的回调函数

Argp 提供了一种称为 argp_failure 的错误报告工具,当解析命令行出现问题时将使用该工具。 当我们使用这个工具时,我们的错误消息具有标准的外观和感觉。

state 参数显示有一个input字段,我们以双重用途的方式使用它:

  1. 知道有多少个参数是正确的参数数量。
  2. 知道我们在命令行上得到了多少参数。

Argp 还在结构体 argp_state 中跟踪到目前为止我们已经处理了多少个参数。 有一个名为“arg_num”的成员可以准确报告此信息。

state 参数的input字段,是传递给 argp_parse 的值。让我们看一下 main 函数,这样我们就可以看到发生了这种情况:

int  main (int argc, char **argv){
    
    struct argp_option options[] = {
        { "dot", 'd', "NUM", 0, "Show some dots on the screen"},
        { "ellipsis", 777, 0, 0, "Show an ellipsis on the screen"},
        { 0 }
    };

    int arg_count = 4;
    struct argp argp = { options, parse_opt, "WORD [WORD [WORD [WORD]]]"};
    return argp_parse (&argp, argc, argv, ARGP_IN_ORDER, 0, &arg_count);
}

而这里我们可以看到,我们确实传入了一个int的指针作为argp_parse函数的第6个参数,它指向的值为4。在 C 程序中使用回调函数时,几乎总有一种方法可以将您自己的数据块传递给回调函数。 argp_parse 函数的第 6 个参数是一种将您想要的任何数据传递到回调函数中的方法。 对于使用 Argp 的命令行处理,此值通常是一个结构,其中包含可由选项设置的标志。

我们还可以看到 struct argp 有更多我们认为的字段。 struct argp 的第三个字段包含我们在此命令行上期望的参数的符号。 在这种情况下,我们期望正好有 4 个参数,所以我们这样说。 对于kicks,我们将称它们为“words”,因为它符合程序的句子构造主题。

让我们试一试这个程序吧!

➜ ./step7 --help
Usage: step7 [OPTION...] WORD [WORD [WORD [WORD]]]

  -d, --dot=NUM              Show some dots on the screen
      --ellipsis             Show an ellipsis on the screen
  -?, --help                 Give this help list
      --usage                Give a short usage message

Mandatory or optional arguments to long options are also mandatory or optional
for any corresponding short options.

➜ ./step7 --usage   
Usage: step7 [-?] [-d NUM] [--dot=NUM] [--ellipsis] [--help] [--usage]
            WORD [WORD [WORD [WORD]]]
            
➜  ./step7 once upon a time
 once upon a time
 
➜ ./step7                 
step7: too few arguments

➜  ./step7 foo bar bza     
 foo bar bza
 
➜ ./step7 once upon a time --ellipsis
 once upon a time...
 
➜  ./step7 --ellip once upon a time  
... once upon a time

➜  ./step7 one two three four five      
 one two three four
step7: too many arguments

首先我们可以看到我们的参数符号已经被纳入了帮助和用法显示中,它表明用户可以给这个程序一个、两个、三个或四个单词,但不能没有或超过四个单词 。

当我们使用四个参数运行它时,单词会按照我们的预期打印出来。

当我们不带参数运行它时,我们会得到一个换行符,然后是一条错误消息。 您能通过查看回调函数了解为什么会出现这种情况吗?

练习:您将如何更改回调函数,以便仅在存在正确的数字参数时才显示换行符? 您可以删除程序显示的第一个单词之前的空格吗?

当我们在命令行末尾使用 --ellipsis 长选项运行程序时,“…”出现在句子的开头而不是结尾。 这是正常的 Argp 行为:选项总是在参数之前解析。 如果这是不可接受的,您可以通过将 ARGP_IN_ORDER 传递到 argp_parse 函数的按位标志参数(第 4 个参数)中,使 Argp 遵守命令行选项的顺序

最后,我们可以看到我们的程序有另一个错误:当我们向程序传递五个参数时,它仍然显示前四个单词,然后显示错误消息。 在以后的步骤中,我们将修复此错误。

实际示例:有时在命令行上接受有限数量的参数很有用。 试着看看“ln”程序。 你可以看到它需要一个或两个参数,就像我们的程序需要一个、两个、三个或四个参数一样。 现在您可以看到如何使启用 Argp 的程序在没有获得正确数量的命令行参数时发出错误消息。

在下一步中,我们将制作一个完全拒绝接受任何命令行参数的程序,并探索隐藏选项的概念。


第八步:隐藏选项

在本节中,我们将制作一个程序,当命令行有任何参数时显示错误消息,我们还将演示另一个选项标志,使选项不可见的 OPTION_HIDDEN 标志。

让我们将程序的主题从句子构造更改为莫尔斯电码主题。 这将缩小我们的程序,但我们将添加一个 --dash 选项,我们将修改 --ellispis 长选项以使其起作用,但在帮助和用法显示中不可见。 程序根本不接受任何参数,就像我们的 step0 程序一样。

#include <stdio.h>
#include <argp.h>

static int
parse_opt (int key, char *arg, struct argp_state *state)
{
    switch (key){
        case 'd': {
            unsigned int i;
            unsigned int dots = 1;
            if (arg != NULL)
                dots = atoi (arg);
            for (i = 0; i < dots; i++)
                printf ("."); 
            break;
        }
            break;
        case 888:
            printf ("-");
            break;
        case 777:
            return parse_opt ('d', "3", state);
        case ARGP_KEY_ARG:
            argp_failure (state, 1, 0, "too many arguments");
            break;
        case ARGP_KEY_END:
            printf ("\n");
            break;
    }
    return 0;
}

int 
main (int argc, char **argv)
{
    struct argp_option options[] = {
        { "dot", 'd', "NUM", OPTION_ARG_OPTIONAL, "Show some dots on the screen"},
        { "ellipsis", 777, 0, OPTION_HIDDEN, "Show an ellipsis on the screen"},
        { "dash", 888, 0, 0, "Show a dash on the screen" },
        { 0 }
    };

    struct argp argp = { options, parse_opt };
    return argp_parse (&argp, argc, argv, 0, 0, 0);
}

我们修改了--dot选项,让它接受一个可选参数。当我们得到一个参数时,程序停止。

我们可以看到我们添加了一个名为--dash 的新 long 选项,并且我们给了 --dot 选项一个可选参数。 我们还可以看到,我们不再将数据挂钩传递给 argp_parse 函数。 此处显示的新内容是 --ellipsis 选项上的 OPTION_HIDDEN 标志。 这将使该选项继续运行,但不会显示在帮助或用法显示中。

如果没有被过度使用,隐藏选项会很有用。 一个好的方面是隐藏选项不会使 --help 显示混乱,并且可以在程序手册中完整记录。 有时,让程序看起来易于使用比显示五个执行相同操作的选项别名更重要。 也许特定的隐藏选项仅用于古老的兼容性目的。 我们希望接受该选项以确保兼容性,但我们并不真的想宣传它。

隐藏选项的一个糟糕方面是,它们可能会干扰较长的选项快捷方式。假设我们有一个名为--ellipsis的隐藏选项和一个名为--elation的可见选项,用户将无法输入“–el”来激活--elation长选项。隐藏的--ellipsis选项会混淆问题(Argp不知道用户指的是哪个选项),即使用户不能很容易地在帮助或使用显示中看到这个选项。

隐藏选项需要在效用和信息管理之间取得平衡相对于易用性。让我们来看看新的隐藏选项的运行情况。

Text 33: Compiling and running the step8 program.
$ make step8
cc step8.c -o step8
$ ./step8 --help
Usage: step8 [OPTION...]
-d, --dot[=NUM] Show some dots on the screen
--dash Show a dash on the screen
-?, --help Give this help list
--usage Give a short usage message
$ ./step8 --usage
Usage: step8 [-?] [-d[NUM]] [--dot[=NUM]] [--dash] [--help] [--usage]
$ ./step8 --ellipsis
...
$ ./step8 -d -d --dot --dash --dash --dash
...---
$ ./step8 --dash --
-
$ ./step8 --d
./step8: option '--d' is ambiguous
Try `step8 --help' or `step8 --usage' for more information.
$ ./step8 dot
step8: too many arguments
$

正如预期的那样,--ellipsis选项没有显示在帮助和使用显示中,但它作为一个有效选项发挥作用。

还演示了模糊选项快捷方式的情况。 程序拒绝运行,因为 Argp 无法确定“–d”是指--dash 还是--dot

现实生活中的例子:有些程序根本不接受程序参数。 看看“tty”程序就可以找到一个真实的例子。 还有一些程序有不可见的选项,这些选项只记录在他们的手册中。 尝试查看“gdb”命令,它是“-c”选项; 它等同于 --core但它没有显示在 gdb 的帮助显示中

现在你知道如何用Argp制作你自己的隐藏选项!去吧并添加一个复活节彩蛋到您的下一个程序。在下一步中,我们将进行填充我们的帮助显示一个版本选项和错误的电子邮件地址


第九步:版本显示全局变量&bug报告地址全局变量

在本节中,将演示 argp_program_versionargp_program_bug_address 全局变量。 除此之外,argp 结构的第四个字段(docs 字段)将用于向帮助显示添加程序描述。 将演示回调函数中的新 argp 键:ARGP_KEY_INIT键。

让我们对我们的程序进行最后的润色。 我们会给它一个标准的 --version 选项,我们会告诉人们这个程序做了什么,以及在哪里发送他们的错误报告。 我们还将向该程序添加第二个用法,以说明运行该程序的替代方法。 为了使这个程序更实用,我们将在完成解析后处理命令行参数。

#include <stdio.h>
#include <argp.h>
#include <argz.h>
#include <stdlib.h>

const char *argp_program_bug_address = "someone@example.com";
const char *argp_program_version = "version 1.0";
 
struct arguments
{
  char *argz;
  size_t argz_len;
};

static int
parse_opt (int key, char *arg, struct argp_state *state)
{
    struct arguments *a = state->input;
    switch (key){
        case 'd': {
			unsigned int i;
			unsigned int dots = 1;
			if (arg != NULL)
				dots = atoi (arg);
			for (i = 0; i < dots; i++)
				printf ("."); 
			break;
        }
        case 888:
			printf ("-");
			break;
        case ARGP_KEY_ARG:
			argz_add (&a->argz, &a->argz_len, arg);
			break;
        case ARGP_KEY_INIT:
			a->argz = 0;
			a->argz_len = 0;
			break;
        case ARGP_KEY_END:{
			size_t count = argz_count (a->argz, a->argz_len);
			if (count > 2)
				argp_failure (state, 1, 0, "too many arguments");
			else if (count < 1)
				argp_failure (state, 1, 0, "too few arguments");
        }
        break;
      }
    return 0;
}

int main (int argc, char **argv)
{
    struct argp_option options[] = {
        { "dot", 'd', "NUM", OPTION_ARG_OPTIONAL, "Show some dots on the screen"},
        { "dash", 888, 0, 0, "Show a dash on the screen" },
        { 0 }
    };

    struct argp argp = { options, parse_opt, "WORD\nWORD WORD", 
      "Show some dots and dashes on the screen.\v"
        "A final newline is also shown regardless of whether any options were given." };

    struct arguments arguments;
    if (argp_parse (&argp, argc, argv, 0, 0, &arguments) == 0){
        const char *prev = NULL;
        char *word;
        while ((word = argz_next (arguments.argz, arguments.argz_len, prev))){
          printf (" %s", word);
          prev = word;
        }
        printf ("\n");
        free (arguments.argz);
      }
    return 0;
}

Argp 在它的 API 中有一些全局变量,这里有两个。 如果设置了 argp_program_version 变量,那么我们的程序中将包含一个 --version long 选项和一个 -V short 选项,它将显示我们为变量设置的任何字符串,然后退出。 如果设置了 argp_program_bug_address 变量,它将修改帮助显示以包含“Report bugs to: foo@bar.”形式的消息。 程序能够报告它的版本以及应该将错误发送到哪里是一种很好的风格。

我们还可以看到一个叫做 argz 的东西。 Argz 是另一个源自 GNU 标准 C 库的工具。 当我们在命令行上遇到参数时,我们将使用这个工具来累积参数。 struct arguments将保存我们的参数,我们将其设置为 argp_parse 函数的输入数据挂钩。 在程序中有 struct arguments一个好主意。它包含在遇到选项时设置的标志,以及参数。 包含 stdlib.h 文件是因为 argz 向量是 malloc 的,我们想使用 free 函数。

此回调函数中的新元素是 ARGP_KEY_INIT。 它在任何解析发生之前被传递到回调函数中。 这里我们使用它来初始化我们的结构参数。

当我们完成收集参数时(在 ARGP_KEY_END 情况下),我们检查我们是否有正确数量的参数。 在这个程序中,正确的参数数量正好是 1 个或正好是 2 个参数。 有关 argz 工具的更多信息,请在 shell 中键入“info argz”

在这个主函数中显示的一个新的Argp元素是 struct argp。它有一个特殊的“\v”字符,这是一个垂直标签。选项卡之前的所有内容都将显示在帮助显示,而字符串的其余部分将显示在选项。这个变量的目的有两个:

  1. 对程序作简短的描述。
  2. 为了进一步描述程序的选项或操作。

此主函数中显示的另一个新 Argp 元素是结构 argp(args_doc 字段)的第三个字段中的换行符。 这是另一种方式,来告诉用户还有其他方法可以运行该程序。

让我们试试我们的新程序。

➜  ./step9 --help
Usage: step9 [OPTION...] WORD
  or:  step9 [OPTION...] WORD WORD
Show some dots and dashes on the screen.

  -d, --dot[=NUM]            Show some dots on the screen
      --dash                 Show a dash on the screen
  -?, --help                 Give this help list
      --usage                Give a short usage message
  -V, --version              Print program version

Mandatory or optional arguments to long options are also mandatory or optional
for any corresponding short options.

A final newline is also shown regardless of whether any options were given.

Report bugs to someone@example.com.

➜  ./step9 --usage
Usage: step9 [-?V] [-d[NUM]] [--dot[=NUM]] [--dash] [--help] [--usage]
            [--version] WORD
  or:  step9 [OPTION...] WORD WORD

➜  ./step9 foo bar
 foo bar

➜  ./step9 foo bar baz
step9: too many arguments

➜  ./step9 foo --dash -d
-. foo

➜  ./step9 --version
version 1.0

在这个演示中,我们可以看到这个帮助输出有两种用法。许多命令的帮助输出中会有许多不同的用法行。以“ln”命令为例;它有4种用法。Argp使它很容易为您的程序指定备用用法。

帮助输出在选项描述下面有一些解释性文本。不要让你的帮助输出时间过长,这是一个很好的练习。复杂的解释不属于你的帮助输出;它们属于手册或用户指南。

我们可以看到程序正确处理了参数过多或过少的情况。 我们还可以看到它以 argp 方式显示错误消息。

最后我们可以看到程序在显示选项后显示参数 (foo)。 这是因为我们在 argz 向量中收集参数并在解析命令行后显示它们。 如果我们将 ARGP_IN_ORDER 传递给 argp_parse 函数,它将对输出没有影响。

获取您正在使用的程序版本的方法不止一种。 有些人可能会告诉您使用系统上的包管理器来查看您正在运行的程序版本。 如果您正在创建一个新程序,您有义务为您的程序提供一个权威版本。 一些程序员甚至从他们使用的版本编号方案中获得了一定的乐趣。 作为程序的作者,您是打包者的上游,并且拥有决定程序将具有的版本号的专有特权。 如果您认为合适,下游的打包程序(例如 Red Hat 和 Ubuntu)会重新分发您的程序。 像 gcc 这样的重要程序被 GNU+Linux 发行版打了很多补丁,以至于当在命令行上给出 --version 选项时,它们会修改显示的版本! 好消息是您的版本几乎总是保持不变,并且修改的形式是通知此版本的 gcc 是由 Red Hat 在给定日期修改和构建的。

argp_program_version 变量将程序员限制为一个简单的字符串。您可以使用自己的函数作为版本选项,而是定义一个类似于:void foo(FILE*, struct argp_state *) 的函数并将其称为 argp_program_version_hook

练习:尝试修改第9步程序,使其具有argp_program_version_hook函数而不是argp_program_version变量。

现在,您已经掌握了足够的知识,可以为您的程序提供一些选项参数。在这一点上休息一下是一个好主意,并检查命令行选项,然后看看是否可以创建这和他们的很像。本书中的步骤现在将更加先进和复杂。

在下一步中,我们将使用“选项组”,以使我们的帮助输出在有许多选项时更具可读性。


第十步:参数分组

在本节中,我们将探讨 struct argp_option 的第 6 个也是最后一个字段,即 group 字段。 我们将看到如何更改帮助输出中选项的顺序,以及如何排列选项集,使它们在帮助输出中显示为一个集群

通过将我们的 --dash--dot 选项放入它们自己的组中,让我们的帮助输出更具可读性

#include <stdio.h>
#include <argp.h>
#include <argz.h>
#include <stdlib.h>

const char *argp_program_bug_address = "someone@example.com";
const char *argp_program_version = "version 1.0";
 
struct arguments
{
  char *argz;
  size_t argz_len;
};

static int
parse_opt (int key, char *arg, struct argp_state *state)
{
    struct arguments *a = state->input;
    switch (key){
        case 'd': {
            unsigned int i;
            unsigned int dots = 1;
            if (arg != NULL)
                dots = atoi (arg);
            for (i = 0; i < dots; i++)
                printf ("."); 
            break;
        }
        case 888:
            printf ("-");
            break;
        case 999:
            parse_opt ('d', "3", state);
            printf (" ");
            parse_opt (888, NULL, state);
            parse_opt (888, NULL, state);
            parse_opt (888, NULL, state);
            printf (" ");
            parse_opt ('d', "3", state);
            printf ("\n");
            exit (0);
            break;
        case ARGP_KEY_ARG:
            argz_add (&a->argz, &a->argz_len, arg);
            break;
        case ARGP_KEY_INIT:
            a->argz = 0;
            a->argz_len = 0;
            break;
        case ARGP_KEY_END:{
            size_t count = argz_count (a->argz, a->argz_len);
            if (count > 2)
                argp_failure (state, 1, 0, "too many arguments");
            else if (count < 1)
                argp_failure (state, 1, 0, "too few arguments");
            }
            break;
        }
    return 0;
}


int main (int argc, char **argv){
    struct argp_option options[] = {
        { 0, 0, 0, 0, "Morse Code Options:", 7},
        { "dot", 'd', "NUM", OPTION_ARG_OPTIONAL, "Show some dots on the screen"},
        { "dash", 888, 0, 0, "Show a dash on the screen" },
        { 0, 0, 0, 0, "Informational Options:", -1},
        { "SOS", 999, 0, 0, "Give some help in morse code" },
        { 0 }
        };

    struct argp argp = { options, parse_opt, "WORD\nWORD WORD", 
        "Show some dots and dashes on the screen.\v"
        "A final newline is also shown regardless of whether any options were given." };

    struct arguments arguments;
    if (argp_parse (&argp, argc, argv, 0, 0, &arguments) == 0){
        const char *prev = NULL;
        char *word;
        while ((word = argz_next (arguments.argz, arguments.argz_len, prev))){
            printf (" %s", word);
            prev = word;
        }
        printf ("\n");
        free (arguments.argz);
    }
    return 0;
}

group字段比它看起来要复杂得多。只有两个struct argp_option记录指定了它,分别为7和-1。这两个记录被称为选项头,通常的约定是它们的文本以冒号结尾。

group 字段用作对help输出中的选项进行排序的主键。其group具有较大非负值的记录出现在较小的非负组之后。 例如,值为 5 的组出现在值为 7 的组之前。具有负值的组出现在非负值之后。 其组具有较小负值的记录出现在较大的负组之后。 例如,值为 -3 的组出现在值为 -1 的组之前。 在argp.h头文件中,它简洁地定义了顺序为:“0, 1, 2, …, n, -m, …, -2, -1”。

其他 struct argp_option 记录没有指定组值。 这是因为组值是为出现在选项标题之后的选项自动设置的。 这意味着我们的 --dash--dot 选项会自动接收组值 7。组值的正常用法是不对非标头选项指定它。

组值为零且不出现在选项标题之后的选项保留其值为零并出现在帮助输出中的所有其他选项之前。 如果我们在选项标题中省略组值,它会自动设置为比前一个选项的组值多 1 的值。 自动设置组值的目的是在简单情况下无需提供组值。

在我们的 options 数组中声明了新的 --SOS 选项,因此我们需要在回调函数中添加一个 case 块来实现它:

这段代码片段中没有任何令人惊讶的地方。 我们只是显示 SOS 序列然后退出。

让我们编译 step10,看看我们新的帮助输出!

➜  ./step10 --help       
Usage: step10 [OPTION...] WORD
  or:  step10 [OPTION...] WORD WORD
Show some dots and dashes on the screen.

 Morse Code Options:
  -d, --dot[=NUM]            Show some dots on the screen
      --dash                 Show a dash on the screen

 Informational Options:
  -?, --help                 Give this help list
      --SOS                  Give some help in morse code
      --usage                Give a short usage message
  -V, --version              Print program version

Mandatory or optional arguments to long options are also mandatory or optional
for any corresponding short options.

A final newline is also shown regardless of whether any options were given.

Report bugs to someone@example.com.
➜  ./step10 --SOS 
... --- ...

在这里我们可以看到莫尔斯电码选项分组在一个标题下,后面跟着一个空行。 许多程序都有以这种方式组织的选项集; 例如,尝试查看 automake 的帮助输出。

尽管 --dash--dot选项具有相同的group值,但 --dot 出现在 --dash 之前,因为二级排序键是短选项,三级排序键是长选项名称。

令人惊讶的是,–SOS 选项穿插着其他帮助选项。这种散布选项的发生是因为默认选项也有 -1 作为它们的组值。

在下一步中,我们将 --dot--dash 选项放入一个库中,并指示我们的程序使用该库中的选项


第十一步:公共选项可以封装成库

在这一步中,struct argp (children) 的第 5 个字段将用于将库中的选项合并到我们的程序中。 还将演示 struct argp_child

将库中的选项混合到程序中是一项激动人心的功能,只有 Argp 才能提供。 也许您有两个程序,并且您希望这两个程序都有一些相同的选项。 Argp 使许多程序可以轻松地使用相同的选项,而无需复制代码。

程序员通常会将程序的功能封装到一个库中,以便其他程序员可以使用该 API 来做新的事情。在许多情况下,API 有一些配置元素(即使它仅限于配置文件所在的位置),程序员必须收集这些信息才能传递给 API。 Argp 允许您为您的库提供选项解析器,以便使用您的 API 的程序员不必编写执行相同操作的代码。 作为库的作者,您对其他程序员将如何使用您的 API 进行了大量思考。 当您向库中添加选项解析器时,您正在考虑与库关联的命令行上的用户体验。 您可以使用 GNU Gettext 将您的选项翻译成其他口语,这样您的选项解析器的用户就不必进行额外的翻译。 如果需要,下游程序员可以使用选项解析器; 如果他们选择使用它,他们将获得标准行为、外观和感觉,就像您希望在命令行上为用户提供的一样,并且他们免费获得。

Argp 使用“子解析器”来混合选项。 为了通知 Argp 我们想要混合选项,我们为传递给 argp_parse 的 struct argp(第 5 个参数)提供了一组子解析器。 子解析器由 struct argp_child 定义,它看起来像这样:

struct argp_child
{
    const struct argp *argp;
    int flags;
    const char *header;
    int group;
};

这里的想法是,我们有另一个“子”struct argp,我们想混合主要的struct argp中。在我们的例子中,子结构argp将是一个从库中导出的符号。在一个有趣的递归转折中,子结构argp也可以拥有它自己的一组子解析器。

flags字段与argp_parse函数的flags字段具有相同的语义,在大多数情况下,为该字段传递0是可以的。

header字段在帮助输出中子项的选项之前放置一个标题。当标题在结构体 argp_child 中指定为空字符串时,它具有对给定选项进行分组的效果,但它不会在选项组后面显示标题或空行。 此标题字段不会覆盖可能已包含在给定结构 argp 的选项中的标题。

group 字段的作用类似于 struct argp_option 的 group 字段。 它将这些选项放在帮助输出中的其他位置。 group 字段不会覆盖给定 struct argp 的 struct argp_option 数组中的任何非零组字段

选项与 Argp 的混合是有限制的。 首先,避免密钥冲突很重要; 第一个选项将隐藏具有相同密钥的第二个选项。 其次,子解析器无法解析命令行上的参数; ARGP_KEY_ARG 值永远不会传递到子解析器的解析函数中。 Argp 只允许混合选项,而不是参数处理。

为了演示选项的混合,让我们将 --dot--dash 选项放入一个名为 libdotdash 的库中。 复制并粘贴以下代码并将其放入名为 dotdash.c 的文件中:

#include "dotdash.h"

static int
parse_opt (int key, char *arg, struct argp_state *state)
{
  switch (key)
    {
    case 'd': 
	{
	  unsigned int i;
	  unsigned int dots = 1;
	  if (arg != NULL)
	    dots = atoi (arg);
	  for (i = 0; i < dots; i++)
	    printf ("."); 
	  break;
	}
    case 888:
      printf ("-");
      break;
    }
  return 0;
}

static struct argp_option options[] =
{
    { "dot", 'd', "NUM", OPTION_ARG_OPTIONAL, "Show some dots on the screen"},
    { "dash", 888, 0, 0, "Show a dash on the screen" },
    { 0 }
};

struct argp dotdash_argp = {
  options, parse_opt, 0, 0, 0
}

现在让我们制作随附的头文件 dotdash.h:

#ifndef DASHDOT_H
#define DASHDOT_H
#include <argp.h>
extern struct argp dotdash_argp;
#endif

现在让我们创建一个使用dotdash库的程序。剪切并粘贴下面的代码,并放入step11.c中。

#include <stdio.h>
#include <argp.h>
#include "dotdash.h"

static int
parse_opt (int key, char *arg, struct argp_state *state)
{
  switch (key)
    {
    case 999:
      printf ("... --- ...");
      break;
    }
  return 0;
}

int main (int argc, char **argv){
    struct argp_option options[] = {
        { "SOS", 999, 0, 0, "Show the SOS sequence on the screen" },
        { 0 }
    };

    struct argp_child children_parsers[] = {
        { &dotdash_argp, 0, "Basic Morse Code Options:", 7 },
        { 0 }
    };
    
    struct argp argp = { options, parse_opt, 0, 0, children_parsers };
    int retval = argp_parse (&argp, argc, argv, 0, 0, 0);
    printf ("\n");
    return retval;
}

现在让我们编译这个库并将它链接到我们的程序:

Text 44: Compiling the dotdash library and linking it to the step11 program.
$ cc -c -o dotdash.o dotdash.c
$ ar cru libdotdash.a dotdash.o
$ ranlib libdotdash.a
$ cc step11.c -L./ -ldotdash -o step11

现在让我们看看程序生成的帮助输出:

➜  ./step11 --help
Usage: step11 [OPTION...]

      --SOS                  Show the SOS sequence on the screen

 Basic Morse Code Options:
  -d, --dot[=NUM]            Show some dots on the screen
      --dash                 Show a dash on the screen

  -?, --help                 Give this help list
      --usage                Give a short usage message

Mandatory or optional arguments to long options are also mandatory or optional
for any corresponding short options.

耶! dotdash 库中的选项与我们自己的 --SOS选项混合在一起。 现在,任何支持 Argp 的程序都可以通过链接到此库并合并 dotdash argp 子解析器来提供 --dot--dash 选项。

练习:尝试将 struct argp_child 的头字段更改为 NULL。 怎么了? 现在尝试将其更改为“”。 您看得出来差别吗?


第十二步

在这一步中,我们将制作一个更大的程序,通过使用 struct argp_state 的 child_inputs 字段来演示从子解析器传回选项输入数据。 我们还将展示如何解析字符串而不是命令行中的选项。 最后,还将演示 struct argp 的 help_filter 字段(第 6 个字段)。

我们编写一个程序,将莫尔斯电码转换为人类可读的文本。 该程序将具有交互模式,您可以在其中输入要转换为摩尔斯电码的字符串,并且它还将具有非交互模式,其中字符串在命令行上作为参数给出。

该程序将使用 GNU Readline 库来实现交互模式。 将有两个选项控制交互模式的历史:--no-history--history-file。 这些选项将在名为 libreadline-argp 的库中提供并混合到我们的程序中。

交互模式将接受两个命令:“quit”(就像您期望的那样)和“tap”,它从 libdotdash 中获取 --dot--dash 选项。如果它得到一个不是命令的字符串,它会将其转换为摩尔斯电码。

首先让我们看看控制命令历史的选项。 readline-argp.c 文件中没有引入新概念:

#include "readline-argp.h"
#include <unistd.h>

char *default_history_file = ".history";

static int
parse_opt (int key, char *arg, struct argp_state *state)
{
  struct readline_arguments *args = state->input;
  switch (key)
    {
    case 411:
      if (access (arg, R_OK))
        args->history_file = arg;
      else if (access (arg, W_OK))
        args->history_file = arg;
      else
        argp_failure (state, 1, 0, "Cannot open file `%s' for reading", arg);
      break;
    case 511:
      args->history_file = NULL;
      break;
    case ARGP_KEY_INIT:
      args->history_file = default_history_file;
      break;
    }
  return 0;
}

static struct argp_option options[] =
{
    { "history-file", 411, "FILE", 0, 
      "Specify a FILE to store the interactive history"},
    { "no-history", 511, 0, 0, "Do not record a history in interactive mode"},
    { 0 }
};

struct argp
readline_argp = 
{
  options, parse_opt, 0, 0, 0
};

struct readline_arguments 是在头文件readline-argp.h中定义的

#ifndef READLINE_ARGP_H
#define READLINE_ARGP_H
#include <argp.h>
extern struct argp readline_argp;
extern char * default_history_file;

struct readline_arguments
{
  char *history_file;
};
#endif

default_history_file变量背后的思想是,库的用户可以将默认值设置为他们想要的任何值。现在让我们来看看程序的选项解析:

#include <stdio.h>
#include <argp.h>
#include <argz.h>
#include <stdlib.h>
#include <readline/readline.h>
#include <readline/history.h>
#include "readline-argp.h"
#include "dotdash.h"

const char *argp_program_bug_address = "samuel@morse.net";
const char *argp_program_version = "version 1.0";

struct arguments
{
  int capitalize;
  char *argz;
  size_t argz_len;
  struct readline_arguments readline;
};

static struct argp_option options[] = 
{
    { "capitalize", 'c', 0, 0, 
      "Show morse translations in capital letters" },
    { 0 }
};
static struct argp_child children_parsers[] = 
{
    { &readline_argp, 0, 0, 0 },
    { 0 }
};
  
static int
parse_opt (int key, char *arg, struct argp_state *state)
{
  struct arguments *a = (struct arguments*) state->input;

  switch (key)
    {
    case 'c':
      a->capitalize = 1;
      break;
    case ARGP_KEY_ARG:
      argz_add (&a->argz, &a->argz_len, arg);
      break;
    case ARGP_KEY_INIT:
      a->argz = NULL;
      a->argz_len = 0;
      a->capitalize = 0;
      state->child_inputs[0] = &a->readline;
      break;
    }
  return 0;
}

static char *
help_filter (int key, const char *text, void *input)
{
  if (key == ARGP_KEY_HELP_POST_DOC)
    {
      char *new_text = NULL;
      if (asprintf (&new_text, text, default_history_file) != -1)
        return new_text;
    }
  return (char *) text;
}

struct argp argp = { options, parse_opt, "[PHRASE]", 
  "Translates to and from morse code.\vThis program starts in interactive mode when PHRASE is not supplied on the command line.  The history for interactive mode is stored in a file called `%s' by default.", 
  children_parsers, help_filter };

static void
interactive_mode (char *prompt, int capitalize)
{
  char *line;
  char *argz = NULL;
  size_t argz_len = 0;
  while ((line = readline (prompt)))
    {
      if (strncmp (line, "tap ", 4) == 0 || strcmp (line, "tap") == 0)
        {
          /* let's parse the tap command with the dotdash options. */
          if (argz_create_sep (line, ' ', &argz, &argz_len) == 0)
            {
              int flags = ARGP_NO_EXIT | ARGP_NO_ARGS;
              int argc = argz_count (argz, argz_len);
              char *argv[argc + 1];
              argz_extract (argz, argz_len, argv);
              argv[argc] = 0;
              if (argp_parse (&dotdash_argp, argc, argv, flags, 0, 0) == 0)
                {
                  add_history (line);
                  printf ("\n");
                }
            }
        }
      else if (strcmp (line, "quit") == 0)
        break;
      else
        {
          if (argz_create_sep (line, ' ', &argz, &argz_len) == 0)
            {
              if (morse_process_line (argz, argz_len, capitalize) == 0)
                add_history (line);
            }
        }
    }
  return;
}

int 
main (int argc, char **argv)
{
  struct arguments arguments;
  default_history_file = ".morse-tool.history";
  int retval = argp_parse (&argp, argc, argv, 0, 0, &arguments);
  if (retval != 0)
    return retval;

  /* silence --version and bug address in the tap command */
  argp_program_version = 0;
  argp_program_bug_address = 0;

  if (arguments.argz_len > 0) 
    {
      morse_process_line (arguments.argz, arguments.argz_len, 
                          arguments.capitalize);
      free (arguments.argz);
    }
  else if (arguments.argz_len == 0)
    {
      if (arguments.readline.history_file)
        read_history (arguments.readline.history_file);
      interactive_mode ("morse> ", arguments.capitalize);
      if (arguments.readline.history_file)
        write_history (arguments.readline.history_file);
    }

  return retval;
}

这里我们为来自新readline argp库的选项数据留出空间。

这里我们看到了readline_arguments结构是如何传递到readline argp解析器的。使用[0]的下标是因为readline_argp结构是一组子解析器(children_parsers变量)的第一个成员。

让我们看看文件的下一部分,它使帮助显示为更多信息:

static char *
help_filter (int key, const char *text, void *input)
{
  if (key == ARGP_KEY_HELP_POST_DOC)
    {
      char *new_text = NULL;
      if (asprintf (&new_text, text, default_history_file) != -1)
        return new_text;
    }
  return (char *) text;
}

struct argp argp = { options, parse_opt, "[PHRASE]", 
  "Translates to and from morse code.\vThis program starts in interactive mode when PHRASE is not supplied on the command line.  The history for interactive mode is stored in a file called `%s' by default.", 
  children_parsers, help_filter };

struct argp 中的 help_filter 字段指向一个改变程序帮助输出外观的函数。 该函数将由 Argp 在我们的 struct argp_option 数组中的每个选项上调用。 它不会为子解析器中的选项回调。 帮助过滤器背后的想法是我们可以在运行时而不是编译时使用信息更改特定选项的帮助输出的外观。 回调函数为我们提供了我们正在处理的选项的键、原始选项帮助文本(这是 struct argp_option 的 doc 字段)以及我们提供给 argp_parse 的输入参数。 该函数要么返回传入的文本(意味着没有更改),要么返回一个包含我们希望选项具有的新文本的 malloc 字符串。 Argp 稍后将释放新的 malloc 字符串。

该函数的关键参数是指struct argp_option 的关键字段。如果该值不是选项的键,则它可以是以下值之一:

  • ARGP_KEY_HELP_PRE_DOC : 改变struct argp的doc字段,包括垂直标签后面的所有内容。
  • ARGP_KEY_HELP_HEADER :更改标题选项(可以有很多)。
  • ARGP_KEY_HELP_EXTRA :一种将帮助文本添加到帮助输出底部的方法。
  • ARGP_KEY_HELP_DUP_ARGS_NOTE :更改“长选项的强制参数也是短选项的强制参数”消息。
  • ARGP_KEY_HELP_ARGS_DOC :修改struct argp的args_doc字段

我们的 help_filter 回调函数只是更改 POST_DOC 部分以插入历史文件的默认文件名。 为方便起见,我们在 struct argp 的 doc 字段中放入一个 ‘%s’,然后我们使用 asprintf 函数填充该值并生成一个新字符串。 asprintf 函数是另一个出现在 GNU C 标准库中的 GNU 特定函数。

我们来看看文件的下一部分,实现我们程序交互模式的部分:

static void
interactive_mode (char *prompt, int capitalize)
{
  char *line;
  char *argz = NULL;
  size_t argz_len = 0;
  while ((line = readline (prompt)))
    {
      if (strncmp (line, "tap ", 4) == 0 || strcmp (line, "tap") == 0)
        {
          /* let's parse the tap command with the dotdash options. */
          if (argz_create_sep (line, ' ', &argz, &argz_len) == 0)
            {
              int flags = ARGP_NO_EXIT | ARGP_NO_ARGS;
              int argc = argz_count (argz, argz_len);
              char *argv[argc + 1];
              argz_extract (argz, argz_len, argv);
              argv[argc] = 0;
              if (argp_parse (&dotdash_argp, argc, argv, flags, 0, 0) == 0)
                {
                  add_history (line);
                  printf ("\n");
                }
            }
        }
      else if (strcmp (line, "quit") == 0)
        break;
      else
        {
          if (argz_create_sep (line, ' ', &argz, &argz_len) == 0)
            {
              if (morse_process_line (argz, argz_len, capitalize) == 0)
                add_history (line);
            }
        }
    }
  return;
}

这里我们从 readline 函数中获取一个字符串并对其进行操作。 如果您需要向用户询问字符串,Readline 是一个可以链接到的很棒的库,因为它可以提供历史记录,并且允许用户以熟悉的方式使用箭头键以及删除和退格键来编辑字符串。 如果命令合法,我们将其存储到历史文件中。

正如我们计划的那样,有两个命令,“quit”和“tap”。 我们希望点击命令具有点划线选项。 我们从经验中知道 argp_parse 接受一个参数向量(例如一个 argv 和一个 argc),并且 argz设施帮助我们制作它。 我们希望我们的 tap 命令有一些帮助输出,并且我们特别注意使其不以 ARGP_NO_EXIT 标志退出我们的程序。 如果用户给了我们一些不是命令的东西,我们会将行分解为单词并尝试将它们转换为摩尔斯电码或从摩尔斯电码转换。

struct argp 中的第 7 个也是最后一个字段是 argp_domain 字段。 这是一个字符串,用于标识包含 Argp 中硬编码字符串的各种翻译的 GNU Gettext 域。 这不会影响您放入 struct argp 或 struct argp_option 中的字符串,它只影响源自 Argp 例程的文本,并且不需要将此值设置为除 0 以外的任何值。

我们现在已经看到了 struct argp 中的所有字段。 结构看起来像这样:

struct argp
{
    struct argp_option *options;
    error_t (*parser) (int key, char *arg, struct argp *state);
    char *args_doc;
    char *doc;
    struct argp_child *children;
    char *(*help_filter) (int key, char *text, void *input);
    char *argp_domain;
};

有关如何将字母转换为莫尔斯电码的更多信息,请参阅 morse.c 中的 process_morse_line 函数:http://svn.sv.nongnu.org/viewvc/trunk/morse.c?root=argpbook。

为了完成 morse-tool.c 文件,它以一个主函数结束,该函数做两件事:首先它启动程序的命令行处理,其次启动交互模式或批处理模式 程序。 我们来看看我们程序的主要功能:

int 
main (int argc, char **argv)
{
  struct arguments arguments;
  default_history_file = ".morse-tool.history";
  int retval = argp_parse (&argp, argc, argv, 0, 0, &arguments);
  if (retval != 0)
    return retval;

  /* silence --version and bug address in the tap command */
  argp_program_version = 0;
  argp_program_bug_address = 0;

  if (arguments.argz_len > 0) 
    {
      morse_process_line (arguments.argz, arguments.argz_len, 
                          arguments.capitalize);
      free (arguments.argz);
    }
  else if (arguments.argz_len == 0)
    {
      if (arguments.readline.history_file)
        read_history (arguments.readline.history_file);
      interactive_mode ("morse> ", arguments.capitalize);
      if (arguments.readline.history_file)
        write_history (arguments.readline.history_file);
    }

  return retval;
}

请注意在我们运行 argp_parse 后如何抑制 --version 选项,以便退出和点击命令不那么混乱并且更简单一些。

好的,让我们编译这个莫尔斯工具程序!

Text 53: Compiling the libreadline-argp library and linking it to the morse-tool program.
$ cc -c -o readline-argp.o readline-argp.c
$ ar cru libreadline-argp.a readline-argp.o
$ ranlib libreadline-argp.a
$ cc morse-tool.c morse.c -L./ -ldotdash -lreadline-argp -lreadline -o morsetool

libdotdash 库是在上一步中构建的。 让我们看看帮助、版本和用法显示是什么:

Text 54: Running the morse-tool program.
$ ./morse-tool --help
Usage: morse-tool [OPTION...] [PHRASE]
Translates to and from morse code.
    -c, --capitalize Show morse translations in capital letters
    --history-file=FILE Specify a FILE to store the interactive history
    --no-history Do not record a history in interactive mode
    -?, --help Give this help list
    --usage Give a short usage message
    -V, --version Print program version
    
This program starts in interactive mode when PHRASE is not supplied on the
command line. The history for interactive mode is stored in a file called
`.morse-tool.history' by default.

Report bugs to samuel@morse.net.
$ ./morse-tool --version
version 1.0
$ ./morse-tool --usage
Usage: morse-tool [-c?V] [--capitalize] [--history-file=FILE] [--no-history]
	[--help] [--usage] [--version] [PHRASE]

在这里我们可以看到三件事:

  1. libreadline-argp 中的选项已与 morsetool 的 --capitalize 选项混合在一起。
  2. 帮助显示显示了我们的 default_history_file 变量的内容。
  3. 程序在命令行上使用一个短语或根本没有短语。

让我们试试非交互模式:

Text 55: Running the morse-tool program in non-interactive mode.
$ ./morse-tool hello world
.... . .-.. .-.. --- .-- --- .-. .-.. -..
$ ./morse-tool -c -- .... . .-.. .-.. --- .-- --- .-. .-.. -..
HELLOWORLD

是的,它有效! 现在让我们尝试交互模式:

Text 56: Running the morse-tool program in interactive mode.
$ ./morse-tool
morse> hello world
.... . .-.. .-.. --- .-- --- .-. .-.. -..
morse> .... . .-.. .-.. --- .-- --- .-. .-.. -..
helloworld
morse> tap --help
Usage: tap [OPTION...]
-d, --dot[=NUM] Show some dots on the screen
--dash Show a dash on the screen
-?, --help Give this help list
--usage Give a short usage message
Mandatory or optional arguments to long options are also mandatory or
optional
for any corresponding short options.
morse> tap --dash --dash --dash
---
morse> ---
o
morse> quit
$ cat .morse-tool.history
hello world
.... . .-.. .-.. --- .-- --- .-. .-.. -..
tap --help
tap --dash --dash --dash
---

您会注意到的一件事是程序在与它交互时感觉如何。 这主要是因为 libreadline 提供的功能

如果我们想翻译“quit”或“tap”,我们可以在非交互模式下进行。

很难证明 libreadline 做了什么,所以显示文件的历史内容以表明 libreadline 确实在做某事。 下次程序以交互模式启动时,用户可以按向上箭头键查看最后给出的命令(例如“—”)。

练习:创建一个名为 --prompt 的隐藏选项来更改提示形式"morse > "的意思。还要防止“短选项的参数与长选项的选项相同”消息出现在点击命令的帮助显示中。

这个莫尔斯工具程序显示了制作带有选项的各种命令的交互式程序是多么容易。 在前面的步骤中,Argp 解析了提供给我们程序的命令行。 现在在这一步中,我们使用 Argp 以便它对我们程序的数据进行操作。 考虑诸如 bc 和 ftp 之类的程序,以及如何使用 libreadline 和 argp 来实现它们。


结束语

本节描述了我们所涵盖的内容、一些限制是什么,以及如何了解有关 Argp 的更多信息。

恭喜您完成了 Argp 的这一分步之旅! 我们传达了很多信息! 我们已经介绍了命令行的基础知识:从什么是长选项和短选项,到什么是参数,什么是选项参数,以及它们如何组合在一起。 我们已经介绍了命令行处理的基础知识。 我们已经看到了如何在命令行上收集强制和可选参数,以及如何在 Argp 回调函数中解析长选项和短选项,其中一些具有强制或可选参数。 我们已经演示了如何从 Argp 回调函数中进行错误报告。 我们已经展示了如何制作选项别名、隐藏选项和选项等效项。 我们还展示了如何将 Argp 解析器混合到一个程序中,以便将选项散布到现有选项中。

我们已经了解了如何以各种方式修改帮助显示。 我们已经看到了如何将选项分组以便它们一起出现在帮助显示中。我们已经演示了如何显示程序的常规和替代用法,如何设置程序功能的描述,以及如何控制帮助显示底部的额外信息。

在使用 Argp 编程一段时间后,您可以感觉到它的一些局限性。 不可能通过对结构argp_option 数组进行 malloc’ing 来制作 argp 解析器。数据元素必须全部预先分配(在堆栈上)和常量。 将选项混合在一起会优先考虑首先混合的选项,因此我们不能覆盖程序员在 struct argp 中给我们的同名选项。 参数不在子解析器中处理。

有关更多信息,GNU libc 手册中有一个关于 Argp6 的部分。 您也可以使用 Google Code Search 之类的服务来搜索使用 Argp7 的程序。 最后,您可以查看系统上的 /usr/include/argp.h ; 其中的大部分内容对您来说看起来很熟悉,并且该文件有很好的注释,供程序员阅读。

娱乐性编程可以带来很多乐趣,现在您可以将 Argp 融入您的程序中! 启用 Argp 的程序将更强大且更易于使用。 Happy hacking!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值