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

Attachment: signature.asc
Description: PGP signature

Reply via email to