Scalable Bash/Zsh startup scripts with just a few lines
Objective
- To split our
.bashrc
/.zshrc
into multiple files, put them inzshrc
older, and load them one by one:
for FILE in ~/zshrc/*; do
source $FILE
done
Background
.bashrc
/.zshrc
are executed mainly when we start the terminal app. Usually, it contains our custom aliases/shortcuts, functions/commands, shell variables, and environment variables to help in our workflow.
However, this could easily get messy through time as we tinker with various frameworks, utilities, or projects. Even at work, we might need to automate the repetitive tasks/commands to boost productivity/efficiency. Hence, we update these files from time to time. The small garden at first could then evolve to a dense forest which is harder to maintain.
Splitting the Vim settings
I had a lenghty .vimrc
/init.vim
settings before, so I split it to be more modular, and so that the main loader could auto-detect/load even the new files. Making my settings more scalable:
runtime settings.vim
runtime plugins.vim
runtime mappings.vim
" `!` is needed to load all files in the folder.
runtime! themes/*.vim
runtime! plugins-config/*.vim
Splitting the Zsh settings
Similarly, I had more than 1,500 lines in my .zshrc
before, and now it just have a few lines. A simplified version of it will be explained below. Most people will have simpler use case also.
CONFIGS=$HOME/dev/configs
source $CONFIGS/zshrc/init.sh
FILES_STR=$(fd --glob '*.sh' --exclude 'init.sh' $CONFIGS/zshrc)
FILES=($(echo $FILES_STR | tr '\n' ' '))
for FILE in $FILES; do
source $FILE
done
Benefits
Having modular settings is a good practice for the following reasons:
- Related contents will be grouped together which means better separation of concerns.
- System will be more scalable since the future split files will also be auto-loaded without updating the
.zshrc
. - Splitting the contents at first will force you to review all your settings one by one and detect the outdated, unused, or duplicated ones. This will force you also to learn more about shell scripting since you’ll notice that there are repetitive patterns and they could be extracted for better code reuse. The end result is a leaner system. Note that shell scripting is one of the top paying tech as per the latest StackOverflow survey.
Distributed Sources
The commands here mainly assume using zsh
(.zshrc
), but the ideas are the same even if you use bash
(.bashrc
/.bash_profile
) or other shells.
1. Create the scripts folder
We need to create the target folder to contain the split files. For simplicity, we could create ~/zshrc
folder (i.e. without a dot to differentiate it from the main ~/.zshrc
file).
2. Create the scripts files
We could start reviewing the contents of .zshrc
file. Then, group the related contents per topic like the files below. Note that the file extension will not matter, the source
shell command just care about the file contents. I just used .sh
as the generic/umbrella term for of zsh
and bash
shells:
~/zshrc
├── git.sh
├── js.sh
├── python.sh
├── django.sh
├── docker.sh
├── general.sh
├── init.sh
We then could have two special files:
-
init.sh
contains the stuff needed by the shell (zsh
) like its initializer, plugins, theme, etc. -
general.sh
contains all the stuff that doesn’t belong in the current split files, and too few to warrant a dedicated file. This will be the default file for stuff that couldn’t be easily classified.
3. Update the main loader
Then, our ~/.zshrc
could simply have this to load/source all the .sh
split files in the folder. You could use *.sh
instead of just *
to be more specific:
for FILE in ~/zshrc/*; do
source $FILE
done
Hence, we’ll have this logical structure:
~/.zshrc
~/zshrc
├── git.sh
├── js.sh
├── python.sh
├── django.sh
├── docker.sh
├── general.sh
├── init.sh
Future Updates
We now have an scalable system. For future changes:
- check if the new alias/command/envvar could be put in the common split files (e.g. in
git.sh
,python.sh
, etc) and put it there - if it doesn’t belong anywhere, just put it in
general.sh
as the default destination - once there are significant number of related stuff already in
general.sh
, you could put them in a new dedicated/split file.
The beauty of this is that the ~/.zshrc
will auto-load even the newly added files. Hence, no need to update its contents.
Critical Sources
Other people might stop at this point if they don’t have issue with the above command in ~/.zshrc
. But sometimes there are scripts that need to be loaded first before the other scripts, which you could put in init.sh
.
There are various strategies to solve this, but the simplest one is:
- load first the
init.sh
- then, load the other scripts
Using the find
utility
We could have something like this:
# Load the 'init.sh'.
source ~/zshrc/init.sh
# Find all '.sh' files in ~/zshrc, exclude 'init.sh'.
FILES_STR=$(find ~/zshrc -name '*.sh' -not -name 'init.sh')
# `tr` is a find-and-replace utility.
# Outer () will convert the output of $() to array.
FILES=($(echo $FILES_STR | tr '\n' ' '))
for FILE in $FILES; do
source $FILE
done
Using the fd
utility
If you’re a fan of fd like me, which is a modern/faster version of find
, you just need to change the FILES_STR
value:
# Load the 'init.sh'.
source ~/zshrc/init.sh
# Find all .sh files in ~/zshrc, exclude 'init.sh'.
FILES_STR=$(fd --glob '*.sh' --exclude 'init.sh' ~/zshrc)
# 'tr' is a find-and-replace utility.
# Outer () will convert the output of $() to array.
FILES=($(echo $FILES_STR | tr '\n' ' '))
for FILE in $FILES; do
source $FILE
done
Key Takeaways
- Shell startup scripts are very useful to improve our efficiency/productivity.
- There’s no observable difference in performance between sourcing from a single
.zshrc
/.bashrc
or sourcing from its split files. - Splitting the files will mean better modularity, separation of concerns, and scalability. Future updates will be easier and more manageable.
- Smaller scope of each file means easier to detect outdated and unused stuff, or overlapping usages which will force you to refactor them. The process could then beef up your shell scripting skills.
- We could load the critical files first, then the other, non-critical files. We could use the
find
/fd
CLI utilities to find/exclude files.
Thank you for reading. If you found some value, kindly follow me, or give a reaction/comment on the article, or buy me a coffee. This will mean a lot to me, and encourage me to create more high-quality contents.
Top comments (0)