C 语言处理命令行参数

吐槽

当编写一些命令行程序时,经常需要为程序注入若干参数。在 C 语言中,命令行传参可以通过主函数的参数表获取:

code c
int main(int argc, char** argv) {
  return 0;
}

argc 是参数表个数,argv 则是参数的字符串数组。

对于简单的程序这就足够了,但是如果传参较为复杂,例如常用的 ffmpeg 截取视频部分生成gif:

code shell
ffmpeg -y -ss 00:00:01 -t 6 -i .mp4 -vf scale=700:-1 -f gif -r 25 some.gif

这样的参数表无论是开发还是维护都要消耗巨大的精力,还好有今天的主角 getopt

getopt

函数原型:

code c
#include<unistd.h>

extern char* optarg
extern int opterr;
extern int optind;
extern int optopt;

int getopt(int argc, char** argv, const char* shortopts);

getopt 函数的前两个参数接收自 main 函数的两个参数,shortopts 用来描述参数表键值对,例如 abc:d:: 对应到命令行就是 -a -b -c=xxx -dc 后面紧跟一个冒号,表示必须为 c 指定一个值,d 后面有两个连续的冒号,表示 d 的值可选。

code c
#include <unistd.h>
#include <stdio.h>

int main(int argc, char** argv) {
  char* shortopts = "abc:d::";
  char key;
  while ((key = getopt(argc, argv, shortopts)) != -1) {
    printf("key[%c] = value[%s]\n", key, optarg);
  }
  return 0;
}

*注:参数 key 和 value 之间的空格可以省略,-c xxx-cxxx 都可以正确识别,但是可选参数如果设置 value,则必须时候后者写法 -dyyy

code shell-session
[root@fangjin test]# make main
gcc -g -c main.c
gcc -g -o main main.o
[root@fangjin test]# ./main -a -b -c xxx -dyyy
key[a] = value[(null)]
key[b] = value[(null)]
key[c] = value[xxx]
key[d] = value[yyy]

optarg 是一个特殊的变量,它负责存储当前命令行传参解析到的 value。opterr 表示是否捕获错误将其输入到标准错误输出中,例如在传参时添加一个未定义的参数 z

code shell-session
[root@fangjin test]# ./main -a -b -c -dyyy -z
key[a] = value[(null)]
key[b] = value[777]
key[c] = value[xxx]
key[d] = value[yyy]
./main: invalid option -- 'z'
Unknown option: z

如果在代码开头将 opterr 设置为 0,则不会有最后两条错误日志输出。

optind 用来表示 argv 参数表的下一个读取的索引位置,optopt 表示不再指定键值对之外的剩余参数个数。

code c
#include <unistd.h>
#include <stdio.h>

int main(int argc, char** argv) {
  opterr = 0;
  char* shortopts = "abc:d::";
  char key;
  while ((key = getopt(argc, argv, shortopts)) != -1) {
    switch (key) {
      case 'a':
      case 'b':
      case 'c':
      case 'd':
        printf("key[%c] = value[%s]\n", key, optarg);
        break;
      case '?':
        printf("Unknown option: %c\n", (char)optopt);
        break;
    }
  }
  for (int i = optind; i < argc; i++) {
    printf("%s\n", argv[i]);
  }
  return 0;
}
code shell-session
[root@fangjin test]# ./main -a -b -c xxx -dyyy -z 123 456 789
key[a] = value[(null)]
key[b] = value[(null)]
key[c] = value[xxx]
key[d] = value[yyy]
Unknown option: z
123
456
789

借助 optind 可以获取到未被表达式捕获的剩余参数,此处有一个容易搞混的地方,注意看下面的传参:

code shell-session
[root@fangjin test]# make main; ./main -a -b 789 -cxxx -dyyy -z 123 456
make: 'main' is up to date.
key[a] = value[(null)]
key[b] = value[(null)]
key[c] = value[xxx]
key[d] = value[yyy]
Unknown option: z
789
123
456

查看上面代码的 [21-23] 行,789 这个参数并没有在参数表的最后,为什么仍可以被 for 循环遍历出来呢?因为 getopt 函数在执行时,会调整 args 中参数的顺序。

getopt_long

getopt_long 与 getopt 无太大差别,只是支持了长选项 --name=zhangsan --age=12

code c
int getopt_long(int argc, char * const argv[],
const char *optstring,
const struct option *longopts, int *longindex);

除了在函数调用上发生了一点变化,其余完全相同,本文不再赘述:

code c
#include <stdio.h>
#include <getopt.h>

int main(int argc, char** argv) {
  int opt_index = 0;
  struct option opts[] = {
    { "name", required_argument, NULL, 'm' },
    { "age", required_argument, NULL, 's' },
  };
  char* shortopts = "m:s:";
  char key;
  while ((key = getopt_long(argc, argv, shortopts, opts, &opt_index)) != -1) {
    switch (key) {
      case 'm':
      case 's':
        printf("key[%c] = value[%s]\n", key, optarg);
        break;
      case '?':
        printf("Unknown option: %c\n", (char)optopt);
        break;
    }
  }
  return 0;
}
code shell-session
[root@fangjin test]# ./main --name zhangsan --age 16
key[m] = value[zhangsan]
key[s] = value[16]