BASH recursion examples - part 2

BASH recursion examples - part 2

Created:30 Jan 2017 17:08:29 , in  Host development

This article is a follow-up on the subject of writing recursive code in BASH. In the first part of the series I focused on a few more popular Maths functions and how one could implement them in BASH. In this instalment focus shifts to tasks one often comes across whilst building automation programs.

Since all but one examples in this article are recursive functions, they contain call to 'recursive' right at the start of them. You can either remove the call or navigate to the first part of this series to get the relevant code. 'recursive' is a safety measure. It will prevent a recursive function from freezing your text console and possibly making your computer unresponsive if you decide to play with the code and make a mistake in the process.

Asking for permission to carry on

Here is the first example of recursive function.

Imagine your script needs to ask for permission to proceed to the next stage of some automation process. Perhaps, that next stage consists of deletion of some important directories and files. You need to get clear yes (y), no (n) or abort (q) from the user before the scripts moves on.

Below is a function that asks for permission to continue. It reads user input and unless it gets y, n or q ( termination condition ) it calls itself again. Entering y as an answer to a question the function asks will set $? to 0, the other two valid answers will set it to 1 (More elaborate code could also distinguish between n and q).


can_begin() {
  recursive
  local answer=${answer:-''}
  local regex='^(y|n|q)$'
  echo "Do you want to begin now ? [y/n/q]" 
  read -r answer
  [[ $answer =~ $regex ]] && {
    [[ $answer == 'y' ]] && {
      return
    } || {
      return 1
    }
  }
  can_begin
}

Here is how one would use the function:


if can_begin; then
  echo 'Beginning ...'
else
  echo 'Aborting ...'
fi

Installing dependencies

Next example is about a recursive function that installs programs (unless they are on the system already) with names stored in array called 'dependencies'. Internally, it uses apt-get utility to that ( root access is necessary ).


dependencies=( rsync unzip gpg )

install_dependencies(){
  recursive
  local -i i="${i:-${#dependencies[@]}}"
  (( i <= 0 )) && {
    return
  } 
   
  [[ -z $( which "${dependencies[$i-1]}" ) ]] && {
    echo "${dependencies[$i-1]} needs to be installed. Installing ..."
    apt-get install -y "${dependencies[$i-1]}"
  } || {
    echo "${dependencies[$i-1]} installed already."
  }
  ((i--))
  install_dependencies
} 

The function takes no arguments. Here is how one would call it:


install_dependencies

Arguably it is a bit overblown example. Normally you would go for a loop when dealing with an array.

Extending directory name

Suppose, you need a directory that is guaranteed not to exist before you create it.

The function below, called extend_dir_name, extends base directory name ( passed as the first argument ) by a word ( passed as the second argument ) recursively. It returns a new directory name, which is a combination of first and second argument joined with forward slash, only if that combination is not an existent directory name on the system. Otherwise it keeps on requesting for a new word.


extend_dir_name(){

  ext="$2"
  base="$1"
  regex='/$'
  
  [[ ! -d "$base" ]] && {
    echo "$base must be an existing directory."
    exit 1
  }

  [[ ! $base =~ $regex ]] && {
    base="$base/"
  }
  
  [[ -z "$ext" ]] && {
    echo "Type name of directory extension (single word):"
    read -r ext
  }
  
  expanded="${base}${ext}"
  [[ ! -d "$expanded" ]] && {
    # return the new name     
    echo "$expanded"
    return
  # try again  
  } || {
    ext=
    extend_dir_name "$base" $ext
  }
}

If you needed a new directory in /tmp you would use extend_dir_name as follows:


$ mkdir $( extend_dir_name /tmp/ tmp_dir_extension )

Checking for existing system user

Next up is a function that asks for an existing system user name to be provided. At first it checks global variable USER ( it should be declared in the script ) for the name. If it finds nothing assigned it uses 'read' to get a name. In the next step the function tries to verify the name using 'id' utility. It calls itself again if if finds there is no user with the name on the system.


check_user_exists(){
  recursive
  local user=${user:-${USER}}
  local exists=
  [[ -z "$user" ]] && {
    echo "Give existing user name:"
    read -r user
  }
  exists=$(id -u "$user" > /dev/null 2>&1; echo $?)
  [[ "$exists" == "0" ]] && {
    USER="$user"
    return
  }
  check_user_exists
}

Checking for existence of mysql database

Next function is somewhat similar to the previous one. When called with a name as its first argument it checks if a mysql database with the name exists on the system. When called with no arguments it ask for a name before carrying out the check. If it cannot find a database with the given name it will assign the name to DBNAME. Otherwise it will call itself.

When the function is called by the root user it looks up a name among all available database names, otherwise it sees only what is available to the current user.

For this function to work, mysql access (user and password) has to be configured in ~/.my.cnf (/root/.my.cnf for root user) file first.


check_mysqldb_exists() {
  local mysqldb=${mysqldb:-$1}
  local is_db=
  [[ -z "${mysqldb}" ]] && {
    echo "Specify database name:"
    read -r mysqldb
  }
 
  is_db=$(mysql -s -N -e "SELECT schema_name FROM information_schema.schemata WHERE schema_name = '${mysqldb}'" information_schema)
   
  [[ -z "$is_db" ]] && {
    DBNAME="${mysqldb}"
    echo "Setting DBNAME to ${mysqldb}"
    return
  } 
   
  echo "Database ${mysqldb} already exists."
   
  mysqldb=
  check_mysqldb_exists "$mysqldb"
}     

Escaping globs and quotes

Recursion is often leveraged to write filters. Here is one of these. escape_globs_qotes_rec accepts a string as its input. It scans it in search of glob characters (glob characters in BASH are *, ?, [, ] ) and quotes ( ' and " ) which then it escapes using backslash (backslash also gets escaped in the process).


escape_globs_qotes_rec(){
  local ESCD=${ESCD:-''}
  local -i count=${#1} 
  local str="$1"
 
  (( $count < 1 )) && { 
    echo "$ESCD"
    return
  }
   
  read -r -n 1 -d '' <<< "$str"
   
  case "$REPLY" in
    '\'| "'" | '"' | '*' | '?' | '[' | ']')
      ESCD+="\\${REPLY}"
    ;;
    *)
      ESCD+="$REPLY"
    ;;   
  esac
   
  escape_globs_qotes_rec "${str:1:$count}" 
}

Calling it might look like this:


escape_globs_qotes_rec "$(< file_with_data_to_escape )"

Unfortunately, possibly due to use of parameter expansions the function is rather slow. It is much slower than its equivalent that is based on while loop:


escape_globs_quotes(){
  ESCD=''
  while read -r -n 1 -d ''; do
    case "$REPLY" in
    '\'| "'" | '"' | '*' | '?' | '[' | ']')
      ESCD+='\'"$REPLY"
    ;;
    *)
      ESCD+="$REPLY"
    ;;
  esac 
  done <<< "$1"
  echo "$ESCD"
}

You would call escape_globs_quotes the same way as you did for escape_globs_quotes_rec :


escape_globs_quotes "$(< file_with_data_to_escape )"

Back to good old loops ...

Conclusion on recursion

As can be seen from the examples above, writing recursive code in BASH is not that different to what one would do in some other programming language. Although they might not be the fastest or the most concise of solutions at times, recursive functions feel tidy, elegant and end with an easy to understand termination case. These features do count.

This post was updated on 27 Apr 2017 16:16:29

Tags:  BASH ,  mysql ,  recursion 


Author, Copyright and citation

Author

Sylwester Wojnowski

Author of the above article, Sylwester Wojnowski, is sWWW admin and owner.He enjoys doing Maths and studying algorithms, writing code in scripting and command languages, Thrash Metal music and playing electric guitar.

Copyrights

©Copyright, 2019 Sylwester Wojnowski. This article may not be reproduced or published as a whole or in parts without permission from the author. If you share it, please give author credit and do not remove embedded links.

Computer code, if present in the article, is excluded from the above and licensed under GPLv3.

Citation

Cite this article as:

Wojnowski, Sylwester. "BASH recursion examples - part 2." From sWWW - Code For The Web . https://wojnowski.net.pl//main/index/bash-recursion-examples-part-2

Post navigation

Previous:
  Essential WordPress plugin hooks

Next:
  Arrays in BASH