Turn any Bash function/alias into a hybrid executable/source-able script.
Tags: bash, tips, intermediate, function
Published: 2021-02-03 23:37:46

# Convert any Bash Function/Alias to Hybrid Executable and Source-able Script with Bash Completion

<sub>...works with arguments and stdin.</sub>

**<mark>NOTE:</mark>**
I will be refactoring `cronstat` (a function you'll find ***[below](#better-example)***) again to process the switches and arguments better. **COMING VERY SOON**, just not sure when.

![Skill Level](https://img.shields.io/static/v1.svg?label=Skill%20Level&message=Intermediate&color=ffc40d&style=for-the-badge&labelColor=1d1d1d&logo=linux)![Skill Level](https://img.shields.io/static/v1.svg?label=But&message=Beginner%20Friendly&color=00a300&style=for-the-badge&labelColor=1d1d1d&logo=linux)

  - [Introduction](#introduction)
  - [Motivation](#motivation)
  - [How it works](#how-it-works)
    - [Description](#description)
    - [Generic Example](#generic-example)
      - [To Execute](#to-execute)
      - [To Source With Completion](#to-source-with-completion)
    - [Better Example](#better-example)
      - [Example When Sourced](#example-when-sourced)
      - [Example When executed As Script](#example-when-executed-as-script)
  - [Methods For Organization](#methods-for-organization)
  - [Conclusion](#conclusion)

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

```Shell
#/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:

```Shell
 $ ./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`):

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

```Shell
#!/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

```Shell
 $ 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

```Shell
 $ 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 '-'.`

```Shell
# 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.