How (and Why) You Should Split Your .bashrc or .zshrc Files
Tags: bash, zsh, shell, terminal
Published: 2021-12-19 15:47:48

_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:

```bash
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](https://github.com/ranelpadon/configs/tree/master/nvim) more scalable:

```vim
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](https://github.com/ranelpadon/configs/blob/master/.zshrc). A simplified version of it will be explained below. Most people will have simpler use case also.

```bash
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](https://insights.stackoverflow.com/survey/2021#section-top-paying-technologies-top-paying-technologies).

### 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:

```bash
~/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:

```bash
for FILE in ~/zshrc/*; do  
    source $FILE  
done
```

Hence, we’ll have this logical structure:

```bash
~/.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:

```bash
# 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](https://github.com/sharkdp/fd) like me, which is a modern/faster version of `find`, you just need to change the `FILES_STR` value:

```bash
# 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](https://ko-fi.com/ranelpadon). This will mean a lot to me, and encourage me to create more high-quality contents.