在 mac 安裝程式最討厭看到 .pkg 的安裝檔,因為大部分都不提供簡易的移除方法,只能手動下指令一一刪除。這篇分享如何將指令用 python 包裝成簡單的 script(趕時間的話可下載立即使用!),以及每個指令背後的意義。
滿腦只想趕快刪除的朋友可以透過 github 下載原始碼直接使用:
原始碼下載:github
並到「如何使用寫好的 python script」章節了解如何使用。
目錄
如何透過 shell 下指令刪除 pkg?
Step 1:查詢套件全名
首先查詢套件的全名,透過可能的套件名稱關鍵字來過濾
$ pkgutil --pkgs | grep <套件可能的名稱>
假如回傳為空,則將套件全部列出透過肉眼尋找
$ pkgutil --pkgs
舉例來說,我想要刪除銀行的套件,透過套件安裝檔名「FBWS_Setup.pkg」猜測會有關鍵字「FBWS」,因此嘗試搜尋
$ pkgutil --pkgs | grep FBWS
shell 會回應
tw.FirstBank.FBWS.第一銀行網銀安控程式.FBWS_data.pkg
tw.FirstBank.FBWS.第一銀行網銀安控程式.nspr.pkg
tw.FirstBank.FBWS.第一銀行網銀安控程式.nss.pkg
tw.FirstBank.FBWS.第一銀行網銀安控程式.FBWS.pkg
tw.FirstBank.FBWS.第一銀行網銀安控程式.FBWS_Plugins.pkg
tw.FirstBank.FBWS.第一銀行網銀安控程式.com.hitrust.startFBWS.pkg
發現這些就是接下來要刪除的 pkg。
此時覺得很崩潰,只透過一個 pkg 檔安裝套件,他卻偷偷幫你裝了六個套件!因此刪除動作總共要做六次!官方竟然還不做刪除的方法,實在是….
所以寫了 python script 以備未來之需。但我們還是先把指令的方式走完。
Step 2:查詢套件的根目錄位置
找到完整套件名稱後,用以下指令查詢套件的基本訊息
$ pkgutil --pkg-info <完整套件名稱>
舉例來說,查詢範例中第一個想要刪除的 pkg
$ pkgutil --pkg-info tw.FirstBank.FBWS.第一銀行網銀安控程式.FBWS_data.pkg
shell 回應
package-id: tw.FirstBank.FBWS.第一銀行網銀安控程式.FBWS_data.pkg
version: 1.1802.04.16
volume: /
location: Library
install-time: 1643377765
得知套件的檔案都放在 /Library 下
Step 3:切換目錄開始移除套件
由上一步知道套件根目錄在 /Library ,因此先切換過去
$ cd /Library
開始移除檔案,這邊需要 superuser 權限
$ sudo pkgutil --only-files --files <完整套件名稱> | tr '\n' '\0' | xargs -n 1 -0 rm -if
$ sudo pkgutil --only-dirs --files <完整套件名稱> | tr '\n' '\0' | xargs -n 1 -0 rm -ifr
這兩個指令需要解釋一下,首先
pkgutil --only-files --files <完整套件名稱>
是透過 pkgutil 列出這個套件所有「檔案」的路徑
同理第二個就是列出套件所有「資料夾」的路徑。
pkgutil --only-dirs --files <完整套件名稱>
接著透過 tr 指令把換行和結尾符號去掉,再一次餵給 rm 指令去做批次刪除。
注意:
下刪除資料夾時要特別小心, pkgutil 可能會列出如 /Application 或 /usr 等路徑,如果刪了系統會有問題,因此保險起見還是先列出來用肉眼確認一下比較好!
Step 4:刪除系統套件記錄
最後一步,刪除套件在系統中的記錄
$ pkgutil --forget <完整套件名稱>
完成!
但如果待刪套件和範例一樣多,就必須再做 step 2 ~ step 4 五次……
讓我們發揮工程師的懶人極致吧!寫一個 script 一勞永逸!
用 python 寫一個智慧的刪除流程吧!
我們的目標是寫一個 python script,他可以
- 依照關鍵字幫我列出待刪除的 pkgs
- 可以預覽待刪除的檔案與資料夾路徑
- 提供 dryrun,避免不小心誤刪
script 如下:
#!python3
from argparse import ArgumentParser
from re import sub
import subprocess
PKG_UTIL = 'pkgutil'
PKG_INFO = '--pkg-info'
PKGS = '--pkgs'
FILES = '--files'
ONLY_FILES = '--only-files'
ONLY_DIRS = '--only-dirs'
FORGET = '--forget'
RM = 'rm'
RM_IF = '-if'
RM_IFR = '-ifr'
SUDO = 'sudo'
VOLUME = 'volume: '
LOCATION = 'location: '
def find_pkgs(pkg_keyword):
result = subprocess.check_output([PKG_UTIL, PKGS]).decode()
return [r for r in result.split('\n') if pkg_keyword.lower() in r.lower()]
def find_location(pkg_name):
volume = ''
location = ''
result = subprocess.check_output([PKG_UTIL, PKG_INFO, pkg_name]).decode()
for line in result.split('\n'):
if VOLUME in line:
volume = line.replace(VOLUME, '')
elif LOCATION in line:
location = line.replace(LOCATION, '')
return f'{volume}{location}'
def get_pkg_files(pkg_name, pkg_location):
return subprocess.check_output([PKG_UTIL, ONLY_FILES, FILES, pkg_name], cwd=pkg_location).decode().split('\n')
def get_pkg_folders(pkg_name, pkg_location):
return subprocess.check_output([PKG_UTIL, ONLY_DIRS, FILES, pkg_name], cwd=pkg_location).decode().split('\n')
def preview_will_remove(pkg_name, pkg_location):
files = get_pkg_files(pkg_name, pkg_location)
print('Will remove files:')
for file in files:
print(file)
print('---------------------')
folders = get_pkg_folders(pkg_name, pkg_location)
print('Will remove folders:')
for folder in folders:
print(folder)
print('---------------------')
def remove_pkg_files(pkg_name, pkg_location):
files = get_pkg_files(pkg_name, pkg_location)
subprocess.call([SUDO, RM, RM_IF] + files, cwd=pkg_location)
def remove_pkg_folders(pkg_name, pkg_location):
folders = get_pkg_folders(pkg_name, pkg_location)
subprocess.call([SUDO, RM, RM_IFR] + folders, cwd=pkg_location)
def remove_pkg_reg(pkg_name, pkg_location):
return subprocess.check_output([SUDO, PKG_UTIL, FORGET, pkg_name], cwd=pkg_location).decode()
def parse_args():
parser = ArgumentParser()
parser.add_argument('pkg_keyword', help='Keyword of pkg which you want to remove', type=str)
parser.add_argument('--apply', action='store_true', help='Apply to macOS')
parser.add_argument('--preview', action='store_true', help='Preview which file or folder will be delete')
parser.add_argument('--no-folders', action='store_true', help='Do not delete folders')
return parser.parse_args()
def main():
args = parse_args()
pkg_keyword = args.pkg_keyword
is_apply = args.apply
is_preview = args.preview
no_folders = args.no_folders
pkg_names = find_pkgs(pkg_keyword)
print('Will remove follows:')
print('---------------------')
for pkg_name in pkg_names:
location = find_location(pkg_name)
print(f'Name: {pkg_name}\nLocation: {location}')
print('---------------------')
if is_preview:
preview_will_remove(pkg_name, location)
if is_apply:
remove_pkg_files(pkg_name, location)
if not no_folders:
remove_pkg_folders(pkg_name, location)
rm_rest = remove_pkg_reg(pkg_name, location)
print(rm_rest)
if is_apply:
print('Removed successfully!')
else:
print('Is dryrun, use --apply to remove from macOS.')
main()
其中大部分是把 shell command 用 function 包裝起來,並且 parse command 回傳的字串來取得結果。
簡單介紹幾個核心 function:
首先透過 find_pkgs() ,他包裝了原來這個指令的功能:
$ pkgutil --pkgs | grep <套件可能的名稱>
把 pkgutil 回傳的 pkg names 用換行符號分開,再轉小寫與使用者輸入的關鍵字作比較過濾後回傳。
def find_pkgs(pkg_keyword):
result = subprocess.check_output([PKG_UTIL, PKGS]).decode()
return [r for r in result.split('\n') if pkg_keyword.lower() in r.lower()]
得到 pkg 完整名稱,再用 find_location() 找出他們所在位置,包裝這個指令:
$ pkgutil --pkg-info <完整套件名稱>
此函數會把 volume 和 location 拼接在一起回傳完整的路徑。
如果有多個套件,可用 for loop 多次呼叫函數批次抓回來,不用像以往人工多次輸入查詢。
def find_location(pkg_name):
volume = ''
location = ''
result = subprocess.check_output([PKG_UTIL, PKG_INFO, pkg_name]).decode()
for line in result.split('\n'):
if VOLUME in line:
volume = line.replace(VOLUME, '')
elif LOCATION in line:
location = line.replace(LOCATION, '')
return f'{volume}{location}'
最後取得 pkg files 並刪除,主要是實現以下指令:
$ sudo pkgutil --only-files --files <完整套件名稱> | tr '\n' '\0' | xargs -n 1 -0 rm -if
但我們需要有 dryrun 和預覽待刪除檔案路徑的功能,所以把「取得 pkg files」和「批次刪除檔案」分別用兩個 function 實現:
def get_pkg_files(pkg_name, pkg_location):
return subprocess.check_output([PKG_UTIL, ONLY_FILES, FILES, pkg_name], cwd=pkg_location).decode().split('\n')
def remove_pkg_files(pkg_name, pkg_location):
files = get_pkg_files(pkg_name, pkg_location)
subprocess.call([SUDO, RM, RM_IF] + files, cwd=pkg_location)
其中 check_output() 裡的參數 cwd 是給定指令運作時的 work folder ,實現 shell 中我們先 cd 到目標資料夾的動作。
pkg folders 的部分同理,這邊就不再贅述,
最後就是 remove_pkg_reg(),刪除 macOS 中的套件記錄,主要實現以下指令:
$ pkgutil --forget <完整套件名稱>
python function 如下:
def remove_pkg_reg(pkg_name, pkg_location):
return subprocess.check_output([SUDO, PKG_UTIL, FORGET, pkg_name], cwd=pkg_location).decode()
如何使用寫好的 python script?
使用一開始的範例,我們搜尋 fbws,python script 會回傳找到的套件全名,以及他所在的 location
$ ./rm_pkg.py fbws
Will remove follows:
---------------------
Name: tw.FirstBank.FBWS.第一銀行網銀安控程式.nspr.pkg
Location: /usr/local/Cellar/
---------------------
Name: tw.FirstBank.FBWS.第一銀行網銀安控程式.nss.pkg
Location: /usr/local/Cellar/
---------------------
Name: tw.FirstBank.FBWS.第一銀行網銀安控程式.FBWS.pkg
Location: /Applications/
---------------------
Name: tw.FirstBank.FBWS.第一銀行網銀安控程式.FBWS_Plugins.pkg
Location: /Library/Internet Plug-Ins
---------------------
Name: tw.FirstBank.FBWS.第一銀行網銀安控程式.com.hitrust.startFBWS.pkg
Location: /Library/LaunchAgents
---------------------
Is dryrun, use --apply to remove from macOS.
如果列出的 pkgs 都是要刪除的,就不用修改關鍵字,反之則需要調整,讓其回傳的套件都是要刪除的。
如果擔心刪除時會刪到系統資料夾,可以用 –preview 確認一下:
$ ./rm_pkg.py fbws --preview
Will remove follows:
---------------------
Name: tw.FirstBank.FBWS.第一銀行網銀安控程式.nspr.pkg
Location: /usr/local/Cellar/
---------------------
Will remove files:
nspr/._.DS_Store
nspr/4.14/._.DS_Store
nspr/4.14/lib/libnspr4.dylib
nspr/4.14/lib/libplc4.dylib
nspr/4.14/lib/libplds4.dylib
---------------------
Will remove folders:
nspr/
nspr/4.14/
.....
確認沒問題後,可以下 –apply 刪除,過程中會詢問密碼,因為需要 sudo 權限
$ ./rm_pkg.py fbws --apply
Will remove follows:
---------------------
Name: tw.FirstBank.FBWS.第一銀行網銀安控程式.nspr.pkg
Location: /usr/local/Cellar/
...
password: <your password>
Removed successfully!
如果發現會刪除到系統資料夾,使用 –no-folders 略過資料夾刪除,之後再手動處理
$ ./rm_pkg.py fbws --apply --no-folders
以後就能輕鬆移除 pkg 套件囉!
原始碼下載:github
延伸閱讀:
如何解決 Python Decimal.quantize() 發生 InvalidOperation
Python 詭譎的 default parameter value ,由踩坑來學習!