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
1 Answer 1
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.
-
\$\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 thecomplete
function in double quotes (single quotes would simply be wrong), runningcomplete -p
showscomplete -W 'product1 product2' sealias sectags secvs semake
in one line which doesn't autocomplete project names, whereas what I've posted would print fourcomplete
statements each with an se user command that does autocomplete. \$\endgroup\$Gao– Gao2016年12月23日 13:31:44 +00:00Commented Dec 23, 2016 at 13:31 -
\$\begingroup\$ 3. There is no need to add
|| exit
as I've addedset -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 writingif-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– Gao2016年12月23日 13:31:59 +00:00Commented Dec 23, 2016 at 13:31