介紹好用工具:duti (用命令列設定 macOS 檔案的預設開啟程式)

最近我想把 Mac 上 .md 檔案的預設開啟程式改成 VS Code。一般的做法是在 Finder 對檔案按「取得資訊」,在「打開檔案的應用程式」選好之後再按「全部更改⋯」。偶爾改一次還好,但這種設定如果能用一行指令完成,就能寫進指令稿,想換到其他不同應用程式時,就可以一鍵切換。

後來找到 duti 這套命令列工具,一行指令就能把某種檔案類型綁定到指定的應用程式,用起來相當順手。不過實際用下去,我也踩到幾個沒有寫在說明文件裡的地方:它只能綁定已經註冊的 .app 應用程式,沒辦法直接指定某個 CLI 工具;在 macOS 26 上,它回傳成功之後設定其實還沒生效,要等使用者按下系統跳出的確認視窗才算數;連 .md 這麼常見的副檔名,背後都可能同時對應好幾個 UTI。

這篇文章就來整理 duti 的安裝與常用指令、我實測時遇到的注意事項,最後再分享一個小技巧:用 AppleScript 做一個很小的 .app 當作「轉接頭」,讓你在 Finder 雙擊檔案時,可以把檔案轉交給任何 CLI 工具執行。

多張不同類型的文件卡片透過彩色纜線接到中央的配線盤,其中一條線經過一個應用程式包裝盒轉接到右側的終端機視窗,配線盤上方浮著一個確認對話框

本文的測試環境為 Apple Silicon 的 macOS 26.6.2 (Tahoe),duti 版本為 Homebrew 提供的 1.5.4,測試時間為 2026 年 10 月。文中的 com.example.open-with-cli 只是示意用的 bundle ID,.owcdemo 也是我為了測試自訂的副檔名,實作時請換成你自己的名稱。


先搞懂幾個基本觀念

macOS 決定「這個檔案要用哪個 App 打開」的機制叫做 Launch Services。它不是直接拿副檔名去對照應用程式,中間還隔了一層 UTI,所以在使用 duti 之前,先把下面幾個名詞分清楚會比較好理解:

名詞 說明 範例
UTI (Uniform Type Identifier) macOS 用來描述檔案內容類型的識別碼,副檔名與 MIME 類型都會對應到 UTI public.plain-text、net.daringfireball.markdown
Bundle ID 應用程式的唯一識別碼,寫在 .app 的 Info.plist 裡的 CFBundleIdentifier com.microsoft.VSCode、com.apple.TextEdit
Role (角色) 應用程式對這種檔案扮演的角色 viewer、editor、shell、all、none
URL scheme 網址的協定名稱,也可以指定預設處理程式 ftp、vscode

duti 做的事情,就是呼叫 Launch Services 的 API,把「某個 UTI (或 URL scheme)」對應到「某個 bundle ID」。所以你會發現,它的參數裡面從頭到尾都沒有應用程式的路徑,只有 bundle ID。這一點很重要,後面的注意事項幾乎都跟這件事有關。


安裝 duti

duti 可以透過 Homebrew 安裝:

brew install duti

安裝過程很快,主要的輸出如下:

==> Fetching downloads for: duti
✔︎ Bottle duti (1.5.4_1)                Downloaded   15.2KB/ 15.2KB
==> Pouring duti--1.5.4_1.arm64_tahoe.bottle.2.tar.gz

裝好之後執行 duti -h,可以看到它的用法其實只有三種:

usage: duti [ -hvV ] [ -d uti ] [ -l uti ] [ settings_path ]
usage: duti -s bundle_id { uti | url_scheme } [ role ]
usage: duti -x extension

如果直接執行 duti -s 不帶任何參數,它什麼都不會輸出,也不會有錯誤訊息,第一次用的時候我還以為它當掉了。


duti 的常用指令

1. 先查出應用程式的 bundle ID

既然 duti 只吃 bundle ID,第一步就是查出目標 App 的 bundle ID。最快的方法是用 osascript:

osascript -e 'id of app "Visual Studio Code"'
com.microsoft.VSCode

這裡要注意,app "..." 裡面要填的是 .app 的實際名稱。我一開始習慣性地打了 VS Code,結果直接得到錯誤:

0:2: execution error: 無法取得「application "VS Code"」。 (-1728)

如果不確定 App 的名稱,也可以直接從 .app 的路徑讀取,這兩種寫法的結果都一樣:

mdls -name kMDItemCFBundleIdentifier -r "/Applications/Visual Studio Code.app"
defaults read "/Applications/Visual Studio Code.app/Contents/Info" CFBundleIdentifier

2. 設定檔案類型的預設開啟程式

有了 bundle ID 之後,就可以用 -s 設定。第二個參數可以寫 UTI,也可以寫以 . 開頭的副檔名,duti 會自動幫你換算成對應的 UTI:

# 用副檔名指定
duti -s com.microsoft.VSCode .md all

# 用 UTI 指定
duti -s com.microsoft.VSCode net.daringfireball.markdown all

最後一個參數是角色,一般想要「雙擊就用這個 App 開」的話,填 all 就對了。如果第三個參數省略,duti 會把第二個參數當成 URL scheme,例如 duti -s com.apple.Finder ftp 就是設定 ftp:// 網址的處理程式。

3. 查詢目前的設定

duti 提供三個查詢用的參數:

# 查詢某個 UTI 目前的預設程式
duti -d net.daringfireball.markdown

# 列出所有宣告可以處理這個 UTI 的程式
duti -l net.daringfireball.markdown

# 用副檔名查詢預設程式
duti -x md

不過 duti -x 的結果不一定可信,後面的注意事項會詳細說明。我自己會以 duti -d 搭配檔案實際的 UTI 來確認設定有沒有生效。

4. 用設定檔一次套用多筆

duti 也可以讀取設定檔,每一行依序寫上 bundle ID、UTI 與角色,很適合放進 dotfiles 裡面:

com.microsoft.VSCode    net.daringfireball.markdown    all
com.microsoft.VSCode    public.json                    all
com.apple.TextEdit      public.plain-text              all

假設這個檔案存成 ~/.duti,執行 duti ~/.duti 就會一次套用。不過在 macOS 26 上,這個用法變得沒那麼方便了,原因請看下一節。


使用 duti 的注意事項

1. macOS 26 會跳出確認視窗,但 duti 照樣回傳成功

這是我這次花最多時間才搞懂的地方。

我在測試時,把 .log 檔暫時綁到自己做的測試 App,測完之後想改回原本的「系統監視程式」(Console),於是執行了:

duti -s com.apple.Console com.apple.log all
echo $?

指令馬上就結束了,結束代碼是 0,也沒有任何錯誤訊息。但用 duti -d com.apple.log 一查,結果還是我的測試 App。我以為是 Launch Services 有快取,又多試了兩次,結果都一樣。

後來才發現,原來螢幕上已經疊了三個系統對話框,正在等我回答:

macOS 系統對話框詢問:你要使用「Open With CLI」打開所有副檔名為「.log」的文件,還是繼續使用「系統監視程式」?下方有使用與保留兩個按鈕

上面這張是我後來重現時截的圖,問的是另一個方向的變更,但對話框的樣式完全一樣。根據 Scripting OS X 的 macOS 26.4 brings more default app confirmation prompts 一文,Apple 從 macOS 26.4 開始,替所有透過 API 或指令稿變更檔案類型預設程式的操作加上這個確認步驟。我在 macOS 26.6.2 上實測整理出以下幾點:

  • duti -s 會立刻回傳 0,但設定在使用者按下「使用」之前都不會生效。
  • 使用者如果按「保留」,這次變更就直接作廢,duti 不會知道,也不會有任何錯誤。
  • 沒有回答的對話框會一直堆疊在畫面上,每多一次會改變設定的 duti -s,就多一個等待確認的視窗。
  • 如果目標已經是目前的預設程式,就不會跳出對話框。
  • 如果該副檔名原本沒有預設程式,對話框的文字會變成「你要使用『某 App』打開所有副檔名為『.xxx』的文件嗎?」,按鈕則是「使用」與「不更改」。

所以在 macOS 26 上使用 duti,請一定要在執行之後看一下畫面上的確認視窗,按下「使用」之後,再用 duti -d 驗證一次。前面提到的 ~/.duti 設定檔,也要有心理準備:設定檔裡有幾筆會變更的設定,就得逐一按下確認。同一篇文章也提到,從 Finder 的「取得資訊」手動修改,則不會跳出這個額外的確認視窗。

老實說,在發現這些對話框之前,我繞了好大一圈。因為 duti -s 有時生效、有時不生效,我一度懷疑是轉接 App 宣告的角色 (Role) 不對,還去修改 Info.plist,加上 LSItemContentTypes、把 CFBundleTypeRole 從 Viewer 改成 Editor。改完之後設定果然生效了,我差點就把「角色要宣告成 Editor」當成結論寫進文章。後來回頭重測才發現,真正讓設定生效的,是確認視窗剛好被按掉了;只補上 bundle ID、什麼宣告都沒改的 App,按下「使用」之後一樣可以綁定。這也是這次最值得記錄的地方:在 macOS 26 上測試預設程式,任何「指令成功但沒生效」的現象,都要先懷疑畫面上是不是有一個還沒回答的視窗。

如果你跟我一樣是在終端機裡反覆實驗,可以用 System Events 檢查目前有幾個等待確認的視窗。這個指令需要終端機 App 擁有「輔助使用」權限:

osascript -e 'tell application "System Events" to count windows of process "CoreServicesUIAgent"'

回傳 0 代表沒有待確認的視窗,這時候再用 duti -d 查到的結果才準確。

2. 只能綁定已註冊的 .app,不能直接指定 CLI

我原本的想法很單純:既然 duti 可以設定預設程式,那能不能直接指定一個 CLI 工具,例如讓 .md 檔案雙擊之後用 glow 在終端機裡預覽?

答案是不行,但 duti 不會告訴你。我第一次測試時,為了不弄亂系統設定,用的是自己編的副檔名 .dutidemo,結果 CLI 路徑得到 error -50,我就以為 duti 會擋下不是 App 的參數。後來才發現,那個 -50 其實跟 CLI 路徑無關,而是未知副檔名造成的 (請看本節第 4 點),連 VS Code 的 bundle ID 也會得到同樣的錯誤。改用真實存在的 UTI 重新測試 CLI 路徑與不存在的 bundle ID,結果完全不同:

duti -s /opt/homebrew/bin/glow net.daringfireball.markdown all; echo $?
duti -s com.example.not-exist net.daringfireball.markdown all; echo $?

兩個指令的結束代碼都是 0,沒有任何錯誤訊息。其中 CLI 路徑那一個,系統還很認真地跳出確認視窗,問我「你要使用『/opt/homebrew/bin/glow』打開所有副檔名為『.md』和『.markdown』的文件,還是繼續使用『Code』?」;不存在的 bundle ID 則是連視窗都沒有出現,設定也沒有任何改變。看到 glow 這種確認視窗時,請直接按「保留」。

也就是說,Launch Services 只能把檔案交給一個已經註冊的 .app,duti 的第一個參數必須是那個 App 的 bundle ID。duti 本身不會幫你檢查第一個參數是不是一個真的 App,所以結束代碼是 0 也不代表設定有意義。這也是本文最後一節要介紹的技巧,會用一個小小的 .app 來解決這個問題。

3. 一個副檔名可能對應好幾個 UTI

我照著前面的方式執行了 duti -s com.microsoft.VSCode .md all,在 Finder 雙擊 .md 檔案也確實會用 VS Code 打開。但是當我用 duti -x md 確認時,看到的卻是 Word:

Microsoft Word.app
/Applications/Microsoft Word.app
com.microsoft.Word

追查之後才發現,在我這台電腦上,.md 這個副檔名同時對應到三個 UTI:系統內建的 public.markdown,以及其他 App 宣告的 net.daringfireball.markdown 與 com.unknown.md。分別查詢的結果如下:

duti -d public.markdown               # com.microsoft.Word
duti -d net.daringfireball.markdown   # com.microsoft.VSCode
duti -d com.unknown.md                # com.google.antigravity-ide

duti -s 用 .md 指定時,只會設定系統認定的「首選 UTI」,也就是這台電腦上的 net.daringfireball.markdown。而 duti -x 用的是另一支較舊的 API (LSGetApplicationForInfo),它查出來的 Word,剛好跟 public.markdown 的設定一致,所以才會跟 Finder 的實際行為對不起來。

要知道某個檔案實際被系統認定成哪一種 UTI,最可靠的方式是拿一個真實檔案,用 mdls 查詢:

mdls -name kMDItemContentType README.md
kMDItemContentType = "net.daringfireball.markdown"

知道實際的 UTI 之後,再用 duti -d 查詢那個 UTI,才是 Finder 雙擊時真正會用的程式,所以只要設定這個 UTI 就夠了。我原本也想順手把 public.markdown 一起設定,結果 duti 直接拒絕:

public.markdown does not conform to any UTI hierarchy

duti 在設定之前,會先用較舊的 API 檢查 UTI 是否屬於已知的類型階層,而 public.markdown 沒有通過這項檢查,所以 duti 無法替它設定預設程式。

4. 系統不認得的副檔名,要用 mdls 查出動態 UTI

如果副檔名沒有任何 App 宣告過,macOS 會替它產生一個以 dyn. 開頭的動態 UTI。這時候用 .ext 的寫法反而會失敗:

duti -s com.example.open-with-cli .owcdemo all
failed to set com.example.open-with-cli as handler for dyn.ah62d4qmxhk2x4551qrwgn5pt (error -50)

我去翻了 duti 的原始碼,它在換算副檔名時,會要求產生的 UTI 必須符合 public.content,算出來的動態 UTI 就跟檔案實際拿到的不一樣。用 mdls 查詢真實檔案,可以看到兩者確實不同:

mdls -raw -name kMDItemContentType "範例 檔案.owcdemo"
dyn.ah62d4rv4ge80875dqvw0452

改用這個查出來的 UTI 來設定就可以成功,當然同樣要按下確認視窗:

duti -s com.example.open-with-cli dyn.ah62d4rv4ge80875dqvw0452 all
duti -d dyn.ah62d4rv4ge80875dqvw0452
com.example.open-with-cli

技巧:用 AppleScript 小程式把檔案轉發給任意 CLI

既然 duti 只認 .app,那就做一個 .app 給它認。這個 App 本身什麼事都不做,只負責把 Finder 交給它的檔案路徑,原封不動地轉交給一支 shell 指令稿,要用哪個 CLI 處理,全部由這支指令稿決定。

原理是這樣的:在 Finder 雙擊檔案時,Launch Services 會啟動預設程式,並透過 Apple Event 把檔案清單傳給它。用 AppleScript 撰寫的小程式 (applet) 只要實作 on open 處理常式,就能收到這份清單,再透過 do shell script 呼叫任何命令列工具。整個流程如下:

Finder 雙擊檔案
  → Launch Services 找到預設程式 Open With CLI.app
  → AppleScript 的 on open 收到檔案清單
  → do shell script 呼叫 ~/.local/bin/open-with-cli
  → 指令稿依副檔名轉給 glow、less 或其他 CLI

步驟一:撰寫 AppleScript 程式碼

先建立 OpenWithCLI.applescript:

on open theItems
    set args to ""
    repeat with anItem in theItems
        set args to args & " " & quoted form of POSIX path of anItem
    end repeat
    do shell script "\"$HOME/.local/bin/open-with-cli\"" & args & " > /dev/null 2>&1 &"
end open

on run
    display dialog "請把檔案拖曳到這個 App,或用 duti 將它設為預設開啟程式。" buttons {"OK"} default button 1
end run

這裡有幾個細節:

  • quoted form of 會替每個路徑加上適當的引號,檔名有空白或中文都不會出問題。
  • 指令最後的 > /dev/null 2>&1 & 讓指令稿在背景執行。少了這段,do shell script 會等指令稿結束才返回,App 就會一直掛在 Dock 上。
  • on run 是直接雙擊 App 本身時會執行的部分,這裡只顯示一段提示文字。

步驟二:編譯成 .app 並補上 bundle ID

用 macOS 內建的 osacompile 編譯成 App,我習慣放在 ~/Applications:

mkdir -p ~/Applications
osacompile -o ~/Applications/"Open With CLI.app" OpenWithCLI.applescript

這一步最容易踩坑的地方是:osacompile 產生的 Info.plist 裡面根本沒有 CFBundleIdentifier。沒有 bundle ID,duti 就無從指定它。所以要自己補上,名稱可以自己取,只要不跟其他 App 重複就好:

/usr/libexec/PlistBuddy -c 'Add :CFBundleIdentifier string com.example.open-with-cli' \
  ~/Applications/"Open With CLI.app/Contents/Info.plist"

修改 Info.plist 之後,App 原本的簽章就失效了,用 codesign --verify 檢查會看到這樣的錯誤:

invalid Info.plist (plist or signature have been modified)

所以我會重新做一次 ad-hoc 簽章,讓簽章與修改後的內容一致,再手動向 Launch Services 註冊:

codesign --force --deep -s - ~/Applications/"Open With CLI.app"

/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister \
  -f ~/Applications/"Open With CLI.app"

osacompile 產生的 App 預設就宣告了可以開啟所有類型的檔案 (CFBundleTypeExtensions 為 *),所以不需要另外修改文件類型的宣告。

步驟三:撰寫轉發用的指令稿

接著建立 ~/.local/bin/open-with-cli,這支指令稿才是真正決定「要交給哪個 CLI」的地方:

#!/bin/zsh
# 由 Open With CLI.app 呼叫,參數是 Finder 傳進來的檔案完整路徑
export PATH="/opt/homebrew/bin:/usr/local/bin:$PATH"

# 需要互動的 CLI,產生一個暫時的 .command 檔交給 Terminal 執行
run_in_terminal() {
  local script
  script=$(mktemp "${TMPDIR:-/tmp}/open-with-cli.XXXXXX") || return 1
  mv "$script" "$script.command" && script="$script.command"
  {
    print -r -- '#!/bin/zsh -l'
    print -r -- "rm -f ${(q)script}"
    print -r -- "${(j: :)${(q)@}}"
  } > "$script"
  chmod +x "$script"
  open -a Terminal "$script"
}

for f in "$@"; do
  case "${f:l}" in
    *.md|*.markdown) run_in_terminal glow -p "$f" ;;
    *.log)           run_in_terminal less +F "$f" ;;
    *)               open -a TextEdit "$f" ;;
  esac
done

記得加上執行權限:

chmod +x ~/.local/bin/open-with-cli

這支指令稿有兩個重點:

  1. 一定要自己設定 PATH。依照 Apple 的 Technical Note TN2065,do shell script 一律使用 /bin/sh,而且不會讀取互動式 shell 的設定檔。從 Finder 啟動的 App 只會繼承很陽春的環境變數,像 Homebrew 安裝在 /opt/homebrew/bin 的 glow 很可能就找不到。
  2. 需要終端機畫面的 CLI,要另外開一個 Terminal 視窗。glow -p、less +F 這類互動式工具需要 TTY,所以我讓指令稿產生一個暫時的 .command 檔,交給 Terminal 執行。.command 檔一開始就會刪除自己,不會在暫存資料夾留下垃圾。用 ${(q)...} 處理參數,則是為了讓含有空白或特殊字元的路徑也能正確傳遞。

如果你要轉發的是不需要互動的 CLI (例如用 pandoc 轉檔),直接在 case 裡面呼叫就好,不需要經過 Terminal。

步驟四:用 duti 綁定並驗證

最後就是用 duti 把檔案類型指向這個 App。以我測試用的 .owcdemo 為例,先用 mdls 查出實際的 UTI,再進行設定:

duti -s com.example.open-with-cli dyn.ah62d4rv4ge80875dqvw0452 all

記得在畫面上按下「使用『Open With CLI』」,然後確認設定確實生效:

duti -d dyn.ah62d4rv4ge80875dqvw0452
com.example.open-with-cli

接著就可以實際測試了。我刻意用了檔名含有空白與中文的檔案:

# 模擬在 Finder 雙擊,會走預設程式
open "範例 檔案.owcdemo"

# 直接指定用這個 App 開啟 Markdown 檔案
open -a ~/Applications/"Open With CLI.app" "週報 草稿.md"

第一個指令走的是 *) 分支,用 TextEdit 開啟了 範例 檔案.owcdemo。第二個指令則開了一個新的 Terminal 視窗,在裡面執行 glow -p,完整路徑 .../週報 草稿.md 被當成單一參數正確傳入。一次選取多個檔案打開也沒問題,on open 會收到完整的清單,全部一起交給指令稿處理。

如果之後不想用了,先用 duti -s 把綁定過的檔案類型指回原本的 App,再反註冊並刪除 App 即可:

/System/Library/Frameworks/CoreServices.framework/Frameworks/LaunchServices.framework/Support/lsregister \
  -u ~/Applications/"Open With CLI.app"
rm -rf ~/Applications/"Open With CLI.app" ~/.local/bin/open-with-cli

實際應用:雙擊 .md 改用 mhtml 在瀏覽器預覽

做好轉接 App 之後,我自己實際用來做的第一件事,就是把 .md 的預設開啟程式換成我寫的 mhtml。它可以把 Markdown 轉成一個 CSS 全部內嵌的獨立 HTML 檔,支援 GFM 表格、程式碼語法醒目提示、數學式與 Mermaid 圖表。執行 mhtml 檔名.md 而不指定輸出檔時,它會在暫存資料夾產生 HTML 並直接用瀏覽器開啟,剛好適合拿來當 Markdown 的預覽器。

mhtml 不需要終端機畫面,所以轉發指令稿反而比前面的範例更簡單:

#!/bin/zsh
# 由 ~/Applications/Open With CLI.app 呼叫,參數是 Finder 傳進來的檔案完整路徑
export PATH="$HOME/.cargo/bin:/opt/homebrew/bin:/usr/local/bin:$PATH"

for f in "$@"; do
  case "${f:l}" in
    *.md|*.markdown) mhtml "$f" ;;
    *)               open -a TextEdit "$f" ;;
  esac
done

我的 mhtml 是用 cargo install 安裝的,執行檔在 ~/.cargo/bin,所以 PATH 一定要把這個資料夾加進去。如果你是用 npm install -g mhtml-cli 安裝,請先在終端機執行 command -v mhtml,再把查到的資料夾加進 PATH。

接著用 duti 綁定 .md 實際使用的 UTI,並按下確認視窗的「使用『Open With CLI』」:

duti -s local.will.open-with-cli net.daringfireball.markdown all
duti -d net.daringfireball.markdown
local.will.open-with-cli

這次確認視窗問的是「你要使用『Open With CLI』打開所有副檔名為『.md』和『.markdown』的文件,還是繼續使用『Code』?」,可見一個 UTI 會同時涵蓋好幾個副檔名。我本來還想順便設定 public.markdown,結果就是在這裡踩到前面第 3 點提到的 does not conform to any UTI hierarchy。好在檔案實際的 UTI 是 net.daringfireball.markdown,只設定這一個就夠了。

設定完成之後,在 Finder 雙擊任何 .md 檔,瀏覽器就會開出排版好的預覽頁面。Markdown 裡用相對路徑引用的圖片也能正常顯示,因為 mhtml 在預覽模式下,會自動把 <base href> 設成 Markdown 檔所在的資料夾。

這樣設定之後,有兩件事要知道:

  • 雙擊 .md 不會再開 VS Code,需要編輯時可以用 code 檔名.md,或在 Finder 按右鍵選「打開檔案的應用程式」。
  • 預覽用的 HTML 會放在 $TMPDIR/mhtml/,mhtml 不會自動刪除。

想改回 VS Code 的話,執行下面這行,再按一次確認視窗就好:

duti -s com.microsoft.VSCode net.daringfireball.markdown all

總結

duti 是一個很小卻很實用的工具,特別適合想把 macOS 環境設定寫成指令稿的人。整理一下這次的重點:

  • duti 只吃 bundle ID:用 osascript -e 'id of app "完整 App 名稱"' 查詢。傳入 CLI 路徑或不存在的 bundle ID,duti 一樣回傳成功,不會替你檢查,遇到 CLI 路徑的確認視窗請按「保留」。
  • macOS 26 需要使用者確認:duti -s 回傳 0 不代表設定已經生效,一定要按下確認視窗,再用 duti -d 驗證。
  • 先查出檔案實際的 UTI:一個副檔名可能對應好幾個 UTI,duti -x 的結果也可能跟 Finder 的實際行為不同,用 mdls -name kMDItemContentType 查真實檔案最可靠。系統不認得的副檔名用 .ext 寫法會得到 error -50,要改用查出來的 dyn. UTI。
  • 指令成功但沒生效,先找確認視窗:我曾經把「角色要宣告成 Editor」、「CLI 路徑會得到 -50」當成原因,最後都證明是誤判。用 CoreServicesUIAgent 的視窗數確認沒有待回答的對話框,再下結論。
  • 想用 CLI 開檔案,就做一個轉接用的 App:osacompile 編譯 AppleScript 小程式,補上 CFBundleIdentifier,重新簽章、註冊之後,就能用 duti 綁定,再由指令稿把檔案轉發給任何 CLI。

這個轉接 App 的好處是只要做一次,之後想換工具或新增檔案類型,只需要修改 ~/.local/bin/open-with-cli 這支指令稿就好,不用重新編譯 App。


相關連結

留言評論