在大部分的情況下,我們不會限制使用者輸入檔案路徑的時候一定要使用絕對路徑還是相對路徑。但是在程式處理的過程中,或是需要輸出一些Log資訊的時候,我們可能就必須要將路徑通通都使用絕對路徑來處理了。Rust程式語言標準函式庫中的fs模組,以及PathPathBuf結構體,雖然有提供canonicalize函數或方法,是可以取得檔案的絕對路徑沒錯,但使用起來有一些嚴重的限制。



使用canonicalize取得檔案的絕對路徑

如以下程式,可以印出目前工作目錄的上層目錄中的myfile.txt檔案的絕對路徑。

use std::path::Path;

fn main(){
    let path = Path::new("../myfile.txt");

    println!("{:?}", path.canonicalize().unwrap());
}

請注意,PathPathBuf結構體的canonicalize方法,實際上是去呼叫fs模組的canonicalize函數,這個函數會回傳一個Result列舉。此時問題來了,在什麼情況下這個Result列舉會是Err變體呢?當目標檔案不存在的時候就會是Err變體(ErrorKindNotFound)。換句話說,Rust程式語言標準函式庫所提供的取得檔案絕對路徑之功能,目標檔案必須要是存在的狀態才能進行。而且如果該目標檔案是一個捷徑或稱符號鏈接(symbolic link)的話,canonicalize函數還會強制地去取得其對應的實體檔案路徑。

那麼要如何單純地去將一個檔案相對路徑解析成絕對路徑,而不去管該路徑的檔案究竟是什麼呢?

Path Absolutize

「Path Absolutize」是筆者開發的套件,可以有效地解析檔案路徑中的點.或是點點..,並根據目前工作目錄,將其轉換成絕對路徑。

Crates.io

Cargo.toml

path-absolutize = "*"

使用方法

使用use關鍵字來將path_absolutize這個crate底下的Absolutize特性給引用到當前的程式範圍下,PathPathBuf結構體就會擁有absolutizeabsolutize_virtually方法了!這兩個方法都可以將相對路徑轉成絕對路徑。

absolutize

absolutize方法的轉換規則如下:

  • 如果路徑不是以...開頭,則.會被忽略,..表示為上一層節點。
    use std::path::Path;
        
    use path_absolutize::*;
        
    let p = Path::new("/path/to/./123/../456");
        
    assert_eq!("/path/to/456", p.absolutize().unwrap().to_str().unwrap());
  • 如果路徑是以...開頭,則.表示目前的工作目錄路徑,..表示目前的工作目錄的上一層目錄。如果目前的工作目錄是根目錄,其上一層目錄也依然還是根目錄。
    use std::path::Path;
    use std::env;
        
    use path_absolutize::*;
        
    let p = Path::new("../path/to/123/456");
    
    let cwd = env::current_dir().unwrap();
        
    let cwd_parent = cwd.parent();
        
    match cwd_parent {
       Some(cwd_parent) => {
           assert_eq!(Path::join(&cwd_parent, Path::new("path/to/123/456")).to_str().unwrap(), p.absolutize().unwrap().to_str().unwrap());
       }
       None => {
           assert_eq!(Path::join(Path::new("/"), Path::new("path/to/123/456")).to_str().unwrap(), p.absolutize().unwrap().to_str().unwrap());
       }
    }
  • 原路徑如果是一個不是以...開頭的相對路徑,其等同於以.作為路徑開頭。
    use std::path::Path;
    use std::env;
        
    use path_absolutize::*;
        
    let p = Path::new("path/../../to/123/456");
    
    let cwd = env::current_dir().unwrap();
        
    let cwd_parent = cwd.parent();
        
    match cwd_parent {
       Some(cwd_parent) => {
           assert_eq!(Path::join(&cwd_parent, Path::new("to/123/456")).to_str().unwrap(), p.absolutize().unwrap().to_str().unwrap());
       }
       None => {
           assert_eq!(Path::join(Path::new("/"), Path::new("to/123/456")).to_str().unwrap(), p.absolutize().unwrap().to_str().unwrap());
       }
    }
  • 原路徑如果是有包含..的絕對路徑,不論原路徑中的..有多少個,轉換出來的絕對路徑均不會向上超出根目錄。
    use std::path::Path;
    
    use path_absolutize::*;
    
    let p = Path::new("/path/to/../../../../123/456/./777/..");
    
    assert_eq!("/123/456", p.absolutize().unwrap().to_str().unwrap());
absolutize_virtually

absolutize_virtually方法可以特別限制轉換出來的絕對路徑必須在哪層節點之下。與「absolutize」方法相比有個最大的差異在於,原路徑如果是一個不是以...開頭的相對路徑,並不會預設是在目前的工作目錄下,而是會設在另外傳給absolutize_virtually方法的路徑節點之下。這個另外傳進的路徑就像是虛擬的根目錄一樣,不論原路徑中的..有多少個,轉換出來的虛擬絕對路徑均不會向上超出這個另外傳進的路徑。

use std::path::Path;
    
use path_absolutize::*;
    
let p = Path::new("path/to/../../../../123/456");
    
assert_eq!("/virtual/root/123/456", p.absolutize_virtually("/virtual/root").unwrap().to_str().unwrap());
快取

預設的情況下,每次absolutizeabsolutize_virtually方法在執行的時候,都會去建立新的PathBuf實體來表示目前的工作目錄。這樣的作法會有明顯的開支。雖然這樣可以讓我們在程式執行階段於任意時間點安全地讓程式本身(例如使用std::env::set_current_dir方法)或是藉由外部控制(例如使用gdb去呼叫chdir)去修改程式目前的工作目錄,但在大多數的情況下我們並不需要這樣的功能。

為了讓路徑的解析更有效率,這個crate還提供了三種方式來快取目前的工作目錄。

once_cell_cache

啟用once_cell_cache特色可以讓這個crate使用once_cell來快取目前的工作目錄。這種快取方式是執行緒安全的(thread-safe),但一旦目前的工作目錄被快取了,在程式執行階段之後就無法再被改變了。

[dependencies.path-absolutize]
version = "*"
features = ["once_cell_cache"]
lazy_static_cache

啟用lazy_static_cache特色可以讓這個crate使用lazy_static來快取目前的工作目錄。這種快取方式是執行緒安全的(thread-safe),但一旦目前的工作目錄被快取了,在程式執行階段之後就無法再被改變了。

[dependencies.path-absolutize]
version = "*"
features = ["lazy_static_cache"]
unsafe_cache

啟用unsafe_cache特色可以讓這個crate使用一個可變的全域靜態變數來快取目前的工作目錄。這種快取方式允許程式可以程式本身去改變程式目前的工作目錄,但並不是執行緒安全的。

您需要使用update_cwd函數來初始化目前的工作目錄,這個函數也應該要在目前的工作目錄被改變後被呼叫。

[dependencies.path-absolutize]
version = "*"
features = ["unsafe_cache"]
use std::path::Path;

use path_absolutize::*;

unsafe {
    update_cwd();
}

let p = Path::new("./path/to/123/456");

println!("{}", p.absolutize().unwrap().to_str().unwrap());

std::env::set_current_dir("/").unwrap();

unsafe {
    update_cwd();
}

println!("{}", p.absolutize().unwrap().to_str().unwrap());
Path Absolutize有跨作業系統嗎?

上面貌似都是以Unix-like作業系統(如Linux、macOS)的檔案路徑來舉例,那如果是要用在Windows作業系統呢?別擔心,「Path Absolutize」也是有支援Windows作業系統的,使用方式也完全一樣!例如:

use std::path::Path;
    
use path_absolutize::*;

let p = Path::new(r".\path\to\123\456");
	
assert_eq!(Path::join(&CWD, Path::new(r"path\to\123\456")).to_str().unwrap(), p.absolutize().unwrap().to_str().unwrap());