Gulp 學習筆記 - Glob 篇

意象說明

glob < pattern < 在眾多相似物中尋找

前言

Glob 類似於 regular expression,但是僅用來匹配檔案路徑名稱。名稱由來是 UNIX 一個負責解析匹配檔案、叫做 global command 的模組。

本文針對 gulp 使用的 node-glob 所支援的語法及選項進行說明,詳細的使用方法可參考 node-glob 的說明。

gulp.src() 基本使用方式

1
2
3
4
5
6
var gulp = require('gulp');
var uglify = require('gulp-uglify');

gulp.src('*/.js')
.pipe(uglify())
.pipe(gulp.dest('dist'));

gulp.src() 還支援陣列形式的多組 glob 參數:

1
2
3
gulp.src(['/*.js', '/*.ts'])
.pipe(uglify())
.pipe(gulp.dest('dist'));

不過這個例子,可以使用後面介紹的『多選結構』加以簡化,而不必使用陣列:

1
2
3
gulp.src('*/.{js,ts}')
.pipe(uglify())
.pipe(gulp.dest('dist'));

或者,直接使用 node-glob 來匹配檔案

安裝

1
npm install --save glob

注意,雖然 GitHub 上的名稱為 node-glob,但是 npm 上卻是註冊為 glob,因此安裝時名稱必須為 glob

呼叫 glob() 函數

1
2
var glob = require('glob');
var files = glob('*/', { dot: true });

檔案路徑匹配結果以陣列回傳,如果沒有匹配任何路徑或檔案,將回傳空陣列。

Glob 元字元

前面兩個例子中,呼叫 gulp.src()glob() 函數的第一個字串參數,就是所謂的 glob,其中 */ 字元,就是所謂的 Glob 元字元。就是這些元字元,讓我們可以以簡單的模式,即可對應各種不同的檔案名稱。

多選結構,『{』及『}』元字元:

首先,在開始解析 glob 之前,其中的『{}』部份,會先被擴展開來,每個以『,』隔開的部份,都會造成整個 glob 被複製一次,成為個別的 glob。系統再分別針對這些個別的 glob,一一去比對檔案系統,然後將吻合的檔案,綜合起來,去除重複的檔案後傳回。

  1. 若 glob 中包含兩個以上的『{}』部份,可想而知,被擴展的 glob 將以等比級數比例增加。譬如,a{b,c}{d,e}?,將擴展為:abd?, abe?, acd?, ace?
  2. 『{}』中的部份,可以包含路徑分隔符號『/』,譬如,a{/b/c,bc} 將被擴展為 a/b/cabc

匹配路徑分隔符號,『/』元字元:

『/』元字元用來標示路徑分隔符號。除了『/』元字元及後面介紹的『**』元字元之外,其他元字元所能夠匹配的檔案路徑字元,皆不包含路徑分隔符號。

任意字元,『*』元字元:

在 glob 中若出現『*』元字元,則在該元字元的位置,可以匹配任意數量的任意檔案路徑字元,包含 0 個字元,也就是可以不匹配任何字元

譬如,a*.js 可以匹配 a.js, a4.js, about.js,但是不包含 app/about.js,因為不匹配『/』路徑分隔符號 (如前所述,匹配的字元不包含『/』路徑分隔符號,後不再贅述)。

任意單一字元,『?』元字元:

在 glob 中若出現『?』元字元,則在該元字元的位置,必須且只能匹配一個任意路徑字元。

譬如,a?.js 可以匹配 a4.jsax.js,但是不能匹配 a.js, about.js

字元組,『[』及『]』元字元:

與 regular expression 類似,字元組的意義為,匹配若干字元之一,且僅匹配一個路徑字元。同樣與 regular expression 類似地,若字元組的第一個字元為『!』或『^』,則為『排除型字元組』,代表該位置不得匹配字元組中列出的字元。

譬如,a[bc].js 可以匹配 ab.jsac.js,但是不能匹配任何其他檔案路徑。
而,a[!bc].jsa[^bc].js,兩種寫法意義相同,可以匹配 a4.jsax.js 及任何其他 a 開頭 .js 結尾的兩個字元檔名的檔案,但是就是不能匹配 ab.jsac.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 的觀點來說明。反過來說,對於匹配的檔案來說,一旦匹配成功,若匹配的檔案含有路徑,而該路徑是透過通配字元匹配的,則匹配的路徑將包含在匹配檔案的匹配路徑中。

所以:

  1. 在指定 glob 時若不含目錄通配字元,則找到的檔案,將忽視其路徑部分,因此,由 gulp.src('src/*.js') 對應到 gulp.dest() 時,檔案將不含目錄,直接寫到 gulp.dest() 指定的目錄。

  2. 若指定了 ?, *** 目錄通用字元,由於目錄通用字元將被視為檔案的一部分,所以輸出到 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
2
3
4
5
6
gulp.src([ 'bower.json', 'app/manifest.json' ])
.pipe(plugins.configSync({
fields: [ 'version' ]
}))
// 注意這裡若指定為 gulp.dest('/') 則真的會輸出到磁碟的根目錄。
.pipe(gulp.dest('.'));

但是由於兩個檔案的根目錄不同,同時目錄也都未使用通配字元,所以最後匹配的檔案都不含路徑,因此,使用 gulp.dest('.') 輸出時,將輸出到專案目錄 (假設專案叫做 project) 下:

1
2
3
project
+-- bower.json
+-- manifest.json

這裡當然也可以藉由指定通配字元 "*/manifest.json" 從而避免 base 的指定。但是,這樣的寫法,很可能會殃及無辜檔案。同時,最重要的是,反而不容易看出我們真正的意圖:就是只要 app/manifest.json 這個檔案。

因此,這裡可以藉由指定 base property 來改變參照路徑,強迫匹配的檔案以目前目錄為相對路徑表示,從而,得以保留檔案的目錄資訊。

1
2
3
4
5
gulp.src([ 'bower.json', 'app/manifest.json' ], { base: '.' })
.pipe(plugins.configSync({
fields: [ 'version' ]
}))
.pipe(gulp.dest('.'));

輸出如下:

1
2
3
4
project
+-- app
+-- manifest.json
+-- bower.json

當匹配的檔案都在共同的子目錄之下時,若輸出目錄仍然希望含有特定目錄,則可以在 gulp.dest() 指定輸出路徑:

1
2
3
gulp.src('app/scripts/*/.js')
.pipe(uglify())
.pipe(gulp.dest('dist/scripts'));

4.透過 gulp-flatten 可消除匹配的路徑。

1
2
3
4
var flatten = require('gulp-flatten');
gulp.src('bower_components/*/.min.js')
.pipe(flatten())
.pipe(gulp.dest('build/js'));

參考資料

相關文章