实现简单ls命令遇到问题的总结
这周要写一个小项目,利用《linux C 编程实战》第6章的内容实现一个简单的 ls
命令,写的时候出现很多问题,现在将问题总结一下。
要实现的ls命令需要实现 -l, -a , -A 等参数。
我们在终端测试一下系统的ls命令:
可以发现系统的ls可以根据终端的宽度来调整输出列数,而不至于输出的内容由于终端大小的限制显示不全。
如果想要实现类似的功能,首先需要获取终端的宽度,然后计算输出文件列表的最大列数,最后按列将文件输出到屏幕上。
终端宽度的获取
查了很多,发现书上提到的 int ioctl(int fd, int cmd, ...)
可以实现这个功能, 先放出代码:
// 获取终端宽度
int get_ter_size (void)
{
struct winsize w;
ioctl(STDOUT_FILENO, TIOCGWINSZ, &w);
return w.ws_col;
}
ioctl
可以控制特殊设备文件的属性,第一个参数是一个以及打开的文件描述符,STDOUT_FILENO
是标准输出的POSIX 名称,他的文件描述符是1,可以在 unistd.h
找到它的宏定义。使用man 2 ioctl_tty
可以查询到其他两个参数的信息:
TIOCGWINSZ
是一个获取终端大小的命令,w是一个struct winsize
,其中的 w.ws_col
就存储了我们想得到的终端宽度。
计算输出列表的最大列数
没想出来有什么比较好的方法来通过已经给定的一系列字符串和一个限制宽度来求出在这个宽度限制内输出的最大列数。
所以最后决定暴力穷举,将列数从大到小一个一个的尝试。
int Cal_Print(char b[][NAME_MAX + 10], int a[], int n) // 暴力运算, 求出打印的列数
{
// b 是一个二维数组,存储了要计算宽度的文件名
int t = get_ter_size(); // 终端宽度
int i = n < MAX_C ? n : MAX_C; // 设定最大列数初始值
int length; // 计算字符串长度
for (int j = 0; j < n; j++)
a[j] = strlen(b[j]);
for (; i >= 1; i--)
{
int c = n % i ? (n / i + 1) : (n / i);
if ((i - 1) * c >= n)
continue;
length = 0;
int max_len[MAX_C] = {0};
for (int j = 0; j < i; j++)
{
int k = j * c;
for (int m = 0; m < c && k < n; m++, k++)
if (a[k] > max_len[j])
max_len[j] = a[k];
length += max_len[j];
if (j != i - 1)
length += 2;
}
if (length < t)
return i;
i--;
}
if (!i)
i = 1;
return i;
}
得到终端宽度后,在代码中我限制了最大宽度为25(MAX_C), 这个数差不多足够了,之后如果文件的总数小于最大宽度,那么就从文件的总数n开始穷举(毕竟最多也就一个文件名放一列)。
有了总数n和列i之后,很简单就能计算出行。
计算每一列字符串最大的长度后加起来,再算上分割列之间的 (i-1)×2 的空格,如果总长度不大于终端宽度,这个列数就是我们所需要的。
计算完行数之后还会有一个问题,我们是为了解决在终端中按行打印文件名的问题,打印出来的结果只有最后一列可以空几个文件名,也就是要保证除了最后一列其他列都要填满。所以加上一个判断条件:if ((i - 1) * c >= n) continue;
在计算 max_len[j]
一定要注意加上 k < n
这个条件,不然如果在 i * c > n
的情况下数组会越界。
还有一种极端情况:终端宽度太小,导致最后算出的 i 值为0, 这种情况就讲i的值设为1,默认输出一列。
按列输出
由于要以整齐的方式按列输出 需要我们之前计算过的字符串长度,为了方便我们直接在计算完列数之后就输出文件目录:
int kneed = c - i * c + n; // 最后一列的行数
for (int p = 0; p < c; p++) // c 行数
{
for (int o = 0; o < i - 1; o++) // i 列数 先输出前 i-1 列
{
printf("%s", b[p + o*c]);
PrintB(max_len[o] - a[p + o*c]);
PrintB(2); // 输出空格以整齐排列
}
if (kneed) // 如果最后一列的第p行有内容就输出
{
printf("%s", b[p + (i-1) * c]);
kneed--;
}
printf("\n");
}
break;
代码中我用了PrintB(int num)来输出空格, 也可以简单一点使用printf
的 *
修饰符来控制格式
printf("%s%*s", b[p + o*c], max_len[o] - a[p + o*c] + 2, " ");
最开始以为这么弄就应该没问题了,也测试过输出格式,但是emmmm…..
如果文件名里面有中文的话,strlen会把中文识别为3个字节,但是一个中文其实差不多只占两个,如果文件名有中文的话会导致很严重的排版错误。。。。
具体可以看一下这两篇博客:
https://whoisnian.com/2018/01/24/Linux%E4%B8%8Bls%E5%91%BD%E4%BB%A4%E6%8E%92%E7%89%88/
http://www.ruanyifeng.com/blog/2007/10/ascii_unicode_and_utf-8.html
UTF-8 的编码规则很简单,只有二条:
1)对于单字节的符号,字节的第一位设为0,后面7位为这个符号的 Unicode 码。因此对于英语字母,UTF-8 编码和 ASCII 码是相同的。
2)对于n字节的符号(n > 1),第一个字节的前n位都设为1,第n + 1位设为0,后面字节的前两位一律设为10。剩下的没有提及的二进制位,全部为这个符号的 Unicode 码。
// 代码来自 https://whoisnian.com/2018/01/24/Linux%E4%B8%8Bls%E5%91%BD%E4%BB%A4%E6%8E%92%E7%89%88/
int Str_UTF(char * str) // 只占两个宽度的中文字符会识别为三个字节
{
int num = 0, i;
while (*str != '\0')
{
if (*str > 0) // 如果是单字节符号(这里指 英文符号),正常计算长度
{
num++;
str++;
}
else
{
// n字节的符号(n > 1),第一个字节的前n位都设为1
for (i = 7; i >= 0; i--)
if (!((*str >> i) & 1)) // 右移运算,把第 n+1 位上的0恰好移到 这个字节的最后一位,第7 ~ i+1次, 最后一位会是1
break;
num += 2; // 中文按两个字节算
str += 7 - i; // 跳过这个中文符号(占用7-i个字节)
}
}
return num;
}
手写一个strlen之后就可以正常显示了。
在这里也吐槽下中文字符排序的问题,以前一直认为中文排序可以用strcmp来解决,这次写ls命令测试发现中文的编码比较乱,而且涉及到多音字的问题,中文的排序比较复杂,不是直接比较编码就能搞定的。
写 ls -R 时关于递归的问题
最开始在实现 -R 参数的时候用了递归,其中解析文件属性用了 stat() 函数。
但是在测试递归主目录的时候发现了一个很严重的问题,平时用 stat 和 lstat 的区别就是在链接文件的识别上,一个识别链接文件指向的文件,另一个是链接文件本身。
我的主目录下有一个文件夹:
/home/username/.deepinwine/Deepin-TIM/dosdevices
查看具体属性后:
lrwxrwxrwx 1 username username 1 7月 26 19:28 z: -> /
emm… 这个文件是指向系统根目录的,如果递归到这个文件会出现无限死循环。。。。
当然递归时候也需要注意跳过.
和 ..
这两个特殊目录。。
不然也会出现无限递归死循环的问题。