TL;DR Writing your own Ruby DSL language to sync and manage your public and private dotfiles
As number of computers I own increased over the years and I couldn’t bring myself to get rid of some of the older ones so I thought that it was time to step down and think about management of dotfiles in more centralized, intuitive and automated way, because old ways of just git cloning weren’t enough anymore. Now there are systems that are actually tailored over that particular task such as GNU Stow and some other tools however being a fairly lazy person I thought that writing my own would be a lot more easier and quicker than learning some tool that already existed. The benefits of writing your own tools are that you don’t have to learn them, they will do exactly what you want them to do and you could potentially design them as simple and straightforward as possible from the very start, not mentioning about learning some new technologies while doing something new. The requirements for this particular system were for it to be easily portable over Unix/Linux/BSD/MacOS systems, hassle-free to kick start and straightforward simple to use. I’ve decided to develop Ruby based DSL targeting 1.9.3 version of Ruby language as most of the operating systems either provide that version or a newer version and because I’ve already had experience developing Ruby based DSL to manage my ssh connections called sshc which proved to be very portable and easy to use. Note that I’ve also decided to not support Windows operating system as there isn’t much benefit or need of doing that as I don’t use it or know anyone who would want some dotfiles management system for it. The result of this endeavor is in my git repo called dotcentral however for learning experience I’m going to recreate it step by step for this blog post. Also note that this repo contains my own dotfiles too alongside Ruby DSL configuration files used to automatically install them. So let’s get started from scratch.
The system we are going to build is called central so let’s create a file called central with following two lines and make it an executable using chmod +x ./central. We don’t have to specify an .rb extension for it as it’s an shebang executable file. Also we require two standard Ruby modules erb and socket which we’ll use later in our functions.
1 2 3 4 5 |
|
This executable will be used to run specialized Ruby DSL files called configurations each describing some steps central script should take in order to install or configure some tool or dotfile. Configuration files are usually named as configuration.rb but could be named any way you desire. Common configurations are kept in common directory and private configurations are kept in private one but I also have each environment-specific configurations in their respective hostname based directories with their own configuration.rb files. Some applications with more complex dotfile structures like vim need more extensive additional steps so I put their configuration in their own directories to keep entire configuration structure simple and intuitive to navigate and manage. Central script executes top-level configuration.rb file which executes all the necessary sub-directory configuration.rb files but it can also be used with only specific configuration.rb files as an arguments to central script in which case only that particular configuration.rb files will be executed. Here is an top-level tree view of my own dotcentral repository containing my own dotfiles and configuration.rb files. Also note some dotfiles have an .erb extension which I’ll explain a little bit later.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
|
As we need to be aware of our environment or more precisely a hostname where the script is running let’s create a function called hostname. This function will be used in our configuration files. Also we might want to execute different scenarios based on which operating system we are running so let’s add an os function to easily determine the operating system name.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Next we need a way to run our configuration.rb files so we need to be aware of our working directory and be able to get absolute file path and file folder name. So let’s define functions for this.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
In order to run our configuration file we need to temporally chdir into it’s folder, load the Ruby file and then chdir back into previous working directory. Also in some cases we might want to optionally run configuration files only if they exists so let’s also define run_if_exists function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Now to run our configurations we are either going to execute the top-level one or the ones provided as arguments to central script.
1 2 3 4 5 |
|
Also since we need not only our common configuration but also our private one which is kept in a separate git repository we might as well add a capability to git clone/pull any repository we want from a configuration file. But before doing that let’s define a special function called sudo which will run any shell command with sudo prefix if command fails to run due to permission denied error. This works only if you have sudo configured without a password and your configuration needs to access some files/executables which need root user privileges but isn’t absolutely necessary if you access only your own files.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
|
Now let’s add git function to clone any repository we might require and pull any repository already cloned to keep everything up-to date.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Before using git we need to check if it’s actually installed in the system. We might as well add checks for some other tools like file, grep, curl and readlink as we’ll need them later in order to manage our configurations.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Now right about time for you to wonder how is this all used? Following example is my top-level configuration.rb file. First I git clone/pull my own private configuration repository. Next I run it’s configuration.rb file followed by running common configuration.rb file. And finally I run hostname based configuration only if it exists for current host.
1 2 3 4 |
|
Now let’s try adding a symlink capability to our DSL in order to install sshc which I use everywhere. So we need a function to manage symlinks and some minor functions to check if symlink exists and which path it points to. So if file/dir exists in place where we want to create a symlink the system will stop with exit 1 and stderr output will contain error description. Otherwise we check the symlink path and if it’s not valid one we delete it and create a symlink with specified new path.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
|
This is how it’s used in my common configuration.rb to install sshc and make a symlink to it’s executable in bin folder which I’ll add to environment PATH via bashrc later.
1 2 3 |
|
Let’s add some more functions to our Ruby DSL to mkdir directories, rm files/directories, touch files and chmod files/directories. You can easily add chown function too if you need it. We’ll need all these functions later in our configurations.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
|
We’ll also need curl function in order to download files.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Now that we have curl and chmod we can easily install ack via our configuration file.
1 2 3 |
|
Next we’ll need ls function in our Ruby DSL which will support multiple options via second Hash type argument such as filtering of file/directory names based on grep pattern and listing either only files or directories as well as including dotfiles if necessary. Quite complicated function indeed, however this is necessary because of how we usually need to use it in our configurations depending on whenever we need to list only files or directories or some specific files or directories based on some predefined pattern.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
With the help of ls now we can finally install Powerline fonts. The code below clones git repository and installs all .ttf and .otf symlinks into either ~/.fonts or ~/Library/Fonts directory based on which operating system you are on.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
At this point we need a way to manage some differences that might arise from different operating systems and deployment path inside our dotfiles itself. For this we need some kind of templating system which will allow us to include or dynamically generate files when processing configuration files. Fortunately Ruby has erb templating system so we don’t have to write our own. This will allow us to generate dotfiles on the fly as well as include content of some files into another ones.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
In order to include files we’ll need a small function to be able to easily read them.
1 2 3 4 5 6 7 8 9 |
|
This is for example how I use this function to centrally manage my tmux configuration in tmux.conf.erb file. It includes common tmux.conf into my host-specific tmux.conf.erb.
1 2 3 4 5 6 7 8 |
|
This tmux.conf.erb file is later processed from configuration.rb file resulting into tmux.conf file being generated.
1 2 |
|
And finally to manage shell configuration I want to source my bashrc into ~/.bashrc file in an automated way. To do this we’ll create a function called source. It’ll add source line to any shell configuration file if it isn’t already added there.
1 2 3 4 5 6 7 8 9 10 |
|
Now finally I can use this function to source my configuration specific bashrc file which is generated from bashrc.erb into ~/.bashrc
1 2 |
|
That’s it folks this how I manage my dotfiles mess. I hope you learned something new today. Have a good coding time!