Skip to content

Instantly share code, notes, and snippets.

@izabera
Created February 1, 2016 03:48
Show Gist options
  • Save izabera/c87659f14818a0dd804f to your computer and use it in GitHub Desktop.
Save izabera/c87659f14818a0dd804f to your computer and use it in GitHub Desktop.
parse a block of code correctly even with heredocs or compound commands or pipelines or ...
#!/bin/bash
shopt -s expand_aliases extglob
__eval () {
unset __code __tmp __commands
while (( $# )); do
if [[ $1 && $1 = *[![:space:]]* ]]; then
printf "==== command ====\n%s\n=================\n" "$1"
time eval "$1"
fi
shift
done
}
__analyzetimer () {
local __code __commands=('') __tmp
IFS= read -rd '' -u"$__timer" __code
exec {__timer}>&-
# stupid way to check if a string is valid syntax
if [[ $(bash -O extglob -nc -- "$__code" 2>&1) ]]; then
# check syntax with bash -n, then check if bash printed an error message
# exit status is not enough, e.g.: bash -nc ': << x' returns 0
echo "that doesn't look like valid syntax" >&2
return 1
fi
# sorta-parser
# this could probably be cleaner with =~
# but this way BASH_REMATCH is kept intact
while [[ ${#__code} -ne 0 && ${__code} = *[$'\n&;#']* ]]; do
# get text up to (and including) next \n or & or ;
__tmp=${__code%%[$'\n&;#']*}
__tmp=${__code::${#__tmp}+1}
__code=${__code:${#__tmp}}
# add it to array
__commands[-1]+=$__tmp
# if this isn't valid syntax, we're inside one of:
# - comsub
# - procsub
# - compound command
# - some kind of quoting
# so we don't start a new command
if [[ ! $(bash -O extglob -nc -- "$__code" 2>&1) ]]; then
# check if the last character was escaped, in which case don't start a new one
__tmp=-2
# if this is a backslash, maybe it also was escaped
while [[ ${__commands[-1]:__tmp--:1} = '\' ]]; do
[[ ${__commands[-1]:__tmp--:1} = '\' ]] || break
done
# now if __tmp is *even*, we found an *odd* number of \ so it was escaped
if (( __tmp % 2 )); then
__tmp=0
# handle special cases
case ${__commands[-1]: -1} in
'#') # remove everything up to end of line
__commands[-1]=${__commands[-1]::-1}
__code=${__code#*$'\n'} ;;
'&') # if it's part of &&, don't start a new command
[[ ${__code::1} = '&' ]] && __tmp=1 ;;
esac
# if everything is ok, we're done!
if [[ __tmp -eq 0 && ! $(bash -O extglob -nc -- "${__commands[-1]}" 2>&1) ]]; then
# clean it up and start a new command
__tmp=${__commands[-1]##+([$' \t\n'])}
__commands[-1]=${__tmp%%+([$' \t\n'])}
__commands+=('')
fi
fi
fi
done
(( ${#__code} )) && commands+=("$__code")
__eval "${__commands[@]}"
}
alias $'timer{=__analyzetimer {__timer}<< \\}timer\n'
timer{
true &&
echo 'hello $( world )' ";" \\\; /!(bin|usr) \
foo bar || echo false \\ \\\\;
if false; then
echo it was true
else
case foo in
foo) echo 'it was false and it matched foo
newline';;
bar) echo something went really wrong ;;
esac
fi
echo "background: $BASHPID" & wait # you can also use comments
echo "foreground: $BASHPID"
while read -r; do
echo "${REPLY^^}!!!!"
done << x
this is $(echo sparta)
x
echo x | cat -n
echo y |& cat -n
}timer
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment