

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 ~/.zshrc
to 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.
- Auto-detect changes in
~/.zshrc
and~/.zsh/
=> With Mac, I found out fswatch is the most suitable solution for this. - The script which detects changes needs to run automatically in the background after restarting the machine => launchd.
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:
- Create a git repository in the local machine and Github.
- Write a script that can detect changes in
~/.zshrc
,~/.zsh/
. I named itsync-zshrc.sh
- Write a script that can copy modified
~/.zshrc
and~/.zsh/
to the local repository then commit, push the changes to the remote repository. I named itcommit-changes.sh
- Modify
sync-zshrc.sh
to executecommit-changes.sh
when there is a new change. - Make
sync-zshrc.sh
run in background mode usinglaunchd
.
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:
- Please be aware with flag arguments of fswatch, for example,
-o
not-0
,-n1
not-n 1
,-I{}
not-I {}
. - If you get this error:
zsh: command not found: fswatch
, please check if fswatch was installed properly, and add it to the $PATH if needed.
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.