Intro to Dotfiles

2,642 阅读10分钟

Keep Knowledge in Plain Text

The Pragmatic Programmer 一书发行于 1999 年,但其中的大多数观点放在今天来看也仍不过时。作者在「纯文本的威力」一节从什么是纯文本开始聊起,引出了将知识作为纯文本保存的观点。接着又从「防备过时」、「利用杠杆效应让已有工具发挥最大优势」和「易于测试」三个角度阐述了纯文本的优势,提出不管互联网如何变化,纯文本一定是所有信息载体的最小公分母,能够成为互相沟通的标准。

What is dotfiles

UN*X 系统大多数工具都是以纯文本的形式配置,文件名以 . 开头,存储在用户的 home 目录中(为了保持 Home 目录的整洁,现在越来越多的程序都开始使用 XDG 规范指定的路径代替 home 目录,默认为 ~/.config),所以这些文件也被统称为 dotfiles。

Why you should build your own dotfiles

优秀的工匠通常有自己专属的工具箱,集合了自己打磨多年,用起来最为趁手的各类工具。dotfiles 就开发者的工具箱,里面有最符合自己品味和习惯的工具配置。对于经常需要在终端上花费大量时间的开发人员来说,管理和优化工具的配置是一项非常值得的投资,有时一些很简单的配置优化也能大幅提高你的工作效率。

image.png

在 Github 上以 dotfiles 为关键词搜索得到的结果

那么在 Github 上共享自己 dotfiles 的意义是什么呢?我觉得最重要的一点是你可以从其他人优秀的配置中获得灵感,从而持续地迭代和改进自己的「工具箱」,同时把自己的配置分享出来,供其他人参考,有时可能还会收到一些改进意见。对我来说,dotfiles 是自己第一个真正意义上的开源项目。以此为起点,在持续的迭代和打磨中,我对 Shell 脚本,Linux 系统和 CLI 工具有了更深的了解,从 dotfiles 中孵化的几个工具也收获了一些 star。如果你想过更多地参与开源社区,但还没有找到一个好的切入点,不妨也从建立自己的 dotfiles 仓库开始,这会是你学习新工具,提升脚本水平,熟悉 CI / CD ,实践自动化的绝佳场所。

Always consider version control

dotfiles 在开发环境中的地位其实和注册中心在微服务架构中的地位是类似的,开发所需的每一个工具对应一个「微服务」。注册中心可以方便我们对微服务的配置进行集中管理和分发,dotfiles 也是一样。建立 dotfiles 仓库以后,你的配置升级和改动都应该通过 dotfiles 完成,然后就可以很容易地同步到其他开发环境,因此 dotfiles 很可能会成为你整个职业生涯从事时间最长的项目。出于这个原因,可维护性和可扩展性对 dotfiles 仓库来说也很重要,我们应该把它纳入版本控制管理之中。

将 dotfiles 纳入版本控制有很多好处,比如你可以放心地尝试新配置而不用担心出问题的时候无法回滚,也能让代码和配置保持干净整洁,你可以随时通过 VCS 查看很久以前的文件内容,而不用经常保留大段大段的老旧代码不舍得删除🐶。

具体的选择上,可以使用一些专门为管理 dotfiles 而设计的工具,比如 rcmyadm 等,这些工具需要额外安装,除了 git 本身的能力以外,所做的事情主要就是拷贝或者链接配置文件到指定位置。我个人更喜欢使用 git 配合一些简单的 setup 脚本来代替这些工具,好处是避免了额外的依赖,当你配置一个新系统的时候需要做的事情更少;另一个好处是,理论上你可以在 setup 脚本里面完成任何事情,这使得我们用非常灵活的方式配置每一个工具。

使用 git 追踪配置文件也有两种选择,你可以将仓库建立在整个 home 目录(或 ~/.config)之上,并通过 gitignore 来忽略非配置文件;也可以将需要追踪的配置文件保存到一个独立的目录中,并通过复制或者软链接的方式将其部署到各自的位置。两种方案都有不少支持者,但我觉得后一种方式要好很多,主要有这么几个原因:

  • 避免意外删除文件的风险。如果将整个 home 目录或 ~/.config 目录纳入 git 的版本控制,运行 git clean 等操作的时候,很可能会不小心删除掉所有未被追踪的文件。

  • 可以追踪不在 home 或者 ~/.config 中的配置文件。

  • 新机器上的安装和配置更简单。

Repository layout

相信大家肯定已经有一些配置文件了,比如 .zshrc ,.vimrc ,.gitconfig等。初始化一个 dotfiles 仓库很简单,新建一个文件夹(建议在 home 目录),然后把所有这些配置文件移动到 dotfiles 里面,然后用 git 提交,完成。

安装通常就是把 dotfiles 中的文件软链接到实际位置,比如:

ln -sf ~/dotfiles/vimrc ~/.vimrc

你可以在每次需要的时候运行这些命令来安装,但我建议你把这些命令写到一个 install 脚本中,避免每次安装的时候回忆或者搜索每个配置文件应该链接到哪个位置。

下面是一个 dotfiles 仓库的布局:

❯ tree dotfiles
dotfiles
├── archlinux
│  ├── mirrorlist
│  ├── pacman.conf
│  ├── paru.conf
│  ├── sddm.conf
│  └── setup.sh
├── osx
│  └── setup.sh
├── brew
│  ├── Brewfile.darwin
│  ├── Brewfile.linux
│  └── setup.sh
├── git
│  └── setup.sh
├── tmux
│  ├── setup.sh
│  ├── tmux.conf
│  └── tmux_osx.conf
├── vim
│  ├── nvim
│  ├── snippets
│  ├── spell
│  ├── ideavimrc
│  ├── setup.sh
│  └── vimrc
├── zsh
│  ├── sheldon
│  ├── setup.sh
│  ├── starship.toml
│  ├── zshenv
│  └── zshrc
└── install

安装的时候,只需要在根目录下执行 ./install <module>,install脚本会进入 module 对应的文件夹中运行setup.sh。每当你配置好了一个新工具,就可以在 dotfiles 中建立一个子目录,把配置文件移动到里面,然后把软链接命令记录在 setup.sh中再运行一下就可以了。

下面是 install 脚本的一个样例:

#!/usr/bin/env bash

# get dir of the current script and cd in it, so you can run the script properly wihtout enter into the root dir
SDIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) && cd "$SDIR" || exit 1

# show usage
usage() { echo "Usage: $(basename "$0") <module> [module]..." >&2; }

# simple log printer
loginfo() { printf "%b[info]%b %s\n" '\e[0;32m\033[1m' '\e[0m' "$@" >&2; }
logerro() { printf "%b[erro]%b %s\n" '\e[0;31m\033[1m' '\e[0m' "$@" >&2; }

[[ "$#" -lt 1 ]] && usage && exit 1

# install a module
install_module() {
    local module="$1"
    loginfo "install $module config ..."
    [[ ! -f "$module/setup.sh" ]] && logerro "$module config not found!" && return 2
    
    "$module/setup.sh" && loginfo "$module config installed successfully!"
}

# install every specified modules
for module in "$@"; do
    install_module "${module%/}"
done

那么每个 setup.sh是什么样子呢,这个其实根据每个要配置的目标确定。比如对于 zsh,可能就是几行简单的软链接命令:

#!/usr/bin/env bash

SDIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)

ln -sf "$SDIR/zshrc"  ~/.zshrc
ln -sf "$SDIR/zshenv" ~/.zshenv

# change the default shell to zsh (if necessary)
[[ "$SHELL" =~ "zsh" ]] || chsh -s "$(command -v zsh)"

Not only dotfiles

Dotfiles 仓库可以管理的内容其实并不局限于类似zshrc,vimrc这样的配置文件,你可以在setup.sh中记录任何可以通过命令完成的操作。举个例子,大家肯定都在用 homebrew 包管理器。当我们拿到一个新 Mac 的时候,需要先去官网查看安装命令,然后一边回忆一边手动安装每一个需要用到的工具,有些记不清名字可能还要 google 一番。但 brew 其实有个命令brew bundle dump可以生成一个 Brewfile 文件,这个文件记录了系统所有已安装的包。你要做的就是把这个文件放到到你的 dotfiles 当中,并配上一个setup.sh脚本:

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

SDIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) && cd "$SDIR" || exit 1

# install homebrew if not installed
if ! hash brew &>/dev/null; then
    bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
fi

# install all the packages in Brewfile
if [[ $OSTYPE =~ darwin ]]; then
    brew bundle install --file Brewfile.darwin
elif [[ -x /home/linuxbrew/.linuxbrew/bin/brew ]]; then
    eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
    brew bundle install --file Brewfile.linux
fi

Homebrew 很早就支持 Linux 了,开发机上 Debian 包管理器软件源太老,比较新的工具基本都没有,建议使用 brew 来获得和 mac 一致的体验。

以后不管你是在 Linux 开发机上,还是在全新的 Mac 环境上,都可以通过运行./install brew把所有的工具给安装好。以 go sdk 为例,如果你想安装最新的版本,只需要一行命令 brew install go,如果想切换到一个老版本,也只需要 3 行命令:

brew install go@1.16 # install go 1.16
brew unlink go       # unlink current go
brew link go@1.16    # link go@1.16 as default go

即使是 GUI 程序,你也应该优先考虑使用 brew 来安装,而不是去 AppStore 搜索,或者去官网手动下载。因为使用brew能很方便地进行更新和卸载,还能通过Brewfile快速恢复安装。Homebrew 的 Cask 仓库收录了很多 GUI 软件,我们常用的像 chromejetbrains-toolbox微信网易云音乐等仓库都有收录,安装时只需要加上一个--cask选项即可(同样可以被brew bundle dump记录下来):

brew install --cask wechat # 安装桌面微信

Dockerize

有些时候我们临时需要在某些机器上工作,如果想使用自己熟悉的工作环境,就得一遍又一遍地安装软件包,配置 dotfiles,工作结束的时候可能还得清理这些痕迹,比较麻烦。提供了 OS 级别文件隔离能力的 Docker 可以帮助我们解决这些问题,你可以将工作环境所需的包以及配置文件,连同操作系统一起打包成镜像,然后直接在其他机器上以容器的方式运行。「懒惰是程序员的美德」,打包和更新镜像的工作我们可以交给 CI 来做,就像这样:

github.com/wfxr/dotfil…

有了自己的 dotfiles 镜像,即使在一穷二白的新机器上,我们只要这样一条命令就可以得到自己熟悉的开发环境:

docker run -v <local-dir>:/root/work -it wfxr/dotfiles zsh

Best Practices

  • 选择支持纯文本配置的软件和工具。

追求纯文本配置不是为了看起来极客,而是因为纯文本易于阅读和测试,和版本控制工具配合的更好,方便自动化,并且在 server 环境中也可以使用。在 Linux 中,如果你使用 AwesomeWM 或者 i3WM 这类支持纯文本配置的桌面管理器,重装系统之后只要链接一下配置目录,系统桌面,启动项以及快捷键绑定等立马就能恢复成你习惯的状态。相反,如果是 Gnome 或者 KDE 这样重度依赖 GUI 来进行配置的桌面,就需要经过一番冗长的设置之后才能开始高效率地工作。

  • 使用插件管理器,而不是手动安装。

zsh,fish,vim,tmux等都有各自的插件管理器,用它们来安装各类插件比自己手写要简单的多,更新起来也很方便。

  • 让安装脚本具备幂等性。

幂等性可以帮我们实现声明式的配置管理,你可以放心地多次执行而不用担心错误和冲突。举例来说,像mkdir foo 和 ln -s src dest这样的语句就是幂等的,重复执行会报错;用幂等的写法应该改成mkdir -p foo和ln -sf src dest。

  • 通过 .local 文件隔离本地配置

有时某些配置只对特定的环境才有意义(比如开发机上经常用到的代理配置),不需要把这些信息放在 dotfiles 仓库中同步到其他地方。我们可以把它记录在一个单独的 .local文件中,然后用类似source或者include语句引入到主配置文件中,像下面这样:

# filename: ~/.zshrc

# ... other configs

# load ~/.zsh.local if exists
[ -f ~/.zsh.local ] && source ~/.zsh.local
# filename: ~/.zsh.local

# http proxy
# example: proxy git clone https://github.com...
proxy() {
    http_proxy=http://sys-proxy-rd-relay.byted.org:8118 \
        https_proxy=http://sys-proxy-rd-relay.byted.org:8118 \
        no_proxy=.byted.org \
        $*
}

# go proxy
go env -w GOPROXY="https://goproxy.byted.org|https://goproxy.cn|direct"
go env -w GOPRIVATE="*.byted.org,*.everphoto.cn,git.smartisan.com"
go env -w GOSUMDB="sum.golang.google.cn"

对于一些私密配置,如果有同步需求,但放在公开的 dotfiles 中又不安全,可以选择用 git-crypt 对仓库中指定的文件进行加密,从而可以安全地共享。git-crypt 在文件在提交的时候会自动加密,签出的时候会自动解密,整个过程是透明的,非常便捷。

Useful links