DEV Community

loading...
Cover image for I think I automated almost the system settings of a MacOS

I think I automated almost the system settings of a MacOS

ulwlu profile image ulwlu Updated on ・8 min read

tl;dr

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

This script contains almost the settings and the configurable options in comments. Anyone can use this script by uncommenting it or putting in arbitrary values.

Many dotfiles around the world have macos.sh, which automates the configuration of MacOS to some extent. However, among all of them I have seen, this is probably the first to describe all the settings and all the options that can be set.

These settings are not destructive and will not be broken even if the key gets no longer valid due to update. The only time they will be broken is if you change files under the ~/ApplicationSupport/Dock directory or insert invalid values in the sqlite group (see below).

What is this article about?

This article is a more detailed explanation of this my issue.

In order to create this configuration, I looked for repositories all over the world as a reference for 20% of it, but 80% of it I had to automate by digging myself because there was no documentation in the world. Apple doesn't publish any documentation on their environment settings, so you have to dig yourself to find out anything.

In this article, I would like to write about how I automated all the settings.

Where are the MacOS settings and how should they be manipulated?

The two main settings are as follows.

  1. Manipulate plist files (basic mac configuration files) in each directory with several different defaults values commands / PlistBuddy
  2. Use AppleScript to manipulate sqlite files that are destructive and in sfl2 format files, which there is no way to decipher.

[1/2] defaults values command group / PlistBuddy

First of all, as I mentioned earlier, Apple has not released any official documentation on plist files. Therefore, I'm going to show you how to find them on your own (please note that this is quite a heavy task).

As a prerequisite, if you have ever tinkered with mac settings, you may have seen the following defaults command at least once. For example, in the following example, DS_STORE will not be generated in shared folders. A group of commands that can change mac settings like this is called Defaults Values Command.

defaults write com.apple.desktopservices DSDontWriteUSBStores -bool true
defaults write com.apple.desktopservices DSDontWriteNetworkStores -bool true
Enter fullscreen mode Exit fullscreen mode

The syntax is as follows.

defaults order[read(reference)/write(add/update)/delete(delete)] domain[Mac configuration items array] value[The valid setting items on domain] type[-Type Value]. 
Enter fullscreen mode Exit fullscreen mode

(By the way) You can use pmset, mdutil, scutil, etc. instead of defaults values command. It's called util commands which are related to root settings such as power supply, time machine, etc. This repository has more details about Util commands.

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

For example, for power configuration, you can use the following

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

1) First, find out which configuration file stores the item you want to change.

  1. change the item you want to check.
  2. find the updated plist.
# The following three plist files are mainly used to store Mac Preferences.
${HOME}/Library/Preferences/*.plist
${HOME}/Library/Preferences/ByHost/*.plist
/private/var/root/Library/*.plist
Enter fullscreen mode Exit fullscreen mode
  1. command defaults read to log the contents, and diff to see the updated items.

The command to do this immediately is as follows

# 1. save the contents of all plists to a suitable log before changing the items you want to check.
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. After changing the items you want to check, save the contents of all the plists to a suitable log again
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. Take a diff to see what has been updated
diff ~/work/a.log ~/work/b.log
Enter fullscreen mode Exit fullscreen mode

I think this is a pretty heavy task, but I'm not sure there is a better way.

2) When you find a configuration file that seems to store an updated item, confirm it with default read, and write down the command in script.

In a terminal, type defaults read ***.plist {setting key name} and check the setting value (e.g. defaults read .GlobalPreferences AppleInterfaceStyle will give you the setting value of the system theme color such as light or dark).

After changing the setting several times and finding that the value is correct, write defaults write ○○.plist {setting key name} {value} in the dotfiles script (e.g., defaults read .GlobalPreferences AppleInterfaceStyle -string "Dark")

3) Use PlistBuddy if you have more than one nest, or if for some reason the plist is not in a place where the defaults command can recognize it.

50% of the settings can be automated using the Defaults Values Command, but unfortunately defaults cannot handle dict-type settings with more than one nest. In such cases, you can use PlistBuddy, which is included in your default MacOS.

For example, in the following, you can change the text size of the desktop icon.

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

Fortunately, PlistBuddy can also accessed by index number. For example, in the following, you can configure the apps to be displayed in the dock.

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
     -c "Add persistent-apps:${idx} dict" \c
     -c "Add persistent-apps:${idx}:tile-data dict" \c
     -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
     -c "Add persistent-apps:${idx}:tile-data:file-data:_CFURLString string ${PATHIDX}" \c
     -c "Add persistent-apps:${idx}:tile-data:file-data:_CFURLStringType integer 15" \c
     ${PLIST}
done
Enter fullscreen mode Exit fullscreen mode

In the following example, it counts the dict length by PlistBuddy -c Print | grep and find the number of internal elements, and the for statement is used to access the 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
Enter fullscreen mode Exit fullscreen mode

With the above methods, almost 80% of the configuration can be automated.

[2/2] Use appleScript for sfl2 group, sqlite group, and other things that can't be manipulated from a command.

First, let's talk about the sfl2 group. It is currently impossible to get the settings for sfl2, a compressed file system for which there is no current export method (To be precise, a project called sidebar seems to be able to get them by using Swift, but it seems to be quite unstable yet. So I don't talk about this). For example, the number of items in Preferences > General > "Recent items:" is stored in ${HOME}/Application Support/com.apple.sharedfilelist/*.sfl2.

Next, let's talk about the sqlite group. The Desktop Picture and the Preferences > Display > Resolution which dynamically obtains different resolutions for each computer are stored in ${HOME}/Application Support/Dock/desktoppicture.db. This operation can be done with sqlite3 ${HOME}/Application Support/Dock/desktoppicture.db -c "~~~", however, this group of files, unlike plist, will break the Dock if an invalid value is inserted. It's very dangerous, so it's best not to mess with it directly.

So what should I do with them? In such a case, let's use AppleScript to automate the operations you are doing manually.

How to automate them with AppleScript?

For example, in the following, we set the Desktop Picture.

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

In the example below, we set Preferences > General > Recent Items: -> "Set to 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
"
Enter fullscreen mode Exit fullscreen mode

As you can see at a glance, it automatically runs the commands the same things you do to setup on GUI. You may think, "If I can do this, I can automate everything by applescript!". Yes, it is indeed powerful, but it is very difficult to find the target of this operation.

For example, if you want to set up a screensaver, the domain "com.apple.preference.desktopscreeneffect" is the target. However, since there is no documentation from Apple, there is no way to know the name of such a domain at first. So, I gave up completely at first, but after a lot of trial and error, I found a way to list all the settings in my own way and wrote down how I did it.

Let's run the following commands in "Script Editor" (an editor that can debug AppleScript. Default MacOS has it from startup).

  1. Pane (screen)
tell application "System Preferences"
  name of panes
end tell
Enter fullscreen mode Exit fullscreen mode
  1. Anchor
tell application "System Preferences"
  reveal pane id "com.apple.preference.general"
  get name of every anchor
end tell
Enter fullscreen mode Exit fullscreen mode
  1. Preferences
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
Enter fullscreen mode Exit fullscreen mode

By using the above method, you can find out what group the item you are looking for belongs to, and whether it has the button attribute. Based on the target elements you get, you can operates the target to tell pop up or click on it.

After the configuration, clear the cache and restart the service (not restart the OS).

killall cfprefsd
killall Dock
Enter fullscreen mode Exit fullscreen mode

It is often thought that only Dock should be restarted after the configuration is done, but depending on the type, the cache may still be alive. So, don't forget to killall cprefsed as well. For example, the group of applications displayed on the Dock will not be changed even if you change the persistent-apps, unless you clear the cache.

Summary

With the above method, all the settings have been automated. The touch bar, for example, has different specifications depending on the Mac series, so I may not be able to cover all series. If there is any information that I am missing, I would appreciate it if you could let me know.

Also, since I am completely self-taught, there may be better ways to do this or settings I don't know. If you have any suggestions, please let me know.

Once again, the script is below.

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

remarks

After all this, I found plutil -p can print easier than defaults read by specifying not domain but file itself. Next time I research I want to use fd "\.plist$" | xargs plutil -p to find the exact formatting settings. Also, ls -alnot can find the target easily.

thanks ocat for reviewing my english.

Discussion (0)

pic
Editor guide