Convert any Bash Function/Alias to Hybrid Executable and Source-able Script with Bash Completion
...works with arguments and stdin.
NOTE:
I will be refactoring cronstat
(a function you'll find below) again to process the switches and arguments better. COMING VERY SOON, just not sure when.
Introduction
Whether you are new or old to scripting/programming in the Bash environment in Linux you have more than likely heard of, used, and/or created Bash functions and aliases. I will not be getting too much into what these are as this post states about the skill level that this is aimed at people who are familiar with these things. Having said that; I don't think that this will be too difficult for newer people to understand and, of course, all are welcome to read.
Motivation
There are a couple of ways to use functions and aliases; one is to source a file/script that contains the code and execute the function in the calling file and the other is to execute a script file with the code and pass arguments to the script and pass it to the function in the script.
Over the years I have written countless functions and scripts and for a long time I would at first keep functions in my normal .bash_profile and just keep transferring it to different machines, but after a while my file started to grow too large and so I started putting them in separate .bash_funcs/.bash_aliases files and just sourced them in whatever main profile file I was using to try and stay organized.
This method was fine, but I soon realized that functions and aliases can only be used in certain environments like scripts and command lines and not in things like Alt+F2, KDE Runner, AutoKey, or just set to a hotkey and so I realised I should just start putting functions in script files always so I can access them from anywhere and in any way.
I soon created this method from my understanding of the Bash shell environment. I don't know if anyone else uses this method, but I have never seen it before and all due respect to those who do use something like this.
How it works
Description
To call a function or alias from a script you must put said function (or alias) in a script and call script with any arguments you would pass to the function and in the same script underneath the function you would either put a bash complete command if the file is sourced or execute the function with any arguments passed to the script.
Generic Example
Generic, non-sense script example file fake_function.bash
(or whatever):
#/usr/bin/env bash
function fake_function {
if [[ $# -gt 0 ]]; then
printf 'Argument: %s\n' "$@"
else return 1; fi
}
# if file is sourced set Bash completion
if $(return >/dev/null 2>&1); then
complete -W "word1 word2 word3" fake_function
else
# else if file is executed pass arguments to it
fake_function "$@"
# or || exit "$?" in some cases for errors
fi
To Execute
and then to execute the script with whatever method; for example:
$ ./fake_function.bash "Line 1" "Line 2"
Line 1
Line 2
To Source With Completion
or to source into a script file with completion (script_file
):
ff_path="/path/to/fake_function.bash"
if [[ -f "$ff_path" ]]; then # if the script exists
. "$ff_path" # source the file and function
fi
fake_function "Line 1" "Line2" # call the function
# and call the function any time from the
# command line
Better Example
*NOTE*: This has been refactored to remove redundancy.
This is an actual example of a function and script I wrote called cronstat
(cronstat.bash
) that is a wrapper for the stat
command that filters out the newest (default) or oldest files, directory, or both (default) in a directory. Great for when I need to find the latest project I did and forgot the name of or finding old, redundant files etc...
#!/usr/bin/env bash
function cronstat {
local path_mode=1 time_mode=1 bare_mode=0 arg
if [[ $# -gt 0 ]]; then
for arg in "$@"; do
if [[ "$arg" =~ ^-([hH]|-[hH][eE][lL][pP])$ ]]; then
cat<<EOF
'cronstat' - 'stat' wrapper to find the oldest
and newest file, directory, or both from an
arrayed or line delimited list.
@USAGE:
cronstat <LIST> [OPTIONS...]
<LIST> | cronstat [OPTIONS...]
@LIST:
Any arrayed list of files or directories
or the output of the 'find' or 'ls'
commands etc...
@OPTIONS:
-h,--help This help screen.
-b,--bare Print the path only, no
extra information.
-f,--file Filter by files.
-d,--directory Filter by directories.
Defaults to any file or
directory.
-o,--oldest Get the oldest item.
Defaults to the newest.
@EXAMPLES:
cronstat \$(find -maxdepth 1)
find -maxdepth 1 | cronstat
IFS=\$(echo -en "\n\b") array=(\$(ls -A --color=auto))
cronstat \${array[@]} --file
printf '%s\n' "\${array[@]}" | cronstat -odb
@EXITCODES:
0 No errors.
1 No array or list passed.
2 No values in list.
EOF
return
fi
if [[ "$arg" =~ ^-([bB]|-[bB][aA][rR][eE])$ ]]; then
bare_mode=1
shift
fi
if [[ "$arg" =~ ^-([fF]|-[fF][iI][lL][eE])$ ]]; then
path_mode=2
shift
fi
if [[ "$arg" =~ ^-([dD]|-[dD][iI][rR][eE][cC][tT][oO][rR][yY])$ ]]; then
path_mode=3
shift
fi
if [[ "$arg" =~ ^-([oO]|-[oO][lL][dD][eE][sS][tT])$ ]]; then
time_mode=2
shift
fi
if [[ "$arg" =~ ^-([oO][fF]|[fF][oO])$ ]]; then
time_mode=2
path_mode=2
shift
fi
if [[ "$arg" =~ ^-([oO][dD]|[dD][oO])$ ]]; then
time_mode=2
path_mode=3
shift
fi
if [[ "$arg" =~ ^-([oO][bB]|[bB][oO])$ ]]; then
bare_mode=1
time_mode=2
shift
fi
if [[ "$arg" =~ ^-([fF][bB]|[bB][fF])$ ]]; then
bare_mode=1
path_mode=2
shift
fi
if [[ "$arg" =~ ^-([dD][bB]|[bB][dD])$ ]]; then
bare_mode=1
path_mode=3
shift
fi
if [[ "$arg" =~ ^-([oO][fF][bB]|[oO][bB][fF]|\
[bB][oO][fF]|[bB][fF][oO]|\
[fF][oO][bB]|[fF][bB][oO])$ ]]; then
bare_mode=1
time_mode=2
path_mode=2
shift
fi
if [[ "$arg" =~ ^-([oO][dD][bB]|[oO][bB][dD]|\
[bB][oO][dD]|[bB][dD][oO]|\
[dD][oO][bB]|[dD][bB][oO])$ ]]; then
bare_mode=1
time_mode=2
path_mode=3
shift
fi
done
fi
local input array date iter index=0 value time_string="Newest" path_string="File Or Directory"
declare -A array
if [[ ! -t 0 ]]; then
while read -r input; do
case "$path_mode" in
1) if [[ -f "$input" ]] ||
[[ -d "$input" ]]; then
date=$(stat -c %Z "$input")
array[$date]="$input"
fi;;
2) if [[ -f "$input" ]]; then
path_string="File"
date=$(stat -c %Z "$input")
array[$date]="$input"
fi;;
3) if [[ -d "$input" ]]; then
path_string="Directory"
date=$(stat -c %Z "$input")
array[$date]="$input"
fi;;
esac
done
else
if [[ $# -gt 0 ]]; then
for input in "$@"; do
case "$path_mode" in
1) if [[ -f "$input" ]] ||
[[ -d "$input" ]]; then
date=$(stat -c %Z "$input")
array[$date]="$input"
fi;;
2) if [[ -f "$input" ]]; then
path_string="File"
date=$(stat -c %Z "$input")
array[$date]="$input"
fi;;
3) if [[ -d "$input" ]]; then
path_string="Directory"
date=$(stat -c %Z "$input")
array[$date]="$input"
fi;;
esac
done
else return 1; fi
fi
if [[ ${#array[@]} -eq 0 ]]; then
return 2
fi
for iter in "${!array[@]}"; do
if [[ $index -eq 0 ]]; then
index=$((index + 1))
value=$iter
fi
case "$time_mode" in
1) if [[ $iter -gt $value ]]; then
value=$iter
fi;;
2) if [[ $iter -lt $value ]]; then
time_string="Oldest"
value=$iter
fi;;
esac
done
value="${array[$value]}"
if [[ $bare_mode -eq 0 ]]; then
printf '\n%s %s:\n%s\n\nLast Changed:\n%s\n\n' \
"$time_string" \
"$path_string" \
"$value" \
"$(stat -c %z "$value")"
else
printf '%s\n' "$value"
fi
}
if $(return >/dev/null 2>&1); then
complete -W "-h --help -o --oldest -f --file -d --directory -b --bare -of -od -ob -fb -db -ofb -odb '\$(find -maxdepth 1)'" cronstat
else cronstat "$@";fi
and just like the generic example above you would then either execute this file with any arguments or source it into your dot or script files and use however tied to any program or hotkey as mentioned before and these examples here:
Example When Sourced
$ printf '%s\n' * .* | cronstat --oldest
Oldest File Or Directory:
examples.desktop
Last Changed:
2020-03-21 11:43:36.356069642 -0500
Example When executed As Script
$ ls -A --color=auto | ~/.bash/profile/functions/cronstat.bash --oldest --directory
Oldest Directory:
.pki
Last Changed:
2020-03-23 08:09:05.447080464 -0500
Methods For Organization
A main issue with script files is that you can end up with a lot of them and it's horrible if you don't keep them organized and I end up dropping all of my function (.bash) files into a functions folder and aliases and then in either a dot profile or func file I will try and import them all like this:
(in .bash_funcs or .bash_profile etc...)
*NOTE*: This has been refactored to work with file names with '-'.
# Load funcs
function bash_func_src {
local file
for file in $(find "${HOME}/.bash/profile/functions/" -maxdepth 1 -type f -name "*.bash"); do
. "$file"
done
}
bash_func_src
# and aliases
function bash_alias_src {
local file
for file in $(find "${HOME}/.bash/profile/aliases/" -maxdepth 1 -type f -name "*.bash"); do
. "$file"
done
}
bash_alias_src
Conclusion
This method essentially makes any of your Bash code portable and accessible in all sorts of environments and allows you keep it in a script, transporting not only your code, but also examples of usage (in terms of the script itself) and a place to store external Bash completion code all the while archiving everything.
This allows you to execute these functions from a script for use anywhere especially in hotkeys (AutoKey), Alt+F2, KDE Runner, and any other way you can call a script with arguments.
I hope this helps someone and I'd love to hear if you do something similar or just any tips or comments are welcome.
Top comments (4)
I def like the idea of having a bash functions folder (DIRECTORY!! lol). Personally, i just append an export PATH to my ~/.bashrc. Mainly because i have a fairly specific directory structure for my open source projects & i like to keep that structure going & not everything in my dev/bash/PROJECT folders is necessarily setup to be executed.
I also have a personal library with a cli "framework" i made that i just add functions too, so its tlf [command group] [command].
I don't do any autocompletion though. Thats still something i have to try out. & i think my setup would require some changes to make that work. Didnt know it was so easy to do. But yeah, I'd have to source files to enable it to a greater degree than.
I have a script to start spacevim that would be a good candidate for your approach.
You can do far more complicated Bash Completion with functions using
COMP
variables,compgen
, and more:Generic example:
where the
func_name
is, of course, the name of the function where you do the more complicated stuff withCOMP
variables and return aCOMPREPLY
:But that's stuff you don't always need in a completion most of the time you can do with a simple word (
-W
) list:or for some trickery:
tab toggling through a list of files only in a current directory:
or dirs only:
A lot can be done with this, including doing background stuff that may not be directly involved in your completion like logging file info, file stamps of latest files etc...
Thanks, if I ever get around to putting in the time/effort to set it up, this'll be v. helpful. Long as I remember to come back here lol.
If you like the scripts in this post, then I can only imagine what this is gonna do to you:
Well, I think you get the idea.
For your function loading woes, try zsh and put something like these lines in your
~/.zshrc
file.In that folder, just put a file named as the desired function and as content put the function body and let zsh do it's magic.
The function or however many you put there, will be lazy-loaded on first execution. For that, I just converted most of my aliases into functions and never looked back.
An approach more similar to yours for sourcing the files is to use shell globbing instead of
find
. With the rightshopt settings
this one-liner in~/.zshrc
or~/.bashrc
should do the trick.I strongly recommend you to try zsh + prezto, you just gonna love the tab completion!
Put that in your
.zpreztorc
as a starting point and play around withssh
andscp
.Some comments have been hidden by the post's author - find out more