1. 程式人生 > >Linux系統程式設計【3.2】——ls命令優化版和ls -l實現

Linux系統程式設計【3.2】——ls命令優化版和ls -l實現

## 前情提要 在筆者的上一篇部落格[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 */