意象說明
glob < pattern < 在眾多相似物中尋找
前言
Glob 類似於 regular expression,但是僅用來匹配檔案路徑名稱。名稱由來是 UNIX 一個負責解析匹配檔案、叫做 global command 的模組。
本文針對 gulp 使用的 node-glob 所支援的語法及選項進行說明,詳細的使用方法可參考 node-glob 的說明。
gulp.src()
基本使用方式
1 | var gulp = require('gulp'); |
gulp.src()
還支援陣列形式的多組 glob 參數:
1 | gulp.src(['/*.js', '/*.ts']) |
不過這個例子,可以使用後面介紹的『多選結構』加以簡化,而不必使用陣列:
1 | gulp.src('*/.{js,ts}') |
或者,直接使用 node-glob
來匹配檔案
安裝
1 | npm install --save glob |
注意,雖然 GitHub 上的名稱為 node-glob
,但是 npm 上卻是註冊為 glob
,因此安裝時名稱必須為 glob
。
呼叫 glob() 函數
1 | var glob = require('glob'); |
檔案路徑匹配結果以陣列回傳,如果沒有匹配任何路徑或檔案,將回傳空陣列。
Glob 元字元
前面兩個例子中,呼叫 gulp.src()
及 glob()
函數的第一個字串參數,就是所謂的 glob,其中 *
及 /
字元,就是所謂的 Glob 元字元。就是這些元字元,讓我們可以以簡單的模式,即可對應各種不同的檔案名稱。
多選結構,『{』及『}』元字元:
首先,在開始解析 glob 之前,其中的『{}』部份,會先被擴展開來,每個以『,』隔開的部份,都會造成整個 glob 被複製一次,成為個別的 glob。系統再分別針對這些個別的 glob,一一去比對檔案系統,然後將吻合的檔案,綜合起來,去除重複的檔案後傳回。
- 若 glob 中包含兩個以上的『{}』部份,可想而知,被擴展的 glob 將以等比級數比例增加。譬如,
a{b,c}{d,e}?
,將擴展為:abd?
,abe?
,acd?
,ace?
。 - 『{}』中的部份,可以包含路徑分隔符號『/』,譬如,
a{/b/c,bc}
將被擴展為a/b/c
及abc
。
匹配路徑分隔符號,『/』元字元:
『/』元字元用來標示路徑分隔符號。除了『/』元字元及後面介紹的『**』元字元之外,其他元字元所能夠匹配的檔案路徑字元,皆不包含路徑分隔符號。
任意字元,『*』元字元:
在 glob 中若出現『*』元字元,則在該元字元的位置,可以匹配任意數量的任意檔案路徑字元,包含 0 個字元,也就是可以不匹配任何字元
譬如,a*.js
可以匹配 a.js
, a4.js
, about.js
,但是不包含 app/about.js
,因為不匹配『/』路徑分隔符號 (如前所述,匹配的字元不包含『/』路徑分隔符號,後不再贅述)。
任意單一字元,『?』元字元:
在 glob 中若出現『?』元字元,則在該元字元的位置,必須且只能匹配一個任意路徑字元。
譬如,a?.js
可以匹配 a4.js
及 ax.js
,但是不能匹配 a.js
, about.js
。
字元組,『[』及『]』元字元:
與 regular expression 類似,字元組的意義為,匹配若干字元之一,且僅匹配一個路徑字元。同樣與 regular expression 類似地,若字元組的第一個字元為『!』或『^』,則為『排除型字元組』,代表該位置不得匹配字元組中列出的字元。
譬如,a[bc].js
可以匹配 ab.js
及 ac.js
,但是不能匹配任何其他檔案路徑。
而,a[!bc].js
或 a[^bc].js
,兩種寫法意義相同,可以匹配 a4.js
及 ax.js
及任何其他 a
開頭 .js
結尾的兩個字元檔名的檔案,但是就是不能匹配 ab.js
及 ac.js
。
模式匹配,『@(pattern|pattern|pattern)』:
匹配的路徑,必須吻合任一 pattern,實際的意義由開頭的字元決定:
- 『!』:不得與列出的任一 pattern 吻合 (0)
- 『?』:可選的吻合,即可以不吻合,或者有一次吻合 (0~1)
- 『*』:可選的吻合,即可以不吻合,或者有一次以上吻合 (0~n)
- 『+』:必須吻合,至少一次,或一次以上 (1~n)
- 『@』:必須吻合,一次,且僅能一次吻合 (1)
注意,『(』與『)』皆是合法的檔案名稱,所以若少了前面的『!』,『?』,『*』,『+』及『@』等元字元,則『(』與『)』不會被當作是模式匹配元字元。
任意路徑,『**』元字元:
又稱為 globstar
元字元,只有當單獨出現在路徑中時才有效,也就是必須以 **/
或 /**/
的形式出現才有效。一旦發揮作用,可以匹配包含任意子目錄的任意路徑。也就是說,全部任意深度的子目錄都將被匹配。但是不包含 symlinked 目錄,即不包含目錄捷徑。
選項
gulp.src()
及 glob()
函數的第二個參數是 options
,可以用來改變 glob 匹配的行為。
dot
選項與 .
字元
.
字元並非元字元。在 Linux 下,目錄或檔案名稱若以 .
字元開頭,則為隱藏檔案。因此,glob 預設不會匹配以 .
字元開頭的檔案,除非明確在 glob 中明確寫出 .
字元。即使在 Windows 底下,也保留相同的行為。
譬如:a*
不會匹配 .access
檔案或目錄,**/b*
不會匹配 abc/.batch
檔案或目錄,除非分別明確寫為 .a*
及 **/.b*
。
如果程式經常需要匹配隱藏檔案,可以在呼叫 glob()
函數時,在第二個參數提供 { dot: true }
選項,讓 glob()
函數像一般字元一樣,對待 .
字元。
base
選項
在 gulp 中,要特別注意的是 base 的決定與作用。
base 的作用,是匹配檔案的路徑參考原點。簡單地說,就是『匹配檔案的路徑,要從哪裡算起』。
所有匹配的檔案,其路徑必須以相對於 base 所指定的路徑為起點,以相對路徑表示。也可以這樣解讀:base 的作用是,最終匹配的檔案路徑及名稱,將排除 base 路徑部分。所以,base 的值,將會影響檔案輸出時是否包含目錄、若包含目錄,又是哪些目錄路徑。
那麼,base 是如何決定的呢? Gulp 關於 base 屬性的預設值的說明提到:
Default: everything before a glob starts
這恐怕有點誤導,事實上整個 pattern 都是 glob,而 gulp 的說明恐怕把 glob 狹義解釋為是指普通文字以外的部分,譬如 *
, ?
等通配字元。
正確地說,base 的預設值,是指未動用到通配字元而能夠匹配的路徑的部分。只有兩種可能情況符合:
1.glob 中不含路徑分隔符號,也就是不匹配任何目錄。所以 base 就是 .
。
2.glob 中含有路徑分隔符號,但是路徑是直接寫出,不含任何通配字元。一旦路徑中含有通配字元,就會被排除在 base 路徑之外。
譬如:
glob | exists | base | match |
---|---|---|---|
app/js/*.js | app/js/app.js | app/js/ | app.js |
app/views/**/*.html | app/views/options/options.html | app/views/ | options/options.html |
app/i18n/zh*/*.json | app/i18n/zh_TW/message.json | app/i18n/ | zh_TW/message.json |
上面是以 base 的觀點來說明。反過來說,對於匹配的檔案來說,一旦匹配成功,若匹配的檔案含有路徑,而該路徑是透過通配字元匹配的,則匹配的路徑將包含在匹配檔案的匹配路徑中。
所以:
在指定 glob 時若不含目錄通配字元,則找到的檔案,將忽視其路徑部分,因此,由
gulp.src('src/*.js')
對應到gulp.dest()
時,檔案將不含目錄,直接寫到gulp.dest()
指定的目錄。若指定了
?
,*
或**
目錄通用字元,由於目錄通用字元將被視為檔案的一部分,所以輸出到gulp.dest()
時,檔案將被寫入到目錄通用字元所匹配的路徑下。
譬如:
假設存在 app/views/options/options.html
,且一律指定 dest('dist')
:
gulp.src(glob) | base (粗體部分) / match path (紅色部分) | gulp.dest('dist') |
---|---|---|
src('app/*/*.html') | app/views/ (假設 app/views 目錄下無 .html 檔案。) | N/A (假設 app/views 目錄下無 .html 檔案。) |
src('app/**/*.html') | app/views/options/options.html | dist/views/options/options.html |
src('app/views/*/*.html') | app/views/options/options.html | dist/options/options.html |
src('app/views/**/*.html') | app/views/options/options.html | dist/options/options.html |
src('app/views/options/*.html') | app/views/options/options.html | dist/options.html |
src('*/views/**/*.html') | app/views/options/options.html | dist/app/views/options/options.html |
3.透過指定 "base" property 來改變參照路徑;或者,在 gulp.dest()
指定輸出路徑。
下面的範例中,對 gulp.src()
指定了兩個不同的 glob:
1 | gulp.src([ 'bower.json', 'app/manifest.json' ]) |
但是由於兩個檔案的根目錄不同,同時目錄也都未使用通配字元,所以最後匹配的檔案都不含路徑,因此,使用 gulp.dest('.')
輸出時,將輸出到專案目錄 (假設專案叫做 project) 下:
1 | project |
這裡當然也可以藉由指定通配字元 "*/manifest.json"
從而避免 base 的指定。但是,這樣的寫法,很可能會殃及無辜檔案。同時,最重要的是,反而不容易看出我們真正的意圖:就是只要 app/manifest.json 這個檔案。
因此,這裡可以藉由指定 base
property 來改變參照路徑,強迫匹配的檔案以目前目錄為相對路徑表示,從而,得以保留檔案的目錄資訊。
1 | gulp.src([ 'bower.json', 'app/manifest.json' ], { base: '.' }) |
輸出如下:
1 | project |
當匹配的檔案都在共同的子目錄之下時,若輸出目錄仍然希望含有特定目錄,則可以在 gulp.dest()
指定輸出路徑:
1 | gulp.src('app/scripts/*/.js') |
4.透過 gulp-flatten
可消除匹配的路徑。
1 | var flatten = require('gulp-flatten'); |