Skip to content

Instantly share code, notes, and snippets.

@hidakatsuya
Last active October 8, 2020 16:09
Show Gist options
  • Save hidakatsuya/6e22264049eb60c23749d5691225a50d to your computer and use it in GitHub Desktop.
Save hidakatsuya/6e22264049eb60c23749d5691225a50d to your computer and use it in GitHub Desktop.
Simple and Cusomizable Dotfile Management #dotfile

Postscript) I have published the management tool introduced in this article as a rubygem named flexdot.


Simple Dotfile Manager

Simple and customizable dotfile manager.

Getting Started

Prerequisite

Ruby 2.4 or higher

Installing

Create the following directory structure:

$HOME/
  dotfiles/
    Rakefile

Then, run rake -T in the $HOME/dotfiles directory and check that the output is as follows:

$ rake -T
rake clear_backups

Usage

See Example.md

Introduction

This example is my actual dotfile environment.

I have two working environments, macOS and ubuntu. The dotfiles in these environments are slightly different, so each dotfile is separated.

Directory Structure

$HOME/dotfiles
├── common
│   ├── bin
│   │   ├── git-delete-other-branches
│   │   └── git-reset-and-clean
│   ├── git
│   │   └── ignore
│   ├── rubygems
│   │   └── .gemrc
│   └── vim
│       └── .vimrc
├── macOS
│   ├── bash
│   │   ├── .bash_profile
│   │   └── .bashrc
│   ├── git
│   │   └── .gitconfig
│   ├── karabiner
│   │   └── tab-emulation.json
│   └── vscode
│       ├── keybindings.json
│       └── settings.json
├── ubuntu
│   ├── bash
│   │   └── .bashrc
│   ├── bin
│   │   ├── upgrade-ghcli
│   │   ├── utils
│   │   ├── x-copy
│   │   └── x-open
│   ├── git
│   │   └── .gitconfig
│   ├── vscode
│   │   ├── keybindings.json
│   │   └── settings.json
│   └── xkeysnail
│       ├── config.py
│       ├── debug.sh
│       ├── restart.sh
│       ├── start.sh
│       └── stop.sh
├── macOS.yml
├── ubuntu.yml
└── Rakefile

Installation Commands

When you run the rake -T command in that directory structure, you should have two installation commands available:

$ rake -T
rake clear_backups   # Clear all backups
rake install:macOS   # Install dotfiles for macOS
rake install:ubuntu  # Install dotfiles for ubuntu

Dotfile Rule File

macOS.yml and ubuntu.yml are for setting the link destination of dotfile. dotfile will be installed according to its setting.

macOS.yml

For example, common -> bin -> git-delete-other-branchs is $HOME/dotfiles/common/bin/git-delete-other-branches. And the value bin means $HOME/bin directory.

So this defines linking $HOME/dotfiles/common/bin/git-delete-other-branches to $HOME/bin/git-delete-other-branches.

See macOS.yml

ubuntu.yml

See ubuntu.yml

Install

See Installing

already_linked means skipped because bin/git-delete-other-branches is already linked. link_created means the link was created. Also, (backup) means that a file exists in the link path and that file was backed up to $HOME/dotfiles/backup/YYYYMMDDHHIISS/filename.

Misc

You can clear all backups in $HOME/dotfiles/backup/YYYYMMDDHHIISS to run rake clear_backups.

$ rake clear_backups
$ rake install:macOS
[already_linked] bin/git-delete-other-branches
[already_linked] bin/git-reset-and-clean
[already_linked] .config/git/ignore
[already_linked] .gemrc
[already_linked] .vimrc
[link_created] .bash_profile (backup)
[link_created] .bashrc (backup)
[link_created] .gitconfig (backup)
[link_created] .config/karabiner/assets/complex_modifications/tab-emulation.json (backup)
[link_created] Library/Application Support/Code/User/keybindings.json (backup)
[link_created] Library/Application Support/Code/User/settings.json (backup)
common:
bin:
git-delete-other-branches: bin
git-reset-and-clean: bin
git:
ignore: .config/git
rubygems:
.gemrc: .
vim:
.vimrc: .
macOS:
bash:
.bash_profile: .
.bashrc: .
git:
.gitconfig: .
karabiner:
tab-emulation.json: .config/karabiner/assets/complex_modifications
vscode:
keybindings.json: Library/Application Support/Code/User
settings.json: Library/Application Support/Code/User
# frozen_string_literal: true
require 'pathname'
require 'fileutils'
require 'forwardable'
require 'yaml'
desc 'Clear all backups'
task :clear_backups do
Backup.clear_all
end
namespace :install do
Pathname.glob('*.yml') do |index_file|
name = index_file.basename('.*')
desc "Install dotfiles for #{name}"
task name do
Installer.new(name).install(index_file)
end
end
end
class Installer
def initialize(name, target_dir: nil)
@name = name
@base_dir = Pathname.pwd
@target_dir = target_dir || base_dir.join('..')
@backup = Backup.new
@logger = Logger.new(@target_dir)
end
def install(index_file)
index = Index.new(YAML.load_file(index_file.to_path))
index.each do |dotfile_path:, target_path:|
install_link(dotfile_path, target_path)
end
end
private
attr_reader :name, :base_dir, :target_dir, :backup, :logger
def install_link(dotfile_path, target_path)
dotfile = @base_dir.join(dotfile_path).expand_path
target_file = @target_dir.join(target_path, dotfile.basename).expand_path
logger.log(target_file) do |status|
if target_file.symlink?
status.result = :already_linked
else
if target_file.exist?
backup.call(target_file)
status.backuped = true
end
target_file.make_symlink(dotfile.to_path)
status.result = :link_created
end
end
end
end
class Logger
Status = Struct.new(:target_file, :result, :backuped)
def initialize(target_dir)
@target_dir = target_dir
end
def log(target_file)
status = Status.new(target_file)
yield(status)
puts message_for(status)
end
private
attr_reader :target_dir
def message_for(status)
[].tap { |msg|
msg << "[#{status.result}]"
msg << status.target_file.relative_path_from(target_dir)
msg << '(backup)' if status.backuped
}.join(' ')
end
end
class Index
def initialize(index)
@index = index
end
def each(&block)
index.each do |root, descendants|
fetch_descendants(descendants, paths: [root], &block)
end
end
private
attr_reader :index
def fetch_descendants(descendants, paths:, &block)
descendants.each do |k, v|
dotfile_path = paths + [k]
case v
when String
block.call(dotfile_path: File.join(*dotfile_path), target_path: v)
when Hash
fetch_descendants(v, paths: dotfile_path, &block)
else
raise ArgumentError, v
end
end
end
end
class Backup
BASE_DIR = 'backup'
class << self
def clear_all
base_dir.glob('*').each(&:rmtree)
end
def base_dir
Pathname.pwd.join(BASE_DIR)
end
end
def initialize
backup_dir.mkpath unless backup_dir.exist?
end
def call(file)
FileUtils.mv(file, backup_dir)
end
private
def backup_dir
@backup_dir ||= self.class.base_dir.join(Time.now.strftime('%Y%m%d%H%M%S'))
end
end
common:
bin:
git-delete-other-branchs: bin
git-reset-and-clean: bin
git:
ignore: .config/git
rubygems:
.gemrc: .
vim:
.vimrc: .
ubuntu:
bash:
.bashrc: .
bin:
upgrade-ghcli: bin
utils: bin
x-copy: bin
x-open: bin
git:
.gitconfig: .
vscode:
keybindings.json: .config/Code/User
settings.json: .config/Code/User
xkeysnail:
config.py: .xkeysnail
debug.sh: .xkeysnail
restart.sh: .xkeysnail
start.sh: .xkeysnail
stop.sh: .xkeysnail
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment