ShellTree 5: Optimizing the Recursive Script by Removing Arrays and Adding a String

This post is part of an educational series on building a shell script to graphically display the structure of a directory.

Previously

  1. We broke down Dem Pilafian’s one line command to display a tree of a directory
  2. We broke down Dem Pilafian’s script that uses the one line command
  3. We modified the one line to show files, as well as directories
  4. We built a recursive function that successfully prints out the graphical tree

Goals

  1. Optimize the previously built recursive function.

Pre-requisites

  1. Understanding how to make recursive functions

Getting the code

Run the following command:

git clone https://github.com/amayem/shell-tree.git

In any step past step-0 you can get the code by issuing the following command but changing the step number to the appropriate one:

git checkout -f step-1

Step-13 Replacing the LevelFlags Arrays with a String

A lot of the current code has to do with the levelFlags array, and generating the string that precedes the actual directory/file name. Also notice that whenever we are listing the contents of one directory they all have the same string prefix. Instead of using the levelFlags array to generate the prefix string let’s simply pass the prefix string as a parameter to the function as follows:

listdir()
{
    local currentPath=$1 prefix=$2
    local -a currentDir=($(ls $1))
    local -i lastIndex=$((${#currentDir[*]} - 1)) index

    for ((index=0; index<lastIndex; index++))
    do
        printf "%s├─%s\n" $prefix ${currentDir[$index]}
        if [ -d "$currentPath/${currentDir[$index]}" ]; then
            listdir "$currentPath/${currentDir[$index]}" $prefix" │"
        fi  
    done

    if [ $lastIndex -ge 0 ]; then
        printf "%s└─%s\n" $prefix ${currentDir[$lastIndex]}
        if [ -d "$currentPath/${currentDir[$index]}" ]; then
            listdir "$currentPath/${currentDir[$index]}" $prefix"  "
        fi
    fi
}
listdir $PWD ' '

The output is as follows:

[ahmed@amayem .git]$ ./../tree.sh
HEAD├─
config├─
description├─
hooks├─
│├─applypatch-msg.sample
│├─commit-msg.sample
│├─post-update.sample
│├─pre-applypatch.sample
│├─pre-commit.sample
│├─pre-push.sample
│├─pre-rebase.sample
│├─prepare-commit-msg.sample
│└─update.sample
index├─
info├─
│└─exclude
logs├─
│├─HEAD
│└─refs
│├─heads
│└─master
│└─remotes
│└─origin
│├─HEAD
│└─master
objects├─
├─info
│└─pack
│├─pack-5d8c6d23ff13eded7a9d401ff91dae0f7fd6d00d.idx
│└─pack-5d8c6d23ff13eded7a9d401ff91dae0f7fd6d00d.pack
packed-refs├─
refs└─
heads├─
│└─master
remotes├─
│└─origin
│├─HEAD
tags└─

It seems that the contents of the base directory are being displayed before the tree stem, for example: HEAD├─. This is because the following line

printf "%s├─%s\n" $prefix ${currentDir[$index]}

isn’t registering $prefix as an actual variable. This is because of the IFS default. As mentioned in the man bash pages:

 IFS    The Internal Field Separator that is used for word splitting after expansion and to split lines into words with the read builtin command. The default value is ``<space><tab><newline>''.

What’s happening is that when we send a space as a parameter to the function it is not registered as a parameter because it is considered a field separator. What we need to to do is to remove the space from the IFS.

Step-14 Removing the Space from IFS

oldIFS=$IFS
IFS=$'\t\n'

listdir()
{
    local currentPath=$1 prefix=$2
    local -a currentDir=($(ls $1))
    local -i lastIndex=$((${#currentDir[*]} - 1)) index

    for ((index=0; index<lastIndex; index++))
    do
        printf "%s├─%s\n" $prefix ${currentDir[$index]}
        if [ -d "$currentPath/${currentDir[$index]}" ]; then
            listdir "$currentPath/${currentDir[$index]}" $prefix"│ "
        fi  
    done

    if [ $lastIndex -ge 0 ]; then
        printf "%s└─%s\n" $prefix ${currentDir[$lastIndex]}
        if [ -d "$currentPath/${currentDir[$index]}" ]; then
            listdir "$currentPath/${currentDir[$index]}" $prefix"  "
        fi
    fi
}
listdir $PWD ' '
IFS=$oldIFS

This produces the following:

[ahmed@amayem .git]$ ./../tree.sh 
 ├─COMMIT_EDITMSG
 ├─HEAD
 ├─config
 ├─description
 ├─hooks
 │ ├─applypatch-msg.sample
 │ ├─commit-msg.sample
 │ ├─post-update.sample
 │ ├─pre-applypatch.sample
 │ ├─pre-commit.sample
 │ ├─pre-push.sample
 │ ├─pre-rebase.sample
 │ ├─prepare-commit-msg.sample
 │ └─update.sample
 ├─index
 ├─info
 │ └─exclude
 ├─logs
 │ ├─HEAD
 │ └─refs
 │   ├─heads
 │   │ └─master
 │   └─remotes
 │     └─origin
 │       ├─HEAD
 │       └─master
 ├─objects
 │ ├─info
 │ └─pack
 │   ├─pack-5d8c6d23ff13eded7a9d401ff91dae0f7fd6d00d.idx
 │   └─pack-5d8c6d23ff13eded7a9d401ff91dae0f7fd6d00d.pack
 ├─packed-refs
 └─refs
   ├─heads
   │ └─master
   ├─remotes
   │ └─origin
   │   ├─HEAD
   │   └─master
   └─tags

Success.

Next Steps

  1. Allow for flags to be used that are compatible with ls
  2. Deal with bad input
  3. Make it a user friendly bash script

References

  1. Hikaru‘s contribution to this thread about modifying the IFS.

Ahmed Amayem has written 90 articles

A Web Application Developer Entrepreneur.