#!/bin/sh

###############################################################################

# Configuration file

CONFIG_FILE=%PREFIX%/etc/pkgfe.conf


# Paths

PORTS_PATH=${PORTSDIR:-/usr/ports}

PKGDB_PATH=${PKG_DBDIR:-/var/db/pkg}

TEMP_DIR=${TMPDIR:-/tmp}


# Miscellaneous

PAGER_PROGRAM=${PAGER:-less}


###############################################################################

#
# Returns program name without path.
#
prog()
{
	basename "$0"
}


#
# Returns "s" if value is not "1".
#
plural()
{
	if [ $1 -ne 1 ]
	then
		echo 's'
	fi
}


#
# Removes file
#
unlink_file()
{
	if [ "$1" -a -f "$1" ]
	then
		rm -f "$1"
	fi
}


#
# Prints error and exists.
#
error()
{
	if [ "$1" ]
	then
		echo "`prog`: $1"
		
		cleanexit 1
	fi
}


#
# Prints error.
#
notice()
{
	if [ "$1" ]
	then
		echo "`prog`: $1"
	fi
}


#
# Does clean exit.
#
cleanexit()
{
	local file
	
	
	if [ ! "$LOCKFILE" ]
	then
		error 'internal error: $LOCKFILE is empty'
	else
		unlink_file "$LOCKFILE"
	fi
	
	
	#if [ ! "$TEMP_PREFIX" ]
	#then
	#	notice 'internal error: $TEMP_PREFIX is empty'
	#	
	#else
	#	for file in "$TEMP_PREFIX."???
	#	do
	#		unlink_file "$file"
	#	done
	#fi
	
	
	exit ${1:-0}
}


#
# Prints error when command fails.
#
exitcode()
{
	if [ $# -ne 2 ]
	then
		return 1
	fi
	
	
	echo
	error "command '$1' failed with exit code $2"
}


#
# Show command before executing it.
#
showcmd()
{
	if [ ! "$1" ]
	then
		return 1
	fi
	
	echo
	echo "> $1"
}


#
# Checks boolean configuration paramaters.
#
checkyesno()
{
	eval local value=\$$1
	
	
	case $value
	in
		[Yy][Ee][Ss]|[Tt][Rr][Uu][Ee]|[Oo][Nn]|1)
			return 0
			;;
		
		[Nn][Oo]|[Ff][Aa][Ll][Ss][Ee]|[Oo][Ff][Ff]|0)
			return 1
			;;
		
		*)
			error "variable $1 is not set properly in $CONFIG_FILE"
			return 1
			;;
	esac
}


#
# Checks if command is failed
# (exit code is greater than zero).
#
cmd_failed()
{
	if [ "$1" -gt 0 ]
	then
		return 0
	else
		return 1
	fi
}


#
# Creates temporary files.
#
make_temp()
{
	if [ ! "$TEMP_PREFIX" ]
	then
		error 'internal error: $TEMP_PREFIX is empty'
	fi
	
	
	mktemp "$TEMP_PREFIX.XXX" || \
	error 'mktemp error'
}


#
# Shows menu or checklist using dialog(1).
#
show_dialog()
{
	local name text options type options_tempfile exitcode selected_options
	
	
	if [ $# -ne 3 -a $# -ne 4 ]
	then
		return 1
	fi
	
	
	name="$1"
	text="$2"
	options="$3"
	
	type=checklist
	
	
	case "$4"
	in
		''|checklist)
			options_tempfile=`make_temp`
			;;
		menu)
			type="$4"
			;;
		*)
			return 1
			;;
	esac
	
	
	if [ ! "$options" ]
	then
		error 'internal error: $options is empty'
	fi
	
	
	eval "dialog --title '`prog`: $name' \
	--$type '\n$text\n' \
	-1 -1 $dialog_list_rows \
	$options \
	2> '${options_tempfile:-/dev/null}'"
	
	
	exitcode=$?
	
	eval selopt_${name}_exitcode=\$exitcode
	
	
	if [ "$type" = checklist ]
	then
		selected_options=`sed 's|"||g' < "$options_tempfile"`
		
		unlink_file "$options_tempfile"
		
		
		eval selopt_${name}_values="\$selected_options"
	fi
}


#
# Shows package deinstall dialog.
#
package_deinstall()
{
	local package NUM DIALOG_OPTIONS PACKAGES
	
	
	PACKAGES="$1"
	
	
	if [ ! "$PACKAGES" ]
	then
		echo
		echo 'No packages selected for deinstall'
		echo
		
		cleanexit
	fi
	
	
	for package in $PACKAGES
	do
		DIALOG_OPTIONS="$DIALOG_OPTIONS '$package' ' '"
		
		NUM=$(($NUM + 1))
	done
	
	
	show_dialog deinstall "Deinstall $NUM package`plural $NUM`?\nARE YOU SURE?" "$DIALOG_OPTIONS" menu
	
	unset NUM DIALOG_OPTIONS
	
	
	if cmd_failed $selopt_deinstall_exitcode
	then
		echo
		echo 'Deinstall cancelled'
		echo
		
		cleanexit
	fi
	
	
	PKG_DEINSTALL_COMMAND="pkg_deinstall $PACKAGES"
	
	
	if ! checkyesno enable_real_deinstall
	then
		cat <<- EOF
		
		Execute that manually:
		
		$PKG_DEINSTALL_COMMAND
		
		EOF
		
		cleanexit
	fi
	
	
	showcmd "$PKG_DEINSTALL_COMMAND"
	
	$PKG_DEINSTALL_COMMAND
	
	
	if cmd_failed $?
	then
		exitcode "$PKG_DEINSTALL_COMMAND" $?
	
	else
		if checkyesno listup_after_deinstall
		then
			echo
			do_list
		fi
	fi
	
	cleanexit
}


#
# Command:
#  shows help. for more info, look EOF.
#
do_help()
{
	grep -A100 '^#__HELP__$' "$0" | \
	sed "s|^#||; s|^__HELP__||; s|%P%|`prog`|g; s|%PORTS_PATH%|$PORTS_PATH|g; s|%PKGDB_PATH%|$PKGDB_PATH|g"
	
	cleanexit
}


#
# Command:
#  shows list of outdated packages.
#
do_list()
{
	local tempfile count PORTVERSION_COMMAND exitcode
	
	
	tempfile=`make_temp`
	
	
	PORTVERSION_COMMAND="portversion -vl <"
	
	showcmd "$PORTVERSION_COMMAND"
	
	$PORTVERSION_COMMAND > "$tempfile"
	
	
	exitcode=$?
	
	if cmd_failed $exitcode
	then
		unlink_file "$tempfile"
		
		exitcode "$PORTVERSION_COMMAND" $exitcode
	fi
	
	
	sed 's|<||; s|needs updating (port has ||; s|) *$||' \
	< "$tempfile" \
	> "$status_file"
	
	unlink_file "$tempfile"
	
	
	if ! checkyesno ignore_held
	then
		tempfile=`make_temp`
		
		cat "$status_file" > "$tempfile"
		
		grep -v '\[held\]' < "$tempfile" > "$status_file"
		
		unlink_file "$tempfile"
	fi
	
	
	count=`wc -l "$status_file" | awk '{print $1}'`
	
	echo
	echo '-----------------------------------'
	echo "Found $count outdated package`plural $count`"
	echo '-----------------------------------'
	
	if [ "$count" -eq 0 -a "$1" = exit-if-none ]
	then
		cleanexit
	fi
	
	echo
}


#
# Command:
#  updates ports tree using csup(1).
#
do_csup()
{
	local old_wd CSUP_COMMAND PORTSDB_COMMAND
	
	
	check_supfile "$csup_supfile"
	
	
	CSUP_COMMAND="csup $csup_args $csup_supfile"
	
	showcmd "$CSUP_COMMAND"
	
	$CSUP_COMMAND
	
	
	if cmd_failed $?
	then
		exitcode "$CSUP_COMMAND" $?
	fi
	
	
	echo
	showcmd "cd $PORTS_PATH && make fetchindex"
	
	old_wd=`pwd`
	
	cd $PORTS_PATH && make fetchindex && \
	cd "$old_wd" || cleanexit 1
	
	
	PORTSDB_COMMAND="portsdb -u"
	
	echo
	showcmd "$PORTSDB_COMMAND"
	
	$PORTSDB_COMMAND
	
	
	if cmd_failed $?
	then
		exitcode "$PORTSDB_COMMAND" $?
	fi
	
	
	echo
	do_list
}


#
# Command:
#  fixes package database.
#
do_fixdb()
{
	local PKGDB_COMMAND
	
	
	PKGDB_COMMAND="pkgdb -F"
	
	showcmd "$PKGDB_COMMAND"
	
	$PKGDB_COMMAND
	
	
	if cmd_failed $?
	then
		exitcode "$PKGDB_COMMAND" $?
	fi
	
	
	echo
	
	cleanexit
}


#
# Command:
#  does search in ports tree.
#
do_find()
{
	local string type
	
	
	string="$1"
	
	type=name
	
	
	case "$1"
	in
		name|key)
			if [ ! "$2" ]
			then
				error 'usage: name|key searchstring'
			fi
			
			
			string="$2"
			
			type="$1"
			;;
	esac
	
	
	if [ ! "$string" ]
	then
		error 'empty search string'
	fi
	
	
	showcmd "cd $PORTS_PATH && make search $type=$string"
	
	cd $PORTS_PATH || cleanexit 1
	
	make search "$type=$string" | $PAGER_PROGRAM
	
	
	cleanexit
}


#
# Command:
#  searches for local packages.
#
do_ls()
{
	local string="$1"
	
	
	if [ ! "$string" ]
	then
		error 'search regex is required'
	fi
	
	
	ls -1 $PKGDB_PATH | \
	grep -v '^pkgdb\.' | \
	grep -iE "$string"
	
	
	if cmd_failed $?
	then
		error "no packages matching '$string' found"
	fi
	
	
	cleanexit
}


#
# Command:
#  deinstalls not required packages.
#
do_notreq()
{
	local old_wd name NUM DIALOG_OPTIONS PACKAGES
	
	
	old_wd=`pwd`
	
	cd $PKGDB_PATH || cleanexit 1
	
	
	for name in *
	do
		if [ ! "$name" = pkgdb.db -a ! -f "$name/+REQUIRED_BY" ]
		then
			DIALOG_OPTIONS="$DIALOG_OPTIONS '$name' 'no +REQUIRED_BY' OFF"
			
			NUM=$(($NUM + 1))
		fi
	done
	
	cd "$old_wd" || cleanexit 1
	
	
	show_dialog notrequired "$NUM not required package`plural $NUM`" "$DIALOG_OPTIONS"
	
	unset NUM DIALOG_OPTIONS
	
	
	package_deinstall "$selopt_notrequired_values"
}


#
# Command:
#  deinstalls packages.
#
do_deinst()
{
	local old_wd name NUM DIALOG_OPTIONS PACKAGES
	
	
	old_wd=`pwd`
	
	cd $PKGDB_PATH || cleanexit 1
	
	
	for name in *
	do
		if [ ! "$name" = pkgdb.db ]
		then
			DIALOG_OPTIONS="$DIALOG_OPTIONS '$name' ' ' OFF"
			
			NUM=$(($NUM + 1))
		fi
	done
	
	cd "$old_wd" || cleanexit 1
	
	
	show_dialog deinstall "Found $NUM package`plural $NUM`.\nSelect one(s) you want to deinstall" "$DIALOG_OPTIONS"
	
	unset NUM DIALOG_OPTIONS
	
	
	package_deinstall "$selopt_deinstall_values"
}


#
# Command:
#  updates portaudit database.
#
do_padb()
{
	local PORTAUDIT_COMMAND
	
	
	PORTAUDIT_COMMAND="portaudit -Fd"
	
	showcmd "$PORTAUDIT_COMMAND"
	
	$PORTAUDIT_COMMAND
	
	
	if cmd_failed $?
	then
		exitcode "$PORTAUDIT_COMMAND" $?
	fi
	
	
	echo
}


#
# Command:
#  checks for existence of required programs.
#
do_checkbinaries()
{
	local binary exitcode
	
	
	exitcode=0
	
	
	echo 'Checking for existence of required programs...'
	echo
	
	
	for binary in portupgrade portversion \
	dialog csup portsdb pkgdb portaudit \
	pkg_deinstall $PAGER_PROGRAM \
	sed grep cat touch id uname
	do
		which "$binary" > /dev/null
		
		if cmd_failed $?
		then
			notice "$binary: program not found"
			
			exitcode=1
		else
			echo "$binary: OK"
		fi
	done
	
	
	cleanexit $exitcode
}


#
# Checks configuration file.
#
check_configfile()
{
	if [ ! -f "$1" ]
	then
		error "$1: configuration file not found"
		
	elif [ ! -r "$1" ]
	then
		error "$1: configuration file not readable"
	fi
}


#
# Checks (and creates) status file.
#
check_statusfile()
{
	if [ ! "$1" ]
	then
		error "please define status_file in $CONFIG_FILE"
	fi
	
	
	if [ ! -r "$1" ]
	then
		touch "$1"
		
		if cmd_failed $?
		then
			error "cannot create status file '$1'"
		fi
	fi
}


#
# Checks csup supfile.
#
check_supfile()
{
	if [ ! "$1" ]
	then
		error "please define csup_supfile in $CONFIG_FILE"
		
	elif [ ! -r "$1" ]
	then
		error "$1: file not readable"
	fi
}


#
# Checks portupgrade log directory.
#
check_portupgrade_logdir()
{
	if [ ! "$1" ]
	then
		return 0
	fi
	
	if [ ! -d "$1" ]
	then
		notice "$1: directory not found"
		
	elif [ ! -w "$1" ]
	then
		notice "$1: directory not writable"
		
	else
		return 0
	fi
	
	
	cat <<- EOF
	
	If you don't want logging,
	comment out following varibles in $CONFIG_FILE:
	
	portupgrade_logdir
	portupgrade_args
	EOF
	
	cleanexit 1
}


#
# Checks (and creates) lock file.
#
check_lockfile()
{
	if [ ! "$LOCKFILE" ]
	then
		error 'internal error: $LOCKFILE is empty'
	fi
	
	
	if [ -f "$LOCKFILE" ]
	then
		notice "lock file '$LOCKFILE' exists"
		
		cat <<- EOF
		
		This probably means that you have
		another `prog` session running.
		If there are no active sessions,
		then it's probably a result of
		unfinished session. In this case,
		you must delete the file manually,
		before you can run `prog` again.
		EOF
		
		exit 1
	fi
	
	
	touch "$LOCKFILE"
	
	if cmd_failed $?
	then
		notice "cannot create lock file '$LOCKFILE'"
		exit 1
	fi
}


##################################################

if [ `id -u` -gt 0 ]
then
	error 'requires root'
fi


if [ `uname -s` != FreeBSD ]
then
	error 'requires FreeBSD'
fi


##################################################

check_configfile "$CONFIG_FILE"

. "$CONFIG_FILE"


: ${portupgrade_args=""}
: ${csup_args=""}
: ${dialog_list_rows="10"}
: ${listup_after_upgrade="YES"}
: ${ignore_held="YES"}
: ${update_portaudit_db="YES"}
: ${enable_real_deinstall="NO"}
: ${listup_after_deinstall="YES"}


check_portupgrade_logdir "$portupgrade_logdir"

check_statusfile "$status_file"


TEMP_PREFIX="$TEMP_DIR/`prog`.tmp"


LOCKFILE="$TEMP_DIR/`prog`.lock"

check_lockfile


trap 'cleanexit 1' 2


##################################################

case "$1"
in
	listup|list)
		do_list
		cleanexit
		;;
	
	cvsup|cvs|csup)
		do_csup
		cleanexit
		;;
	
	fixdb|fix)
		do_fixdb
		;;
	
	find|portfind)
		do_find "$2" "$3"
		;;
	
	ls|lspkg)
		do_ls "$2"
		;;
	
	chkreq|notreq|noreq)
		do_notreq
		;;
	
	deinst|deins)
		do_deinst
		;;
	
	padb)
		do_padb
		cleanexit
		;;
	
	all)
		do_csup
		;;
	
	listup+run|list+run)
		do_list
		;;
	
	chkbin)
		do_checkbinaries
		;;
	
	help)
		do_help
		;;
	
	'')
		;;
	
	*)
		error "$1: invalid parameter"
		;;
esac


##################################################

if [ ! -s "$status_file" ]
then
	do_list exit-if-none
fi


while read name comments
do
	DIALOG_OPTIONS="$DIALOG_OPTIONS '$name' '< $comments' OFF"
	
	NUM=$(($NUM + 1))

done < \
"$status_file"


show_dialog list "Found $NUM outdated package`plural $NUM`" "$DIALOG_OPTIONS"

unset name comments NUM DIALOG_OPTIONS

PACKAGES="$selopt_list_values"


if [ ! "$PACKAGES" ]
then
	echo
	echo 'No packages selected for upgrade'
	echo
	
	cleanexit
fi


for package in $PACKAGES
do
	DIALOG_OPTIONS="$DIALOG_OPTIONS '$package' ' '"
	
	NUM=$(($NUM + 1))
done


show_dialog upgrade "Upgrade $NUM package`plural $NUM`?" "$DIALOG_OPTIONS" menu

unset package NUM DIALOG_OPTIONS


if cmd_failed $selopt_upgrade_exitcode
then
	echo
	echo 'Upgrade cancelled'
	echo
	
	cleanexit
fi


if checkyesno update_portaudit_db
then
	echo
	do_padb
fi


PORTUPGRADE_COMMAND="portupgrade -v $portupgrade_args $PACKAGES"

showcmd "$PORTUPGRADE_COMMAND"

$PORTUPGRADE_COMMAND


exitcode=$?

if cmd_failed $exitcode
then
	exitcode "$PORTUPGRADE_COMMAND" $exitcode
	
else
	if checkyesno listup_after_upgrade
	then
		echo
		do_list
	fi
fi

cleanexit


#__HELP__
# Package Frontend help
#-----------------------
#
#%P%           Show upgrade dialog
#
#%P% list      Update list of outdated packages,
#                using "portversion -vl <"
#
#%P% cvs       Update ports tree using csup
#                Run "make fetchindex" in %PORTS_PATH%
#                Run "portsdb -u"
#                Run %P% list
#
#%P% all       Run %P% cvs
#                Run %P%
#
#%P% list+run  Run %P% list
#                Run %P%
#	
#%P% fix       Run "pkgdb -F"
#	
#%P% find      Run "make search" in %PORTS_PATH%
#
#%P% ls        Search packages in %PKGDB_PATH%
#
#%P% noreq     Check for not required packages
#
#%P% deins     Deinstall package(s) by name
#
#%P% padb      Update portaudit database, using "portaudit -Fd"
#
#%P% chkbin    Check for existence of required programs
#
#%P% help      This help
#
