Linux C语言编写实现"ls"命令
一、作业要求
编程实现程序list.c,列表普通磁盘文件,包括文件名和文件大小。
使用vi编辑文件,熟悉工具vi。
使用Linux的系统调用和库函数。
体会Shell文件通配符的处理方式以及命令对选项的处理方式。
(对选项的处理,自行编程逐个分析命令行参数。不考虑多选项挤 在一个命令行参数内的情况)
-
与ls命令类似,处理对象可以有0到多个
➢0个:列出当前目录下所有文件
➢普通文件:列出文件
➢目录:列出目录下所有文件
-
实现自定义选项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、编译运行
2、运行举例(和老师的进行对比)
(1) 基本测试
-
./list –l 100 –h 5000 ~/moocNotes
:列出大小在100~5000之间的文件(由于目录下无/bin/etc
,这里用根目录的子目录代替-
老师提供的
list
文件的输出: -
我编写的
2myList
文件的输出:
-
-
./list –a -r -l 50000 –m 2
:递归式列出当前目录树下大小 超50KB且2天内修改过的文件(包括文件名首字符为圆点的文件)-
老师提供的
list
文件的输出: -
我编写的
2myList
文件的输出:
-
-
./list -- -l
:列出名为-l
的文件-
老师提供的
list
文件的输出: -
我编写的
2myList
文件的输出:
-
-
./list *
:列出当前目录下的所有普通文件(不包括以.
开头的文件)-
老师提供的
list
文件的输出: -
我编写的
2myList
文件的输出:
-
(2) 自测
为保证程序的功能正常,这里我自己再设置了几组测试。
-
./list -ar
:由于不符合语法,这里会弹出usage
段落 -
./list -r -l 500 -h 1700
:递归遍历根目录,找出500字节~1700字节之间的文件(不包括以.
开头的文件) -
./list -a -r -l 500 -h 1700
:递归遍历根目录,找出所有500字节~1700字节之间的文件(包括以.
开头的文件) -
./list -a -r -- -m
:递归遍历目录-m
,输出其中所有文件(包括以.
开头的文件)
四、作业总结
本次作业让我更加深刻地理解了linux系统中系统调用和C语言库函数的区别。使用vi
编辑命令更加得心应手,同时对ls
、gcc
等shell
命令也更加熟悉,使用起来熟练了许多。同时编写该程序也让我对于linux系统底层实现的认识更进一步,增强了我对于linux下C语言的库函数的调用、编写实现功能。
当然,编写该程序并不是一帆风顺的,期间遇到了挺多问题。其中最主要的问题是在从无选项过渡到有选项的对各函数的增改,以使它们实现新的功能时,频繁遇到“段错误”(Segmentation fault (core dumped))。当时估计是给定字符串内存过大(使用的MAX_PATH宏定义),后来使用gdb
进行调试,发现其实是读入文件时,指针越界了(又是指针!)幸亏gdb
调试方便快速,最终成功解决。这里也记录一下gdb
常用命令,以加深印象:
- 编译:gcc -o xxx xxx.c
- 生成可调试文件:gcc -o xxx -g xxx.c (注:要进行gdb调试应当使用此命令,否则无法调试)
- 调试程序:gdb xxx
- 设置参数:set args xxx xxx
- 执行程序:
start 从程序第一步开始执行
run 直接运行程序到结束或者断点处- 设置断点:break line(行数)或函数名或者条件表达式
break 6 在第6行设置断点
break Swap 在Swap函数入口设置断点
break 6 if i == 10 在第6行设置断点,要求i == 10- 删除断点:
clear 删除所有断点
clear 行号 : 删除这行的断点
clear 函数名 : 删除该函数的断点- info 查看断点
- c 继续执行到下一个断点
- print + 变量 打印变量值
- n 下一步
- s 进入函数块
- q 退出调试