3
\$\begingroup\$

I wrote this script template at work to save typing repetitive commands and to improve my Bash-fu. I'd like to know of any moderate-to-severe quoting, robustness, security or usability issue. All code provided in answers should work in Bash 4.


.sedevrc:

#!/bin/bash
se_projects=()
se_cvs_dir="${USER}@172.0.0.1/export/home/cvsrootdir"
#============PROJECT CONFIG START================#
declare -A product
product[CVSROOT]="${se_cvs_dir}/product"
product[SOURCEDIR]="${HOME}/product"
product[WORKDIR]="${HOME}/test"
product[MAINPRG]="${product[SOURCEDIR]}/build/product"
se_projects+=("${!product@}")
#================================================#
# Other products in the same format as above
#=============PROJECT CONFIG END=================#

# se user commands
function secvs
{
    se_loop_projects "_se_cvs" "$@"
}

function sectags
{
    se_loop_projects "_se_ctags" "$@"
}

function semake
{
    se_loop_projects "_se_make" "$@"
}

function sealias
{
    se_loop_projects "_se_alias" "$@"
}

# se helper functions
function _se_cvs
{
    echo_info "Updating ${project}..."
    local cvsroot=${project}[CVSROOT]
    CVSROOT=:pserver:${!cvsroot:?}
    local sourcedir=${project}[SOURCEDIR]
    cd ${!sourcedir:?}
    cvs -q update -Pd && echo_noerror "Update ${project} complete" || echo_error "Update ${project} failed"
}

function _se_ctags
{
    echo_info "Tagging ${project}..."
    local sourcedir=${project}[SOURCEDIR]
    cd ${!sourcedir:?}/src
    { file tags | grep 'Ctags tag' >/dev/null; } || rm -f tags
    ctags -R --exclude=obj && echo_noerror "Tag ${project} complete" || echo_error "Tag ${project} failed"
}

function _se_make
{
    echo_info "Building ${project}..."
    local sourcedir=${project}[SOURCEDIR]
    cd ${!sourcedir:?}
    if [[ -d build ]] && (( $(bc <<< "$(date +%s) - $(date -r build +%s)") > 60 * 60 * 24 * 5 ))
    then
        make clean || echo_error "Clean ${project} failed"
    fi
    make && echo_noerror "Build ${project} complete" || echo_error "Build ${project} failed"
}

function _se_alias
{
    local workdir=${project}[WORKDIR]
    local mainprg=${project}[MAINPRG]
    alias ${project}="cd ${!workdir:?} && ${!mainprg:?}"
}

# Run an se helper function over selected projects
function se_loop_projects
{
    local helper_function_name=$(declare -F $1)
    if [[ ! ${helper_function_name} =~ ^_se_* ]]
    then
        echo_error "'$1' is not in the list of available se helper functions: $(declare -F | cut -d' ' -f3 | grep '^_se_*' | xargs)"
        if [[ -z $1 ]]
        then
            echo_error "Usage: ${FUNCNAME[0]} FUNCTION [PROJECTS]"
        fi
        return 1
    fi
    shift
    local -i project_count=$(( $# > 0 && $# < ${#se_projects[@]} ? $# : ${#se_projects[@]} ))
    for (( i=0; i<${project_count}; i++ ))
    do
        local project=${1:-${se_projects[$i]:?}}
        shift
        if [[ ${se_projects[@]} =~ ${project} ]]
        then
            local helper_function=helper_function_name
            if [[ ${helper_function_name} == '_se_alias' ]] # FIXME: hardcoding
            then
                ${!helper_function} # Safer than `eval '"${helper_function}"'`
            else
                set -e
                (${!helper_function}) &
                set +e
            fi
        else
            local IFS=','
            echo_warning "'${project}' is not in the list of available se projects: ${se_projects[*]}"
        fi
    done
    wait
    while (( $# > 0 ))
    do
        echo_warning "Too many arguments, omitting '$1'"
        shift
    done
}

# Customized echoes
echo_reset_color=$(tput sgr0)

function echo_info
{
    local echo_info_color=$(tput setaf 4) # Blue
    echo "${echo_info_color}$@${echo_reset_color}"
}

function echo_noerror
{
    local echo_noerror_color=$(tput setaf 2) # Green
    echo "${echo_noerror_color}$@${echo_reset_color}"
}

function echo_warning
{
    local echo_warning_color=$(tput setaf 3) # Orange
    >&2 echo "${echo_warning_color}$@${echo_reset_color}"
}

function echo_error
{
    local echo_error_color=$(tput setaf 1) # Red
    >&2 echo "${echo_error_color}$@${echo_reset_color}"
}

# Autocomplete project names for se user commands
complete -W "${se_projects[*]}" $(declare -F | cut -d' ' -f3 | grep '^se[a-z]\+' | xargs)

To run it interactively,

[gao@hostname ~]-bash4.1.2$ secvs product1
[1] 16516
Updating product1...
? src/hello.cc
M Makefile
Update product1 complete
[1]+  Done                    ( ${!helper_function} )
[gao@hostname ~]-bash4.1.2$ secvs hats product1 stackoverflow
'hats' is not in the list of available se projects: product1,product2
[1] 16590
Updating product1...
M Makefile
Update product1 complete
[1]+  Done                    ( ${!helper_function} )
Too many arguments, omitting 'stackoverflow'
[gao@hostname ~]-bash4.1.2$ sectags
[1] 16519
Tagging product2...
[2] 16520
Tagging product1...
Tag product1 complete
Tag product2 complete
[1]-  Done                    ( ${!helper_function} )
[2]+  Done                    ( ${!helper_function} )

To run it via crontab,

[gao@hostname ~]-bash4.1.2$ cat ~/cronjob
#!/bin/bash
# Nullify commands that would trigger unwanted mail notification when run by cron
function echo { return; }
function tput { return; }

# Run se user commands
source "${HOME}/.sedevrc"
secvs | grep '^C ' # Show only conflicts
sectags
semake >/dev/null
[gao@hostname ~]-bash4.1.2$ crontab -l
7 7 * * 1-5 ${HOME}/cronjob
\$\endgroup\$
2
\$\begingroup\$

I see some things that may help you improve your code.

Use the correct form for associative arrays

The code contains this line:

local cvsroot=${project}[CVSROOT]

But I am pretty sure that what was meant was this:

local cvsroot=${project[CVSROOT]}

Quote to prevent word splitting

This line needs quotes to prevent word splitting:

cd ${!sourcedir:?}

If the contents of sourcedir contains a path with an embedded space, the cd will fail. Add quotes to prevent this problem:

cd "${!sourcedir:?}"

The same issue exists for the last clause of the complete function.

Use || exit if a command fails

If the cd command mentioned above actually fails, the script will proceed anyway and this is unlikely to be desired. Instead, you could use this:

cd "${!sourcedir:?}" || exit 1

Use if-else if you need that functionality

One of the lines in the current code is this:

ctags -R --exclude=obj && echo_noerror "Tag ${project} complete" || echo_error "Tag ${project} failed"

The intent appears to be to print one string or the other depending on the outcome of the ctags program. However, be aware that the second message might be printed even if ctags runs without error. This is because echo_noerror might fail and then the echo_error function would be invoked. To make the code more robust, don't use this trick. Instead, use the plain old if-else construct:

if ctags -R --exclude=obj; then echo_noerror "Tag ${project} complete"; else echo_error "Tag ${project} failed"; fi

Or as I'd probably prefer formatting it:

if ctags -R --exclude=obj 
then 
    echo_noerror "Tag ${project} complete" 
else 
    echo_error "Tag ${project} failed"
fi

Understand the use of numeric variables

In this context:

for (( i=0; i<${project_count}; i++ ))

The ${} is not needed because it's a numeric variable. Instead, just write this:

for (( i=0; i<project_count; i++ ))

Don't mix string and array

In this line

echo "${echo_noerror_color}$@${echo_reset_color}"

The colors expand to strings, but the $@ is an array. To make it a string, use this instead:

echo "${echo_noerror_color}$*${echo_reset_color}"

There's more, but it's all I have time for at the moment.

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Thank you for the feedback. 1. I was using indirect reference for associative arrays, so ${!sourcedir} actually refers to ${product[SOURCEDIR]}. 2. If I enclose the last clause of the complete function in double quotes (single quotes would simply be wrong), running complete -p shows complete -W 'product1 product2' sealias sectags secvs semake in one line which doesn't autocomplete project names, whereas what I've posted would print four complete statements each with an se user command that does autocomplete. \$\endgroup\$ – Gao Dec 23 '16 at 13:31
  • \$\begingroup\$ 3. There is no need to add || exit as I've added set -e before running the se helper function, the effect of which is that the process will stop executing after any simple statement returns a non-zero exit code. 4. You are right, but I've found a way to avoid writing if-else or &&-|| and which further reduces code duplication. Maybe I'll post it as an answer on Monday. 2., 5. & 6. Advice taken. Thank you for teaching me new things and improving my understanding of Bash. \$\endgroup\$ – Gao Dec 23 '16 at 13:31

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.