大綱
(0) 可攜性問題
(1) 絕對 / 相對路徑 < too easy >
(2) 取得當前路徑
(3) 設定當前路徑
(4) 建立資料夾
(5) 刪除資料夾
(6) system call 誤失
(7) 列舉資料夾下所有檔案 < Q&A 等級 >
(8) 取得特殊資料夾路徑 < 我的文件、最近開啟檔案等>
(9) 檔案管理 *
* 本文所提及之 system 指令,部份可以 popen / _popen 方式處理。
* 對於字集編碼問題,不為本文探討內容。
* 撰完文後才發現,基於 VC 太深 < 筆者非願如此 > ,於此告知 。
* 外部程式執行之方式 (system, WinExec, CreateProcess 等) 雖有點相關,但不在本文探討內容 。
* 目錄管理與檔案管理常放在一起探討,但篇幅有限,直接連到 以前寫過的檔案管理 做探討。
0. 可攜性問題
標準函式庫中,並沒強制要求目錄管理之相關函式該放於何處,
也沒強制要求這些函式名稱該如何定義,
但妙的是從 Turbo C 開始就提供了部份相關函式,
至今對任何 compiler 而言,
header file、function name、function prototype 等,
還是沒有一致性的結論,目前自制、宣稱能達到可攜性的作法,
是研究各環境、compiler 之相關函式使用方式,
再以 pre-process (#ifdef) 方式進行封裝。
唯此文並不以可攜性為前提,所撰之程式碼以 VC 10.0 為主,
MinGW gcc 應大多執行 ( 筆者只有測試列舉所有檔案是相容的 ),
目前筆者所知大概有幾個 header 可以查
dir.h 或 direct.h , io.h 。
另外 VC 對於這類函式,幾乎全都是以底線開頭當函式名稱。
* 一個引戰點:既然沒有一個非常標準的作法,筆者認為為了省事,
適當(當必要時)調用 system 相關指令也不是一個太壞的選擇。
如同有人強調不要用 system("pause"); 但實際上就是沒有可攜性作法可完全達到相同目的...
1. 絕對 / 相對路徑
一般程式在執行時之預設路徑,為該執行程式所在位置。
假設於 D:\F1\FF1 存在一個 main.exe (console),而 F1 底下又存在這些資料夾
D:\F1\FF1 (含 a.txt)
D:\F1\FF2 (含 b.txt)
D:\F1\FF3 (含 c.txt)
當 main.exe 快點兩下執行時,目前之目錄設定即為 D:\F1\FF1,
這便是程式預設之「當前目錄」。所有的 fopen、open 等相關函式,
若只寫檔名時,便是搜尋當前目錄,如 fopen("a.txt", "r");
為執行開啟 D:\F1\FF1\a.txt 檔案之動作,這屬相對路徑之指定,
實際在指定時只指定檔名,由當前目錄進行搜尋。
另一種是絕對路徑,假設要開啟的檔案是 D:\F1\FF2\b.txt,
可以直接指定,如 fopen("D:\\F1\\FF2\\b.txt", "r");
即使當前目錄並不在 FF2 裡,但指定完整路徑後,便可跳到該目錄下搜尋。
一般而言若 資料夾/檔案 結構 已被拿來當作是小型資料庫使用時 ,
筆者較習慣使用絕對路徑方式處理,在 debugging / trace 顯得較方便。
相對路徑裡還有一種表示,常見於較多層資料夾情況一次複製給 user。
假設目前位於 D:\F1\FF1,要搜尋「上一層目錄」(也就是 D:\F1 )
裡面之 FF3 資料夾,裡面之 c.txt 時,可以使用這種表示法
fopen("..\\FF3\\c.txt");
拆解一下
..\\ 回到上一層 的
FF3\\ FF3 資料夾 的
c.txt
2. 取得當前路徑
較常見之作法為 getcwd ( vs 2003 後開頭加底線為佳 )
#include <direct.h> #include <stdio.h> char *getcwd( char *buffer, int maxlen ); int main() { char buf[2000]; getcwd(buf, sizeof(buf)-1); puts(buf); getchar(); return 0; }
Win32 API 可見 GetCurrentDirectory。
3. 設定當前路徑
較常見之作法為 chdir ( vs 2003 後開頭加底線為佳 )
#include <direct.h> #include <stdio.h> int chdir(const char* Path); int main() { char buf[2000]; if(!chdir("C:\\")){ getcwd(buf, sizeof(buf)-1); puts(buf); } else { puts("chdir fail"); } getchar(); return 0; }
Win32 API 可見 SetCurrentDirectory。
4. 建立資料夾
較常見之作法為 mkdir ( vs 2003 後開頭加底線為佳 )
#include <direct.h> #include <stdio.h> int main() { if(mkdir("C:\\folder")==0) puts("create folder success"); else puts("create folder success"); getchar(); return 0; }
Win32 API 可見 CreateDirectory 相關函式。
大多 mkdir 之設計在成功時會返回0,失敗時會返回 -1。
注意到的是大多 compiler 給予 mkdir 之特性:
一般而言不能用在建立多層資料夾之情形,如
mkdir("C:\\F1\\SUB1\\SUB2\\SUB3");
必須一層一層建立起來,即使是 CreateDirectory 也如此。
但 mkdir 即使這種情形建立失敗後,返回值仍為 0 (個人是覺得有寫錯之嫌),這是該注意之處。
要建立深入資料夾有幾種作法
ˇ 直接進行 strtok (... ,"\\", ...) 作分析 < 這也是較常用的 >
ˇ 調用 Win32 DbgHelp API : MakeSureDirectoryPathExists < 不建議, 沒 unicode 版本 >
ˇ 調用 SHCreateDirectoryEx : 放在 <Shlobj.h> / <Shell32.lib> 。
通常較建議直接做 strtok 分析 (或 stringstream 取得) 即可。
5. 刪除資料夾
較常見之作法為 rmdir ( vs 2003 後開頭加底線為佳 )
#include <direct.h> #include <stdio.h> int main() { if(rmdir("C:\\folder")==0) puts("remove folder success"); else puts("remove folder success"); getchar(); return 0; }
成功時傳回0,失敗時傳回-1。但若本來要移除的資料夾就不存在時,它會傳回 0 < 成功 >。
Win32 API 請見 RemoveDirectory。
rmdir 算是一個非常基本款、不非常方便的指令。要移除一個資料夾之前,
必須先將裡面的檔案與其他資料夾全都清空 (也就是刪除) 才可以完成,
但大多使用時是希望,不論這資料夾裡面有沒有東西,
直接把它移掉就對了 < 裡面有東西的話就順便移掉 >。
這作法就麻煩了,遞迴列舉資料夾底下所有檔案/資料夾,逐一刪除;
再來是使用 SHFileOperator ( Shell32.lib, Shellapi.h ),
使用起來並不容易。
6. system call 誤失
由於使用 system 呼叫 cd、dir 等指令時,由於環境變數不會保存下來,故
system("cd /D C:\\F1\\FF2");
這是無效的。但一些指令確實非常好用,像是剛剛所提到的,
要直接刪除某個目錄時,直接調用 system("del C:\\F1 /f /s /q");
便可深度刪除。而 system("rm C:\\1\\2\\3\\4");
也可深度建立資料夾。
7. 列舉資料夾下所有檔案
這問題可以講很久,這裡點到為止。
列舉資料夾底下所有檔案筆者保守推估至少存在 3 ~ 5 種作法。
(7.1) GUI 拉子控制出來
這是蠻偷懶的方式,一些別人已經包好了 combox、list 等子控制,裡面包含了 function member - Dir,直接拉一個子控制出來,顯示設隱藏,需要的時候再調用即可。
(7.2) system("dir")
假設要查 C:\ 底下所有 .txt 檔案,先做 system("C:\\*.txt /b > tmp.rec") ; 之動作,最後 tmp.rec 裡便是所有檔名,所以只要讀 tmp.rec 便可。
(7.3) direct.h / io.h
#include <stdio.h> #include <string.h> #include <io.h> #include <direct.h> #include <time.h> #define _MAX_FNAME 2000 void EnumAllFileTree(char *path); int main() { const char* path = "D:\\Couse"; const char* filter = "*.*"; char attrib[_MAX_FNAME]; char filename[_MAX_FNAME]; struct _finddata_t file; long hFile; struct tm *tmCreate, *tmWrite, *tmAccess; chdir(path); hFile = _findfirst(filter, &file); if(hFile!=-1){ do{ // get filename sprintf(filename, "%s\\%s", path, file.name); printf("%s\n", filename); memset((void*)attrib, 0, _MAX_FNAME); // file attribute strcpy(attrib, "\tattrib:[ "); if(file.attrib & _A_ARCH) strcat(attrib, "檔案 "); if(file.attrib & _A_HIDDEN) strcat(attrib, "隱藏 "); if(file.attrib & _A_NORMAL) strcat(attrib, "普通 "); if(file.attrib & _A_RDONLY) strcat(attrib, "唯讀 "); if(file.attrib & _A_SUBDIR) strcat(attrib, "資料夾 "); if(file.attrib & _A_SYSTEM) strcat(attrib, "系統 "); strcat(attrib, "]"); printf("%s\n", attrib); printf("\tfilesize:%u\n",file.size); // get access, create, write time, and ouput // printf("\tcreate:%s", ctime(&file.time_create)); // printf("\twrite:%s", ctime(&file.time_write)); // printf("\taccess:%s", ctime(&file.time_access)); // trans to struct tm, tmCreate as sample. // tmCreate = localtime(&file.time_create); printf("\t%d/%d/%d,%d:%d:%d:, (%d)\n", tmCreate->tm_year+1900, tmCreate->tm_mon+1, tmCreate->tm_mday, tmCreate->tm_hour, tmCreate->tm_min, tmCreate->tm_sec, tmCreate->tm_wday); }while(_findnext(hFile, &file)==0); _findclose(hFile); } getchar(); return 0; }
(7.4) 其他方式
筆者說過,這問題還是必須看 compiler 與環境為主,所以可能找到的是 opendir、readdir 這組;
而 Win32 API ,可參考 FindFirstFile、FindNextFile、struct WIN32_FIND_DATA 等函式;
另外 boost 裡面也有,在 boost / filesystem.hpp 裡面,這裡筆者不熟,就不贅述。
(7.5) 深度掃描問題
像是防毒軟體,可能會掃描整個資料夾、整個 C 槽,或整顆硬碟,基於上述之程式碼,這裡提供另一組。
#include <stdio.h> #include <string.h> #include <io.h> #include <direct.h> #define _MAX_FNAME 2000 void EnumFiles(char *path); int main() { EnumFiles("C:\\"); getchar(); return 0; } void EnumFiles(const char *path) { const char *filter = "*.*"; char filename[_MAX_FNAME]; char attrib[_MAX_FNAME]; struct _finddata_t file; long hFile; chdir(path); hFile = _findfirst(filter, &file); if(hFile!=-1){ do{ // important (!) if(!strcmp(file.name, ".") || !strcmp(file.name,"..")){ continue; } sprintf(filename, "%s\\%s", path, file.name); printf("%s\n", filename); if(file.attrib & _A_SUBDIR) { printf("%s <folder>\n", filename); EnumFiles(filename); } }while(_findnext(hFile, &file)==0); _findclose(hFile); } }
有興趣可跑跑看,但這會跑很久。另外筆者不確定用這種方式最後會不會因為 recursive 太深層關係,最後造成 stack overflow。
(7.6) 只窮舉特定之檔案
ˇ 有沒有辦法只列舉「資料夾」即可?
ˇ 有沒有辦法只列舉「唯讀之 *.txt 」即可?
ˇ 有沒有辦法依「建立時間」之順序拿到?
類似的問題還很多。目前筆者不知道除了一一窮舉、再判斷檔案/資料夾屬性外,
能有其他辦法可一次就拿到上面這些目的。
「依 xxx 時間」之順序拿應是較令人感興趣的問題,目前實作很多人是全都塞到記憶體裡面去,
直接呼叫 qsort 出來做排序。另一派 < 也就是筆者,犯懶 > ,
直接用 system("dir") 之相關指令,與 io redirection 完成,這部份有興趣可研究 dir 指令。
(7.7) 筆者的建議..
由於窮舉資料夾底下之特定檔案,幾乎是必遇之問題,在 VBA 裡更是頻繁,
任何程式語言,較建議包成 callback function 處理 (如果它可以的話...),一勞永逸。
< 代價可能是 function call 之時間 ,但若處理之引數複雜時,可能就不適合用 callback function>
8. 取得特殊資料夾路徑 < 我的文件、最近開啟檔案等>
通常這是用 Win32 API 完成,參考 SHGetSpecialFolderPath。
而特殊資料夾路徑之代號,參考 CSIDL ,裡面的參數並非在所有電腦裡都可順利取得。
這裡拿取得「我的最愛」路徑為例。
#include <windows.h> #include <stdio.h> #include <shlobj.h> int main() { char path[2000]; SHGetSpecialFolderPath(NULL,path,CSIDL_COMMON_FAVORITES,FALSE); puts(path); getchar(); return 0; }
取得特殊資料夾路徑後,要進行窮舉,方法與上述 (section 7 ) 相同。
9. 其他檔案管理 *
檔案管理和目錄管理幾乎都是一起提的,唯篇幅限制,且筆者之前也寫過一份,有興趣請連至 此篇文章。