ZSH Sourcer
ZSH Sourcer
Use Case
I try to have all my own stuff when it comes to my config so I know how it works. I don’t always remember, but it helps me try to be a better programmer. When it comes to my ZSH config, I have some aliases that are work specific and others that are language specific and others that are just nice to have regardless. I broke out these to help my organization in general. I wanted my work ones separate because those do change more than I really care to admit (the shortcuts not the job), and many of them I don’t want to share as part of my public repo. I also found myself using more and more language specific shortcuts for various reasons as well, as the tasks of the job change. One month I am doing more Bamboo related work, and the next I am on Azure, and then the next I am migrating all that to AWS. So there are many things that are changing that are not just job related.
What this means, I have the standard .zsh_alias file, and that has my general information in it. Things I like to share, and things I will never touch. For “work-related” aliases, I created another Git repo that I made as a submodule for my primary zsh repo, and then made that submodule repo private, and I can share it with those whom I work with, and also make sure that some work secrets (like how bad I am at remember simple commands) don’t get out. Then, I created another submodule repo for all my languages in a similar way.
For years, I just had two lines in my primary .zsh_alias file that just did source work/.zsh_work_aliases and source language/.zsh_language_aliases. Simple and easy enough. For most people, this will be enough.
Over the years though, I started collecting scripts, languages, and I reorganized several times and more. I had several files in those repos that I was sourcing. At work, we also went through several transitions, a few buyouts, and a few other companies joined us. That meant a plethora of other servers, domains, environments, and scripts, all leading to more shortcuts and more organization for more files.
Needless to say, I did not want to write the massive 8+ lines of source work/.zsh_otherwork_aliases and such, and then update it again in a few months. So I decided to do this where I wrote a zsh function that sources based on the folder and then I don’t have to worry.
As per usual, I could go on for too long trying to justify “why” things are how they are, but I am sure you could find a use-case for yourself as well. Especially if you are reading this.
Many people will also say “why not use X”, and that sounds great, but I am trying to understand how things work and get things just how I want it.
Journey with me as I make this and then take it to the next level.
First Attempt
Code
function source_aliases() {
local givenDirectory="$1"
# main function
local fullPath="$HOME/$givenDirectory"
setopt NULL_GLOB DOTGLOB
## NOTE: NULL_GLOB ensures script doesn't fail if empty, DOTGLOB looks for dot files and directories
for givenFile in "$fullPath"/*; do
local base="$(basename "$givenFile")"
# Exclude unwanted patterns
case "$base" in
*.swp|*.swo|*.bak|*.tmp|*.md|*.txt|README*|.git|.gitignore|.DS_Store)
continue ;;
esac
## Only source if it's a file and:
## (a) ends in .zsh, (b) end .sh, (c) is a trusted dotfile (e.g., starts with a dot but no extension)
if [[ -f "$givenFile" && ( "$givenFile" == *.zsh || "$givenFile" == *.sh || "$base" == .* ) ]]; then
source "$givenFile"
fi
done
unsetopt NULL_GLOB DOTGLOB
}
Explanation
With this first attempt things are pretty straightforward, I think.
We start off assigning the only variable that could be passed to it as givenDirectory, which then uses that as part of the fullPath. In theory, we should always have a good valid path being passed, but sometimes we can make mistakes and that is what I want to add to the next version.
After that, we loop through all the files in that fullPath and look for certain things. My original “pre-first-attempt” did not have these checks, but I added them in. The first check is excluding some basic file types. I didn’t want any .git, txt, md, or other temp files. I could go crazy with this, but that covers my basis.
Next, I am only sourcing it IF its a .zsh, .sh or another dotfile.
You may be wondering, why have the exclusion check AND the check for only certain file types.
Initially, I wanted to only exclude the .git files, but then I added more to that exclusion check. Then I realized as I was looping through, I wanted to make sure I was only sourcing things that could be sourced. With zsh and other shell scripting languages you don’t necessarily need to have the files explicity end with that extension. That is what I did, and I didn’t have a good consistency or good organization, inspite of my efforts. Many of my files are named closer to the function they serve in the area. Here I am just making sure that it follows a name convention in some way.
This is probably good enough for most, and like I said, I had less before, but lost that code (not too hard to imagine or recreate).
Lets try to make it better now.
Second Version
Code
function source_aliases() {
local givenDirectory="$1"
# Check if directory is provided - else show a "help"
if [[ -z "$givenDirectory" ]]; then
echo "Usage: source_aliases <directory>" >&2
return 1
fi
# main function
local fullPath="$HOME/$givenDirectory"
if [ -d "$fullPath" ]; then
setopt NULL_GLOB DOTGLOB ## NOTE: NULL_GLOB ensure it doesn't fail if empty and DOTGLOB looks for dot files and directories
for givenFile in "$fullPath"/*; do
local base="$(basename "$givenFile")"
# Exclude unwanted patterns
case "$base" in
*.swp|*.swo|*.bak|*.tmp|*.md|*.txt|README*|.git|.gitignore|.DS_Store)
continue ;;
esac
## Only source if it's a file and:
## (a) ends in .zsh, (b) end .sh, (c) is a trusted dotfile (e.g., starts with a dot but no extension)
if [[ -f "$givenFile" && ( "$givenFile" == *.zsh || "$givenFile" == *.sh || "$base" == .* ) ]]; then
source "$givenFile"
fi
done
unsetopt NULL_GLOB DOTGLOB
else
echo " ❌ Directory 'fullPath' does not exist." >&2
return 1
fi
echo
}
Explanation
I got thinking that if this is a zsh function, it would be accessible via the command line anytime. If I am making a new file or updates to my aliases, then I could easily source/re-source it. Let’s set it up so we can give some feedback.
Here we are still leveraging just one given directory, but then we are seeing if anything at all was given. If not, then lets give a message for everyone to see.
Additionally, we check if the fullPath is a directory with that -d directive. Otherwise, we say “Directory does not exist” and exit the function.
Now that we have some feedback, which is just nice to have, lets add more feedback to make it nicer…especially when I forget everything in 3 months.
Third Edition
Code
function source_aliases() {
local verbose=false
local showHelp=false
local givenDirectory=""
# Parse Arguments
for arg in "$@"; do
case "$arg" in
-v|--verbose) verbose=true ;;
-i|--interactive) interactive=true ;;
-h|--help) showHelp=true ;;
*) givenDirectory="$arg" ;;
esac
done
# Show help
if [[ "$showHelp" == true || -z "$givenDirectory" ]]; then
echo "Usage: source_aliases [--verbose|-v] [--help|-h] <directory>"
echo
echo "OPTIONS:"
echo " -v, --verbose Show which files are being sourced"
echo " -h, --help Show this help message"
return 0
fi
# main function
local fullPath="$HOME/$givenDirectory"
if [ -d "$fullPath" ]; then
setopt NULL_GLOB DOTGLOB ## NOTE: NULL_GLOB ensure it doesn't fail if empty and DOTGLOB looks for dot files and directories
for givenFile in "$fullPath"/*; do
local base="$(basename "$givenFile")"
# Exclude unwanted patterns
case "$base" in
*.swp|*.swo|*.bak|*.tmp|*.md|*.txt|README*|.git|.gitignore|.DS_Store)
continue ;;
esac
## Only source if it's a file and:
## (a) ends in .zsh, (b) end .sh, (c) is a trusted dotfile (e.g., starts with a dot but no extension)
if [[ -f "$givenFile" && ( "$givenFile" == *.zsh || "$givenFile" == *.sh || "$base" == .* ) ]]; then
if [[ "$verbose" == true ]]; then
echo "🔗 Sourcing: $givenFile"
fi
source "$givenFile"
fi
done
unsetopt NULL_GLOB DOTGLOB
else
echo " ❌ Directory 'fullPath' does not exist." >&2
return 1
fi
echo
}
Explanation
Here we changed the rudementary “show help” oneliner to a more flushed out valid “Show Help” segment. Something you would expect to see in a real function or tool, right? We replaced the check if the givenDirectory was sent, and added on to that the options that can be provided.
Additionally, we added this verbose flag. If that is set, then every file that is sourced we will display a message. This is handy if you just aren’t sure its sourcing the correct files.
With these two changes we had to also make local variables so there isn’t a memory leak and the variables become global and used in other ways. Not that it would harm things, it would just be silly. As we did that, we couldn’t have givenDirectory to be the only arg of $1, we had to default it to nothing. Then we parse the arguments and look for where they should be matched respectively.
Lastly, we then add the log message of what its sourcing to the screen with the verbose check.
What else is there? Lets make it interactive, as an option.
Final Iteration
Code
function source_aliases() {
local verbose=false
local interactive=false
local showHelp=false
local givenDirectory=""
# Parse Arguments
for arg in "$@"; do
case "$arg" in
-v|--verbose) verbose=true ;;
-i|--interactive) interactive=true ;;
-h|--help) showHelp=true ;;
*) givenDirectory="$arg" ;;
esac
done
# Show help
if [[ "$showHelp" == true || -z "$givenDirectory" ]]; then
echo "Usage: source_aliases [--verbose|-v] [--interactive|-i] <directory>"
echo
echo "OPTIONS:"
echo " -v, --verbose Show which files are being sourced"
echo " -i, --interactive Prompt before sourcing each file"
echo " -h, --help Show this help message"
return 0
fi
# main function
local fullPath="$HOME/$givenDirectory"
if [ -d "$fullPath" ]; then
setopt NULL_GLOB DOTGLOB ## NOTE: NULL_GLOB ensure it doesn't fail if empty and DOTGLOB looks for dot files and directories
for givenFile in "$fullPath"/*; do
local base="$(basename "$givenFile")"
# Exclude unwanted patterns
case "$base" in
*.swp|*.swo|*.bak|*.tmp|*.md|*.txt|README*|.git|.gitignore|.DS_Store)
continue ;;
esac
## Only source if it's a file and:
## (a) ends in .zsh, (b) end .sh, (c) is a trusted dotfile (e.g., starts with a dot but no extension)
if [[ -f "$givenFile" && ( "$givenFile" == *.zsh || "$givenFile" == *.sh || "$base" == .* ) ]]; then
if [[ "$verbose" == true ]]; then
echo "🔗 Sourcing: $givenFile"
fi
if [[ "$interactive" == true ]]; then
read -q "REPLY? ⚡ Source '$givenFile'?[y/N] "
echo ## newline after prompt above
[[ "$REPLY" == [yY] ]] || continue ## skip if not y or Y
fi
source "$givenFile"
fi
done
unsetopt NULL_GLOB DOTGLOB
else
echo " ❌ Directory 'fullPath' does not exist." >&2
return 1
fi
echo
}
Explanation
Here we added the interactive flag and set the default. Added the part to the help area, and then added a check in the sourcing loop to prompt the user IF you want it sourced. This is great if you are editing only one file and you know another file may have errors or be too big. Now you will be prompted for each file, select Y or N and it will load that file.
Outro
Right now, this works for me. The original also worked for me, but I liked to learn more about this and make it work for me.
What about the future? What’s next?
Possibly make it accept multiple source directories… …that isn’t too much of my use-case but I like the idea. I think I can make it work, but because its it not my usecase, its not a priority. Possibly make it recursive… …and traverse to subdirectories of the submodules and so on…that sounds interesting. I don’t think I have that organization need yet. Again, not high on the priority because its not something I need for my use-case.
Any other thoughts?
I just had to share this as I found it to be helpful and useful to me. Again, having 10+ lines saying source ... was way too cumbersome. Now, I can blindly add files in about 60 lines of code.
I hope you enjoyed it as well.