OceanSide On my own FEET
Fork me on GitHub

Linux C语言编写实现"ls"命令

一、作业要求

编程实现程序list.c,列表普通磁盘文件,包括文件名和文件大小。

使用vi编辑文件,熟悉工具vi。

使用Linux的系统调用和库函数。

体会Shell文件通配符的处理方式以及命令对选项的处理方式。

(对选项的处理,自行编程逐个分析命令行参数。不考虑多选项挤 在一个命令行参数内的情况)

  1. 与ls命令类似,处理对象可以有0到多个

    ➢0个:列出当前目录下所有文件

    ➢普通文件:列出文件

    ➢目录:列出目录下所有文件

  2. 实现自定义选项r,a,l,h,m以及--

    ➢r 递归方式列出子目录(每项要含路径,类似find的-print输出风格,需要设 计递归程序)

    ➢a 列出文件名第一个字符为圆点的普通文件(默认情况下不列出文件名首字 符为圆点的文件) ➢l 后跟一整数,限定文件大小的最小值(字节)

    ➢h 后跟一整数,限定文件大小的最大值(字节)

    ➢m 后跟一整数n,限定文件的最近修改时间必须在n天内

    ➢-- 显式地终止命令选项分析


二、实现思路

1、从最简单的开始:考虑没有参数的情况

  如果没有参数,则只需要把当前的工作目录列出。获得当前工作目录的库函数:getcwd()

  此时可以确定为目录,编写函数listDir()用来遍历目录。

  listDir()通过dirent结构体可以获取到目录中的文件。代码实现如下:

// 该函数负责列出目录文件
// 参数buf为目录名,flag:判断目录名是否带有路径
void listDir(char buf[], int flag) {
     // 列出当前目录的文件
     DIR *dir;
     struct dirent *entry;
     dir = opendir(buf);
     // 如果打开错误则报错
     if (dir == NULL) {
         printf("OPEN directory \"%s\": %m (ERROR %d)\n", buf, errno);
         return 1;
     }
     // 遍历目录文件,只打印文件,子目录和以.开头的文件不打印
     while ((entry = readdir(dir)) != NULL) {
         if (entry -> d_type != DT_DIR && entry -> d_name[0] != '.') {
             // 为获取文件大小,要得到stat结构体,所以这里使用snprintf获得文件的绝对路径
             char pathname[PATH_MAX];
             snprintf(pathname, (size_t)PATH_MAX, "%s/%s", buf, entry -> d_name);
             struct stat temp;
             stat(pathname, &temp);
             // 非本目录下的文件或者参数中带有路径,要输出路径+文件名
             if (flag == 1)
                 printf("%10d  %s \n", temp.st_size, pathname);
             // 本目录下的文件,只需要输出文件名
             else
                 printf("%10d  %s \n", temp.st_size, entry -> d_name);

         }
     }
     closedir(dir);
                      

2、多个参数的情况:考虑无选项和有选项

(1) 无选项

  即参数为文件或者目录,因此只需要判断其文件类型,编写不同函数进行处理即可。

  这里我在1的基础上新编写了两个函数:dirOptFile()listFile()

  dirOptFile()用于判断参数是文件还是目录:如果是目录,调用1编写的listDir()进行遍历输出;如果是文件,调用listFile()进行输出。判断的方法:使用stat结构体的属性:st_mode进行判断目录:S_ISDIR(st.st_mode)(返回true就是目录),同理,S_ISREG(st.st_mod)判断文件(返回true就是文件)。

  listFile()用于输出文件,由于要输出文件的大小,所以需要传入文件的stat结构体,st.st_size

// 该函数判断输入字符串是文件还是目录
// 如果是目录,调用listFiles函数输出目录的文件;如果是文件,直接输出即可
void dirOptOrFile(char buf[]) {                                                 	
    struct stat st;
    stat(buf, &st);
    // 目录,则调用listDir函数输出目录文件
    else if (S_ISDIR(st.st_mode))
    	listDir(buf, 1);
    // 文件,调用listFile
    else
    	listFile(buf, st);
}

(2) 有选项

  考虑选项:-r, -a, -l, -h, -m, --,使用简单的if...else语句进行判断。要注意的是,-l, -h, -m三个选项后面的参数要视作数字。这里我设置了几个全局变量用来保存选项是否出现、并且设置三个int类型来保存数字参数:

// 记录选项信息 
bool _r = false, _a = false, _l = false, _h = false, _m = false, _nn = false;
// 记录选项需要的数字:文件大小限制、修改时间限制 
int low = -1, high = -1, day = -1;

  错误处理:多个选项杂合情况、-l, -h, -m选项后没有数字等,要打印出帮助文档解释命令2myList

  编写函数handleOpt()如下(有错误处理函数errorPrint(),不过不重要,这里不列出):

// 处理argv参数中前几个选项信息
// 参数:同main 
// 返回值:argv中不为选项信息的参数位置 
int handleOpt(int argc, char *argv[]) {
	int i;
	for (i = 1; i < argc; i++) {
		
		// 非选项,先处理选项 l、h、m 后面该跟的数字
		// 赋值 low、high、day后,要将 _l、_h、_m 置为false,防止下一次循环又进入 
		if (_l || _h || _m) {
			if ( _l ) {
				int j = 0;
				while (argv[i][j] != '\0') {
					low = low * 10 + argv[i][j++] - '0';
				}
				_l = false;
			} 
			else if ( _h ) {
				int j = 0;
				while (argv[i][j] != '\0') {
					high = high * 10 + argv[i][j++] - '0';
				}
				_h = false;
			} 
			else if ( _m ) {
				int j = 0;
				while (argv[i][j] != '\0') {
					day = day * 10 + argv[i][j++] - '0';
				}
				_m = false;
			} 
		} 
		// 如果是选项,则分 -r, -a, -l, -h, -m, --几种情况 
		else if (argv[i][0] == '-' && ! _nn) {
			// 选项参数错误,或者请求帮助信息 
			if (argv[i][2] != '\0' || argv[i][1] == 'H') {
				errorPrint();
				exit(1);
			}
			// 选项:r
			else if (argv[i][1] == 'r')
				_r = true;
			// 选项:a
			else if (argv[i][1] == 'a')
				_a = true;
			// 选项:l
			else if (argv[i][1] == 'l') {
				_l = true;
				low = 0;
			}
			// 选项:h
			else if (argv[i][1] == 'h') {
				_h = true;
				high = 0;
			}
			// 选项:m
			else if (argv[i][1] == 'm') {
				_m = true;
				day = 0;
			}
			// 选项:--
			else if (argv[i][1] == '-')
				_nn = true;
		}
		// 选项部分结束,则退出循环,开始输出文件
		else {
			break; 
		}
	}
	
	// 如果选项 -l, -h, -m 后面没有跟上数字,则格式错误,打印帮助文档
	if (_l || _h || _m) {
		errorPrint();
		exit(1);
	} 
	
	return i;
}

3、整合上述思路:得到最终结果

  因为考虑了选项,所以要有递归遍历和非递归遍历,同时要注意选项对输出结果的影响。这里由于我设置了全局变量来保存,所以只用简单的if语句就可以控制输出。

  路径问题:由于输出工作目录下的文件不用带上路径,而子目录和绝对路径参数下,要带上路径,所以要注意flag的取值。

  参照老师的list运行结果,不需要输出以.开头的目录,这里也要进行屏蔽。

  递归调用时使用snprintf()将本目录路径和子目录名合并,再进行递归遍历。

  整体代码如下(充足注释):

#include <stdio.h>
#include <stdlib.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdbool.h>
#include <time.h>

#define MAX_PATHLEN 256

// 该函数用于遍历目录,参数buf为目录名,flag判断目录名是否带有路径 
void listDir(char buf[], int flag);						
// 用于判断是目录、选项还是文件,区别处理。参数buf为路径名
void dirOrFile(char buf[]);					
// 输出文件,buf为文件名,st为该文件的stat结构体
void listFile(char buf[], struct dirent *entry, int flag);	
// 在输入-H或者参数错误时打印帮助文档 
void errorPrint();								
// 处理argv参数的选项信息,参数:同main;返回值:argv数组不为选项参数的开始 
int handleOpt(int argc, char *argv[]);						

// 记录选项信息 
bool _r = false, _a = false, _l = false, _h = false, _m = false, _nn = false;
// 记录选项需要的数字:文件大小限制、修改时间限制 
int low = -1, high = -1, day = -1;

int main(int argc, char *argv[])
{
	// 记录选项信息 
	int pos = handleOpt(argc, argv);
	
	// 如果上面返回的pos == argc,说明未指定目录或文件,则为当前目录 
	if (pos == argc) {
		char buf[MAX_PATHLEN] = {0};
		getcwd(buf, sizeof(buf));	// 获取当前工作目录
	
		// 传入目录名,打印当前目录的文件
		listDir(buf, 0);
	}
	
	// 如果还有参数,说明后面的参数是指定要查找的目录或文件 
	else {
		for (; pos < argc; pos++) {
			dirOrFile(argv[pos]);
		}
	}

	return 0;
}

// 该函数负责列出目录文件
// 参数buf为目录名,flag:判断目录名是否带有路径 
void listDir(char buf[], int flag) {
	
	// 列出当前目录的文件
    DIR *dir;
    struct dirent *entry;
    dir = opendir(buf);
    struct stat st;
    
    // 非法路径
	if (stat(buf, &st) < 0 || ! S_ISDIR(st.st_mode)) {
		printf("ERROR: Invalid path: %s\n", buf);
		return;
	} 
    
	// 如果打开错误则报错
    if (dir == NULL) {
		printf("ERROR: OPEN directory \"%s\": %m (ERROR %d)\n", buf, errno);
        return;
    }
	// 遍历目录文件,只打印文件,子目录和以.开头的文件不打印
    while ((entry = readdir(dir)) != NULL) {
    	
    	// 非递归方式 
    	if (! _r) {
			
			if (entry -> d_type != DT_DIR && (entry -> d_name[0] != '.' || (entry -> d_name[0] == '.' && _a)))
				// 输出文件 
				listFile(buf, entry, flag);
			
		}
		
		// 递归方式
		else {
			
			// 不输出目录,且如果没有-a选项不输出以.开头的文件 
			if (entry -> d_type != DT_DIR && (entry -> d_name[0] != '.' || (entry -> d_name[0] == '.' && _a))) 
				// 输出文件 
				listFile(buf, entry, flag);
				
			// 如果是目录,递归调用本身,进入子目录继续遍历 
			if (entry -> d_type == DT_DIR && (entry -> d_name[0] != '.' || (entry -> d_name[0] == '.' && _a))) {
				
	    		// 目录下的.和..不用列出(为本目录和上级目录)   
				if (strcmp(".", entry -> d_name) == 0 || strcmp("..", entry -> d_name) == 0)
					continue;
					
				char pathname[MAX_PATHLEN];
				if (flag != 0) {
					snprintf(pathname, (size_t)MAX_PATHLEN, "%s/%s", buf, entry -> d_name);
					listDir(pathname, 1);
				}
				else {
					listDir(entry -> d_name, 1);
				}
			}
			
		} 
    }
    closedir(dir);
}

// 该函数判断输入字符串是文件还是目录
// 如果是目录,调用listFiles函数输出目录的文件;如果是文件,直接输出即可
void dirOrFile(char buf[]) {
	struct stat st;
	stat(buf, &st);
	if (S_ISDIR(st.st_mode))
    	listDir(buf, 1);
	// 文件,调用listFile
    else if (S_ISREG(st.st_mode)) {
		printf("%10d  %s \n", st.st_size, buf);
	}
	else {
		printf("%s: Not a directory or file\n", buf);
	}
}

// 如果要查看的是文件,那么直接输出文件信息 
// 函数参数:buf:文件名,st:文件的stat结构体 
void listFile(char buf[], struct dirent *entry, int flag) {
	
	// 为获取文件大小,要得到stat结构体,所以这里使用snprintf获得文件的绝对路径
	char pathname[MAX_PATHLEN];
	snprintf(pathname, (size_t)MAX_PATHLEN, "%s/%s", buf, entry -> d_name);
	struct stat temp;
	stat(pathname, &temp);
	
	// 如果有-l选项,则要注意文件大小应 >= low
	if (low != -1 && temp.st_size < low)
		return;
	// 如果有-h选项,则要注意文件大小应 <= high 
	if (high != -1 && temp.st_size > high)
		return;
	// 如果有-m选项,则要注意系统当前时间与文件修改时间的差应 <= day 
	if (day != -1) {
		time_t timep;
		time(&timep);			// 系统当前时间 
		int diffTime = (temp.st_mtime - timep) / 86400;	// 计算出相差的天数
		if (diffTime > day)
			return; 
	}
	
	// 非本目录下的文件或者参数中带有路径,要输出路径+文件名
	if (flag == 1)
		printf("%10d  %s \n", temp.st_size, pathname);
	// 本目录下的文件,只需要输出文件名
	else 
		printf("%10d  %s \n", temp.st_size, entry -> d_name);
	
	
}

// 处理argv参数中前几个选项信息
// 参数:同main 
// 返回值:argv中不为选项信息的参数位置 
int handleOpt(int argc, char *argv[]) {
	int i;
	for (i = 1; i < argc; i++) {
		
		// 非选项,先处理选项 l、h、m 后面该跟的数字
		// 赋值 low、high、day后,要将 _l、_h、_m 置为false,防止下一次循环又进入 
		if (_l || _h || _m) {
			if ( _l ) {
				int j = 0;
				while (argv[i][j] != '\0') {
					low = low * 10 + argv[i][j++] - '0';
				}
				_l = false;
			} 
			else if ( _h ) {
				int j = 0;
				while (argv[i][j] != '\0') {
					high = high * 10 + argv[i][j++] - '0';
				}
				_h = false;
			} 
			else if ( _m ) {
				int j = 0;
				while (argv[i][j] != '\0') {
					day = day * 10 + argv[i][j++] - '0';
				}
				_m = false;
			} 
		} 
		// 如果是选项,则分 -r, -a, -l, -h, -m, --几种情况 
		else if (argv[i][0] == '-' && ! _nn) {
			// 选项参数错误,或者请求帮助信息 
			if (argv[i][2] != '\0' || argv[i][1] == 'H') {
				errorPrint();
				exit(1);
			}
			// 选项:r
			else if (argv[i][1] == 'r')
				_r = true;
			// 选项:a
			else if (argv[i][1] == 'a')
				_a = true;
			// 选项:l
			else if (argv[i][1] == 'l') {
				_l = true;
				low = 0;
			}
			// 选项:h
			else if (argv[i][1] == 'h') {
				_h = true;
				high = 0;
			}
			// 选项:m
			else if (argv[i][1] == 'm') {
				_m = true;
				day = 0;
			}
			// 选项:--
			else if (argv[i][1] == '-')
				_nn = true;
		}
		// 选项部分结束,则退出循环,开始输出文件
		else {
			break; 
		}
	}
	
	// 如果选项 -l, -h, -m 后面没有跟上数字,则格式错误,打印帮助文档
	if (_l || _h || _m) {
		errorPrint();
		exit(1);
	} 
	
	return i;
}

void errorPrint() {
	printf("LIST by @a520, 2018211520@bupt.edu.cn, Apr 21 2021 15:27:04\n");
	printf("Usage: ./list [OPTION]... [FILE]...\n");
	printf("List information about the FILEs (the current directory by default)\n");
	printf("\n");
	printf("  -H           Display this help and exit\n");
	printf("  -a           Do not hide entries starting with .\n");
	printf("  -r           List subdirectories recursively\n");
	printf("  -l <bytes>   Minimum of file size\n");
	printf("  -h <bytes>   Maximum of file size\n");
	printf("  -m <days>    Limit file last modified time\n");
	printf("  --		   End option\n");
}

三、结果演示

1、编译运行

image-20210421232827473


2、运行举例(和老师的进行对比)

(1) 基本测试

  • ./list –l 100 –h 5000 ~/moocNotes :列出大小在100~5000之间的文件(由于目录下无/bin/etc,这里用根目录的子目录代替

    • 老师提供的list文件的输出:

      image-20210421233204377

    • 我编写的2myList文件的输出:

      image-20210421233735460

  • ./list –a -r -l 50000 –m 2:递归式列出当前目录树下大小 超50KB且2天内修改过的文件(包括文件名首字符为圆点的文件)

    • 老师提供的list文件的输出:

      image-20210421233919052

    • 我编写的2myList文件的输出:

image-20210421233958795

  • ./list -- -l:列出名为-l的文件

    • 老师提供的list文件的输出:

      image-20210421234216967

    • 我编写的2myList文件的输出:

      image-20210421234247702

  • ./list *:列出当前目录下的所有普通文件(不包括以.开头的文件)

    • 老师提供的list文件的输出:

      image-20210421234402110

    • 我编写的2myList文件的输出:

      image-20210421234445028

(2) 自测

  为保证程序的功能正常,这里我自己再设置了几组测试。

  • ./list -ar:由于不符合语法,这里会弹出usage段落

    image-20210422001433180

  • ./list -r -l 500 -h 1700:递归遍历根目录,找出500字节~1700字节之间的文件(不包括以.开头的文件)

    image-20210422000233779

  • ./list -a -r -l 500 -h 1700:递归遍历根目录,找出所有500字节~1700字节之间的文件(包括以.开头的文件)

    image-20210422000644642

  • ./list -a -r -- -m:递归遍历目录-m,输出其中所有文件(包括以.开头的文件)

image-20210422001050057


四、作业总结

  本次作业让我更加深刻地理解了linux系统中系统调用和C语言库函数的区别。使用vi编辑命令更加得心应手,同时对lsgccshell命令也更加熟悉,使用起来熟练了许多。同时编写该程序也让我对于linux系统底层实现的认识更进一步,增强了我对于linux下C语言的库函数的调用、编写实现功能。

  当然,编写该程序并不是一帆风顺的,期间遇到了挺多问题。其中最主要的问题是在从无选项过渡到有选项的对各函数的增改,以使它们实现新的功能时,频繁遇到“段错误”(Segmentation fault (core dumped))。当时估计是给定字符串内存过大(使用的MAX_PATH宏定义),后来使用gdb进行调试,发现其实是读入文件时,指针越界了(又是指针!)幸亏gdb调试方便快速,最终成功解决。这里也记录一下gdb常用命令,以加深印象:

  1. 编译:gcc -o xxx xxx.c
  2. 生成可调试文件:gcc -o xxx -g xxx.c (注:要进行gdb调试应当使用此命令,否则无法调试)
  3. 调试程序:gdb xxx
  4. 设置参数:set args xxx xxx
  5. 执行程序:
      start 从程序第一步开始执行
      run 直接运行程序到结束或者断点处
  6. 设置断点:break line(行数)或函数名或者条件表达式
      break 6 在第6行设置断点
      break Swap 在Swap函数入口设置断点
      break 6 if i == 10 在第6行设置断点,要求i == 10
  7. 删除断点:
      clear 删除所有断点
      clear 行号 : 删除这行的断点
      clear 函数名 : 删除该函数的断点
  8. info 查看断点
  9. c 继续执行到下一个断点
  10. print + 变量 打印变量值
  11. n 下一步
  12. s 进入函数块
  13. q 退出调试
posted @ 2021-04-22 11:17  EEthunder  阅读(1584)  评论(0)    收藏  举报