#!/bin/bash

# $Header$

# bash replacement for the original euse by Arun Bhanu
# Author: Marius Mauch <genone@gentoo.org>
# Version: 0.2
# Licensed under the GPL v2

PROGRAM_NAME=euse
PROGRAM_VERSION=0.1

MAKE_CONF_PATH=/etc/make.conf
MAKE_GLOBALS_PATH=/etc/make.globals
MAKE_PROFILE_PATH=/etc/make.profile
MAKE_CONF_BACKUP_PATH=/etc/make.conf.euse_backup

[ -z "${MODE}" ] && MODE="showhelp"		# available operation modes: showhelp, showversion, showdesc, showflags, modify

parse_arguments() {
	if [ -z "${1}" ]; then
		return
	fi
	while [ -n "${1}" ]; do
		case "${1}" in
			-h | --help)    MODE="showhelp";;
			-v | --version) MODE="showversion";;
			-i | --info)    MODE="showdesc";;
			-l | --local)   SCOPE="local";;
			-g | --global)  SCOPE="global";;
			-a | --active)  MODE="showflags";;
			-E | --enable)  MODE="modify"; ACTION="add";;
			-D | --disable) MODE="modify"; ACTION="remove";;
			-P | --prune)   MODE="modify"; ACTION="prune";;
			-*)
				echo "ERROR: unknown option ${1} specified."
				echo
				MODE="showhelp"
				;;
			"%active")
				get_useflags
				ARGUMENTS="${ARGUMENTS} ${ACTIVE_FLAGS[9]}"
				;;
			*)
				ARGUMENTS="${ARGUMENTS} ${1}"
				;;
		esac
		shift
	done
}

error() {
	echo "ERROR: ${1}"
	set +f
	exit 1
}

get_real_path() {
	set -P
	cd "$1"
	pwd
	cd "$OLDPWD"
	set +P
}

check_sanity() {
	# file permission tests
	local descdir
	
	descdir="$(get_portdir)/profiles"
	
	[ ! -r "${MAKE_CONF_PATH}" ] && error "${MAKE_CONF_PATH} is not readable"
	[ ! -r "${MAKE_GLOBALS_PATH}" ] && error "${MAKE_GLOBALS_PATH} is not readable"
	[ ! -h "${MAKE_PROFILE_PATH}" ] && error "${MAKE_PROFILE_PATH} is not a symlink"
	[ -z "$(get_portdir)" ] && error "\$PORTDIR couldn't be determined"
	[ ! -d "${descdir}" ] && error "${descdir} does not exist or is not a directory"
	[ ! -r "${descdir}/use.desc" ] && error "${descdir}/use.desc is not readable"
	[ ! -r "${descdir}/use.local.desc" ] && error "${descdir}/use.local.desc is not readable"
	[ ! -r "$(get_make_defaults)" ] && error "$(get_make_defaults) is not readable"
	[ "${MODE}" == "modify" -a ! -w "${MAKE_CONF_PATH}" ] && error ""${MAKE_CONF_PATH}" is not writable"
}

showhelp() {
	echo "${PROGRAM_NAME} v${PROGRAM_VERSION}"
	echo
	echo "Syntax: ${PROGRAM_NAME} <option> [suboptions] [useflaglist]"
	echo
	echo "Options: -h, --help      - show this message"
	echo "         -v, --version   - show version information"
	echo "         -i, --info      - show descriptions for the given useflags"
	echo "         -g, --global    - show only global use flags (suboption)"
	echo "         -l, --local     - show only local use flags (suboption)"
	echo "         -a, --active    - show currently active useflags and their origin"
	echo "         -E, --enable    - enable the given useflags"
	echo "         -D, --disable   - disable the given useflags"
	echo "         -P, --prune     - remove all references to the given flags from"
	echo "                           make.conf to revert to default settings"
	echo
	echo "Notes: ${PROGRAM_NAME} currently only works for global flags defined"
	echo "       in make.globals, make.defaults or make.conf, it doesn't handle"
	echo "       use.defaults, use.mask or package.use yet (see portage(5) for details on"
	echo "       these files). It also might have issues with cascaded profiles."
	echo "       If multiple options are specified only the last one will be used."
}

showversion() {
	echo "${PROGRAM_NAME} v${PROGRAM_VERSION}"
	echo "Written by Marius Mauch"
	echo
	echo "Copyright (C) 2004 Gentoo Foundation, Inc."
	echo "This is free software; see the source for copying conditions."
}

# the following function creates a bash array ACTIVE_FLAGS that contains the
# global use flags, indexed by origin: 0: environment, 1: make.conf, 
# 2: make.defaults, 3: make.globals
get_useflags() {
	# only calculate once as calling emerge is painfully slow
	[ -n "${USE_FLAGS_CALCULATED}" ] && return
	
	# backup portdir so get_portdir() doesn't give false results later
	portdir_backup="${PORTDIR}"

	ACTIVE_FLAGS[0]="${USE}"
	USE=""
	source "${MAKE_CONF_PATH}"
	ACTIVE_FLAGS[1]="${USE}"
	USE=""
	for x in $(get_all_make_defaults); do
		source "${x}"
		ACTIVE_FLAGS[2]="${ACTIVE_FLAGS[2]} ${USE}"
	done
	USE=""
	source "${MAKE_GLOBALS_PATH}"
	ACTIVE_FLAGS[3]="${USE}"

	# restore saved env variables
	USE="${ACTIVE_FLAGS[0]}"
	PORTDIR="${portdir_backup}"

	# get the currently active USE flags as seen by portage, this has to be after
	# restoring USE or portage won't see the original environment
	ACTIVE_FLAGS[9]="$(emerge --info | grep 'USE=' | cut -b 5- | sed -e 's:"::g')" #'
	USE_FLAGS_CALCULATED=1
}

# get the list of all known USE flags by reading use.desc and/or use.local.desc
# (depending on the value of $SCOPE)
get_useflaglist() {
	local descdir

	descdir="$(get_portdir)/profiles"
	
	if [ -z "${SCOPE}" -o "${SCOPE}" == "global" ]; then
		egrep "^[^# ]+ +-" "${descdir}/use.desc" | cut -d\  -f 1
	fi
	if [ -z "${SCOPE}" -o "${SCOPE}" == "local" ]; then
		egrep "^[^# :]+:[^ ]+ +-" "${descdir}/use.local.desc" | cut -d: -f 2 | cut -d\  -f 1
	fi
}

# get all make.defaults by traversing the cascaded profile directories
get_all_make_defaults() {
	local curdir
	local parent
	local rvalue
	
	curdir="$(get_real_path ${MAKE_PROFILE_PATH})"
	
	while [ -f "${curdir}/parent" ]; do
		[ -f "${curdir}/make.defaults" ] && rvalue="${curdir}/make.defaults ${rvalue}"
		parent="$(egrep -v '(^#|^ *$)' ${curdir}/parent)"
		curdir="$(get_real_path ${curdir}/${parent})"
	done

	echo "${rvalue}"
}

# get the path to make.defaults by traversing the cascaded profile directories
get_make_defaults() {
	local curdir
	local parent
	
	curdir="$(get_real_path ${MAKE_PROFILE_PATH})"
	
	while [ ! -f "${curdir}/make.defaults" -a -f "${curdir}/parent" ]; do
		parent="$(egrep -v '(^#|^ *$)' ${curdir}/parent)"
		curdir="$(get_real_path ${curdir}/${parent})"
	done

	echo "${curdir}/make.defaults"
}

# little helper function to get the status of a given flag in one of the 
# ACTIVE_FLAGS elements. Arguments are 1: flag to test, 2: index of ACTIVE_FLAGS,
# 3: echo value for positive (and as lowercase for negative) test result, 
# 4 (optional): echo value for "missing" test result, defaults to blank
get_flagstatus_helper() {
	if echo " ${ACTIVE_FLAGS[${2}]} " | grep " ${1} " > /dev/null; then
		echo -n "${3}"
	elif echo " ${ACTIVE_FLAGS[${2}]} " | grep " -${1} " > /dev/null; then
		echo -n "$(echo ${3} | tr [[:upper:]] [[:lower:]])"
	else
		echo -n "${4:- }"
	fi
}

# prints a status string for the given flag, each column indicating the presence
# for portage, in the environment, in make.conf, in make.defaults and in make.globals.
# full positive value would be "[+ECDG]", full negative value would be [-ecdg],
# full missing value would be "[-    ]" (portage only sees present or not present)
get_flagstatus() {
	get_useflags

	echo -n '['
	get_flagstatus_helper "${1}" 9 "+" "-"
	get_flagstatus_helper "${1}" 0 "E"
	get_flagstatus_helper "${1}" 1 "C"
	get_flagstatus_helper "${1}" 2 "D"
	get_flagstatus_helper "${1}" 3 "G"
	echo -n '] '
}

# faster replacement to `portageq portdir`
get_portdir() {
	if [ -z "${PORTDIR}" ]; then
		use_backup="${USE}"
		source "${MAKE_GLOBALS_PATH}"
		for x in $(get_all_make_defaults); do
			source "${x}"
		done
		source "${MAKE_CONF_PATH}"
		USE="${use_backup}"
	fi
	echo "${PORTDIR}"
}

# This function takes a list of use flags and shows the status and 
# the description for each one, honoring $SCOPE
showdesc() {
	local descdir
	local current_desc
	local found_one
	local args
	
	args="${*:-*}"
	
	if [ -z "${SCOPE}" ]; then
		SCOPE="global" showdesc ${args}
		echo
		SCOPE="local" showdesc ${args}
		return
	fi
	
	descdir="$(get_portdir)/profiles"
	
	[ "${SCOPE}" == "global" ] && echo "global use flags (searching: ${args})"
	[ "${SCOPE}" == "local" ] && echo "local use flags (searching: ${args})"
	echo "************************************************************"
	
	if [ "${args}" == "*" ]; then
		args="$(get_useflaglist | sort -u)"
	fi
	
	set ${args}
	
	foundone=0
	while [ -n "${1}" ]; do
		if [ "${SCOPE}" == "global" ]; then
			if grep "^${1}  *-" "${descdir}/use.desc" > /dev/null; then
				get_flagstatus "${1}"
				foundone=1
			fi
			grep "^${1}  *-" "${descdir}/use.desc"
		fi
		# local flags are a bit more complicated as there can be multiple 
		# entries per flag and we can't pipe into printf
		if [ "${SCOPE}" == "local" ]; then
			if grep ":${1}  *-" "${descdir}/use.local.desc" > /dev/null; then
				foundone=1
			fi
			grep ":${1}  *-" "${descdir}/use.local.desc" \
				| sed -e "s/^\([^:]\+\):\(${1}\) *- *\(.\+\)/\1|\2|\3/g" \
				| while read line; do
					pkg="$(echo $line | cut -d\| -f 1)"
					flag="$(echo $line | cut -d\| -f 2)"
					desc="$(echo $line | cut -d\| -f 3)"
					get_flagstatus "${flag}"
					printf "%s (%s):\n%s\n\n" "${flag}" "${pkg}" "${desc}"
				done
		fi
		shift
	done
	
	if [ ${foundone} == 0 ]; then
		echo "no matching entries found"
	fi
}

# show a list of all currently active flags and where they are activated
showflags() {
	local args

	get_useflags
	
	args="${*:-*}"
	
	if [ "${args}" == "*" ]; then
		args="$(get_useflaglist | sort -u)"
	fi
	
	set ${args}
	
	while [ -n "${1}" ]; do
		if echo " ${ACTIVE_FLAGS[9]} " | grep " ${1} " > /dev/null; then
			printf "%-20s" ${1}
			get_flagstatus ${1}
			echo
		fi
		shift
	done
}

# two small helpers to add or remove a flag from a USE string
add_flag() {
	NEW_MAKE_CONF_USE="${NEW_MAKE_CONF_USE} ${1}"
}

remove_flag() {
	NEW_MAKE_CONF_USE="${NEW_MAKE_CONF_USE// ${1} / }"
}

# USE flag modification function. Mainly a loop with calls to add_flag and 
# remove_flag to create a new USE string which is then inserted into make.conf.
modify() {
	if [ -z "${*}" ]; then
		if [ "${ACTION}" != "prune" ]; then
			echo "WARNING: no USE flags listed for modification, do you really"
			echo "         want to ${ACTION} *all* known USE flags?"
			echo "         If you don't please press Ctrl-C NOW!!!"
			sleep 5
			set $(get_useflaglist | sort -u)
		fi
	fi
	
	get_useflags
	
	NEW_MAKE_CONF_USE=" ${ACTIVE_FLAGS[1]} "
	
	while [ -n "${1}" ]; do
		if [ "${ACTION}" == "add" ]; then
			if echo " ${NEW_MAKE_CONF_USE} " | grep " ${1} " > /dev/null; then
				shift
			elif echo " ${NEW_MAKE_CONF_USE} " | grep " -${1} " > /dev/null; then
				remove_flag "-${1}"
			else
				add_flag "${1}"
				shift
			fi
		elif [ "${ACTION}" == "remove" ]; then
			if echo " ${NEW_MAKE_CONF_USE} " | grep " -${1} " > /dev/null; then
				shift
			elif echo " ${NEW_MAKE_CONF_USE} " | grep " ${1} " > /dev/null; then
				remove_flag "${1}"
			else
				add_flag "-${1}"
				shift
			fi
		elif [ "${ACTION}" == "prune" ]; then
			if echo " ${NEW_MAKE_CONF_USE} " | grep " ${1} " > /dev/null; then
				remove_flag "${1}"
			elif echo " ${NEW_MAKE_CONF_USE} " | grep " -${1} " > /dev/null; then
				remove_flag "-${1}"
			fi
			shift
		fi
	done
	
	#echo "old flags:"
	#echo ${ACTIVE_FLAGS[1]}
	#echo
	#echo "new flags:"
	#echo ${NEW_MAKE_CONF_USE}
	
	# a little loop to add linebreaks so we don't end with one ultra-long line
	NEW_MAKE_CONF_USE_2=""
	for x in ${NEW_MAKE_CONF_USE}; do
		if [ $(((${#NEW_MAKE_CONF_USE_2}%70)+${#x}+2)) -gt 70 ]; then
			NEW_MAKE_CONF_USE_2="${NEW_MAKE_CONF_USE_2}\\ \\n     $x "
		else
			NEW_MAKE_CONF_USE_2="${NEW_MAKE_CONF_USE_2}${x} "
		fi
	done

	# make a backup just in case the user doesn't like the new make.conf
	cp -p "${MAKE_CONF_PATH}" "${MAKE_CONF_BACKUP_PATH}"
	
	# as sed doesn't really work with multi-line patterns we have to replace USE
	# on our own here. Basically just skip everything between USE=" and the 
	# closing ", printing our new USE line there instead.
	inuse=0
	had_use=0
	(while read -r line; do
		[ "${line:0:4}" == "USE=" ] && inuse=1
		[ "${inuse}" == "0" ] && echo -E "${line}"
		if [ "${inuse}" == "1" ] && echo "${line}" | egrep '" *(#.*)?$' > /dev/null; then
			echo -n 'USE="'
			echo -ne "${NEW_MAKE_CONF_USE_2%% }"
			echo '"'
		 	inuse=0
			had_use=1
		fi
	done
	if [ ${had_use} -eq 0 ]; then
		echo -n 'USE="'
		echo -ne "${NEW_MAKE_CONF_USE_2%% }"
		echo '"'
	fi ) < "${MAKE_CONF_BACKUP_PATH}" | sed -e 's:\\ $:\\:' > "${MAKE_CONF_PATH}"
	
	echo "${MAKE_CONF_PATH} was modified, a backup copy has been placed at ${MAKE_CONF_BACKUP_PATH}"
}

##### main program comes now #####

# disable globbing as it fucks up with args=*
set -f
parse_arguments "$@"
check_sanity

eval ${MODE} ${ARGUMENTS}
set +f
