DEV Community

Cover image for Turn any Bash function/alias into a hybrid executable/source-able script.
Ian Pride
Ian Pride

Posted on • Edited on

Turn any Bash function/alias into a hybrid executable/source-able script.

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.

Skill LevelSkill Level

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 
Enter fullscreen mode Exit fullscreen mode

To Execute

and then to execute the script with whatever method; for example:

 $ ./fake_function.bash "Line 1" "Line 2" Line 1 Line 2 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
thereedybear profile image
Reed

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.

Collapse
 
thefluxapex profile image
Ian Pride

Didnt know it was so easy to do.
-- Reed

You can do far more complicated Bash Completion with functions using COMP variables, compgen, and more:

Generic example:

 $ complete -F func_name command_name 
Enter fullscreen mode Exit fullscreen mode

where the func_name is, of course, the name of the function where you do the more complicated stuff with COMP variables and return a COMPREPLY:

function func_name { # ... calculate COMP stuff and more ... COMPREPLY=( $(compgen -W "<DO_STUFF>") ) # DO_STUFF: calculated stuff from above or compgen inline expression  } 
Enter fullscreen mode Exit fullscreen mode

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:

 $ complete -W "-h --help -a --add" command_name 
Enter fullscreen mode Exit fullscreen mode

or for some trickery:

tab toggling through a list of files only in a current directory:

 $ complete -W "$(find -maxdepth 1 -type f)" command_name 
Enter fullscreen mode Exit fullscreen mode

or dirs only:

 $ complete -W "$(find -maxdepth 1 -type d)" command_name 
Enter fullscreen mode Exit fullscreen mode

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...

Collapse
 
thereedybear profile image
Reed

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.

Collapse
 
flexible profile image
Info Comment hidden by post author - thread only accessible via permalink
Felix Franz

If you like the scripts in this post, then I can only imagine what this is gonna do to you:

$ alias ls="ls --group-directories-first --time-style=full-iso -l -A -g -G" $ ls --reverse -t -c drwxr-xr-x 15 480 2022-03-07 18:02:46.137044599 +0100 puppet drwxr-xr-x 47 1504 2022-03-20 18:46:38.562413488 +0100 puppetlabs-motd drwxr-xr-x 23 736 2022-03-26 19:20:07.091106952 +0100 _ansible drwxr-xr-x 4 128 2022-03-30 12:14:26.936141669 +0200 puppet5 drwxr-xr-x 10 320 2022-04-08 22:40:52.012258343 +0200 _tmp -rw-r--r-- 1 8196 2022-03-09 04:10:13.278738177 +0100 .DS_Store -rw-r--r-- 1 49022 2022-03-16 13:37:21.047662608 +0100 BrokerKeys.kdbx $ ls --reverse -t | grep '^d' | tail -n 1 drwxr-xr-x 10 320 2022-04-08 22:40:52.012258343 +0200 _tmp 
Enter fullscreen mode Exit fullscreen mode

Well, I think you get the idea.
For your function loading woes, try zsh and put something like these lines in your ~/.zshrc file.

pmodload 'helper' # autoload functions ZSH_FUNC_DIR="${0:h}/.zsh/functions" if [[ -z ${fpath[(r)$ZSH_FUNC_DIR]} ]]; then fpath+=( "$ZSH_FUNC_DIR" ) autoload -U "$ZSH_FUNC_DIR"/*(.:t) fi 
Enter fullscreen mode Exit fullscreen mode

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.

# filename: dbg # Activate shell debug mode for next command. set -o xtrace; eval "$@"; rc=$?; set +o xtrace return $rc 
Enter fullscreen mode Exit fullscreen mode

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.

$ which dbg dbg () { local -a fpath fpath=("/Users/www-data/.zsh/functions") builtin autoload -X -U } $ dbg "echo oh yes" # output omitted $ which dbg dbg () { set -o xtrace eval "$@" rc=$? set +o xtrace return $rc } 
Enter fullscreen mode Exit fullscreen mode

An approach more similar to yours for sourcing the files is to use shell globbing instead of find. With the right shopt settings this one-liner in ~/.zshrc or ~/.bashrc should do the trick.

for file in .zsh/{functions,aliases}/*.zsh; do source "$(realpath "$file")" done 
Enter fullscreen mode Exit fullscreen mode

I strongly recommend you to try zsh + prezto, you just gonna love the tab completion!

Put that in your .zpreztorcas a starting point and play around with ssh and scp.

zstyle ':filter-select:highlight' matched fg=yellow,bold zstyle ':filter-select' max-lines 18 # restrict lines for filter-select zstyle ':filter-select' rotate-list yes # enable rotation for filter-select zstyle ':filter-select' case-insensitive yes # enable case-insensitive search zstyle ':filter-select' extended-search yes # see below zstyle ':filter-select' hist-find-no-dups yes # ignore duplicates in history source zstyle ':filter-select' escape-descriptions no # display literal newlines, not \n, etc zstyle ':completion:*' menu select zstyle ':completion:*' accept-exact '*(N)' zstyle ':completion::complete:*' cache-path "${XDG_CACHE_HOME:-$HOME/.cache}/zinit/zcompcache" # Set the entries to ignore in static */etc/hosts* for host completion. zstyle ':prezto:module:completion:*:hosts' etc-host-ignores \ '0.0.0.0' '127.0.0.1' 
Enter fullscreen mode Exit fullscreen mode

Some comments have been hidden by the post's author - find out more