带命令行参数解析的C程序到底怎么写?

1 写在前面

最近工作上,遇到这样一个问题:我需要写一个C语言的程序,这个程序要求带命令行输入,之前有了解一些这方面的知识,本文将带大家好好梳理一下,希望对大家有所帮助。

2 需求分析

如上面所说,具体的功能需求是这样的:

比如:
命令行输入 
    ./test -i in.bin -o out.bin -f -v2
或者
    ./test -i in.bin -o out.bin -f -v
表示的含义是: 执行test程序,输入一个in.bin文件,输出一个out.bin文件,-t表示强制执行(忽略错误), -v2表示使用第1版本功能,如果没有2只有-v,则使用默认的第1版本。

整个功能需求比较简单,核心诉求就是能够从命令行参数中筛选出输入文件名和输出文件名,以及是否强制执行(忽略错误);同时判别使用的版本号。

3 编程实现

下面我们考虑在Linux平台下,使用C语言实现上述简单需求。

3.1 main函数的另一种写法

在某些C语言教科书里面,可能只会跟你说,main函数的原型是:

int main(void);

但却没有告诉你,其实它还有另一种写法:

int main(int argc, const char *argv[]);

如果你是一个细心的编程者,一定会发现,在不带命令行参数的时候,两种main函数的原型都可以跑出预期的效果。

但是,如果像本专题中的需求,那就不得不使用第二种原型接口来玩了,因为你从运行的命令行那里输入的参数列表,就会通过main函数的argc和argv来体现。

3.2 粗暴一点的方式来实现

我们先用粗暴一点的方式来实现:直接去遍历argv列表,一个个判断即可。示例代码如下:

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

int main(int argc, char *argv[])
{
	int i = 0;
	char *in = NULL;
	char *out = NULL;
	int is_force = 0;
	int version_n = 1;
	int is_found_in = 0;
	int is_found_out = 0;

	for (i = 0; i < argc; i++) {
		//show all input param
		printf("param %d: %s\n", i + 1, argv[i]);

		if (!strcmp(argv[i], "-i")) {
			is_found_in = 1;
			continue;
		} else if (!strcmp(argv[i], "-o")) {
			is_found_out = 1;
			continue;
		} else if (!strcmp(argv[i], "-f")) {
			is_force = 1;
			printf("is_force: %d\n", is_force);
		} else if (!strncmp(argv[i], "-v", 2)) {
			if (strlen(argv[i]) != 2) {
				version_n = atoi(argv[i] + 2);
			}
			printf("version_n: %d\n", version_n);
		}

		if (is_found_in) {
			in = argv[i];
			printf("in -> %s\n", argv[i]);
			is_found_in = 0;
		}

		if (is_found_out) {
			out = argv[i];
			printf("out -> %s\n", argv[i]);
			is_found_out = 0;
		}
	}

	return 0;
}

gcc编译后,运行结果如下:

./test -i in.bin -o out.bin -f -v2
param 1: ./test
param 2: -i
param 3: in.bin
in -> in.bin
param 4: -o
param 5: out.bin
out -> out.bin
param 6: -f
is_force: 1
param 7: -v2
version_n: 2

./test -i in.bin -o out.bin -f -v
param 1: ./test
param 2: -i
param 3: in.bin
in -> in.bin
param 4: -o
param 5: out.bin
out -> out.bin
param 6: -f
is_force: 1
param 7: -v
version_n: 1

基本达到预期。

3.3 优雅一点的方式来实现

上面的方面太粗暴,有没有优雅一点的方式,当然有,了解一下getopt函数。

man一下大概看看:

GETOPT(1)                                                                   User Commands                                                                   GETOPT(1)

NAME
       getopt - parse command options (enhanced)

SYNOPSIS
       getopt optstring parameters
       getopt [options] [--] optstring parameters
       getopt [options] -o|--options optstring [options] [--] parameters

DESCRIPTION
       getopt  is  used to break up (parse) options in command lines for easy parsing by shell procedures, and to check for valid options.  It uses the GNU getopt(3)
       routines to do this.

       The parameters getopt is called with can be divided into two parts: options which modify the way getopt will do the parsing (the options and the optstring  in
       the  SYNOPSIS),  and the parameters which are to be parsed (parameters in the SYNOPSIS).  The second part will start at the first non-option parameter that is
       not an option argument, or after the first occurrence of '--'.  If no '-o' or '--options' option is found in the first part, the first parameter of the second
       part is used as the short options string.

       If  the  environment  variable GETOPT_COMPATIBLE is set, or if the first parameter is not an option (does not start with a '-', the first format in the SYNOP‐
       SIS), getopt will generate output that is compatible with that of other versions of getopt(1).  It will still do parameter shuffling  and  recognize  optional
       arguments (see section COMPATIBILITY for more information).

       Traditional  implementations of getopt(1) are unable to cope with whitespace and other (shell-specific) special characters in arguments and non-option parame‐
       ters.  To solve this problem, this implementation can generate quoted output which must once again be interpreted by the shell (usually by using the eval com‐
       mand).   This has the effect of preserving those characters, but you must call getopt in a way that is no longer compatible with other versions (the second or
       third format in the SYNOPSIS).  To determine whether this enhanced version of getopt(1) is installed, a special test option (-T) can be used.

从这里我们可以看到它就是为解析命令行参数而诞生的。

我们来试一下用它来实现本例程中的功能需求:

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

int main(int argc, char *argv[]) 
{
    int opt;
    char *in = NULL;
    char *out = NULL;
    int is_force = 0;
    int version_n = 1;

    while ((opt = getopt(argc, argv, "i:o:fv::")) != -1) {
        switch (opt) {
            case 'i':
                in = optarg;
                printf("Option i was selected, in: %s\n", in);
                break;

            case 'o':
                out = optarg;
                printf("Option b was selected, out: %s\n", out);
                break;

            case 'f':
                is_force = 1;
                printf("Option b was selected, force: %d\n", is_force);
                break;

            case 'v':
                if (!optarg) {
                    printf("Option c was selected with default value %d\n", version_n);
                } else {
                    printf("Option c was selected with value %d\n", atoi(optarg));
                }                
                break;

            default:
                fprintf(stderr, "Usage: %s [-i in] [-o out] [-f] [ -vn]\n", argv[0]);
                exit(EXIT_FAILURE);
        }
    }

    return 0;
}

编译一下,看调试结果:

./test -i in.bin -o out.bin -v
Option i was selected, in: in.bin
Option b was selected, out: out.bin
Option c was selected with default value 1

./test -i in.bin -o out.bin -v -f
Option i was selected, in: in.bin
Option b was selected, out: out.bin
Option c was selected with default value 1
Option b was selected, force: 1

$./test -i in.bin -o out.bin -v2 -f
Option i was selected, in: in.bin
Option b was selected, out: out.bin
Option c was selected with value 2
Option b was selected, force: 1

从调试的结果来看,基本是可以满足需求的。

3.4 更为优秀一点的方式来实现

其实还有一个getopt_long函数可以更加详细地描述和使用命令行参数,参考例程如下:

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

int main(int argc, char *argv[]) 
{
    int opt;
    char *in = NULL;
    char *out = NULL;
    int is_force = 0;
    int version_n = 1;

    struct option long_options[] = {
        {"in",      required_argument,      NULL, 'i'},
        {"out",     required_argument,      NULL, 'o'},
        {"force",   no_argument,            NULL, 'f'},
        {"version", optional_argument,      NULL, 'v'},
        
        {NULL, 0, NULL, 0}
    };

    while ((opt = getopt_long_only(argc, argv, "i:o:fv::", long_options, NULL)) != -1) {
        switch (opt) {
            case 'i':
                in = optarg;
                printf("Option i was selected, in: %s\n", in);
                break;

            case 'o':
                out = optarg;
                printf("Option b was selected, out: %s\n", out);
                break;

            case 'f':
                is_force = 1;
                printf("Option b was selected, force: %d\n", is_force);
                break;

            case 'v':
                if (!optarg) {
                    printf("Option c was selected with default value %d\n", version_n);
                } else {
                    printf("Option c was selected with value %d\n", atoi(optarg));
                }                
                break;

            default:
                fprintf(stderr, "Usage: %s [-h] [-v] [-f file]\n", argv[0]);
                exit(EXIT_FAILURE);
        }
    }

    return 0;
}

编译之后,调试结果如下:

./test --in xxx.in --out xxx.out --force --version=2
Option i was selected, in: xxx.in
Option b was selected, out: xxx.out
Option b was selected, force: 1
Option c was selected with value 2

./test -i xxx.in -o xxx.out -f -v2
Option i was selected, in: xxx.in
Option b was selected, out: xxx.out
Option b was selected, force: 1
Option c was selected with value 2

可以看到短参数和长参数是等价的,但是需要注意的是:option的长参数输入的方式是 –xxx=yy

否则会报错:

./test --in xxx.in --out xxx.out --force --version2
Option i was selected, in: xxx.in
Option b was selected, out: xxx.out
Option b was selected, force: 1
./test: unrecognized option '--version2'
Usage: ./test [-h] [-v] [-f file]

4 经验总结

  • 了解main函数还有另一个写法,以便于支持带命令行参数的传入;
  • 学会使用getopt,你要搞个命令行参数解析,那不是so easy吗?
  • 还有个getopt_long满足命令行参数更优雅的需求,可以深入了解下。

5 文末福利

04-28 00:25