Kind-of decorator functions in bash

bash Oct 29, 2024

The idea in Python

In Python, decorators are an elegant and flexible way to modify the behaviour of a function. The idea is simple: create a function that starts its execution before the function which behaviour we want to modify and this function will call the original function returning its result.

import datetime
from urllib.request import urlopen

def time_ellapsed(func):
    """
    Time wrapper function - It gets the start time before calling the original 
    function, it gets the end time after the original function returns and it
    returns the result provided by the original function and the time ellapsed
        
    This wrapper returns a tuple with the original result and the time ellapsed
    in milliseconds
    """
    def wrapper(*args, **kwargs):
        start_time = datetime.datetime.now(datetime.UTC)
        result = func(*args, **kwargs)
        end_time = datetime.datetime.now(datetime.UTC)
        # I know... the next line is not a best practice
        return result, int((end_time-start_time).microseconds/1000)
    return wrapper

@time_ellapsed
def curl_jicg_eu():
    """
    This simply opens an url and returns the "html" returned by the function.
    """
    url = "https://jicg.eu"
    f = urlopen(url)
    data = f.read()
    return data

if __name__ == "__main__":
    url_data, time_spent = curl_jicg_eu()
    print(f"Retrieved {len(url_data)} bytes in {time_spent} millis")

example of a decorator in Python: measures the time a function takes to execute.

My kind of bash decorators

So, following the same idea of "python decorators", I could write some function in bash to modify the behaviour of my classical linux commands. Lets show a few useful examples:

Helloween cat

I took the artwork from: https://www.asciiartcopy.com/halloween-ascii-art.html - Thank you.

As a first example of "wrapper" function, and given we are 3 days away from helloween, I created a new cat function this way:

cat() {
 if  [[ $@ == '/dev/null' ]]; then
/usr/bin/cat << EOT


                ████
               █░░███
               █░░████
                ███▒██     ████████
      ████████     █▒█  ████▒▒▒▒▒▒████
    ███▒▒▒▒▒▒████████████░░████▒▒▒▒▒███
  ██▒▒▒▒░▒▒████░░██░░░░██░░░░░█▒▒▒▒▒▒▒███
 ██▒▒░░░░▒██░░░░░█▒░░░░░██▒░░░░░░░▒▒▒▒▒▒█
██▒░░░░░▒░░░░░░░░░▒░░░░░░░▒▒░░░░░░░▒▒▒▒▒██
█░░░░░░▒░░░██░░░░░░░░░░░░░██░░░░░░░░▒▒▒▒▒█
█░░░░░░░░█▒▒███░░░░░░░░░█▒▒███░░░░░░░▒▒▒▒█
█░░░░░░░████████░░░░░░░████████░░░░░░▒▒▒▒█
█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒█
██░░░█░░░░░░░░░░░░░░░░░░░░░░░░░░░░░█░▒▒▒▒█
 █░░░░██░█░░░░░░░░░░░░░░░░░░░░░░░███▒▒▒▒▒█
 █▒▒░░░░█████░░░█░░░░██░░░██░░████░▒▒▒▒▒▒█
 ██▒▒░░░░░█████████████████████░░░▒▒▒▒▒▒██
  ██▒▒▒▒░░░░░██░░░███░░░██░░░█░░░▒▒▒▒▒▒██
   ███▒▒▒░░░░░░░░░░░░░░░░░░░░░░▒▒▒▒█████
     ███▒▒▒▒▒▒░░░░░░░░░░░░░▒▒▒▒▒▒████
        ██████████████████████████


EOT
 else
     /usr/bin/cat $@
 fi

}

cat as a function, wrapping the cat command.

This function behaves as a normal "cat" command, however if you type cat /dev/nullit will show a Helloween Pumkin.

Meassure the time spent by a command

As in the python example, I could measure the time spent by a command with this function:

function meassure() {
  local ts=$(date +%s%N)
  $@
  local r=$?
  tspent=$((($(date +%s%N) - $ts)/1000000))
  return r
}

The function will calculate the time spent in a command and will set the variable tspentto the time spent as shown here:

caveat: The variable tspent doesn't seem to be set if the output of the function is piped to another command. The workaround for this would be:

❯ r=$(meassure curl https://jicg.eu 2>/dev/null)
❯ echo $r | wc -c ; echo $tspent
32389
222
Same example as previous python example.

Log somewhere

Let's say we want to log one command to a file. Whatever the file is marked by the global variable MYLOGFILE. And if the variable doesn't exists, then it will take the default value of /tmp/mylogfile.log

logit() {
    MYLOGFILE=${MYLOGFILE:-"/tmp/mylogfile.log"}
    echo ">>>>>" $(date) ">>>>> " $@ >> $MYLOGFILE
    $@ | tee -a $MYLOGFILE
    local r=$?
    echo -e "<<<<<\n" >> $MYLOGFILE
    return r
}

So, if we test a couple of commands, then we'll check the "logging":

Final thoughts

It is funny and useful creating this kind of functions in order to help us making our scripts somehow more "controllable", to simplify writing scripts without repeating code or making that easier.

However, writing functions this way doesn't have the power we can have in python, as an example:

lsis an alias to ls --color. Inside the function, ls is no longer an alias. However if we used eval @! instead of @!would help us sometimes to run the alias instead of the plain command.

Tags