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