Skip to content

Instantly share code, notes, and snippets.

@jlinoff
Last active March 9, 2021 14:45
Show Gist options
  • Save jlinoff/011c06cc0d1cf6014f0f939ed4b3c2b2 to your computer and use it in GitHub Desktop.
Save jlinoff/011c06cc0d1cf6014f0f939ed4b3c2b2 to your computer and use it in GitHub Desktop.
fix-python-headers - bash tool to fix python file header references
#!/bin/bash
#
# Fix the first line of python a file to point to the
# new python interpreter.
#
# This mess required to make sure that script works on
# the Mac. Normally we would just use readlink.
set -e
RootDir=$(cd $(dirname $0) && pwd)
source $RootDir/libutils.sh
set +e
# ========================================================================
# Functions
# ========================================================================
function _help() {
cat <<EOF
USAGE
$BASENAME [OPTIONS] [FILES]
DESCRIPTION
Fix the shebang headers in python files.
This tool is useful for modifying python scripts
to use a different interpreter by default.
OPTIONS
-d, --dryrun Do a fry run.
Do not change any files.
-h, --help This help message.
-s HDR, --shebang-header HDR
The shebang header for the python file.
It must start with '#!'.
Here is an examle:
-s '#!/opt/cr/bin/python'
-v, --verbose Increase the verbosity.
Normally the program runs silently.
This option allows the user to see
what is happening.
-v Shows the phases of the operation.
-v -v Shows the files being modified.
-v -v -v Shows skipped non-python files.
-V, --version Print the program version and exit.
EXAMPLE
# Help
\$ $BASENAME -h
# Change all my programs to use python3.5.
\$ $BASENAME -s '#!/usr/local/bin/python3.5' myprogs/*
# Change all my programs to use python3.5 and
# show me what is happening.
\$ $BASENAME -v -s '#!/usr/local/bin/python3.5' myprogs/*
# Change all my programs to use python3.5 and
# really show me what is happening.
\$ $BASENAME -v -v -s '#!/usr/local/bin/python3.5' myprogs/*
# Change all my programs to the currently available python.
\$ $BASENAME -s '#!/usr/bin/env python'
EOF
exit 0
}
# ========================================================================
# Main
# ========================================================================
BASENAME=$(basename $0)
VERSION="1.0.0"
HEADER=""
FILES=()
DRYRUN=0
VERBOSE=0
while (( $# )) ; do
OPT="$1"
shift
case "$OPT" in
-d|--dryrun)
DRYRUN=1
;;
-h|--help)
_help
;;
-s|--shebang-header)
HEADER="$1"
shift
;;
-v|--verbose)
VERBOSE=$(( VERBOSE + 1 ))
;;
-V|--version)
echo "$BASENAME $VERSION"
exit 0
;;
-*)
_err "Unrecognized option '$OPT'."
;;
*)
FILES+=($OPT)
;;
esac
done
# Tell'em who we are.
(( VERBOSE )) && _banner "Fixing python shebang headers."
# Make sure that the user specified a header with a
# shebang. The reason that we enforce the existence
# of the shebang is purely for consistency. The user
# is replacing the entire header, not just a single
# part of it.
[[ -z "$HEADER" ]] && _err "Shebang header not specified."
[[ ! "$HEADER" =~ "#!" ]] && _err "Shebang header missing shebang (#!)."
# Walk through all of the files and directories specified
# and fix them.
# Ignore files that are not python executables.
# Provide a verbose mode that allows the developer
# to see what is going on (-v -v).
for FILE in ${FILES[@]} ; do
PYFILES=()
if [[ -e $FILE ]] ; then
if [[ -d $FILE ]] ; then
# This is a directory, process all of the files in it.
#PYFILES=($(file $FILE/* | grep -i 'python script text' | awk -F: '{print $1}'))
# Cannot reliably use the file command, it fails under certain
# conditions. Awk works much better. It is also very fast.
PYFILES=($(awk 'FNR==1 && /#!.*python/ {print FILENAME ": PYTHON : " $0; }; FNR>1 {nextfile}' $FILE/* | awk -F: '{print $1}'))
elif [[ -r $FILE ]] ; then
PYFILES=($(awk 'FNR==1 && /#!.*python/ {print FILENAME ": PYTHON : " $0; }; FNR>1 {nextfile}' $FILE | awk -F: '{print $1}'))
if (( ${#PYFILES} == 0 )) ; then
(( VERBOSE > 2 )) && _warn "File is not a python file: $FILE."
fi
else
(( VERBOSE )) && _warn "File is not readable, skipping: $FILE."
fi
else
(( VERBOSE )) && _warn "File does not exist: $FILE."
fi
# Now process the python files and update if necessary.
for PYFILE in ${PYFILES[@]} ; do
if [[ ! -w $PYFILE ]] ; then
(( VERBOSE )) && _warn "File is not writable: $FILE"
continue
fi
CURR_HEADER=$(head -1 $PYFILE)
if echo "$CURR_HEADER" | grep -q '#[ ]*!' ; then
if [[ "$CURR_HEADER" == "$HEADER" ]] ; then
(( VERBOSE > 1 )) && _info "Skipping $PYFILE, already updated."
else
if (( DRYRUN == 0 )) ; then
# This is not a dryrun, change the first line of
# the file.
(( VERBOSE > 1 )) && _info "Updating $PYFILE."
# There some odd cases where surrounding quotes are
# added. This causes execution to fail. The quote
# stripping removes them.
sed -i -e "1c$HEADER" $PYFILE
sed -i -e '1s/"//g' -e "1s/'//g" $PYFILE
else
(( VERBOSE > 1 )) && _info "Updating $PYFILE, dryrun, no change."
fi
fi
else
(( VERBOSE > 1 )) && _warn "Skipping $PYFILE, no header."
fi
done
done
# Tell them we done if they are interested.
(( VERBOSE )) && _info "Done."
#!/bin/bash
#
# Utilities for setup scripts.
#
# ================================================================
# Functions
# ================================================================
function __msg() {
local LineNo=$1
local Type=$2
local Code="$3"
shift
shift
shift
printf "$Code"
printf "%-28s %-7s %5s: " "$(date +'%Y-%m-%d %H:%M:%S %z %Z')" "$Type" $LineNo
echo "$*"
printf "\033[0m"
}
# Print an info message to stdout.
function _info() {
__msg ${BASH_LINENO[0]} "INFO" "\033[0m" $*
}
# Print an info message to stdout in green.
function _info_green() {
__msg ${BASH_LINENO[0]} "INFO" "\033[32m" $*
}
# Print an info message to stdout in red.
function _info_red() {
__msg ${BASH_LINENO[0]} "INFO" "\033[31m" $*
}
# Print an info message to stdout in bold.
function _info_bold() {
__msg ${BASH_LINENO[0]} "INFO" "\033[1m" $*
}
# Print a warning message to stderr and exit.
function _warn() {
__msg ${BASH_LINENO[0]} "WARNING" "\033[34m" $*
}
# Print an error message to stderr and exit.
function _err() {
__msg ${BASH_LINENO[0]} "ERROR" "\033[31m" $* >&2
exit 1
}
# Print an error message to stderr.
# Do not exit.
function _err_nox() {
__msg ${BASH_LINENO[0]} "ERROR" "\033[31m" $* >&2
}
# Decorate a command and exit if the return code is not zero.
function _exec() {
local Cmd="$*"
__msg ${BASH_LINENO[0]} "INFO" "\033[1m" "cmd.cmd=$Cmd"
eval "$Cmd"
local Status=$?
if (( $Status )) ; then
__msg ${BASH_LINENO[0]} "INFO" "\033[31m" "cmd.code=$Status"
__msg ${BASH_LINENO[0]} "ERROR" "\033[31m" "cmd.status=FAILED"
exit 1
else
__msg ${BASH_LINENO[0]} "INFO" "\033[32m" "cmd.code=$Status"
__msg ${BASH_LINENO[0]} "INFO" "\033[32m" "cmd.status=PASSED"
fi
}
# Execute a command quietly.
# Only decorate if an error occurs.
function _exeq() {
local Cmd="$*"
eval "$Cmd"
local Status=$?
if (( $Status )) ; then
__msg ${BASH_LINENO[0]} "INFO" "\033[1m" "cmd.cmd=$Cmd"
__msg ${BASH_LINENO[0]} "INFO" "\033[31m" "cmd.code=$Status"
__msg ${BASH_LINENO[0]} "ERROR" "\033[31m" "cmd.status=FAILED"
exit 1
fi
}
# Decorate a command. Do not exit if the return code is not zero.
function _exec_nox() {
local Cmd="$*"
__msg ${BASH_LINENO[0]} "INFO" "\033[1m" "cmd.cmd=$Cmd"
eval "$Cmd"
local Status=$?
if (( $Status )) ; then
__msg ${BASH_LINENO[0]} "INFO" "\033[31m" "cmd.code=$Status"
__msg ${BASH_LINENO[0]} "ERROR" "\033[31m" "cmd.status=FAILED (IGNORED)"
else
__msg ${BASH_LINENO[0]} "INFO" "\033[32m" "cmd.code=$Status"
__msg ${BASH_LINENO[0]} "INFO" "\033[32m" "cmd.status=PASSED"
fi
return $Status
}
# Banner.
function _banner() {
echo
echo "# ================================================================"
echo "# $*"
echo "# ================================================================"
}
# Get the version number.
function _get_version() {
local VERSION='1.0.0'
echo $VERSION
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment