实现几个简单的命令

  近期闲的无聊,看了一下关于 Shell 编程的相关知识,好像是回忆了一下,果不其然脚本语言写起来还是一如既往的别扭。写了一个 Shell 版的素数筛直接扔下面了,真的丑。

  言归正传,本文主要介绍如何使用 Linux 下的 getopt() 函数实现几个简单的命令。
函数在 <unistd.h> 里面,函数原型如下:

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

参数解释:
argc:main 函数接收参数的个数
argv:main 函数接收的参数
optstring:一个包含准确选项字符的字符串
返回值:返回下一个选项

不仅如此根据 man 手册中的描述 GETOPT 设置的一些全局变量,如下
1. extern char* optarg:指向当前选项参数字串的指针(如果有)
2. extern int optind:下一个检索位置,即下一个 argv,由于 main 函数的第一个参数是它本身,所以 optind 的初始值是 1
3. extern int opterr:是否将错误信息输出到 stderr,为 0 时表示不输出,初值为 1
4. extern int optopt:不在选项字符串 optstring 中的选项

概念很多,不容易记住,后面慢慢说,慢慢举例。 timg

The first example

首先引入两个概念:

  1. 选项:例如在执行 gcc hello.c -o hello,其中 -o 代表选项,意思是要命名,而 hello 是选项的参数。而且,有些选项是不需要参数的,而这些不带参数的选项可以写在一起,例如 rm -rfls -al 等等。
  2. 选项字符串:
    • 单个字符,表示选项;
    • 单个字符后紧接一个 :,表示该选项后面必须跟一个参数值。参数紧跟在选项后或者以空格隔开,该参数的指针赋给 optarg。例如 -atest 也可以写成 -a test
    • 单个字符后面紧跟 ::,表示该选项后买呢可以带参数,也可以不带参数。但是如果带参数,参数必须紧跟在选项后面,不能以空格隔开,该参数的指针赋值给 optarg

  例如:xy:z:: 表示 x 这个选项没有选项参数 y 这个选项后面必须有参数,中间可以空格也可以没有, z 这个选项后面可以有参数也可以没有,但是如果有选项参数的话必须紧跟在 z 后面。

// getopt() 函数的一般用法
while((c = getopt(argc, argv, "xy:z::")) != -1) {
    switch (c) {
        case 'x' :
            ......
        case 'y':
            ......
        case 'z':
            ......
        ......
    }
}

接下来,实现一个命令,其中 -n 代表姓名,-a 代表年龄,-s 代表性别,-t 代表身份且身份可有可无。

  首先,分析需求,其中, n, a, s 都是必须有选项参数的,t 的选项参数可有可无。所以选项字符串可以写成 n:a:s:t::
  然后就可以直接写代码了。

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

int main(int argc, char **argv) {
    char c;//记录选项字符
    char name[20] = {0};//保存姓名
    int age = 0;//保存年龄
    char sex[5] = {0};//性别
    char title[10] = {0};//身份
    while((c = getopt(argc, argv, "n:a:s:t::")) != -1) {
        switch (c) {
            case 'n':
                strcpy(name, optarg);
                break;
            case 'a':
                age = atoi(optarg);
                break;
            case 's':
                strcpy(sex, optarg);
                break;
            case 't': {
                if(optarg == NULL) { //如果有 t 选项但是其参数为空,或者 t 的选项参数没有紧跟 t,防止段错误,提前处理
                    fprintf(stderr, "Usage: %s -ttitle!\n", argv[0]);
                    exit(1);
                }
                strcpy(title, optarg);
                break;
            }
            default: //读到了 n,s,a,t 之外的选项字符
                fprintf(stderr, "Usage: %s -n name -s sex _a age -t title!\n", argv[0]);
            exit(1);
        }
    }
    printf("%s is %d years old, %s, with title %s!\n", name, age, sex, title); //打印相关信息
    return 0;
}

运行结果如下:
正常运行:
截屏20201208 21.26.09.png

错误运行:

  1. t 后面的参数没有紧跟在选项后面时, getopt 就会报错。
    截屏20201209 11.06.18.png
  2. n, a, s 后面没有选项参数时
    截屏20201209 11.09.10.png

到这里,一个简单的 getoptdemo 就完成了。

The second example

编写一个程序能够实现 cat -n -b 的效果。
首先,先观察下 cat -n -b 的效果:

  1. 只使用 cat 命令
    截屏20201209 11.11.42.png

  2. cat -n,打开文件并标号,同时对空行进行了标号。
    image.png

  3. cat -b,不对空行进行标号。
    image.png

  4. cat -n -b,与 cat -b 相同,不对空行进行标号。
    image.png

  熟知了上述的效果,现在开始通过 getopt 自行实现。要实现的功能为 mycat -n -b filename,所以很明显选项字符串 optstring = "nb",因为它俩都不需要选项参数。其次,针对一些 C 语言的文件操作函数,本文不在进行相关讲解。对于功能实现的过程请看代码注释。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
// ./a.out -n -b filename
void Cat_File(const char* file); //打开文件,读取其中内容
int Check(char *s);//判断读出的字符串是否是空行
int cnt = 1;//行号标记
int b_flag = 0, n_flag = 0;//标记是那种打开方式
int main(int argc, char **argv) {
    char c;
    int len = 1;
    while((c = getopt(argc, argv, "nb")) != -1) {
        switch (c) {
            case 'b':
                b_flag = 1;
                break;
            case 'n':
                n_flag = 1;
                break;
            default:
                fprintf(stderr, "Usage: %s [-b|-n] file\n", argv[0]);
                exit(1);
        }
    }
    for(int i = optind;i < argc;i++) {//optind 带表下一个参数的位置,那么意味着当所有选项都被处理完成后,剩下的就是选项后面紧跟的文件名了,这样就可以拿到需要的文件名称。
        Cat_File(argv[i]);
    }
    return 0;
}

void Cat_File(const char* file) { //很简单的逻辑判断
    FILE* fp;
    if ((fp = fopen(file, "r")) == NULL) {
        perror(file);
        exit(1);
    }
    char buff[BUFSIZ] = {0};
    while(fgets(buff, BUFSIZ, fp) != NULL) {
        if(b_flag == 1) {
            if(Check(buff))
                printf("%d %s",cnt++, buff);
            else
                printf("%s", buff);

        } else if(n_flag == 1) {
            printf("%d %s",cnt++, buff);
        } else {
            printf("%s", buff);
        }
    }
    fclose(fp);
}

int Check(char *s) {
    int len = strlen(s);
    for(int i = 0;i < len;i++) {
        if(s[i] != '\r' && s[i] != '\n')
            return 1;
    }
    return 0;
}

运行结果如下:
image.png

image.png

image.png

写在后面

  1. getopt 会改变 argv[] 中参数的顺序。经过多次 getopt() 后,argv[]中的选项和选项的参数会被放置在数组前面,而 optind 会指向第一个非选项和参数的位置。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char **argv) {
    char c;
    while((c = getopt(argc, argv, "nb")) != -1) {
        switch (c) {
            case 'b':
                break;
            case 'n':
                break;
            default:
                fprintf(stderr, "Usage: %s [-b|-n] file\n", argv[0]);
                exit(1);
        }
    }
    for(int i = 1;i < argc;i++) {
        printf("%s\n", argv[i]);
    }
    return 0;
}

执行结果如下:
image.png

The third example

实现一个简单了 ls 命令
To be continue...