Hello, all. Attaching two patches: one that practically rewrites python_fix_shebang, and the other one that adds proper tests for it.
Major changes: 1. I replaced unclear space magic that attempted to handle corner cases with plain 'for i in ${shebang}' -- trying to match various python* patterns left-to-right to shebang. This specifically fixes corner cases like: /usr/bin/python2 python (not that it's meaningful but we mangle it correctly now -- always the leftmost matching thingie is replaced!) 2. I've added --quiet and --force options, the former to silence the 'fixing shebang in ...' output, the latter to force replacing even incompatible shebangs (e.g. python3 -> python2.7). 3. Added proper tests for a lot of cases, including corner cases like: /mnt/python2/usr/bin/python3 Please review. -- Best regards, Michał Górny
Index: python-utils-r1.eclass =================================================================== RCS file: /var/cvsroot/gentoo-x86/eclass/python-utils-r1.eclass,v retrieving revision 1.56 diff -u -B -r1.56 python-utils-r1.eclass --- python-utils-r1.eclass 26 May 2014 16:13:35 -0000 1.56 +++ python-utils-r1.eclass 13 Jun 2014 22:28:39 -0000 @@ -670,8 +670,7 @@ # don't use this at home, just call python_doscript() instead if [[ ${_PYTHON_REWRITE_SHEBANG} ]]; then - local _PYTHON_FIX_SHEBANG_QUIET=1 - python_fix_shebang "${ED%/}/${d}/${newfn}" + python_fix_shebang -q "${ED%/}/${d}/${newfn}" fi } @@ -935,7 +934,7 @@ } # @FUNCTION: python_fix_shebang -# @USAGE: <path>... +# @USAGE: [-f|--force] [-q|--quiet] <path>... # @DESCRIPTION: # Replace the shebang in Python scripts with the current Python # implementation (EPYTHON). If a directory is passed, works recursively @@ -947,13 +946,28 @@ # # Shebangs matching explicitly current Python version will be left # unmodified. Shebangs requesting another Python version will be treated -# as fatal error. +# as fatal error, unless --force is given. +# +# --force causes the function to replace even shebangs that require +# incompatible Python version. --quiet causes the function not to list +# modified files verbosely. python_fix_shebang() { debug-print-function ${FUNCNAME} "${@}" - [[ ${1} ]] || die "${FUNCNAME}: no paths given" [[ ${EPYTHON} ]] || die "${FUNCNAME}: EPYTHON unset (pkg_setup not called?)" + local force quiet + while [[ ${@} ]]; do + case "${1}" in + -f|--force) force=1; shift;; + -q|--quiet) quiet=1; shift;; + --) shift; break;; + *) break;; + esac + done + + [[ ${1} ]] || die "${FUNCNAME}: no paths given" + local path f for path; do local any_correct any_fixed is_recursive @@ -961,54 +975,88 @@ [[ -d ${path} ]] && is_recursive=1 while IFS= read -r -d '' f; do - local shebang=$(head -n 1 "${f}") - local error + local shebang i + local error from - case "${shebang} " in - '#!'*"${EPYTHON} "*) - debug-print "${FUNCNAME}: in file ${f#${D}}" - debug-print "${FUNCNAME}: shebang matches EPYTHON: ${shebang}" - - # Nothing to do, move along. - any_correct=1 - ;; - '#!'*python" "*|'#!'*python[23]" "*) - debug-print "${FUNCNAME}: in file ${f#${D}}" - debug-print "${FUNCNAME}: rewriting shebang: ${shebang}" - - # Note: for internal use. - if [[ ! ${_PYTHON_FIX_SHEBANG_QUIET} ]]; then - einfo "Fixing shebang in ${f#${D}}." - fi - - local from - if [[ "${shebang} " == *'python2 '* ]]; then - from=python2 - python_is_python3 "${EPYTHON}" && error=1 - elif [[ "${shebang} " == *'python3 '* ]]; then - from=python3 - python_is_python3 "${EPYTHON}" || error=1 - else - from=python - fi - - if [[ ! ${error} ]]; then - sed -i -e "1s:${from}:${EPYTHON}:" "${f}" || die - any_fixed=1 - fi - ;; - '#!'*python[23].[0123456789]" "*|'#!'*pypy" "*|'#!'*jython[23].[0123456789]" "*) - # Explicit mismatch. - error=1 - ;; - *) - # Non-Python shebang. Allowed in recursive mode, - # disallowed when specifying file explicitly. - [[ ${is_recursive} ]] || error=1 - ;; - esac + read shebang <"${f}" - if [[ ${error} ]]; then + # First, check if it's shebang at all... + if [[ ${shebang} == '#!'* ]]; then + # Match left-to-right in a loop, to avoid matching random + # repetitions like 'python2.7 python2'. + for i in ${shebang}; do + case "${i}" in + *"${EPYTHON}") + debug-print "${FUNCNAME}: in file ${f#${D}}" + debug-print "${FUNCNAME}: shebang matches EPYTHON: ${shebang}" + + # Nothing to do, move along. + any_correct=1 + from=${EPYTHON} + break + ;; + *python|*python[23]) + debug-print "${FUNCNAME}: in file ${f#${D}}" + debug-print "${FUNCNAME}: rewriting shebang: ${shebang}" + + if [[ ${i} == *python2 ]]; then + from=python2 + if [[ ! ${force} ]]; then + python_is_python3 "${EPYTHON}" && error=1 + fi + elif [[ ${i} == *python3 ]]; then + from=python3 + if [[ ! ${force} ]]; then + python_is_python3 "${EPYTHON}" || error=1 + fi + else + from=python + fi + break + ;; + *python[23].[0123456789]|*pypy|*jython[23].[0123456789]) + # Explicit mismatch. + if [[ ! ${force} ]]; then + error=1 + else + case "${i}" in + *python[23].[0123456789]) + from="python[23].[0123456789]";; + *pypy) + from="pypy";; + *jython[23].[0123456789]) + from="jython[23].[0123456789]";; + *) + die "${FUNCNAME}: internal error in 2nd pattern match";; + esac + fi + break + ;; + esac + done + fi + + if [[ ! ${error} && ! ${from} ]]; then + # Non-Python shebang. Allowed in recursive mode, + # disallowed when specifying file explicitly. + [[ ${is_recursive} ]] && continue + error=1 + fi + + if [[ ! ${quiet} ]]; then + einfo "Fixing shebang in ${f#${D}}." + fi + + if [[ ! ${error} ]]; then + # We either want to match ${from} followed by space + # or at end-of-string. + if [[ ${shebang} == *${from}" "* ]]; then + sed -i -e "1s:${from} :${EPYTHON} :" "${f}" || die + else + sed -i -e "1s:${from}$:${EPYTHON}:" "${f}" || die + fi + any_fixed=1 + else eerror "The file has incompatible shebang:" eerror " file: ${f#${D}}" eerror " current shebang: ${shebang}"
Index: python-utils-r1.sh =================================================================== RCS file: /var/cvsroot/gentoo-x86/eclass/tests/python-utils-r1.sh,v retrieving revision 1.6 diff -u -B -r1.6 python-utils-r1.sh --- python-utils-r1.sh 8 Apr 2014 16:05:30 -0000 1.6 +++ python-utils-r1.sh 13 Jun 2014 22:28:50 -0000 @@ -30,6 +30,33 @@ tend ${?} } +test_fix_shebang() { + local from=${1} + local to=${2} + local expect=${3} + local args=( "${@:4}" ) + + tbegin "python_fix_shebang${args[@]+ ${args[*]}} from ${from} to ${to} (exp: ${expect})" + + echo "${from}" > "${tmpfile}" + output=$( EPYTHON=${to} python_fix_shebang "${args[@]}" -q "${tmpfile}" 2>&1 ) + + if [[ ${?} != 0 ]]; then + if [[ ${expect} != FAIL ]]; then + echo "${output}" + tend 1 + else + tend 0 + fi + else + [[ $(<"${tmpfile}") == ${expect} ]] \ + || eerror "${from} -> ${to}: $(<"${tmpfile}") != ${expect}" + tend ${?} + fi +} + +tmpfile=$(mktemp) + inherit python-utils-r1 test_var EPYTHON python2_7 python2.7 @@ -66,4 +93,47 @@ test_is python_is_python3 jython2.7 1 test_is python_is_python3 pypy 1 +# generic shebangs +test_fix_shebang '#!/usr/bin/python' python2.7 '#!/usr/bin/python2.7' +test_fix_shebang '#!/usr/bin/python' python3.4 '#!/usr/bin/python3.4' +test_fix_shebang '#!/usr/bin/python' pypy '#!/usr/bin/pypy' +test_fix_shebang '#!/usr/bin/python' jython2.7 '#!/usr/bin/jython2.7' + +# python2/python3 matching +test_fix_shebang '#!/usr/bin/python2' python2.7 '#!/usr/bin/python2.7' +test_fix_shebang '#!/usr/bin/python3' python2.7 FAIL +test_fix_shebang '#!/usr/bin/python3' python2.7 '#!/usr/bin/python2.7' --force +test_fix_shebang '#!/usr/bin/python3' python3.4 '#!/usr/bin/python3.4' +test_fix_shebang '#!/usr/bin/python2' python3.4 FAIL +test_fix_shebang '#!/usr/bin/python2' python3.4 '#!/usr/bin/python3.4' --force + +# pythonX.Y matching (those mostly test the patterns) +test_fix_shebang '#!/usr/bin/python2.7' python2.7 '#!/usr/bin/python2.7' +test_fix_shebang '#!/usr/bin/python2.7' python3.2 FAIL +test_fix_shebang '#!/usr/bin/python2.7' python3.2 '#!/usr/bin/python3.2' --force +test_fix_shebang '#!/usr/bin/python3.2' python3.2 '#!/usr/bin/python3.2' +test_fix_shebang '#!/usr/bin/python3.2' python2.7 FAIL +test_fix_shebang '#!/usr/bin/python3.2' python2.7 '#!/usr/bin/python2.7' --force +test_fix_shebang '#!/usr/bin/pypy' pypy '#!/usr/bin/pypy' +test_fix_shebang '#!/usr/bin/pypy' python2.7 FAIL +test_fix_shebang '#!/usr/bin/pypy' python2.7 '#!/usr/bin/python2.7' --force +test_fix_shebang '#!/usr/bin/jython2.7' jython2.7 '#!/usr/bin/jython2.7' +test_fix_shebang '#!/usr/bin/jython2.7' jython3.2 FAIL +test_fix_shebang '#!/usr/bin/jython2.7' jython3.2 '#!/usr/bin/jython3.2' --force + +# fancy path handling +test_fix_shebang '#!/mnt/python2/usr/bin/python' python3.4 \ + '#!/mnt/python2/usr/bin/python3.4' +test_fix_shebang '#!/mnt/python2/usr/bin/python2' python2.7 \ + '#!/mnt/python2/usr/bin/python2.7' +test_fix_shebang '#!/mnt/python2/usr/bin/env python' python2.7 \ + '#!/mnt/python2/usr/bin/env python2.7' +test_fix_shebang '#!/mnt/python2/usr/bin/python2 python2' python2.7 \ + '#!/mnt/python2/usr/bin/python2.7 python2' +test_fix_shebang '#!/mnt/python2/usr/bin/python3 python2' python2.7 FAIL +test_fix_shebang '#!/mnt/python2/usr/bin/python3 python2' python2.7 \ + '#!/mnt/python2/usr/bin/python2.7 python2' --force + +rm "${tmpfile}" + texit
signature.asc
Description: PGP signature