A Modular Approach to Storing Home Directory Config Files (Dotfiles) in Git using Bash, Zsh, or Powershell
Categories: Dev
In this article, I offer an approach for managing dotfiles in a modular fashion. I find a modular approach important because only some config files are useful in all contexts, while others are unique to a specific environment. For instance, my text editor configuration (.vimrc, in my case) is used on my Windows laptop, Linux laptop, FreeBSD server, and even my phone. On the other hand, files for configuring a Linux graphical environment, a developer’s Macbook, Windows Subsystem for Linux (WSL), or Windows Powershell, may not make sense to clutter or confuse environments to which they do not apply.
It would be nice to use multiple Git repositories, or multiple branches of one Git repository, in order to customize various environments.
In the previous two articles, I outlined two slightly different strategies for tracking dotfiles:
- A simple approach that makes the entire home directory a git repo
- A bare repo approach that tracks the files in a separate directory, but still allows home to be the working directory
With either approach, one can think in a modular fashion. Admittedly, the second strategy offers a tad more flexibility, in that you could have two files in the same directory, but tracked by two different repositories or branches. But, in general, that flexibility requires a little more complexity. Let’s explore.
Modularize your config files
The first step to a layered or modular approach is to think in modules. Rather than imagining a single config file that changes per system, use that file to load other config files if they are present.
Here are some ideas, specific to various tools:
Unix shell configs
Shell configurations like .bashrc or .zsheenv or .profile can source files from other directories.
For instance, in .bashrc we might place something like the following:
for file in "$(find $HOME/.config/dotfiles -name 'bashrc')"; do . $file; done
This would load any or all of the following files if they exist:
~/.config/dotfiles/bashrc
~/.config/dotfiles/work/bashrc
~/.config/dotfiles/personal/bashrc
~/.config/dotfiles/wsl/bashrc
~/.config/dotfiles/linuxui/bashrcSSH configs
In .ssh/config, the Include keyword can be used like so
Include *.ssh/config
For example, this will include any of the following files, if they are available:
~/.ssh/personal.ssh/config
~/.ssh/work.ssh/config
~/.ssh/datacenter.ssh/config
Or a different Include line to find files in other directories:
Include ~/.config/dotfiles/*/*.ssh
This will include any files ending in .ssh (such as config.ssh) in any subdirectory of ~/.config/dotfiles.
And so on. Name the directories in ways that suit you.
Powershell
In Windows, the Powershell profile in ~\Documents\WindowsPowerShell\Microsoft.PowerShell_profile.ps1 is automatically loaded. Within that file, you could loop through files in a directory of your choice and load them:
Get-ChildItem -Recurse "$HOME\.config\dotfiles\*.ps1" | ForEach-Object {& $_.FullName}
For example, this will include the following files, if they are available:
~\.config\dotfiles\dc1\profile.ps1
~\.config\dotfiles\work\profile.ps1
~\.config\dotfiles\personal\extra.ps1Vim
With Vim or Neovim, you can place any vim script ending with .vim in ~/.vim/plugin of subdirectory thereof, and it will autoload. In Vim 8 and above, a more modern location is something like ~/.vim/pack/work/start or ~/.vim/pack/personal/start and so on.
You can also load multiple files from a directory of your choice, using the runtime command. For instance, add something like the following to ~/.vimrc:
runtime! ../.config/dotfiles/**/*.vim
This will load any and all files ending with .vim in ~/.config/dotfiles or any subdirectory thereof.
Structure determined by Git method
The above are examples to demonstrate a theme: create base config files that simply load an arbitrary number of other files, in a directory of your choosing. Once this is done, individual files and directories of files can be added to Git repo(s).
Again, there are two different methods I find useful for managing dotfiles in Git: the basic approach and the bare repo approach.
The method you choose will determine best practices for organizing the files in a modular fashion.
The bare repo approach modularizes by tracking files one “layer” per git directory, which exists apart from the actual home directory. This allows you to have multiple tracked files in the same directory, but with each file being tracked in a different git directory (repository). For instance, imagine that we have two layers: base and personal, with corresponding git directories in ~/.dotfiles/base and ~/.dotfiles/personal. We want to track two corresponding Vim configurations: ~/.vim/plugin/base.vim and ~/.vim/plugin/personal.vim. Even though both files live in the same directory, we can track them separately like this:
git --git-dir=$HOME/.dotfiles/base --work-tree=$HOME add ~/.vim/plugin/base.vim
git --git-dir=$HOME/.dotfiles/personal --work-tree=$HOME add ~/.vim/plugin/personal.vim
(We will discuss convenience functions a bit later to make this easier and more succinct.)
The basic approach, on the other hand, requires you to think through your directory and file structure a bit more carefully, because files are tracked in entirely separate git directories. But that careful organization pays off with simplicity: cd into the module directory, then use git as you like, no extra command line options needed. For instance, imagine that we have two modules: base and personal, with base being our main repo in $HOME and personal having an additional person Vim config in ~/.config/dotfiles/personal/personal.vim. Initiating the tracking could look something like this:
cd ~
git add ~/.vimrc
cd ~/.config/dotfiles/personal
git add personal.vimBranches or repositories?
Each “module” in its own repo
Branching
Modular according to git strategy
I hope this offers you some ideas and inspiration for your own configurations.