git 管理している dotfiles をインストールするスクリプトを書いてみた

gintenlabo.hatenablog.com

の続き。


課題だったインストールスクリプトを書いてみました。

#!/usr/bin/env bash
# file: install-script.bash
set -ueo pipefail
cd "$(dirname "$0")"

CMDNAME=$(basename "$0")
print_usage() {
  cat - << EOF
usage: ${CMDNAME} (-n|-x) [options...]
    -n
        Executes dry run mode; don't actually do anything, just show what will be done.
    -x
        Executes install. This option must be specified if you want to install.
    -S <suffix>
        Specify backup suffix.
        If not given, BACKUP_SUFFIX is used; neither is given, '~' is used.
        This argument cannot be empty string.
EOF
}

MODE=
BACKUP_SUFFIX=${BACKUP_SUFFIX:-\~}

quote_each_args() {
  for i in $(seq 1 $#); do
    if [[ $i -lt $# ]]; then
      printf '%q ' "${!i}"
    else
      printf '%q' "${!i}"
    fi
  done
}
print_dry_run_message() {
  echo -e "will exec '$*'"
}
print_executing_message() {
  echo -e "executing '$*'..."
}
run() {
  if [[ "${MODE}" == 'dry-run' ]]; then
    print_dry_run_message "$(quote_each_args "$@")"
  else
    print_executing_message "$(quote_each_args "$@")"
    "$@"
    echo 'done.'
  fi
}

while getopts 'nxoS:u:m:' opt; do
  case $opt in
    n) MODE='dry-run' ;;
    x) MODE='execute' ;;
    S) BACKUP_SUFFIX="$OPTARG" ;;
    *) print_usage >&2
       exit 1 ;;
  esac
done
if [[ -z "${MODE}" || -z "${BACKUP_SUFFIX}" ]]; then
  print_usage >&2
  exit 1
fi

# init submodules
run git submodule update --init

# create symbolic links
echo
./install-script-tools/ls-linking-files.bash | while read -r file; do
  run ln -srvb -S "${BACKUP_SUFFIX}" -T "${file}" "${HOME}/.${file}"
done

# ここに残った初期化処理を書く( .gitconfig.local ファイルの作成とか vim の設定とか)
# 今回は省略
#!/usr/bin/env bash
# file: install-script-tools/ls-linking-files.bash
set -ueo pipefail

cd "$(dirname "$0")/.." # move to project root

LINK_IGNORE=${LINK_IGNORE:-.linkignore}

CMDNAME=$(basename "$0")
print_usage() {
  cat - << EOF
usage: ${CMDNAME} [options...]
    -v
        Verbose mode; print tracked files and ignored files.
    -h
        Print this message and exit.
EOF
}

VERBOSE=

while getopts 'vh' opt; do
  case $opt in
    v) VERBOSE='on' ;;
    h) print_usage
       exit ;;
    *) print_usage >&2
       exit 1 ;;
  esac
done

remove_directory_contents() {
  cat - | sed 's/\/.*//g' | sort -u
}

TRACKED_FILES=$(git ls-files -c | grep -v '^\.')
if [[ -n "${VERBOSE}" ]]; then
  echo "tracked files:" >&2
  echo -e "${TRACKED_FILES}\n" >&2
fi

IGNORED_FILES=$(git ls-files -ic --exclude-from="${LINK_IGNORE}")
if [[ -n "${VERBOSE}" ]]; then
  echo "ignored files:" >&2
  echo -e "${IGNORED_FILES}\n" >&2
fi

echo "${TRACKED_FILES}" | grep -vxFf <(echo "${IGNORED_FILES}") | remove_directory_contents
# file: .linkignore
# install-script でリンクさせたくないファイル/ディレクトリをここに置く
# 形式は .gitignore と一緒
# サブディレクトリ内のファイルは関係ないので、誤解なきよう原則として / 始まりで指定すること
/README*
/install-script*


使い方:

$ ./install-script.bash -n

と入力すると、実際に実行されるコマンドが表示される(実行はされない)。
問題ないようなら

$ ./install-script.bash -x

で実行。
この際、リンクされるファイルが既に存在していた場合にバックアップが(~/.zshrc~のような名前で)作られるので、問題なくインストール出来ているようなら削除する。
また、バックアップから復元したい場合のために、以下のようなスクリプトも書いた:

#!/usr/bin/env bash
# file: install-script-tools/restore-dotfiles-from-backup.bash
set -ueo pipefail
WORKDIR=$(pwd)
cd "$(dirname "$0")"

CMDNAME=$(basename "$0")
print_usage() {
  cat - << EOF
usage: ${CMDNAME} (-n|-x) [options...] [files...]
    -n
        Executes dry run mode; don't actually do anything, just show what will be done.
    -x
        Executes restoration. This option must be specified if you want to restore.
    -d
        Deletes given file if no backup found.
    -S <suffix>
        Specify backup suffix.
        If not given, BACKUP_SUFFIX is used; neither is given, '~' is used.
        This argument cannot be empty string.
    files
        Specify file paths to restore.
        If not given, files would be linked by install-script and ~/.gitconfig.local is restored.
EOF
}

MODE=
DELETE=
BACKUP_SUFFIX=${BACKUP_SUFFIX:-\~}

quote_each_args() {
  for i in $(seq 1 $#); do
    if [[ $i -lt $# ]]; then
      printf '%q ' "${!i}"
    else
      printf '%q' "${!i}"
    fi
  done
}
print_dry_run_message() {
  echo -e "will exec '$*'"
}
print_executing_message() {
  echo -e "executing '$*'..."
}
run() {
  if [[ "${MODE}" == 'dry-run' ]]; then
    print_dry_run_message "$(quote_each_args "$@")"
  else
    print_executing_message "$(quote_each_args "$@")"
    "$@"
    echo 'done.'
  fi
}
restore() {
  for file in "$@"; do
    local backup="${file}${BACKUP_SUFFIX}"
    if [[ -e "${backup}" ]]; then
      run rm -f "${file}"
      run mv -T "${backup}" "${file}"
    elif [[ -n "${DELETE}" ]]; then
      run rm -f "${file}"
    fi
  done
}

while getopts 'nxS:d' opt; do
  case $opt in
    n) MODE='dry-run' ;;
    x) MODE='execute' ;;
    S) BACKUP_SUFFIX="$OPTARG" ;;
    d) DELETE='on' ;;
    *) print_usage >&2
       exit 1 ;;
  esac
done
if [[ -z "${MODE}" || -z "${BACKUP_SUFFIX}" ]]; then
  print_usage >&2
  exit 1
fi

shift $((OPTIND - 1))

if [[ $# -eq 0 ]]; then
  ./ls-linking-files.bash | while read -r filename; do
    restore "${HOME}/.${filename}"
  done
else
  (cd "${WORKDIR}" && restore "$@")
fi
$ ./install-script-tools/restore-dotfiles-from-backup.bash -x ~/.zshrc

のようにして復元できる(ファイル名指定を省略した場合はリンクされるもの全部が対象になる)。


注意点として、 Mac だと ln コマンドのオプションが違うので、このままでは使えない。
後で Mac 版も書くつもり(blog で公開するかどうかは置いといて)。


実際のコードはこちら:

github.com


参考にして頂ければ幸い。