Scalable Bash/Zsh startup scripts with just a few lines
- To split our
.zshrcinto multiple files, put them in
zshrcolder, and load them one by one:
for FILE in ~/zshrc/*; do source $FILE done
.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.
I had a lenghty
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
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
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
- 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.
The commands here mainly assume using
.zshrc), but the ideas are the same even if you use
.bash_profile) or other shells.
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
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
~/zshrc ├── git.sh ├── js.sh ├── python.sh ├── django.sh ├── docker.sh ├── general.sh ├── init.sh
We then could have two special files:
init.shcontains the stuff needed by the shell (
zsh) like its initializer, plugins, theme, etc.
general.shcontains 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.
~/.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
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
python.sh, etc) and put it there
- if it doesn’t belong anywhere, just put it in
general.shas 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.
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
There are various strategies to solve this, but the simplest one is:
- load first the
- then, load the other scripts
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
If you’re a fan of fd like me, which is a modern/faster version of
find, you just need to change the
# 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
- Shell startup scripts are very useful to improve our efficiency/productivity.
- There’s no observable difference in performance between sourcing from a single
.bashrcor 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
fdCLI 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.