Linux系統程式設計【3.2】——ls命令優化版和ls -l實現
阿新 • • 發佈:2021-02-10
## 前情提要
在筆者的上一篇部落格[Linux系統程式設計【3.1】——編寫ls命令](https://www.cnblogs.com/lularible/p/14386358.html)中,實現了初級版的ls命令,但是與原版ls命令相比,還存在著顯示格式和無顏色標記的不同。經過筆者近兩天的學習,基本解決了這兩個問題,且實現了"ls -l",並對於可選引數"-a"和"-l"有了更好的支援(不管-a,-l輸入順序如何,是"ls -a -l",還是"ls -l -a",還是"ls -al",亦或是"ls -ls",出現位置幾何,重複與否,都能正確執行)。
## ls顯示格式的解決
首先,讓我們來觀察一下原版ls顯示的格式:
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20210209220131296.PNG)
筆者總結出的原版ls顯示規律:
- 1.按序顯示
- 2.按照排序規則按列從上往下顯示,當前列顯示完成後轉到下一列繼續顯示
- 3.每一列都是左對齊的
- 4.每一列的寬度都是該列最長檔名長度加2
- 5.列的數目要儘量保證每一行都被檔名“填充滿”,而又不會導致行中最後一個檔名換行
在我們自己實現ls顯示時,如何滿足這5個規律呢?
對於規律1,筆者已經採用排序演算法來滿足了。規律3可以採用printf函式中的"%-*s"來滿足。其他三條規律好像不太好滿足。問題的關鍵在於,我們不知道要顯示的行數和列數,以及每一列的最大字串長度。為了獲得這些資料,接下來介紹筆者琢磨出來的一種演算法,姑且將其稱為“分欄演算法”吧。
### 分欄演算法
問題可以簡化為:給你一個已經排序的字串指標陣列,求能滿足上述5個規律輸出顯示這些字串的行/列數,以及每一列的最大字串長度。
那麼就讓我們來構建這樣一個函式簽名:
由於要返回三個不同資料,所以將其中一個作為返回值,另外兩個作為指標傳入引數。
1.確定函式返回值:返回行數
2.確定函式引數:字串指標陣列、字串個數、列數指標、每列的最大字串長度陣列
函式簽名如下所示:
```c
int cal_row(char** filenames,int file_cnt,int* cal_col,int* col_max_arr);
```
#### "囫圇吞棗"版分欄演算法
在函式主體中,先計算出
```bash
最少佔用字元數 =(每個字串長度+2)* 總字串個數
```
加2是因為在顯示時每個字串後面至少跟兩個空格。利用ioctl函式呼叫,獲取當前螢幕寬度(一行能容納的字元數)。用最少佔用字元數除以螢幕寬度,得到一個基準行數。這個基準行數是最少所需行數,因為只有在每個字串長度都一致,且加2之後的長度能整除螢幕寬度時,才只需要這麼少的行數就能裝得下。
當字串之間長度差距較大時,所需的空間就越多。比如同一列中,一個字串老長了,其他的比較短,為了滿足規律4,那麼短的字串後面跟的空格數就大大增加。
獲得基準行數之後,預分配基準行數*螢幕寬度的字元數大小,就將字串一一從上到下(排基準行數行),從左到右排列(列數儲存給列數指標),同時對於每一列都獲取最大字串長度(存到每列最大字串長度陣列中),重新計算所佔總字元數時,將每一列最大字串長度乘以行數的值算進去。然後將所佔總字元數減去預分配的大小,繼續分配足夠的行來容納這個差值。
迭代進行上述步驟,直到所佔總字元數不大於預分配的大小。返回最後分配的行數。
如下所示為圖解:
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20210210103639668.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2x1bGFyaWJsZQ==,size_16,color_FFFFFF,t_70)
之所以說它是“囫圇吞棗”,是因為在處理超額長度時,直接取最長的那一個代表最後一列的所有字串長度,一刀切了。這可能導致在某些情況下格式顯示不正確。
原始碼如下:
```c
int cal_row(char** filenames,int file_cnt,int* cal_col,int* col_max_arr)
{
//獲取路徑名中的檔名之前的長度
int path_len = strlen(filenames[0]);
while(filenames[0][path_len] != '/'){
--path_len;
}
++path_len;
struct winsize size;
ioctl(STDIN_FILENO,TIOCGWINSZ,&size);
int col = size.ws_col; //獲得當前視窗的寬度(字元數)
int col_max = 0;
int cur_file_size = 0;
int filenames_len[file_cnt];
*cal_col = 0;
int i = 0;
//獲得每個檔名的字元長度
for(i = 0;i < file_cnt;++i){
filenames_len[i] = strlen(filenames[i]) - path_len;
cur_file_size += (filenames_len[i] + 2); //計算所有檔名字元數加兩個空格的總字元數
}
//最小行數,在此基礎上迭代
int base_row = cur_file_size / col;
if(cur_file_size % col){
base_row++;
}
int pre_allc_size = 0;
do{
*cal_col = 0;
pre_allc_size = base_row * col;
cur_file_size = 0;
for(i = 0;i < file_cnt;++i){
//當前列的最後一行
if(i % base_row == base_row - 1){
++(*cal_col);
col_max = (filenames_len[i] > col_max) ? filenames_len[i] : col_max;
cur_file_size += (col_max + 2) * base_row;
col_max_arr[*cal_col-1] = col_max;
col_max = 0;
}
//非最後一行
else{
col_max = (filenames_len[i] > col_max) ? filenames_len[i] : col_max;
}
}
//最後一列未滿
if(i % base_row){
++(*cal_col);
cur_file_size += (col_max + 2) * (i % base_row);
col_max_arr[*cal_col-1] = col_max;
col_max = 0;
}
int dis = 0;
if(cur_file_size > pre_allc_size){
dis = cur_file_size - pre_allc_size;
}
base_row += (dis / col);
if(dis % col){
++base_row;
}
}while(cur_file_size > pre_allc_size);
return base_row;
}
```
在進行輸出時,從左往右,從上到下列印,後續在程式碼中體現。
#### "精打細算"版分欄演算法
在計算基準行數時,與"囫圇吞棗"的分欄演算法一樣。區別就在於對於超額列的處理上。設定一個當前螢幕剩餘寬度變數,對於每一個字串,當其長度超過剩餘寬度,則終止排列,所需的額外空間大小為剩餘未排列的所有字串塊長度之和。
下圖所示為圖解:
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20210210104132173.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2x1bGFyaWJsZQ==,size_16,color_FFFFFF,t_70)
正因為計算超額空間時,算的是每個未排列字串塊長度的真實長度,所以是“精打細算”。並且每次都檢測是否超剩餘寬度,所以能嚴格保證最終顯示格式的正確性。
原始碼如下:
```c
int cal_row(char** filenames,int file_cnt,int* cal_col,int* col_max_arr)
{
//獲取路徑名中的檔名之前的長度
int path_len = strlen(filenames[0]);
while(filenames[0][path_len] != '/'){
--path_len;
}
++path_len;
struct winsize size;
ioctl(STDIN_FILENO,TIOCGWINSZ,&size);
int col = size.ws_col; //獲得當前視窗的寬度(字元數)
int col_max = 0;
int cur_file_size = 0;
int filenames_len[file_cnt];
*cal_col = 0;
int i = 0;
int j = 0;
//獲得每個檔名的字元長度
for(i = 0;i < file_cnt;++i){
filenames_len[i] = strlen(filenames[i]) - path_len + 2; //字串至少帶兩個空格
/*特殊情況:當最大字串長度比螢幕寬度大時,直接返回行數:file_cnt,列數:1,最大寬度:最大的字串長度*/
if(filenames_len[i] > col){
*cal_col = 1;
col_max_arr[0] = filenames_len[i];
return file_cnt;
}
cur_file_size += filenames_len[i];
}
//最小行數,在此基礎上迭代
int base_row = cur_file_size / col;
if(cur_file_size % col){
base_row++;
}
int flag_succeed = 0; //標記是否排列完成
//開始排列
while(!flag_succeed){
int remind_width = col; //當前可用寬度
*cal_col = -1;
for(i = 0;i < file_cnt;++i){
/*如果剩餘寬度不足以容納當前字串,則跳出並分配額外的行空間*/
if(filenames_len[i] > remind_width){
break;
}
//新起的一列
if(i % base_row == 0){
++(*cal_col);
col_max_arr[*cal_col] = filenames_len[i];
}
else{
col_max_arr[*cal_col] = (filenames_len[i] > col_max_arr[*cal_col]) ? filenames_len[i] : col_max_arr[*cal_col];
}
//最後一行,更新剩餘的寬度
if(i % base_row == base_row - 1){
remind_width -= col_max_arr[*cal_col];
}
}
//判斷是否排列完成
if(i == file_cnt){
flag_succeed = 1;
}
//再分配額外行空間
else{
int extra = 0; //所需額外的字元數
while(i < file_cnt){
extra += filenames_len[i++];
}
if(extra % col){
base_row += (extra / col + 1);
}
else{
base_row += (extra / col);
}
}
}
++(*cal_col); //列標從0開始,所以最後加1
return base_row;
}
```
### 分欄效果展示與不足
#### 兩種分欄演算法的比對
1."囫圇吞棗"版分欄演算法和原版ls的對比
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20210209231918822.PNG?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2x1bGFyaWJsZQ==,size_16,color_FFFFFF,t_70)
前一個是原版ls命令顯示效果,後一個是筆者實現的ls03("囫圇吞棗"版)命令顯示效果,基本一致。
2."精打細算"版分欄演算法和"囫圇吞棗"版分欄演算法比對
之前提到過在某些情況下,"囫圇吞棗"版分欄演算法格式顯示不正確。下圖就是一個例子:
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20210210105846739.PNG?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2x1bGFyaWJsZQ==,size_16,color_FFFFFF,t_70)
可以看到,"囫圇吞棗(ls03)"顯示格式不對,但"精打細算(ls04)"就沒問題。所以"精打細算"版分欄演算法是"囫圇吞棗"版分欄演算法的優化。
#### 不足之處
目前來看,即使是更厲害的"精打細算"版分欄演算法,也存在兩個主要的不足點。
其一是排序與原版不同,筆者利用的是ASCII值來進行字串排序的,對於大小寫字母,特殊符號和漢字,沒有做到像原版ls那樣合適。
其二是在顯示漢字檔名時,格式不正確,如下所示:
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20210210110712615.PNG)
顯示出來的存在沒有左對齊的列。筆者推測,漢字的字串單位長度和顯示出的所佔寬度不是一比一的關係,導致計算時存在偏差。進一步推測,除了漢字,對於其他的特殊符號,只要單位長度和顯示出的所佔寬度不是一比一,都會存在偏差。如果想要解決這個問題,就要查詢這個對應關係,然後進行特定的轉換。
## ls顯示顏色的解決
因為要查詢有關顏色方面的資訊,筆者通過linux指導資訊,確定dircolors這個命令可以給我提供有用的資訊,輸入:
```powershell
man dircolors
```
顯示資訊如下:
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20210210122852233.PNG?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2x1bGFyaWJsZQ==,size_16,color_FFFFFF,t_70)
其中有一個選項是:dircolors -p,能夠打印出所有預設的顏色資訊。我們輸入:
```powershell
dircolors -p > color
```
">"表示將dircolors -p命令的輸出結果儲存到color檔案中(若沒有則自動建立)。
然後輸入:
```powershell
vim color
```
就可以進入檔案檢視其中內容了(當然用"more color"命令也行,只是筆者習慣用"vim"命令),如下所示:
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20210210124021654.PNG?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2x1bGFyaWJsZQ==,size_16,color_FFFFFF,t_70)
對於每種型別的檔案,都給出了預設的顏色值。
其中關於c語言顏色列印,為了防止本文篇幅過長,影響閱讀體驗,可自行查閱資料,這裡隨便給出一個連結供參考:
[printf列印顏色](https://blog.csdn.net/judgejames/article/details/82735738)
接下來的事情就是如何獲得指定的檔案型別,到這裡就引出了下一節內容:ls -l的實現。
## ls -l的實現
### 獲取stat結構體內容
首先看一下原版ls -l的輸出內容:
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20210210125150883.PNG?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2x1bGFyaWJsZQ==,size_16,color_FFFFFF,t_70)
- 第一部分:模式(mode),每行的第一個字元表示檔案型別。"-"表示普通檔案,"d"表示目錄等等。接下來的9個字元表示檔案訪問許可權,分為讀許可權、寫許可權和執行許可權,又分別針對3種物件:使用者、同組使用者和其他使用者。
- 第二部分:連結數列(links),表示該檔案被引用次數。
- 第三部分:檔案所有者(owner),表示檔案所有者的使用者名稱
- 第四部分:組(group),表示檔案所有者所在的組名
- 第五部分:大小(size),表示檔案所佔位元組數
- 第六部分:最後修改時間(laste-modified)
- 第七部分:檔名(name)
如何獲得這些資訊?筆者通過查閱書籍,知道了一個"stat"函式,能幫助我們拿到上述資訊。
輸入:
```powershell
man 2 stat
```
ps:直接輸入man stat得到的是stat(1),是一個終端命令,不是我們想要的,在其中的“SEE ALSO”找到的stat(2)才是我們需要的。
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20210210130633664.PNG?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2x1bGFyaWJsZQ==,size_16,color_FFFFFF,t_70)
將檔案路徑傳入stat函式,可以獲得一個stat型別的結構體,該結構體定義如下:
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20210210130817109.PNG?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2x1bGFyaWJsZQ==,size_16,color_FFFFFF,t_70)
太棒了,我們要的資訊它都有!
### 格式轉換
但是不要高興的太早,如果直接列印這個stat結構體裡面的st_mode、st_uid、st_gid和st_mtime,顯示的是一些數字,所以還要對它們一一轉換成字串再輸出。
#### st_mode位運算
st_mode實際上是一個16位的二進位制數,其結構如下:
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20210210132554342.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2x1bGFyaWJsZQ==,size_16,color_FFFFFF,t_70)
Linux中檔案被分為7大類,也就是有7種不同的編碼,我們只要提取其中的type位,然後與定義的檔案類比對就能知道該檔案型別了。同樣的,對於許可權位,只要此位為1,就置為'r'、'w'或'x',否則置為'-'。
如何判斷特定的位的值呢?這裡就需要掩碼這個東西了,筆者在計算機網路中學過類似的玩意--ip地址掩碼。掩碼的作用就是將其他位遮擋住,只暴露所需的位。即掩碼將其他的位設為0,所需的位設為1,利用"與"運算(&),把待處理的數和掩碼相與,將結果與預先存放的數對比看是否一致。
```c
//定義判斷檔案型別的巨集
#define S_ISFIFO(m) (((m)&(0170000))==(0010000))
#define S_ISDIR(m) (((m)&(0170000))==(0040000))
#define S_ISCHR(m) (((m)&(0170000))==(0020000))
#define S_ISBLK(m) (((m)&(0170000))==(0060000))
#define S_ISREG(m) (((m)&(0170000))==(0100000))
#define S_ISLNK(m) (((m)&(0170000))==(0120000))
#define S_ISSOCK(m) (((m)&(0170000))==(0140000))
#define S_ISEXEC(m) (((m)&(0000111))!=(0))
```
舉例來說:上圖中第一個掩碼是0170000,第一個0表示這個數是八進位制的,轉換為二進位制為1111000000000000,即type位全部為1,假設st_mode為0001000000000000,相與之後得到0001000000000000,即八進位制的0100000,查表就知道這是一個regular檔案。
同理,上圖中第二個掩碼是0000111,它把'x'位設為1,其他位為0,與st_mode相與後,如果st_mode中任意一個'x'位為1,那麼結果就不等於0,由此可以判斷檔案是否為可執行檔案。
這樣,我們就可以利用掩碼和預定義的值來協助完成模式字串的轉換。
#### st_uid/st_gid/st_mtime格式轉換
藉助getpwuid函式,getgrgid函式和ctime函式分別完成使用者名稱、組名和時間格式的轉換。
### ls -l效果展示
![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20210210140839153.PNG?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2x1bGFyaWJsZQ==,size_16,color_FFFFFF,t_70)
## 原始碼
ls可選引數:-a ,-l
```c
/*
* ls04.c
* writed by lularible
* 2021/02/09
* ls -a -l
*/