本部分内容的考试形式:对手撕完整代码基本不作要求,主要通过代码填空或补全代码的题型进行考察。
1. cp命令的简单实现
这里我们仅实现一个简单的cp命令,用于将文件file1复制成文件file2(其中file1和file2可包含文件相对于当前目录的相对路径): cp file1 file2
1.1 利用C标准库实现
在《高级语言程序设计》这门课中,我们已经接触过C语言标准库自带的文件操作函数。我们可以先尝试使用它们来编写代码。
代码如下:
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char* argv[]) {
// 如果输入参数不合法,则直接输出报错信息并退出程序
if (argc != 3) {
printf("Usage: %s file_source file_target\n", argv[0]);
return 1;
}
const char* fileName_src = argv[1];
const char* fileName_dest = argv[2];
// 调用fopen函数访问源文件
// 参数"rb+"表示以读写方式访问(二进制)文件
// 如果要访问的文件不存在,则fopen函数会返回一个NULL指针
FILE* file_src = fopen(fileName_src, "rb+");
if (!file_src) {
printf("Error: Fail to open file %s\n", fileName_src);
return 1;
}
// 访问目标文件
// 参数"wb+"表示以读写方式方式访问(二进制)文件
// 与参数"rb+"不同,当要访问的文件不存在时,此时fopen函数会创建一个新文件
// 当要访问的文件存在时,fopen函数会清空其原有内容,重新从文件开头开始写入
// 当创建或清空文件失败时,fopen函数会返回NULL指针
FILE* file_dest = fopen(fileName_dest, "wb+");
if (!file_dest) {
printf("Error: Fail to open file %s\n", fileName_dest);
return 1;
}
int buffer_size = 4096;
unsigned char buffer[buffer_size]; // 定义一个4096字节(4KB)大小的临时缓存区
int bytes_read;
// 使用fread函数从源文件中读取数据,该函数的用法如下:
// fread(void *ptr, size_t size, size_t nmemb, FILE *stream);
// ptr表示读取所得数据的储存地址,此处为我们刚才定义的缓存区buffer
// size表示读取的每个数据元素的大小,这里我们取1个字节
// nmemb表示要读取多少个数据元素,这里我们一次读取4096个字节将缓存区填满
// stream表示要读取文件对应的指针
// fread返回实际读取到的数据元素数量,这里用其来判断成功读取到了数据
while ((bytes_read = fread(buffer, 1, buffer_size, file_src)) > 0) {
// 使用fwrite函数将所得数据写入目标文件,其用法与fread类似:
// fwrite(const void *buffer, size_t size, size_t count , FILE *stream)
fwrite(buffer, 1, bytes_read, file_dest);
}
// fclose函数的功能:
// 刷新I/O缓冲区,以将文件操作过程中尚未写入硬盘的数据刷新到硬盘上
// 并释放FILE指针指向的内存
fclose(file_src);
fclose(file_dest);
return 0;
}
代码执行效果如下:
1.2 利用Linux平台的接口实现
我们也可以使用使用Linux系统自带的unistd库中的相关函数来完成实现。
代码如下:
#include <fcntl.h> // 文件控制头文件
#include <unistd.h> // POSIX API头文件
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(int argc, char* argv[]) {
if (argc != 3) {
printf("Usage: %s file_source file_target\n", argv[0]);
return 1;
}
const char* fileName_src = argv[1];
const char* fileName_dest = argv[2];
// open函数执行成功时,返回当前访问文件的一个id号(文件描述符)
// 执行失败时,返回一个负数
int file_src = open(fileName_src, O_RDONLY);
if (file_src < 0) {
printf("Error: Fail to open file %s\n", fileName_src);
return 1;
}
mode_t file_mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
int file_dest = open(fileName_dest, O_WRONLY | O_CREAT | O_TRUNC, file_mode);
if (file_dest < 0) {
printf("Error: Fail to open file %s\n", fileName_dest);
return 1;
}
int buffer_size = 4096;
unsigned char buffer[buffer_size];
int bytes_read;
while ((bytes_read = read(file_src, buffer, buffer_size)) > 0) {
write(file_dest, buffer, bytes_read);
}
close(file_src);
close(file_dest);
return 0;
}
我们注意到这段程序大体上与前面我们用C标准库进行实现的程序是相似的。这里我们主要分析一下这部分代码:
mode_t file_mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH;
int file_dest = open(fileName_dest, O_WRONLY | O_CREAT | O_TRUNC, file_mode);
O_WRONLY | O_CREAT | O_TRUNC利用按位或运算符将三种访问目标文件的方式组合起来,表示当文件存在时,以可写模式打开它(O_WRONLY),将其清空(O_TRUNC);当文件不存在时,创建它(O_CREAT)。
当利用open函数创建新文件时,需要根据第三个参数file_mode对新建的文件的权限进行设置。在这段代码中,file_mode表示的是文件拥有者具有读写权限(S_IRUSR、S_IWUSR),当前组的用户及其他用户具有读的权限(S_IRGRP、S_IROTH)。
2. cat命令的简单实现
我们同样仅实现一个简单的cat命令,用于将文本文件的内容输出到终端:cat filename
利用Linux平台接口实现的代码如下:
#include <fcntl.h> // 文件控制头文件
#include <unistd.h> // POSIX API头文件
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
int main(int argc, char* argv[]) {
if (argc != 2) {
printf("Usage: %s filename\n", argv[0]);
return 1;
}
const char* fileName = argv[1];
// 访问目标文件
int file = open(fileName, O_RDONLY);
if (file < 0) {
printf("Error: Fail to open file %s", fileName);
return 1;
}
int buffer_size = 4096;
unsigned char buffer[buffer_size];
int bytes_read;
while ((bytes_read = read(file, buffer, buffer_size)) > 0) {
// 将读取到的内容通过标准输出打印到终端上
// 这里不要混淆stdout和STDOUT_FILENO:
// 前者为标准输出对应的FILE*指针,后者为标准输出对应文件描述符
write(STDOUT_FILENO, buffer, bytes_read);
}
putchar('\n');
return 0;
}
执行效果如下:
这里再补充一点东西。在调用fopen/open函数出错的时候(例如文件不存在、没有访问权限等等),如果我们引入了errno.h库,open函数将会将相应的错误代码赋值给一个叫errno的全局变量,我们再通过string.h库中的strerror函数就可以很方便地查询到当前错误代码对应的文本错误信息。
下面是我在网上找的Windows平台下的演示截图,在Linux平台中这是类似的。
3. pwd命令的简单实现
该命令的实现需要借助对类Unix平台文件系统inode机制的理解。
具体如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <dirent.h>
#include <stdlib.h>
// getInode函数用于查询文件(目录)的inode编号
// 该函数对Linux平台的stat系统函数进行封装,实现对文件信息的查询
ino_t getInode(char fname[]) {
struct stat info;
if (stat(fname, &info) < 0) {
printf("Error: Fail to stat\n");
exit(1);
}
return info.st_ino;
}
// getNameByInode函数根据目录的inode编号查询目录的名称
void getNameByInode(ino_t inode, char* name) {
// 调用chdir函数将程序工作目录切换到父级目录
chdir("..");
// 调用opendir系统函数访问当前目录(即要查询目录的父目录)
// 该函数返回一个指向访问目录信息的结构体指针dir_ptr
DIR* dir_ptr = opendir(".");
if (dir_ptr == NULL) {
printf("Error: Fail to opendir\n");
exit(1);
}
// 调用readdir函数对当前目录下所有条目(包括文件和子目录)进行遍历
// 每次调用该函数,都会返回指向当前条目结构体的指针,结构体中包含inode和文件名等信息
// 当所有条目遍历完成后,readdir函数会返回NULL,表示遍历完成
struct dirent* direntp;
while ((direntp = readdir(dir_ptr)) != NULL) {
// 找到目标目录,则记录下其名称并退出函数
if (direntp->d_ino == inode) {
strcpy(name, direntp->d_name);
// 调用chdir将程序工作目录重新切回目标目录
chdir(name);
// 关闭当前目录,释放存储相关信息的空间
closedir(dir_ptr);
return;
}
}
// 遍历完成后如仍为查询成功。则输出报错
printf("Error: Fail to look for file by inode %d\n", (int)inode);
exit(1);
}
int main() {
char its_name[256] = {0};
char ans[256] = {0};
// 获取当前目录的inode编号
ino_t this_inode = getInode(".");
// 回溯查询当前目录的父目录,直至根目录
// 这里通过根目录与其父目录inode相等的特点来判断是否已回溯到根目录
while (getInode("..") != this_inode) {
// 查询当前目录的名称,保存到its_name数组
getNameByInode(this_inode, its_name);
// 拼接字符串,记录结果
if (strlen(ans) == 0) {
strcpy(ans, its_name);
} else {
strcat(strcat(its_name, "/"), ans);
strcpy(ans, its_name);
}
// 调用chdir系统函数,将程序的工作目录移动到父目录
chdir("..");
// 获取父目录的inode编号
this_inode = getInode(".");
}
// 输出结果
printf("/%s\n", ans);
return 0;
}
程序运行效果:
4. ls命令的简单实现
ls命令在这里我们主要实现两个功能:
- 打印当前目录下的所有文件名称:
ls - 打印当前目录下某文件的具体信息:
ls -l filename
第一个功能是非常简单的,我们可以复用实现pwd命令程序中遍历目录的那部分代码:
第二个功能中,要获取文件的信息并不困难,通过调用stat函数即可。主要的难点在于对求得的struct stat结构体中的信息进行加工处理,其中又以处理表示权限的八进制整型难度最大。
具体代码如下:
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <dirent.h>
#include <stdlib.h>
#include <time.h>
#include <pwd.h>
#include <grp.h>
#define N 256
void convertPermission(int octal, char* output) {
// 二进制数的第10~12位表示文件的类型
int file_type = (octal >> 9) & 0b111;
// 二进制数的第7~9位表示文件所属用户的rwx权限
int user_permission = (octal >> 6) & 0b111;
// 二进制数的第4~6位表示用户所属组的rwx权限
int group_permission = (octal >> 3) & 0b111;
// 二进制数的第1~3位表示其他用户的rwx权限
int other_permission = octal & 0b111;
// 判断文件类型
switch (file_type) {
case 0:
output[0] = '-';
break;
case 1:
output[0] = 'd';
break;
default:
output[0] = '?';
}
// 判断文件所属用户权限
output[1] = (user_permission & 0b100) ? 'r' : '-';
output[2] = (user_permission & 0b010) ? 'w' : '-';
output[3] = (user_permission & 0b001) ? 'x' : '-';
// 判断用户所属组的权限
output[4] = (group_permission & 0b100) ? 'r' : '-';
output[5] = (group_permission & 0b010) ? 'w' : '-';
output[6] = (group_permission & 0b001) ? 'x' : '-';
// 判断其他用户的权限
output[7] = (other_permission & 0b100) ? 'r' : '-';
output[8] = (other_permission & 0b010) ? 'w' : '-';
output[9] = (other_permission & 0b001) ? 'x' : '-';
output[10] = '\0';
}
void printFileList() {
DIR* dir_ptr = opendir(".");
if (dir_ptr == NULL) {
printf("Error: Fail to opendir\n");
exit(1);
}
struct dirent* direntp;
while ((direntp = readdir(dir_ptr)) != NULL) {
char name[N];
strcpy(name, direntp->d_name);
// 忽略表示父目录的".."和当前目录的"."
if (strcmp(name, "..") == 0 || strcmp(name, ".") == 0) continue;
puts(name);
}
}
void printFileInfo(char fileName[]) {
struct stat info;
if (stat(fileName, &info) < 0) {
printf("Error: Fail to stat %s\n", fileName);
exit(1);
}
printf(" name : %s\n", fileName);
// 将表示文件权限的16位二进制整型转换为权限字符串
char permeStr[N];
convertPermission(info.st_mode, permeStr);
printf(" mode : %s\n", permeStr);
// 打印指向当前文件的硬链接数量。如果没有为文件创建额外的硬链接,则会输出1。
printf(" links : %d\n", (int)info.st_nlink);
// 调用pwd.h中的getwuid查询所属用户名称
uid_t uid = info.st_uid;
struct passwd* pw = getpwuid(uid);
printf(" user : %s\n", pw->pw_name);
// 调用grp.h中的getgrgid函数查询组名称
gid_t gid = info.st_gid;
struct group *gr = getgrgid(gid);
printf(" group : %s\n", gr->gr_name);
// 打印文件大小
printf(" size : %d bytes\n", (int)info.st_size);
// 调用time.h中的ctime函数将时间戳转换为日期文本
time_t timeStamp = info.st_mtime;
printf(" modtime: %s", ctime(&timeStamp));
}
int main(int argc, char* argv[]) {
if (argc == 1) {
printFileList();
}
else if (argc == 3 && strcmp(argv[1], "-l") == 0) {
char* fileName = argv[2];
printFileInfo(fileName);
}
else {
puts("Error: invalid arguments!");
exit(1);
}
return 0;
}
程序运行效果: