$ rm -rf ~ 가 얼마나 무서운 명령어인지 아는가? 리눅스 또는 맥 OS에서 사용자의 홈 디렉토리를 삭제하는 명령어다.

얼마 전 warp라는 터미널이 좋아 보여서 맥북에 설치하고 사용해 봤다. 터미널 클라이언트로 iTerm2를 사용한 지 오래되기도 했고 warp은 AI로 명령어를 추천해 주기도 한다고 해서 구미가 당겨 사용해 보고 있었다.

튜토리얼을 따라 해보다가 잘못해서 ~ 라는 폴더를 home에 만들게 되었다. ~/~ 경로가 생성된 것이다. 홈 경로에서 작업하고 있었고, 쓸모없는 디렉토리가 만들어졌으니 별생각 없이 rm -rf ~ 라는 명령어를 쳤다.

rm -rf 명령어는 제거하라(rm)는 명령어에 재귀적으로(-rf) 라는 옵션이 붙은 명령어이다. 따라서 사용자의 홈 디렉터리와 그 하위에 있는 모든 파일과 디렉터리를 강제로 삭제하게 된다. 하지만 아무것도 들어있지 않은 디렉토리를 제거하기에는 생각보다 시간이 오래걸렸고, 어떤 권한까지 물어보는 것이었다.

갸우뚱하다가 실수한 것을 깨닫게 되었다. 내가 home 디렉토리를 지금 날리고 있구나. 얼른 명령어를 취소시켰다. 하지만 이미 컴퓨터의 대부분의 자료가 날아간 상태였다. 컴퓨터를 재부팅했더니 맥북을 처음 샀을 때 한껏 들뜨게 만들었던 맥 초기 화면이 나를 기다리고 있었다.

mac-init


chezmoi

이전에 dotfiles의 존재를 어렴풋이 알고 있었다. dotfiles는 . 으로 시작하는 숨김 처리된 파일이다. .git 같은 파일은 맥의 Finder를 통해 디렉토리를 살펴보면 보이지 않는 숨김 처리된 파일이다. dotfiles 중에서도 어떤 프로그램의 설정값이나, 컴퓨터 전체의 설정값을 저장하고 있는 파일을 말한다.

홈 디렉토리를 잃거나, 맥을 새로 장만했을 때 내가 자주 사용하는 어플리케이션, 설정값들을 복원할 수 있다면 빠르게 개발 환경을 만들 수 있다. 즉, dotfiles를 잘 관리해두면 처음부터 컴퓨터의 세팅을 하지 않아도 된다.

chezmoi라는 툴은 dotfiles를 관리해 주는 툴이다. 기본적으로 Command Line Interface(CLI)를 통해 직접 명령을 내리며 내가 관리하고 싶은 dotfiles를 지정해 주면 스냅샷을 떠서 그대로 관리해 준다. 그리고 맥, 윈도우, 리눅스 운영체제 별로 실행해야 하는 스크립트나 설정 파일을 따로 지정할 수 있고 운영 체제별로 dotfiles를 관리할 수 있다. 다른 사람에게 공개되지 않아야 하는 비밀 키도 관리할 수 있다.

chezmoi는 Go라는 프로그래밍 언어로 작성되었기 때문에 무척 빠르다. chezmoi를 사용하면서 알게 된 새로운 몇몇 툴과 가벼운 지식을 이번 글에 기록해 보고자 한다. 아래는 전부 맥 OS 기준으로 작성한다.

먼저 설치부터 해보자. homebrew를 통해 설치할 수 있다.

$ brew install chezmoi

기본 명령어

init

$ chezmoi init

~/.local/share/chezmoi 경로가 생성되고, git 로컬 저장소가 생성된다. 이를 chezmoi에서는 소스 디렉토리(source directory)라고 부른다.

※ 소스 디렉토리

소스 디렉토리는 마치 git의 staging area와 비슷하다. chezmoi add 명령어로 본을 뜬 녀석은 소스 디렉토리에 저장되고 후에 알아볼 chezmoi apply 를 통해 실제 파일에 저장된다. 참고로 git add를 통해 git에게 관리되는 파일은 staging area에 저장되게 되는데, 이는 .git/index 에 바이너리 파일로 저장된다.

소스 디렉토리로 이동하는 명령어는 $ chezmoi cd 다.

add

$ chezmoi add ~/.zshrc

zsh를 사용하고 있다면 홈디렉토리에 .zshrc 파일이 있을 것이다. 이는 zsh 설정 파일이다.

# homebrew path
export PATH=/opt/homebrew/bin:$PATH

plugins=(
  git
  encode64
)

# oh-my-zsh path, init
export ZSH="$HOME/.oh-my-zsh"
source $ZSH/oh-my-zsh.sh

# autosuggestions, syntax-highlighting
source $(brew --prefix)/share/zsh-autosuggestions/zsh-autosuggestions.zsh
source $(brew --prefix)/share/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh

# pure theme
fpath+=("$(brew --prefix)/share/zsh/site-functions")
autoload -U promptinit; promptinit
prompt pure

# fnm
eval "$(fnm env --use-on-cd)"

보통은 이렇게 생겼다. chezmoi add 명령어로 이 파일을 관리하겠다고 하면 소스 디렉토리(~/.local/share/chezmoi)에 dot_zshrc 파일이 생성된다. 여기서 dot_. 을 의미한다. 즉 파일 명이나 디렉토리 명에서 파일의 이름을 유추할 수 있다. 이는 chezmoi의 철학이다.

현재 .zshrc 파일을 스냅샷을 떠 관리를 시작했다. 모든 내용은 복사되어 소스 디렉토리에 존재한다.

edit

.zshrc 파일을 수정해야 한다면 홈 디렉토리에 있는 .zshrc 파일을 직접 수정해도 되지만, chezmoi를 사용하면 add 명령어를 쳤을 그 시점에 가지고 있는 설정값으로 원상복구될 것이다. 따라서 edit 명령어를 통해 chezmoi의 소스 디렉토리에 있는 파일을 수정 후, 적용해 주어야 한다.

$ chezmoi edit ~/.zshrc

diff

chezmoi로 dotfile을 수정했으니 이제 실제 ~/.zshrc 파일에 적용해 줄 차례인데, 적용하기 전에 어떤 것이 변경되었는지 명령어를 통해 볼 수 있는데 diff 명령어를 통해 알 수 있다.

$ chezmoi diff

diff 명령어는 파일 명을 명시해 주지 않아도 관리되고 있는 dotfiles 중에서 변경된 모든 파일의 변경사항을 보여준다.

apply

이제 적용 명령이다.

$ chezmoi apply -v

그냥 apply만 해도 되지만, -v 옵션을 주면 적용 전에 diff를 통해 어떤 겻이 변경될 것인지 알려준다. 주로 -v(verbose) 옵션을 주어 확인 후 적용하도록 하자.

cd

$ chezmoi cd

터미널에서 소스 디렉토리(~/.local/share/chezmoi)로 바로 이동하는 명령어다. chezmoi가 관리하는 dotfiles를 git을 통해 버전 관리할 수 있다. 물론 GitHub에도 올릴 수 있다. 컴퓨터가 초기화되었다면 GitHub에서 이전에 관리하던 dotfiles를 그대로 내려받아 똑같은 위치에 dotfiles를 원상복구할 수 있다.


내가 관리하고 있는 것들

내 저장소는 pozafly/dotfiles 이곳이다. chezmoi를 통해 관리하고 있는 파일들이다. 가볍게 git config 파일부터 살펴보자.


git config

원래 ~/.gitconfig 파일이 있었다. 하지만, ~/.config/git 경로에 config 파일을 생성해 설정 파일을 두어도 똑같은 역할을 한다. 레파지토리를 보면 dot_config/git/config.tmpl 파일이 있다.

tmpl이란, chezmoi의 템플릿 파일이다. tmpl은 chezmoi만의 특별한 확장자로, 템플릿 파일을 만들 수 있는데 분기문을 사용해 OS 별로 실행 명령어를 다르게 하거나 설정 파일을 다르게 할 수 있다. 또한 chezmoi 설정값을 가져와 적용하게 할 수도 있다.

예를 들어 tmpl 파일 안에 {{ .chezmoi.sourceDir }} 이런 표현식이 있다면 그 자리에는 실행 시점에 소스 디렉토리 경로(~/.local/share/chezmoi)로 대체된다. 또한 분기문은

{{ if eq .chezmoi.os "darwin" }}
# darwin
{{ else if eq .chezmoi.os "linux" }}
# linux
{{ else }}
# other operating system
{{ end }}

이런 방식으로 사용할 수 있다. 템플릿에서 사용하는 .chezmoi. 내부의 값은 $ chezmoi data 명령어를 통해 알아볼 수 있다.

[alias]
  st = status

[user]
  name = {{ .chezmoi.config.data.name | quote }}
  email = {{ .chezmoi.config.data.email | quote }}

[core]
  editor = code --wait
  precomposeunicode = true
  quotepath = false
  excludesfile = {{ .chezmoi.homeDir }}/.gitignore_global
  autocrlf = input

[pull]
  rebase = true

[init]
  defaultBranch = main

이렇게 되어있다. {{ .chezmoi.config.data.name | quote }}

quote는 Go 템플릿에서 사용할 수 있는 “파이프라인”을 사용하는 부분이다. 파이프라인을 통해 데이터를 수정하는 함수나 명령을 적용할 수 있다. 예를 들어, 사용자의 이름이 O'Reilly라면, quote를 적용하지 않은 상태에서 JSON 파일에 직접 삽입하면 문제를 일으킬 수 있다 ("name": "O'Reilly"). quote를 사용하면 이 이름이 "O'Reilly"처럼 안전하게 인용 부호 안에 포함되어 처리된다.

.chezmoi.config.data 부분은, chezmoi의 설정 파일에 있는 데이터를 가져오겠다는 말이 된다.

chezmoi 설정 파일

chezmoi 툴의 설정 파일이 있다. ~/.config/chezmoi/chezmoi.toml 으로, toml 파일로 관리된다.

[data]
  email = "pozafly@gmail.com"
  name = "pozafly"
  env = "home"

encryption = "age"
[age]
  identity = "/Users/pozafly/age-key.txt"
  recipient = "age1k3dd5wtylpz6um3wz2sgyr6ek0zn4mvc8mknaf9tjdjxh7n953psf8gw40"

# chezmoi를 vscode로도 관리한다.
[edit]
  command = "code"
  args = ["--wait"]

# plist diff을 보여줄 때 바이너리가 아닌, xml로 보여줌
[[textconv]]
  pattern = "**/*.plist"
  command = "plutil"
  args = ["-convert", "xml1", "-o", "-", "-"]

[git]
#  autoCommit = true

이곳의 [data] 부분에 명시된 이름을 가져와서 git/config 파일에 템플릿으로 넣어주는 것이다.

이 설정 파일마저도 chezmoi로 관리해 줄 수 있다. 소스 디렉토리에 .chezmoi.toml.tmpl 파일을 보자. 이마저도 템플릿 파일로 이루어져 있다.

{{- $email := promptStringOnce . "email" "Email address" -}}
{{- $name := promptStringOnce . "name" "Name" -}}
{{- $env := promptStringOnce . "env" "env (예: home | work)" -}}

[data]
  email = {{ $email | quote }}
  name = {{ $name | quote }}
  env = {{ $env | quote }}

...

템플릿 구문을 볼 수 있는데, {{- $email := promptStringOnce . "email" "Email address" -}} 이 부분은 무엇인가? chezmoi init 명령어로 chezmoi를 초기화할 때, cli로 email, name, env를 직접 타이핑해서 입력해 줄 수 있다.

chezmoi-init

이런 방식으로 말이다.


git global ignore

git이 관리하는 파일에 .gitignore 이 있지만, 로컬 컴퓨터 전체에 적용되는 global ignore도 있다. 이를 git config에서

[core]
  excludesfile = {{ .chezmoi.homeDir }}/.gitignore_global
  ...

이렇게 적어줄 수 있다. macOS라면 .DS_Store 를 넣어주는 것을 추천한다.


스크립트 파일

스크립트 파일은 특정 시점에 실행되는 bash 스크립트 파일이다. 레파지토리를 살펴보면 .chezmoiscrips/darwin 내부에 run_once_before_01_install-packages-darwin.sh.tmpl 과 같은 파일이 존재한다. .chezmoiscrips는 특정 시점에 실행할 bash 스크립트가 들어있는 파일임을 디렉토리 명을 통해 chezmoi에게 알려준다. 내부에 darwin이라는 디렉토리가 있는데 이는 맥 OS 환경에서만 실행될 것임을 chezmoi에게 알려준다.

※ darwin?

darwin이란, Apple이 만든 오픈 소스 유닉스 컴퓨터 운영체제다. macOS, IOS, watchOS, iPadOS 등의 운영체제들은 모두 Darwin을 기반으로 한다. 맥에서 터미널에 uname 을 입력하면 Darwin 이 뜨는 것을 볼 수 있을 것이다.

그렇다면 run_once_before_01_install-packages-darwin.sh.tmpl 파일은 어떻게 해석해야 하나? chezmoi는 디렉토리 명 또는 파일 명을 통해 어떤 파일인지, 어떤 시점에 실행해야 하는지 알려준다.

  • run : 실행한다
  • once : 단 한 번만
  • before : 이전에
  • 01 : 순서 표기
  • install-package : 패키지를 설치하는 시점에
  • darwin : macOS에서
  • sh : 쉘 스크립트 파일 확장자
  • tmpl : chezmoi의 템플릿 파일

붙여 의미를 설명하면 chezmoi의 초기 설정 단계에서 macOS 시스템에 필요한 패키지를 설치하는 스크립트로 사용되며, 첫 번째 실행 시에만 작동하고, 그 이후에는 실행되지 않도록 설계되어 있다.

그럼 파일 내부를 한번 보자.

#!/bin/bash

# If Homebrew is not installed on the system, it will be installed here
if test ! $(which brew); then
   printf '\n\n\e[33mHomebrew not found. \e[0mInstalling Homebrew...'
   /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
else
  printf '\n\n\e[0mHomebrew found. Continuing...'
fi

# If Oh-my-zsh is not installed on the system, it will be installed here
if [ ! -d $HOME/.oh-my-zsh ]; then
  printf '\n\n\e[33mOh-my-zsh not found. \e[0mInstalling Oh-my-zsh...'
  sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
else
  printf '\n\n\e[0mOh-my-zsh found. Continuing...'
fi

# Update homebrew packages and apps
printf '\nInitiating Homebrew update...\n'
brew bundle --no-lock --file="{{ .chezmoi.sourceDir }}/Brewfile"

# Install age dependency
if which age > /dev/null; then
  echo "'age' is already installed. Skip installation."
else
  echo "'age' is not installed. Start installation."
  brew install age
  echo "Successfully installed 'age'."
fi

printf '\nbefore install package 01 Done!!\n'
  1. 컴퓨터에 homebrew가 설치되어 있지 않다면 homebrew를 설치한다.
  2. 컴퓨터에 .oh-my-zsh 파일이 없다면 oh-my-zsh를 설치한다.
  3. Brewfile에 명시된 프로그램을 설치한다(Brewfile은 밑에서 설명할 것임).
  4. age라는 패키지가 설치되어 있지 않다면 설치한다(age 패키지는 밑에서 설명할 것임).

Brewfile

맥 사용 개발자라면 brew 명령어를 통해 macOS에서 여러 파일을 설치해 봤을 것이다. $ brew list 명령어를 터미널에서 입력하면 brew를 통해 설치된 파일 리스트를 볼 수 있다. brew list는 어떤 한 패키지에서 종속성을 가지고 있다면 종속성을 가진 패키지까지 모두 설치하고, 그 리스트 모두를 보여준다. 즉, brew install 명령어로 설치된 파일 외, 컴퓨터에 homebrew로 깔린 모든 파일을 보여준다.

Brewfile이란, npm의 package.json 파일 또는 gradle의 build.gradle 파일과 같다. 즉, 종속성을 관리하는 파일이다. Brewfile을 까보면

tap "homebrew/bundle"
brew "age"
brew "chezmoi"
brew "fnm"
brew "git"
brew "mas"
brew "php"
brew "pnpm"
brew "pure"
brew "zsh"
brew "zsh-autosuggestions"
brew "zsh-completions"
brew "zsh-syntax-highlighting"
cask "alfred"
cask "alt-tab"
cask "appcleaner"
cask "google-chrome"
cask "iterm2"
cask "itsycal"
cask "notion"
cask "openineditor-lite"
cask "openinterminal-lite"
cask "scroll-reverser"
cask "slack"
cask "sublime-text"
cask "typora"
cask "visual-studio-code"
mas "Gifski", id: 1351639930
mas "Hidden Bar", id: 1452453066
mas "jandi", id: 1551460285
mas "Keynote", id: 409183694
mas "Magnet", id: 441258766
mas "Memory Diag", id: 748212890
mas "Numbers", id: 409203825
mas "Pages", id: 409201541
mas "PhotoScape X", id: 929507092
mas "카카오톡", id: 869223134

이렇게 생겼다. brew, cask가 붙은 녀석들은 실제 brew install 을 통해 설치한 어플리케이션을 나타낸다. 따라서, 어플리케이션을 이렇게 관리하고 싶다면 웬만하면 brew 명령어를 통해 설치하는 것을 추천한다. 또한 brew update, brew upgrade 명령어를 통해 손쉽게 패키지를 업데이트할 수 있으므로 매우 편리하다.

※ 얼마 전 xz 패키지 이슈가 있었다.

xz 패키지는 압축 유틸리티인데, 많은 패키지에서 종속성을 가지고 있는 패키지다. 취약점이 발견되어 떠들썩했던 사건이다. 이때, $ brew update && brew upgrade 명령어로 패키지를 빠르게 업데이트해준다면 취약점을 조금 더 빠르게 방지할 수도 있다.

run_once_before_01_install-packages-darwin.sh.tmpl 스크립트에서 brew bundle --no-lock --file="{{ .chezmoi.sourceDir }}/Brewfile" 명령어로 Brewfile에 명시된 종속성을 설치하고 있다.

그렇다면 Brewfile을 생성하는 방법은 brew bundle dump 으로 생성할 수 있다. brew bundle이란, Homebrew를 위한 번들러다. 이 과정을 brew install ... 명령어를 실행하면 자동으로 dump를 뜨게 할 수는 없을까? 아쉽지만 brew bundle에서 hook 기능은 존재하지 않는다. 이 이슈를 보자. 5건이나 install hook을 만들어 달라고 요청했지만 메인테이너가 이를 들어주지 않았다.

어쨌든 Brewfile에 보면 brew로 mas 를 설치하고 있고, mas 뒤에 어플리케이션 명이 적힌 것을 볼 수 있다. 이는 맥의 App Store를 통해 설치한 어플리케이션을 말한다. brew로 설치하지 않고 App Store에서 설치한 파일을 명시해두었다. 이들 또한 Brewfile에 명시가 되며 알아서 설치된다. 자세한 사항은 mac-cli를 통해 확인할 수 있다.


age (암호화 라이브러리)

스크립트에서 age를 설치하고 있다. Brewfile에도 명시가 되어있지만, Brewfile을 통해 어플리케이션들을 설치한 뒤 다시 age를 확인하는 이유는 age로 외부에 노출되어서는 안되는 특정 파일을 암호화하고 있으며 이를 chezmoi로 관리하고 있기 때문이다. 즉, age는 암호화, 복호화 라이브러리다.

chezmoi의 암호화 라이브러리

chezmoi의 암호화 연동 라이브러리는 age, gpg, rage가 있다. 각각 독립적인 암호화 라이브러리고, gpg가 가장 유명해 보였다. 하지만, gpg는 복잡하고 무거운 기술을 사용한다. 나는 간편하게 공개키 전략을 사용해 비밀키와 공개키 한 쌍으로 암호화, 복호화를 하기 원했고 쉽고 가볍기를 원했다.

age를 brew로 설치했다면 아래 명령어로 먼저 공개키를 만든다.

$ age-keygen -o $HOME/age-key.txt
Public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p

터미널에 즉시 공개키를 알려준다. 그리고 age-key.txt가 생성되는데 이는 비밀키이다. 외부에 노출되면 안 된다. age-key.txt의 형태는 아래와 같다.

# created: 2024-04-14T13:15:05+09:00
# public key: age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p
AGE-SECRET-KEY-...

공개키도 주석 처리되어 함께 적혀있다.

그러면, 위에서 봤던 chezmoi 설정 파일인 ~/.config/chezmoi/chezmoi.toml 에서도 chezmoi에게 age를 통해 암호화하겠다고 명시해 주어야 한다.

~/.config/chezmoi/chezmoi.toml

encryption = "age"
[age]
    identity = "/home/pozafly/age-key.txt"
    recipient = "age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p"
  • identity : 앞으로 암호화하고 복호화 할 비밀키의 경로.
  • recipient : 공개키

이러면 이제 $ chezmoi add --encrypt [파일명] 을 통해 암호화할 파일을 chezmoi에게 알려주어 관리할 수 있다. 암호화 한 파일은 아래와 같이 생겼다.

-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBGNHcyMW41dXFaVjlEVGV4
Y3MxT0RTT0hxUUw4ekN6eW1JYTMxR2RaWEdRCnZPek9WVnp4R0RVQ0FUemhYQW85
NVNkVnJud0ttQ2Z4L1ZaYkFYQzBXam8KLS0tIHVrdlZJWmYzVzJhTGxzWE1OUUhE
M08zUDFWRW9Bb1FiRVpTVEFxazlwc0UK9kfzYOoKK+75/heyQp2x4uzkH8y0w7CA
qJYb4g+muRjhrRL29YpbTNIZy+U/RBa+3qAMP47+DX36TtFj0pQCJSLpKDOqSrpz
VA3Jfriu01udMoTR5dWEKswp
-----END AGE ENCRYPTED FILE-----

AWS pem 키나, GitHub의 ssh 키 등을 --encrypt 옵션을 통해 암호화할 수 있다. 이렇게 암호화된 파일은 GitHub의 public repository에 올라가도 상관없다. 비밀키는 내가 갖고 있기 때문이다. 따라서 비밀키를 안전하게 들고 있는 것이 중요하다.

mac을 백업하기 전 반드시 비밀키를 옮겨야 하는데 나는 iCloud Drive 깊숙한 곳에 미리 저장해두었다. 다시 복호화 해서 실제로 필요한 경우에는 chezmoi apply 명령어를 사용하면 저장했던 경로로 복호화 된 파일이 나타날 것이다.

ssh key뿐 아니라 일반 text 파일도 가능하니, 암호화를 원한다면 age, chezmoi를 이용해 보자.


.macos

3번째 스크립트 파일에는 아래와 같은 명령어도 있다.

sh {{ .chezmoi.sourceDir }}/.macos

.macos 쉘 스크립트를 실행하라는 명령어다. 아래와 같이 생겼다.

#!/usr/bin/env bash

osascript -e 'tell application "System Preferences" to quit'

# 독 설정
# 독 아이콘 크기 45
defaults write com.apple.dock "tilesize" -int "45"

# Finder 설정
# 파일 확장자 모두 표시
defaults write NSGlobalDomain "AppleShowAllExtensions" -bool "true"
# 하단 위치 표시줄 표시
defaults write com.apple.finder "ShowPathbar" -bool "true"
// ...

# 트랙패트 설정
# 트랙패드 스피드 (가장 빠르게)
defaults write -g com.apple.trackpad.scaling 3
// ...

# 키보드 설정
# 자동으로 대문자 시작안함
defaults write NSGlobalDomain NSAutomaticCapitalizationEnabled -bool false
# 자동 수정 안함
defaults write NSGlobalDomain NSAutomaticSpellingCorrectionEnabled -bool false
// ...

# 메뉴바
# 배터리 잔량 표시
defaults write com.apple.menuextra.battery ShowPercent -int 1
# spotlight 메뉴바에서 감추기
defaults -currentHost write com.apple.Spotlight MenuItemHidden -int 1
// ...

killall Dock
killall Finder
killall SystemUIServer
killall ControlCenter

위의 내용은 간추린 버전이고, 풀버전은 이곳이다.

osascript는 alfred의 워크플로우를 개발할 때 사용해 봤었다. apple script이다. defaults 로 시작하는 명령어는 맥의 설정을 cli로 조작할 수 있다. 맥을 처음부터 세팅하려면 무척이나 귀찮은 작업이다.

defaults 명령어를 모아둔 macOS defaults라는 곳을 많이 참고했다. 하지만 꼭 적용하고 싶지만 없는 명령어도 있으니 참고해야 한다. 더 많은 defaults 관련 DB는 real-world-systems에서 찾아볼 수 있을 것이다.

참고로 마지막 줄의 killall 명령어는 defaults로 맥의 설정을 변경시켰지만 아직 적용되지 않은 구간을 재부팅 시켜줘서 적용할 수 있게 만들어준다.


plist 관리

마지막으로 plist에 대해 알아보자. 어플리케이션을 이용하면 자기 입맛대로 설정을 하기 마련이다. 대표적으로 iTerms2을 다시 설치하면 이전 설정 그대로 쓰기 위해 다시 세팅하는 과정을 반드시 거친다.

plist란, Property List로, 주로 macOS와 iOS 시스템에서 사용되는 설정 파일 형식이다. 애플리케이션의 설정, 사용자 환경 설정, 시스템 구성 정보 등을 저장하는 데 사용된다. plist 파일은 바이너리 파일로 작성되어 있는데, 일련의 과정을 거치면 XML 형식으로 볼 수 있다.

iTerms2의 plist의 형태를 한번 보자.

iTerms2-plist

이렇게 생겼다. plist는 맥에서 주로 ~/Library/Preperences 경로에 있다. iTerm2는 com.googlecode.iterm2.plist 라는 이름을 가지고 있다. 이 설정 파일을 chezmoi로부터 복원하면 어플리케이션 설정이 그대로 복원된다.

하지만 맥 설정인 com.apple... 로 시작하는 설정 파일은 복원했을 경우 그대로 적용이 되지 않는 경우도 있다. 나 같은 경우 단축키 활용을 많이 하는 편인데 com.apple.symbolichotkeys.plist 를 그대로 복원해도 단축키가 돌아오지 않았다. 구글링한 결과 애플 측에서 UI를 통해 명시적으로 설정하는 것을 정책으로 삼아두었다는 이야기가 있다.

단, plist는 chezmoi diff 명령어로 변경된 것을 찾을 때, diff 결과에 무척이나 자주 뜰 것이다. 어플리케이션의 마지막 창 위치를 가지고 있는 plist도 있고, update 체크 날짜를 plist에서 관리하는 어플리케이션도 있기 때문이다.

나 같은 경우는 chezmoi - Handle configuration files which are externally modified을 보고 여러 번 따라 해봐도 제대로 적용되지가 않았다.

그래서 plist를 ignore 처리해두고 쉘 스크립트로 원할 때 백업(로컬에 저장된 plist를 chezmoi 관리 파일에 덮어씀) 하고, run_once_before… 스크립트로 맨 처음 mac에 설치할 때 복원하는 방법을 선택했다.

백업 스크립트

#!/bin/bash

# plist 관리 (백업)

# .chezmoiignore 파일의 내용을 백업합니다.
cp ~/.local/share/chezmoi/.chezmoiignore ~/.local/share/chezmoi/.chezmoiignore.backup

# .chezmoiignore 파일의 내용을 비웁니다.
> ~/.local/share/chezmoi/.chezmoiignore

# /private_Library/private_Preferences 에 존재하는 plist 파일 이름을 가져와서
for file in ~/.local/share/chezmoi/private_Library/private_Preferences/*.plist; do
    # 파일이 심볼릭 링크인지 확인하고, 심볼릭 링크라면 건너뜀
    if [ -L "$file" ]; then
        continue
    fi

    # 파일 이름 추출
    filename=$(basename "$file")

    new_filename=$(echo "$filename" | sed 's/^private_//')
    echo 'new_filename' $new_filename

    # 파일을 /Library/Preferences로 복사
    chezmoi add "~/Library/Preferences/$new_filename"
done

# .chezmoiignore 파일을 원래 상태로 되돌립니다.
cp ~/.local/share/chezmoi/.chezmoiignore.backup ~/.local/share/chezmoi/.chezmoiignore

# 백업 파일을 삭제합니다.
rm ~/.local/share/chezmoi/.chezmoiignore.backup

printf '\nplist backup Done!!\n'

.chezmoiignore 파일에 경로를 등록해두면 경로 하위의 파일들은 chezmoi diff, add, apply 등의 명령어에 적용되지 않기 때문에 잠시 ignore 파일을 .chezmoiignore.backup 파일로 변경시켜 둔 뒤 chezmoi add 명령어를 통해 chezmoi에 등록하고 다시 .chezmoiignore.backup 파일을 .chezmoiignore 로 원상복구 시킨다.

복원 스크립트

#!/bin/bash

# plist 관리

# /private_Library/private_Preferences 에 존재하는 plist 파일 이름을 가져와서
# /Library/Preferences로 덮어씀(복사).
for file in {{ .chezmoi.sourceDir }}/private_Library/private_Preferences/*.plist; do
    # 파일이 심볼릭 링크인지 확인하고, 심볼릭 링크라면 건너뜀
    if [ -L "$file" ]; then
        continue
    fi

    # 파일 이름 추출
    filename=$(basename "$file")

    new_filename=$(echo "$filename" | sed 's/^private_//')
    echo 'new_filename' $new_filename

    # 파일을 /Library/Preferences로 복사
    cp "$file" {{ .chezmoi.homeDir }}"/Library/Preferences/$new_filename"
done

printf '\nbefore install package 02 Done!!\n'

managed-plists

이런 방식으로 chezmoi source 디렉토리에 저장된다. 하지만 자세히 보면 private_ 라는 접두사가 붙어있는 것을 볼 수 있는데, 해당 파일이 민감한 정보를 포함하고 있어 보안을 강화하기 위한 경우에 자동으로 붙는다.

하지만 이 파일들은 /Library/Preferences 경로에 원래 plist로 존재할 때는 private_ 접두사가 붙어버리면 제대로 적용이 되지 않기 때문에 없애주는 것도 스크립트에 포함되어 있다.


마치며

rm -rf ~ 라는 명령어로 시작해 plist까지 맥 운영체제가 어떤 방식으로 설정 파일들을 관리하고 있는지 알아봤다. 맥 OS가 darwin 운영체제 위에서 돌아가는 것과, 모든 어플리케이션을 설정할 때 설정값들을 컴퓨터의 파일에 저장해두고 구동 시 설정 파일을 읽어 적용하는 사실을 알게 되었다.

프로그램의 매직은 사실 뒤에서 돌아가고 있는 것이 이렇게 많다는 것을 알 수 있는 시간이었다. 그리고 컴퓨터는 단지 연산과 I/O 작업만 하는 기계라는 생각도 동시에 든다.

그리고 사실 chezmoi의 공식 문서는 생각보다 읽기 어려웠다. 삽질을 많이 했고 템플릿에 적응하기도 쉽지 않았다. 특히 plist를 정석대로 관리하고, chezmoi diff 명령어에 걸리지 않게 하고 싶어 무척이나 많은 시간을 날렸다. 결국 우회한 방법을 사용하긴 했지만.. 물론 chezmoi를 가볍게 사용하려면 $ chezmoi add, $ chezmoi apply 정도만 알아도 무난하게 사용할 수 있을 것이다.

chezmoi를 사용하면서 회사에서 신규 입사자가 오면 chezmoi를 통해 필수 어플리케이션을 설치하고, 설정해 주면 시간을 아끼고 체크해야 하는 시간을 줄일 수 있겠다는 생각을 했다. 특히 Brewfile을 잘 이용하면 좋겠다고 생각했다.


참고