🍎

Mac を買ったら必ずやっておきたい初期設定を、全て自動化してみた

commits10 min read 3

成果物

https://github.com/ulwlu/dotfiles/blob/master/system/macos.sh

このスクリプトに全ての設定と、設定可能なオプションをコメントで記載しています。誰でもこのスクリプトのコメントを外したり任意の値を入れる事で使用可能です。

世界中のいくつかの dotfiles にはmacos.shが存在し、ある程度の MacOS の設定自動化を実現しています。しかし何百と見た中で、全設定と設定可能なオプションを全て網羅して記載しているのは恐らく初です。

これらの設定は破壊的なものではなく、いつかアプデによりキーが有効でなくなっても壊れる事はありません。壊れるのは~/ApplicationSupport/Dock ディレクトリ配下のファイルを移動したり、sqlite 群に無効な値をいれた時のみです(後述)。

この記事は何か

dotfiles Advent Calendar 2020 の二日目の記事です。

この設定を作るために、20%は世界中のリポジトリを参考にしましたが、80%はこの世に情報がなかったので自分で掘り続けて自動化しました。Apple は環境設定に関するドキュメントを一切公開していないため、何かを調べるには自分で掘る必要があるのです。

今回の記事では、いかにして全ての設定を自動化したのかを書いてみたいと思います。

(ちなみに)この記事は、2020 年 3 月に書いた個人ブログの記事のほぼ和訳となります。

MacOS の設定はどこにあり、どう操作すべきなのか

主に下記の2つです。

  1. 各ディレクトリに存在する plist ファイル群(基本的な mac 設定の保管ファイル)を、数種類ある defaults values command 群 / PlistBuddy で操作する
  2. 解読方法のない sfl2 形式、また破壊的な sqlite 群は AppleScript で操作する

[1/2] defaults values command 群 / PlistBuddy

まず、先程書いたとおり Apple は plist ファイル群について公式ドキュメントを過去一切出しておりません。そのため、自分一人で探す方法を下記します(かなり力技なのでご了承ください)。

前提知識として mac の設定をいじったことがある方なら下記のような defaults コマンドを一度は見たことあるかもしれません。例えば以下の例では、DS_STORE を共有フォルダなどでは生成しないようにしています。このように mac の設定を変更できるコマンド群を Defaults Values Command と言います。

defaults write com.apple.desktopservices DSDontWriteUSBStores -bool true
defaults write com.apple.desktopservices DSDontWriteNetworkStores -bool true

文法は以下の通りです。

defaults order[read(reference)/write(add/update)/delete(delete)] domain[Mac configuration items array. I wrote lists below] The valid setting items on domain[I wrote lists below] value[-Type Value]

(ちなみに) 電源やタイムマシンなどのルート設定に関する util 系は、defaults ではなく、pmset や mdutil や scutil といったコマンドも使えます。Util 系コマンドについてはこちらのリポジトリが詳しいです。

https://github.com/kevinSuttle/macOS-Defaults/blob/master/REFERENCE.md

例えば電源設定については下記が使えます。

# ========== Turn display off after ==========
# @int: minutes
sudo pmset -b displaysleep 3

① まずは調べたい項目がどの設定ファイルに保存されているかを調べる

  • 1: 調べたい項目を変更する
  • 2: 更新された plist を探す
Mac環境設定の保存先plistは主に下記の3つがあります。
${HOME}/Library/Preferences/*.plist
${HOME}/Library/Preferences/ByHost/*.plist
/private/var/root/Library/*.plist
  • 3: defaults read で中身をログに書き出し、diff を取って更新された項目を確認する

これをすぐに実現するコマンドは下記の通りです。

# 1. 調べたい項目を変更する前に、全plistの中身を適当なログに保存
defaults domains | sed 's/ /\\n/g' | xargs -I % sh -c 'echo % && defaults read %' > ~/work/a.log && defaults domains --currentHost | sed 's/ /\\n/g' | xargs -I % sh -c 'echo % && defaults read --currentHost %' >> ~/work/a.log

# 2. 調べたい項目を変更したら、再度、全plistの中身を適当なログに保存
defaults domains | sed 's/ /\\n/g' | xargs -I % sh -c 'echo % && defaults read %' > ~/work/b.log && defaults domains --currentHost | sed 's/ /\\n/g' | xargs -I % sh -c 'echo % && defaults read --currentHost %' >> ~/work/b.log

# 3. diffを取って、更新された項目を確認する
diff ~/work/a.log ~/work/b.log

かなり悪手だと思いますが、これより良い方法がないのではと思ってます。

② 更新箇所と思わしき設定項目を見つけたら、read で確認し、設定を付与するコマンドをメモする

ターミナルで defaults read ○○.plist {{設定項目名}} と打ち、設定値を確認(例:defaults read .GlobalPreferences AppleInterfaceStyle でテーマカラーの設定値が出ます)

何回か設定を変えてみてその設定値が正しい事がわかったら、defaults write ○○.plist {{設定項目名}} {{設定値}} と dotfiles のスクリプトに書く(例: defaults read .GlobalPreferences AppleInterfaceStyle -string "Dark"

③ ネストが 1 以上あるもの、また事情があって plist が defaults コマンドが認識できる場所にないものは PlistBuddy を使う

Defaults Values Command の方法で 50%の設定は自動化可能ですが、defaults は残念ながらネストが 1 以上ある dict 型の設定は操作できません。ですのでそういう場合は、Mac に最初から入っている PlistBuddy を使います。

例えば下記の例では、デスクトップの → アイコンの → テキストサイズを変更しています。

/usr/libexec/PlistBuddy -c "Set :DesktopViewSettings:IconViewSettings:textSize 12" ~/Library/Preferences/com.apple.finder.plist

このように複雑な Dict になると非常に検索・操作が大変になってくるのですが、幸いな事に PlistBuddy は index 数でのアクセスも可能です。例えば以下の例では、Dock に表示するアプリを設定できます。設定時に index で指定しています。

dockitem=(
 	"Rectangle"           "com.knollsoft.Rectangle"                   "file:///Applications/Rectangle.app/"
 	"Visual Studio Code"  "com.microsoft.VSCode"                      "file:///Applications/Visual%20Studio%20C"
 	"Wireshark"           "org.wireshark.Wireshark"                   "file:///Applications/Wireshark.app/pass.app/"
 	"LimeChat"            "net.limechat.LimeChat-AppStore"            "file:///Applications/LimeChat.app/"
 	"Android Studio"      "com.google.android.studio"                 "file:///Applications/Android%20Studio.app/"
 	"Burp Suite"          "com.install4j.9806-1938-4586-6531.70"      "file:///Applications/Burp%20Suite%20Community%20Edition.app/"
 	"Notion"              "notion.id"                                 "file:///Applications/Notion.app/"
)

PLIST="${HOME}/Library/Preferences/com.apple.dock.plist"
/usr/libexec/PlistBuddy -c "Add persistent-apps array" ${PLIST}
DNUM=$(expr ${dockitem[(I)$dockitem[-1]]} / 3)
for idx in $(seq 0 $(expr ${DNUM} - 1)); do
 	NAMEIDX=${dockitem[$(( ${idx} * 3 + 1 ))]}
 	ITEMIDX=${dockitem[$(( ${idx} * 3 + 2 ))]}
 	PATHIDX=${dockitem[$(( ${idx} * 3 + 3 ))]}

 	/usr/libexec/PlistBuddy \
 		-c "Add persistent-apps:${idx} dict" \
 		-c "Add persistent-apps:${idx}:tile-data dict" \
 		-c "Add persistent-apps:${idx}:tile-data:file-label string ${NAMEIDX}" \
 		-c "Add persistent-apps:${idx}:tile-data:bundle-identifier string ${ITEMIDX}" \
 		-c "Add persistent-apps:${idx}:tile-data:file-data dict" \
 		-c "Add persistent-apps:${idx}:tile-data:file-data:_CFURLString string ${PATHIDX}" \
 		-c "Add persistent-apps:${idx}:tile-data:file-data:_CFURLStringType integer 15" \
 		${PLIST}
done

下記の例では、先に PlistBuddy * grep で内部要素の数をはじきだし、for 文で回すことで index にアクセス実現しています。

LSSC=("http" "https" "mailto")
LSCT=("public.xhtml" "public.html")
PLIST="${HOME}/Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist"
HNUM=$(/usr/libexec/PlistBuddy -c "Print LSHandlers:" ${PLIST} | grep -P '^[\s]*Dict' | wc -l | tr -d ' ')
for idx in $(seq 0 $(expr ${HNUM} - 1)); do
	THIS_LSSC=$(/usr/libexec/PlistBuddy -c "Print LSHandlers:${idx}:LSHandlerURLScheme" ${PLIST} 2>/dev/null)
	if [[ ${LSSC[@]} =~ $THIS_LSSC ]]; then
		/usr/libexec/PlistBuddy -c "Set LSHandlers:${idx}:LSHandlerRoleAll com.google.chrome" ${PLIST}
	fi
	THIS_LSCT=$(/usr/libexec/PlistBuddy -c "Print LSHandlers:${idx}:LSHandlerContentType" ${PLIST} 2>/dev/null)
	if [[ ${LSCT[@]} =~ $THIS_LSCT ]]; then
		/usr/libexec/PlistBuddy -c "Set LSHandlers:${idx}:LSHandlerRoleAll com.google.chrome" ${PLIST}
	fi
done

以上の方法で、ほぼ 80%の設定は自動化可能です。

[2/2] sfl2 群や sqlite 群など、コマンドから叩けない物は AppleScript から叩く

まず sfl2 群について。sfl2 という現状エクスポート方法が存在しない圧縮ファイル系に存在する設定は、現状取得不可能です(正確にいうと sidebar というプロジェクトで Swift で取得できているようだが、かなり不安定らしい?ので一旦無視)。例えば、環境設定>一般>「最近みた項目を何個表示する?」の個数などがこれにあたり、${HOME}/Application Support/com.apple.sharedfilelist/*.sfl2に保存されています。

次に sqlite 群について。「デスクトップ画像」やパソコン毎に異なる解像度を動的に取得している「環境設定>ディスプレイ>スペースを広く」は、${HOME}/Application Support/Dock/desktoppicture.db に保存されています。この操作はsqlite3 ${HOME}/Application Support/Dock/desktoppicture.db -c "~~~" で可能です・・・・が!このファイル群、plist と異なり設定に無効な値が入ると Dock 自体が死にます。非常に危険なので直接いじらない方がいいです。

ではこれらはどうすればいいのか?そんなときは AppleScript を使って、「手動で行っている操作を自動化」しましょう。

AppleScript でどうやって自動化するのか?

例えば以下の例では「デスクトップ画像を設定」しています。

osascript -e "
	tell application \"Finder\"
		set desktop picture to \"desktop.jpg\" as POSIX file
	end tell
"

以下の例では、「環境設定>一般>「最近みた項目を何個表示する?」->5個に設定」しています。

osascript -e "
	tell application \"System Preferences\"
		activate
		set current pane to pane \"com.apple.preference.general\"
	end tell
	tell application \"System Events\"
		tell application process \"System Preferences\"
			repeat while not (window 1 exists)
			end repeat
			tell window 1
				tell pop up button 4
					delay 1
					click
					tell menu 1
						click menu item \"5\"
					end tell
				end tell
			end tell
		end tell
	end tell
"

パッと見てなんとなくわかるかと思いますが、日頃みなさんがやっているコマンドを GUI 上で自動で動いてやってくれています。「これができるならもう無敵じゃないか!」と思われるかもしれません。確かに強力なのですが、しかしこの操作する対象を見つけるのが非常に難しいのです。

例えばスクリーンセーバーの設定をする場合、「com.apple.preference.desktopscreeneffect」というドメインが対象になります。しかしドキュメントがないので、そんなドメイン名なんか最初はわからないわけです。そのため、最初は完全に諦めていたのですが、色々と試行錯誤した結果、全ての設定を一覧できる方法を自分なりに見つけてまとめられたのでいかに記します。

下記のコマンド群を「Script Editor(AppleScript をデバッグできるエディタ)」で実行してみましょう。

ペイン(画面)

tell application "System Preferences"
  name of panes
end tell

アンカー

tell application "System Preferences"
  reveal pane id "com.apple.preference.general"
  get name of every anchor
end tell

設定項目

tell application "System Preferences"
  activate
  reveal pane id "com.apple.preference.general"
end tell
tell application "System Events"
  tell application process "System Preferences"
    repeat while not (window 1 exists)
    end repeat
    tell window "General"
      every UI element
    end tell
  end tell
end tell

上記の方法を使うことで、自分の探している項目は何番の group に属しているのか?button 属性なのか?という事がわかります。これらをもとにして操作対象をみつけて、tell pop up だったり click をすれば操作ができるというわけです。

設定が終わったらキャッシュを消して service の再起動をする(OS の再起動ではない)

killall cfprefsd
killall Dock

設定が終わったらよく、Dock だけ service 再起動すればよいと思われがちなのですが、種類によってはキャッシュが生きたまま残っていたりします。ですので、cprefsed も忘れず killall しましょう。例えば Dock 上に表示する Application 群は、persistent-apps を変えてもキャッシュを消さない限り永遠に変更されません。

まとめ

以上の方法をもって全ての設定を自動化しました。例えば touch bar なんかは Mac 本体のシリーズによって仕様が違うので、全てのシリーズを網羅できてるわけではないかもしれません。もし何か足りない情報がありましたら教えていただけますと幸いです。

また、完全に独学でやったため、もっといい方法や知らない設定があるかもしれません。そちらももしどなたかありましたら教えていただけると幸いです。

改めて成果物 ↓

https://github.com/ulwlu/dotfiles/blob/master/system/macos.sh

追記

plutil -p でもreadできる。次からは新しいパソコンかったら、出荷状態で~/Library/Preferences配下でfd ".plist$" -X plutil -pをして全保存しておきたい。また特定範囲はls -alnotである程度絞れるので高速化できるはず。

この記事に贈られたバッジ