Organize and auto back up your .zshrc files to Github

by Nitikesh Pattanayak
169 views
A+A-
Reset
Tai Duong
github.com/imbaggaarm/zshrc-auto-sync

It was a nice Sunday evening, while I was trying to install the Flutter SDK to start a new side project. I needed to add Flutter binary’s path to $PATH, instead of open the .zshrc file and add the line below:

export PATH="$PATH:`pwd`/flutter/bin"

I used this:

echo 'export PATH="$PATH:$HOME/development/flutter/bin"' > ~/.zshrc

Do you realize what is wrong with my command? If you don’t, please back up your file and try it yourself :)).

Or simply, you can just continue reading my post ?.

Here is what happened. I was supposed to use >> instead of > in the above command. Because if you use >> , it will append your text to the end of the file, otherwise, with >, it will overwrite the whole file. I used that command many times before, and yeah, it’s faster than opening the file and append a new line, I might save 3 seconds but ended up spending 3 hours fixing it.

After running that command, as usual, I used cat ~/.zshrcto see the content of .zshrc , and I got this:

export PATH="$PATH:$HOME/development/flutter/bin"

I tried using cat ~/.zshrc one more time, and opened the .zshrc file to see if there was any difference, but the moment I looked back at my echo command, you know, my face was exactly like this ?. I believe you understand what my feeling was at that point. Because if you are a software engineer, you must make at least one mistake like this before, accidentally delete some configuration files, or even worse, delete the whole production database.

I started googling to looking for any solution to recover my .zshrc , and tried some suggestions which I found, but none of them worked in my case. Then I started searching in my local machine to see if I have any backup for it, and fortunately, I found one, the file’s last modified date is 26 May 2021, but that was all I need.

I also was lucky because I didn’t store all of my configurations in ~/.zshrc. I have a .zsh directory with a structure like this in my machine:

.
├── aliases.zsh
├── cli.zsh
├── fp_kube.zsh
├── pablo.zsh
├── pandora.zsh

And in ~/.zshrc, I just need to add this line:

for config (~/.zsh/*.zsh) source $config

With this configuration, I can organize my environment variables easily, and my ~/.zshrc file will not become a mess in the future as I need to add more configurations. Please remember, don’t put all your eggs in one basket. So basically, my ~/.zshrc file just contains oh-my-zsh setup, $PATH, and some generic things, but losing it still costs me a lot of time.

To prevent this from happening again in the future, I decided to back up my .zsh configurations to a cloud service. But I also don’t want to manually upload it whenever I modify my configurations. I need it to sync automatically with the remote repository. I ended up writing my own solution for that, here are the requirements:

  • A cloud service has CLI which I can use to upload files => Git is the best choice.

After listing all requirements and knowing all the tools, services that I need to use, I started developing my solution, I broke it down into smaller tasks:

  1. Create a git repository in the local machine and Github.

That’s all. No big deal, let’s start the implementation.

…But, if you are too lazy or don’t have enough time to finish reading this post, I published a repository that will help you achieve this easier, there is a short and simple instruction in the README file, it will take less than 5 mins to complete.

Github repository: https://github.com/imbaggaarm/zshrc-auto-sync

Create a git repository

I place all of the scripts, backup files for my ZSH run configuration in a directory following this path: /Users/eastagile/code/zsh-backup, so if you see this text in any file, please replace it with your own value.

First, create a new directory and navigate to it:

# Use this if you haven't had the `code` directory yet
mkdir ~/code# Then use this to create a new directory inside `code` directory
mkdir ~/code/zsh-backup# Navigate to it
cd ~/code/zsh-backup

Then, let’s initialize the git repository, you can use Github CLI to create a remote repository on the command line, and please remember to set the repo’s visibility to Private.

git init
gh repo create

Write a script to detect .zshrc changes

To detect file changes, we will use fswatch as I mentioned above.

You can install fswatch via Homebrew:

brew install fswatch

Then, let’s try out if fswatch can detect changes, type this command on your terminal:

fswatch -o $HOME/.zshrc $HOME/.zsh/ | xargs -n1 -I{} echo "file changed"

Basically, this script will print “file changed” on your terminal whenever you change .zshrc or .zsh/ folder.

After verifying that this script works as expected, let’s create a new file named sync_zshrc.sh in zsh-backup folder and copy the script above into the new file.

touch sync_zshrc.sh

Here is the content of the file:

#!/bin/zshfswatch -o $HOME/.zshrc $HOME/.zsh/ | xargs -n1 -I{} echo "file changed"

Make the script executable:

chmod +x sync_zshrc.sh

Remember to test the script again by modifying your .zshrc, and please notice that this script will be changed later on, this is not its final version.

Let’s push the first commit to the remote repository and move to the next section:

git add .
git commit -m "Add script that detects zshrc changes"
git push

Troubleshooting:

  1. Please be aware with flag arguments of fswatch, for example, -o not -0, -n1 not -n 1, -I{} not -I {}.

Write a script to push new changes to the remote repository

This script basically just copy the content of .zshrc and zsh/ into zsh-backup folder and then push them to the remote repository. I named it commit_changes.sh

Here is the script:

#!/bin/zshcd `dirname "$0"`cp $HOME/.zshrc .
cp -R $HOME/.zsh .git add --all
git commit -m "Sync zsh configs"
git push origin

The reason why I need to use cd `dirname "$0"` is that we might execute the script outside thezsh-backup folder, so we might copy the files to the wrong location, using dirname will ensure that you will copy them to the same directory with commit_changes.sh script.

Remember to make this script executable:

chmod +x commit_changes.sh

Please run the script to verify its functionality too :)).

Connect the scripts

Now, we have separated scripts that work properly. The next part is connecting them.

Let’s modify the sync_zshrc.sh to execute commit_changes.sh when there are new changes.

#!/bin/zshDIR_PATH=`dirname "$0"`fswatch -o $HOME/.zshrc $HOME/.zsh/ | xargs -n1 zsh $DIR_PATH/commit_changes.sh

Because I place two scripts in the same directory, so I use dirname "$0" to get the absolute path of the directory that contains commit_changes.sh. You have to replace the path if you store them in different locations.

I also removed the -I{} argument from the fswatch script since it is not necessary anymore.

Once again, please use ./sync_zshrc.sh to start the script, change the content of the .zshrc file and check the remote repository to see if there is any new commit.

Make the scripts run as a service in macOS

We have to make the scripts run in background mode, and they must be automatically started when we restart our machine. To achieve this in macOS, we have to use launchd.

You can read this article to understand more about launchd and how to use it, but in summary, because we have to use git commands to push to the remote repository, and ZSH run configuration is specific for each user, so we should use User Agents to run services on behave of the logged-in user.

Launchd uses a property list (.plist) file which is really familiar for developers who are using macOS to define the service/job (you can call it by whatever you want :)). So let’s create it in our zsh-backup directory.

touch com..auto-sync-zshrc.plist

You have to replace your-username with the current username. Actually, you can use any name for the file, but following Apple’s naming convention is better, cause we are engineers, right? ?

Here is the content of my .plist file:


http://www.apple.com/DTDs/PropertyList-1.0.dtd">


Label
com.imbaggaarm.auto-sync-zshrc
ServiceDescription
Auto sync zsh configs to git repository
ProgramArguments

zsh
-c
/Users/eastagile/code/zsh-backup/sync_zshrc.sh

StandardOutPath
/Users/eastagile/tmp/com.imbaggaarm.sync-zshrc.stdout
StandardErrorPath
/Users/eastagile/tmp/com.imbaggaarm.sync-zshrc.stderr
WorkingDirectory
/Users/eastagile/code/zsh-backup
KeepAlive


You can see that I use KeepAlive option because I want our script to continue running when it ends or crashes.

You have to replace bolded texts with the right values in your machine. For StandardOutPath and StandardErrorPath, Launchd will write logs into these files, later on, you can check error logs and info logs there. You can use any location that you want, but please remember to make sure that the logged user has enough permissions to write to that location.

After having the file, you have to copy it to ~/Library/LaunchAgents, this directory is the place where we store all configurations for services that will be run on behave of the logged-in user.

cp com..auto-sync-zshrc.plist ~/Library/LaunchAgents

Let’s navigate to ~/Library/LaunchAgents, it now should contain our configuration file.

We use launchctl load command to start our service, please navigate to ~/Library/LaunchAgents and run this:

launchctl load com..auto-sync-zshrc.plist

If you get Load failed: 5: Input/output error, please check if the .plist file is in the correct format and you are in the right directory.

After calling this, you should check the file at StandardErrorPath to see if there is an error. You can also use launchctl list to check the status of your service.

launchctl list | grep auto-sync-zshrc

If the status is 0 and the PID value is not empty, then congratulations, your script is running properly now. But if you see other values, or even there is no result, probably there is something wrong with your scripts.

For our scripts, you might encounter this error: command not found: fswatch. Please use this to see the error:

cat /com..sync-zshrc.stderr

If you get this but you already installed launchctl, you have to modify the sync_zshrc.sh file:

#!/bin/zshDIR_PATH=`dirname "$0"`/opt/homebrew/bin/fswatch -o $HOME/.zshrc $HOME/.zsh/ | xargs -n1 zsh $DIR_PATH/commit_changes.sh

I changed from fswatch to /opt/homebrew/bin/fswatch because launchd can’t find the binary, maybe there were other solutions but this was the simplest solution that I had. You have to replace this path with the value in your machine, you can use which fswatch to get it.

After modifying and saving the property list. You should use launchctl unload to stop the service:

launchctl unload com..auto-sync-zshrc.plist

And then restart it:

launchctl load com..auto-sync-zshrc.plist

There is a GUI application for checking the launchd services which is LaunchControl. To install it, you can use Homebrew Cask:

brew --cask install launchcontrol

Here is a screenshot of the app:

I would suggest you use this application if you encounter any errors.

And yeah, if you see all fields are in green like my screenshot, your script is working properly (remember to select UserAgents in the top left button of the LaunchControl application).

You should change your ~/.zshrc or ~/.zsh and check your remote repository, all configurations are synced.

After all, your zsh-backup should have the structure like this:

➜  zsh-backup git:(master) tree -a -L 1
.
├── .git
├── .zsh
├── .zshrc
├── com..auto-sync-zshrc.plist
├── commit_changes.sh
└── sync_zshrc.sh

You might have to install tree using Homebrew: brew install tree.

And your commit history looks like this:

I hope my post will somehow help you to organize and backup your ZSH run configuration so you won’t have to deal with the situation that I’ve encountered. You also can use the same approach to sync your Vim configuration. I think this will work properly with bash, you might just need to change file names, but it probably required more effort to make this to be run on other operating systems, but overall, the approach is the same.

For tokens, credentials, secrets, you should place them all in other files such as secrets.zsh (remember to addsource secrets.sh into .zshrc), and don’t copy them in the commit_changes.sh script. The Github repository is private but we might accidentally publish it or in case Github is hacked, our credentials won’t be leaked. I will update the script later to mask the secrets by * in the same way Django handles its secret variables.

And last but not least, the situation might be even worse if I stored all of my configurations in the ~/.zsrhc, I was really lucky, but this luck wouldn’t come if I didn’t organize my ZSH run configuration carefully. I’m inspired by an initiative in my company, we are trying to reduce toils by making things that can be run automatically to be run automatically, and a lot more.

Hope that I will find more valuable things to share in the future, and folks, please remember, manage your machine in the same way you manage your code.

You may also like

Leave a Reply

Are you sure want to unlock this post?
Unlock left : 0
Are you sure want to cancel subscription?

This website uses cookies to improve your experience. We'll assume you're ok with this, but you can opt-out if you wish. Accept Read More

-
00:00
00:00
Update Required Flash plugin
-
00:00
00:00
Verified by MonsterInsights