5
\$\begingroup\$

I want ls to print a message when run on an empty or full of dotfiles directory. Instead of:

$ ls empty_dir/ dotfiles/
$ 

I want:

$ ls empty_dir/ dotfiles/
empty_dir is empty

dotfiles contains only hidden files
$ 

If dir contains only dotfiles, and if the ls command is able to show them, the following should appear:

$ ls dotfiles/
dotfiles contains only hidden files
$ ls dotfiles/ -A
.hidden_dir

I searched a solution for that behavior, but can't found anything, so end up to implement it myself, using zsh:

# Wrapper around ls function, printing dedicated messages
#  if directory is empty/full of hidden files.
#  Parse parameters in order to call verbosels_onefile correctly.
function verbosels() {
    # extract options and filepaths
    ls_options=""
    ls_filepaths=""
    for parameter in "$@"
    do
        #echo "PARAM: $parameter"
        # it's an option if it's start with a dash
        if [[ "$parameter" =~ ^-.* ]]; then
            #echo "OPTION"
            ls_options="$ls_options $parameter"
        else  # it's a path: call ls on it
            #echo "FILEPATH"
            # get escaped version of given filename
            filepath=$(print -r -- "${(q)parameter}")
            #echo "filepath: $filepath"
            ls_filepaths="$ls_filepaths $filepath"
        fi
    done
    # remove options surrounding spaces
    ls_options=$(echo "$ls_options" | tr -d "[:space:]")
    isnotfirst=  # set to true after the first iteration
    # call verbosels_onefile for each filepath
    for filepath in ${(z)ls_filepaths}
    do
        # print space only between ls calls
        if [ "$isnotfirst" ]; then
            echo ""  # line jump
        else
            isnotfirst=1
        fi
        #echo "CMD: |verbosels_onefile $filepath $ls_options|"
        verbosels_onefile $filepath $ls_options
    done
}




# Perform an ls call on only one directory/file, that must be in first parameter.
# The other parameters remain untouched.
# The ls call take into account any aliases on ls.
# This function is called by the higher level verbosels function.
function verbosels_onefile() {
    if [ "$1" ]; then
        1="$1"
    else
        1="."
    fi
    if [ -d "$1" ]; then
        # contains files (hidden included)
        if [ -n "$(command ls -A "$1")" ]; then
            if [ -n "$(command ls "$1")" ]; then
                ls "$@"
            else
                # the directory is not empty, and contains only hidden files:
                # print message only if the ls command returns nothing
                # NOTE: run the ls command twice. Could be costly.
                if [ -n "$(ls "$@")" ]; then
                    ls "$@"
                else
                    echo "$1 contains only hidden files" 1>&2
                fi
            fi
        else
            echo "$1 is empty" 1>&2
        fi
    elif [ -e "$1" ]; then
        ls "$@"
    else
        echo "$1 doesn't exists" 1>&2
    fi
}

Remarks:

  • seems over-complicated
  • non-perfect handling of some ls options, notably -l in case of only-dotfiles directory (print total 0)
  • if a directory contains only dot files, the ls command is runned twice. Could be costly if the directory contains a lot of dot files.

The following is not valid, because ls formatting (colors, columns,…) are not kept:

ls_result=$(ls "$@")
if [ "$ls_result" ]; then
    echo "$ls_result"
else
    echo "$1 contains only hidden files" 1>&2
fi

I'm using zsh, because it provides some easier treatment. A solution involving only generalist bash could be better because of the portability. I'm looking for any readability/efficiency improvement, eventually for modules/programs that already do the job.

\$\endgroup\$
  • 1
    \$\begingroup\$ Don't use the function name() {} syntax to declare functions. That notation is considered to be obsolete. Though not explicitly marked as deprecated in bash, using this notation is strongly discouraged, and not guaranteed to be supported (eg AT&T ksh) see bash-hackers wiki \$\endgroup\$ – Elias Van Ootegem Aug 30 '16 at 13:51
1
\$\begingroup\$

Portability issues

The function collects the given options and when it finally calls ls, it puts the options at the end. Unfortunately this doesn't work on OSX, where the options must come before the filenames.

Your remarks

  • seems over-complicated

Yes. Unfortunately, to get the behavior that you want, I don't think this can get much simpler.

  • non-perfect handling of some ls options, notably -l in case of only-dotfiles directory (print total 0)

Unfortunately, the only way to avoid that will only make the script even more complicated.

  • if a directory contains only dot files, the ls command is runned twice. Could be costly if the directory contains a lot of dot files.

Perhaps you forgot to count the 2 runs of command ls in the if-else chain. So in fact for each file the ls command is executed 4 times.

Filenames with spaces

This will not work when the filenames have spaces:

ls_filepaths="$ls_filepaths $filepath"

You can make it work with filenames with spaces, and at the same time cleaner, by using arrays.

"Remove options surrounding spaces"

I'm not sure what's going on here:

# remove options surrounding spaces
ls_options=$(echo "$ls_options" | tr -d "[:space:]")

For example -l -a would become -l-a which will not work.

Avoid negatives in variable names

It's generally not recommended to use negatives in variable names like isnotfirst, because it could lead to strange conditions like not isnotfirst, which is hard to read and confusing. I suggest to rename it to first and use ! "$first" in conditions for a negative meaning.

Terminology

# Wrapper around ls function, printing dedicated messages

ls is not a "function", it's a command. For example, verbosels is a function.

Looping over "$@"

When looping over "$@", you can omit the "$@". So instead of this:

for parameter in "$@"; do

You can write simply:

for parameter; do

Pattern matching

Instead of matching by regular expressions like this:

if [[ "$parameter" =~ ^-.* ]]; then

It would be slightly simpler to use pattern matching like this:

if [[ "$parameter" == -* ]]; then

Setting a variable to empty

Instead of this:

ls_options=""
ls_filepaths=""

You can simplify as:

ls_options=
ls_filepaths=

echo is the same as echo ""

You can replace echo "" with simply echo with no parameters.

\$\endgroup\$
  • \$\begingroup\$ Thanks ! I've followed all your remarks. The final code is here. \$\endgroup\$ – aluriak Aug 23 '16 at 9:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.