1
\$\begingroup\$

I wrote this code in my .bashrc to remap ls to honor .hidden files. I'm pretty new to shell/bash scripting, so I feel like this is not the most performant way to handle this, and it has some limitations. I would appreciate any suggestions regarding performance improvements, limitation reduction, or common best practices for shell/bash scripting (or improving my post since I'm new to the StackExchange network).

# File names in .hidden files are ignored by ls
# NOTES: Not tolerant of empty lines (or lines with only whitespace) in the .hidden file(s)
#    If any argument doesn't begin with a '-', this assumes it's a file argument
#    If you pass multiple folders, it will essentially concatenate the .hidden files and ignore all files matching any of those names (so if you have a file name TEST in two folders, but only have TEST in the .hidden file of one folder, calling ls on both folders won't show either TEST file)
#    It does honor the -a flag
#    I don't know if there's a limit on total number of flags or total command length, but if there is, this may surpass it depending on the number of file names in the .hidden file
ls () {
    DEFAULT_ARGS="--color=auto" # Here, because I want colors (and you can't have a function and an alias with the same name)
    ALL=false
    A=0
    F=0
    I=0
    declare -a ARGS
    declare -a FILES
    declare -a IGNORE
    for a in $@
    do
        if [[ $a =~ ^-(a|[^-]+a) ]]; then # If the -a flag is present
            ALL=true
            break
        fi
        if [[ $a =~ ^[^-].* ]]; then # If the argument doesn't start with '-', add it to the Files array
            FILES[$F]=$a
            ((F++))
        else                 # Otherwise add it to the Args array
            ARGS[$A]=$a
            ((A++))
        fi
    done
    if $ALL; then
        command ls ${DEFAULT_ARGS} ${@}
        return
    else
        if [ ${#FILES[@]} -eq 0 ]; then # If no files/directories are present, explicitly add the implied .
            FILES[$F]="."
        fi
        for f in ${FILES[*]}
        do
            if [ -f "${f}/.hidden" ]; then # Read the .hidden file, if present, and generate the -I flags
                IGNORE[$I]=$(awk '{print "-I " $0}' "${f}/.hidden" | tr "\n" " ")
                ((I++))
            fi
        done
        command ls ${DEFAULT_ARGS} ${ARGS[*]} ${IGNORE[*]} ${FILES[*]}
        return
    fi
}

Note: A .hidden file is a file (named .hidden) containing a list of file names from the same directory that should be treated as if they begin with a '.'

\$\endgroup\$
2
\$\begingroup\$

Some suggestions:

  • Upper case names are by convention reserved for exported variables.
  • Use More Quotes™, especially around $@
  • Use arrays for constructing commands
  • Variables are by default declared in the global scope. It would be better to declare the variables in the function local.
  • Use getopt to parse options - the default pattern is a bit wonky but it's easier to deal with for complex cases.
  • You can use readarray to read .hidden into an array in a single command.
  • The returns are redundant.
  • in $@, or more correctly in "$@", is redundant. for argument does the same thing.
  • You don't need semicolons if you put then on the next line like you already do with do.
  • You shouldn't have to use index variables. For example, both times $F is used you can instead use FILES[${#FILES[@]}]=…
  • $* and its accomplice ${name[*]} split every argument on $IFS, and is therefore extremely rarely useful. Use $@ and ${name[@]} instead.
\$\endgroup\$
  • 1
    \$\begingroup\$ Thanks! Also, that wiki looks like it'll be a incredibly useful resource. Thanks for sharing that. \$\endgroup\$ – UrsineRaven Jul 1 at 16:06

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.