How (and Why) You Should Split Your .bashrc or .zshrc Files

Scalable Bash/Zsh startup scripts with just a few lines

Objective

  • To split our .bashrc/.zshrc into multiple files, put them in zshrc 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 content.