#!/bin/ksh
# @(#) config.ksh 1.1 94/04/16
# 93/05/06 john h. dubois iii (john@armory.com)
# 93/06/07 Added userpath option
# 94/01/02 Fixed bug that made userpath fail for csh users
# 94/01/12 Added erase option
# 94/03/05 Added -u option
# 94/03/13 Added -d option.  Make ksh expand $userpath to avoid csh error.
# 94/04/16 Give more detailed error messages when checking path.

# Possible additional options: password, mail notification

alias istrue="test 0 -ne"
alias isfalse="test 0 -eq"

# Vars[i]	Variable names.
# NumPref	Number of config options.
# Descrip[i]	Short descriptions of config options.
# Verbose[i]	Long (multi-line) descriptions of config options.
# Values[i]	Values of config options
# Defaults[i]	Default values

# Ind varname		returns index of varname
# ShowValue varname	prints varname followed by its value
# SetVal var [val]	sets var in Values to val or shell param value
# GetConf		Reads config from config file into Values[] & shell
# AssignVal var=value	Sets var in Values to value
# ShowConfig		Show current configuration

# Usage: Ind varname
# Returns the index of varname if it is valid.
# Prints an error message and returns 0 if it is not.
function Ind {
    typeset -i i=1
    while [ i -le NumPref ]; do
	if [ "$1" = ${Vars[i]} ]; then
	    return $i
	fi
	let i+=1
    done
    print -u2 "Unknown preference type: $1"
    return 0
}

# Usage: SetVal varname [value]
# Sets the variable's shell value and value in Values[] to value, if given,
# or to the value of the shell parameter with the same name if not.
# Prints an error message and returns 0 if varname is invalid.
function SetVal {
    typeset -i Index

    Ind "$1"
    Index=$?
    isfalse Index && return 1
    if [ $# = 2 ]; then
	Values[Index]=$2
	eval $1="'$2'"
    else
	eval 'Values[Index]'=\$$1
    fi
    return 0
}

# Reads configuration assignments from config file and sets Values[] to them.
# Also sets shell parameters with the var names to the values.
# Uses globals: Vars[]
# If an argument is given, a complaint is printed if no config file is found.
function GetConfig {
    typeset v

    if [ ! -f "$PrefFile" ]; then
	[ $# -gt 1 ] && print -u2 \
"You do not have an account configuration file yet.
Use  $name -i  to create one."
	return 1
    elif [ ! -r "$PrefFile" ]; then
	[ $# -gt 1 ] && print -u2 "Your config file is not readable."
	return 1
    else
	unset ${Vars[*]}
	eval "`set_vars $PrefFile`"
	for v in ${Vars[*]}; do
	    SetVal $v
	done
    fi
    return 0
}

# set_vars 1.1 91/01/23
# set_vars: Read variable assignments, 
# convert values to forms that won't be messed with by the shell,
# and output them in a form that is ready for the shell to evaluate
# usage: eval "`set_vars [ filename ]`"
# where the lines in filename are of the form
# var=value
# value may contain spaces, backslashes, quote characters, etc.;
# they will become part of the value assigned to var by eval.
# Lines that begin with a # (optionally preceded by whitespace)
# and lines that do not contain a '=' are ignored.
function set_vars {
/bin/sed "
/[ 	]*#/d
/=/!d
s/'/'\\\\''/g
s/=/='/
s/$/'/" $*
}

# Uses globals: NumPref, Vars[], GetConfig(), debug
# Usage: Setup b|c
function Setup {
    GetConfig c || exit 1
    typeset -i i=1
    typeset var val equals shelltype=$1 mesgcmd sttycmd

    # Can't use aliases here due to aliasing bug
    if istrue debug; then
	mesgcmd="print -u2 debug: Would set mesg"
	sttycmd="print -u2 debug: Would do stty"
    else
	mesgcmd=/bin/mesg
	sttycmd=/bin/stty
    fi

    [ $shelltype = b ] && equals== || equals=' '
    if [ "$askterm" = y ]; then
	print -u2 -n "TERM = ($TERM) " 1>&2
	read term
	export TERM=${term:-$TERM}
    fi
    [ -n "$mesg" ] && $mesgcmd "$mesg"

    unset sttyopt[*]
    [ -n "$interrupt" ] && set -A sttyopt -- intr "$interrupt"
    [ -n "$erase" ] && set -A sttyopt -- "${sttyopt[@]}" erase "$erase"
    [ ${#sttyopt[*]} -gt 0 ] && $sttycmd "${sttyopt[@]}"

    if [ -n "$userpath" ]; then
	if [ $shelltype = b ]; then
	    echo "PATH=$PATH:$userpath"
	    echo "export PATH"
	else
	    # userpath is liable to have $HOME:other-stuff in it.
	    # If $PATH:$userpath is echoed for csh, $HOME::other-stuff
	    # will still be in it when csh sees it, and it will
	    # think that is a variable modification if : isn't escaped.
	    # So, let ksh expand userpath instead of leaving it up to csh.
	    eval echo setenv PATH $PATH:$userpath
	fi
    fi

    while [ i -le $((NumPref)) ]; do
	var=${Vars[i]}
	eval val=\$${Vars[i]}
	if [[ $var = *([A-Z]) ]]; then
	    if [ -n "$val" ]; then
		# An = is used between var and value even for csh 
		# env vars so that the single-quote-escaping sed script
		# can recognize the start of the value.
		if [ $shelltype = b ]; then
		    echo "$var=$val"
		    echo "export $var"
		else
		    echo "setenv $var=$val"
		fi
	    fi
	else
	    [[ $var != ?(mesg|askterm|interrupt|erase|userpath) ]] &&
		print -u2 "Unknown config option: $var"
	fi
	let i+=1
    done | /bin/sed "
    /=/s/'/'\\\\''/g
    /=/s/\$/'/
    s/=/$equals'/"
    # On variable assignment lines, enclose the values in single quotes,
    # and replace ' in values with '\'' (close the single-quotes,
    # add an escaped single quote, then reopen the single-quotes).
    # For csh, also replace the = with a space.
    exit 0
}

# Usage: AssignVal var=value
function AssignVal {
    SetVal "${1%%=*}" "${1#*=}"
}

function InitArrs {
# Brief descriptions of configuration options
set -A Descrip \
"" \
"Writability" \
"Mail Name" \
"Default Termtype" \
"Termtype Query" \
"Default Pager" \
"Default Editor" \
"Interrupt Key" \
"Erase Key" \
"User Path"

# Possible values
set -A Possib \
"" \
"y (yes) or n (no)" \
"Anything" \
"ansi, vt100, tvi912, etc." \
"y (yes) or n (no)" \
"less, more, pg, etc." \
"vi, emacs, uemacs, joe, elmedit, pico, etc." \
"^C for control-C, ^? for delete" \
"^H for control-H, ^? for delete" \
"Colon-separated list of directories"

# Tests for valid values
set -A Tests \
'' \
'[[ "$val" = [yn] ]]' \
'' \
'TERM="$val" tput lines' \
'[[ "$val" = [yn] ]]' \
'type "$val"' \
'type "$val"' \
'[[ "$val" = ^? ]]' \
'[[ "$val" = ^? ]]' \
'CheckPath "$val"'

# Warnings for invalid values
set -A Warnings \
"" \
"value should be y or n" \
"" \
"invalid terminal type" \
"value should be y or n" \
"no such program" \
"no such program" \
"not a control character" \
"not a control character" \
"path contains bad directories"

# Verbose descriptions of configuration options
set -A Verbose \
"" \
"Your writability determines whether other users can use the write and
talk commands to write you or talk to you.  It can be turned on or off for
the duration of a login session by issuing the command 'mesg y' or 'mesg n'.
You are initially unwritable when you log in.  Turning on writability
within $name causes you to become writable immediately after logging in. 
You can still turn off writability for the login session with 'mesg n'." \
"Your mail name is set by the value of the \$NAME environment variable.
By default it is set to the value of your 'real name' as given in the
/etc/passwd file.  \$NAME is also used for various other purposes, like
the high score list of various games.  Changing your mail name within
$name changes the value that \$NAME is set to when you log in." \
"Your default terminal type is the type that you get if you hit return
at the 'TERM = (termtype)' prompt.  You can specify a different terminal
type by typing its name before hitting return." \
"By default, you are asked for a terminal type when you log in on a dialup
(modem) line.  If you select 'no' to 'Terminal type query', your terminal
type will be set to the value you give for 'Default Terminal Type' without
you being asked.  You can change your terminal type at any time by typing

TERM=newtermtype

in the Bourne, Korn, and Bourne Again shells (sh, ksh, and bash), or

setenv TERM newtermtype

in the C shell (csh and tcsh), with newtermtype being the terminal type
you wish to set." \
"The pager is used to view text one screen at a time.  It is invoked by
various utilities (like mail) when it is neccessary to view more than one
screenful of output." \
"The editor is used to edit text.  It is automatically invoked by some
applications (like rn), and is entered by other applications (like mail)
when requested." \
"The interrupt key is used to kill running programs.
It is equivalent to control-C and control-break under DOS.
The initial value is delete (the delete key), which is specified as ^?.
The other common value on UNIX systems is control-C, specified as ^C.
Other control keys may be specified as ^x, where x is the control key
to use for the interrupt key.  Various other control keys have special
meanings, so it is safest to use ^C or delete." \
"The erase key is used to delete the last character typed.  It causes the
cursor to move back one space.  Depending on other settings, the character that
was there may or may not be erased on the screen (actually, overwritten with a
space), but in either case it is removed from the terminal input buffer.  The
initial value is control-H, which is specified as ^H.  The other common value
is delete (the delete key), which is specified as ^?.  Other control keys may
be specified as ^x, where x is the control key to use for the erase key. 
Various other control keys have special meanings, so it is safest to use ^H or
delete.  If you use delete, make sure you don't set the interrupt key to be
delete.  On a terminal, the backspace key (which sometimes has a left pointing
arrow on it) most often sends control-H, and the DEL (delete) key almost always
sends delete.  If the terminal has a cursor keypad, its left-arrow key usually
sends an escape sequence rather than control-H or delete; if it does, it can't
be used for erase.  If you use control + H for erase, set erase to ^H; if you
use the delete key, set it to ^?; if you use the backspace key, you'll need to
determine whether it sends control-H or delete; if you use your cursor keypad,
you may have to change unless you are using a computer with communications
software that lets you configure the key to send control-H or delete." \
"Your path determines where the shell looks for a program to execute when you
type its name.  The default path includes the directories that most of the
system utilities reside in, and a directory under your home directory called
'bin'.  It does not contain your home directory.  If you want to be able to run
utilities that will reside in your account, create the 'bin' directory if it
doesn't already exist (use the command: mkdir \$HOME/bin) and put them there. 
The default path also does not contain the current directory; this prevents you
from accidently running a possibly dangerous executable when you are in a
publicly writable directory or a directory belonging to another user.  The user
path, if any, is added to the end of the standard path.  To include your home
directory in your path, use '\$HOME'.  To include your current directory, use
'.'.  Don't type in the quotes.  Another directory that some users may want in
their path is /etc.  If you want more than one, separate them with colons,
e.g.: \$HOME:/etc:."
}

# Usage: ShowValue varname
# Prints varname followed by its value.
# Prints an error message and returns 0 if varname is invalid.
function ShowValue {
    typeset -i Ind
    typeset val

    Ind "$1"
    Index=$?
    if isfalse Index; then
	return 1
    fi
    eval val=\$${Vars[Index]}
    print "$1: $val"
}

function SetConfig {
    typeset -i i=1
    if > $PrefFile; then :; else
	print -u2 "Failed to write configuration file $PrefFile."
	exit 1
    fi
    while [ i -le NumPref ]; do
	eval val=\$${Vars[i]}
	if [ -n "$val" ]; then
	    echo "${Vars[i]}=$val"
	fi
	let i+=1
    done > $PrefFile
    if istrue u; then
	chown $p_uid $PrefFile
	chgrp $p_gid $PrefFile
    fi
}

# Usage: ShowConfig
function ShowConfig {
    typeset -i i=1
    typeset val

    while [ i -le NumPref ]; do
	eval val=\$${Vars[i]}
	if [ -n "$val" ]; then
	    print "${Descrip[i]}: $val"
	fi
	let i+=1
    done
}

function CheckPath {
    typeset dir IFS=: 
    typeset -i ret=0 CRet

    cd /usr/tmp		# Try to make all relative dirs except . and .. be bad.
    for dir in $1
    do
	CRet=1
	# expand vars
	eval dir=$dir
	if [ ! -a "$dir" ]; then
	    print -u3 "$dir: does not exist."
	elif [ ! -d "$dir" ]; then
	    print -u3 "$dir: not a directory."
	elif [ ! -x "$dir" ]; then
	    print -u3 "$dir: not executable."
	elif [ ! -r "$dir" ]; then
	    print -u3 "$dir: not readable."
	else
	    CRet=0
	fi
        istrue CRet && ret=1
    done
    return $ret
}

function Interactive {
    typeset -i i=1
    typeset Resp r val r2 Menu
    typeset -L16 D

    export TERM
    # Set this first in case we're running config for another user,
    # in which case TERM is liable to be changed.
    clear=$(tput clear 2>/dev/null)
    GetConfig || InitVars
    PS3="Select an option by number: "
    while :; do
	print -n "$clear"
	D="Option"
	print "    $D    Current Value"
	D="------"
	print "    $D    -------------"
	while [ i -le NumPref ]; do
	    D=${Descrip[i]}
	    Menu[i]="$D    ${Values[i]}"
	    let i+=1
	done
	set -- "${Menu[@]}" "Save & quit" "Abort (no update)"
	# Print menu for each iteration
	select Resp; do break; done
	case "$REPLY" in
	$#)
	    exit 0;;
	$(($#-1)))
	    break;;
	esac
	[ -z "$Resp" ] && continue
	i=REPLY
	r=
	var=${Vars[i]}
	while [ -z "$r" ]; do
	    print -n "
Current value of ${Descrip[i]} ($var): ${Values[i]}
Possible values: ${Possib[i]}
Enter: a value to set this option to; 'h' for a description of this option;
'-' to remove this option from your configuration; or press return to retain
the current value: "
	    read r
	    case "$r" in
	    "")
		break
		;;
	    h)
		print "${Verbose[i]}"
		r=
		;;
	    -)
		SetVal $var ""
		;;
	    *)
		if CheckVal $i "$r"; then
		    SetVal $var "$r"
		else
		    print -n "Really set $var to \"$r\"? "
		    read r2
		    if [[ "$r2" = [yY]* ]]; then
			SetVal $var "$r"
		    else
			print -n \
			"Value of $var not changed.  Press return to continue. "
			read
		    fi
		fi
		;;
	    esac
	done
    done
}

# Usage: CheckVal var-index val
function CheckVal {
    typeset val=$2 stty

    # Discard normal & error output, but save stderr as fd 3
    # for tests that really want to print
    eval ${Tests[$1]} >/dev/null 3>&2 2>&1 && return 0
    print -u2 "Warning: ${Warnings[$1]}."
    return 1
}

# Set initial config values for a user who is running config for the first time
# Get NAME from /etc/passwd, mesg from mesg command,
# TERM, PAGER, and EDITOR from environment, interrupt and erase from stty.
# userpath is left null, askterm is set to y.
function InitVars {
    if [ -z "$NAME" ]; then
	# get name from /etc/passwd
NAME=$(sed -n "/^$USER:[^:]*:[^:]*:[^:]*:/{s///;s/[:,].*//;p;}" /etc/passwd)
	export NAME
    fi
    askterm=y

    if istrue u; then
	# If running config for another user, don't set these.
	unset TERM PAGER EDITOR mesg interrupt erase
    else
	set -- $(mesg)
	mesg=$2
	[ "$mesg" != n -a "$mesg" != y ] && mesg=y

	export TERM=${TERM:-vt100}
	export PAGER=${PAGER:-/usr/bin/more}
	export EDITOR=${VISUAL:-${EDITOR:-/usr/bin/vi}}

	# Relevant stty -a output line looks like this:
	# line = 0; intr = ^C; quit = ^\; erase = ^H; kill = ^U; eof = ^D;
	# eol = ^@; 
	stty=$(/bin/stty -a)
	stty=${stty##*intr = }
	interrupt=${stty%%;*}
	[ $interrupt = DEL ] && interrupt='^?'
	stty=${stty##*erase = }
	erase=${stty%%;*}
	[ $erase = DEL ] && erase='^?'
    fi

    for v in ${Vars[*]}; do
	SetVal $v
    done
}

function Initialize {
    InitVars
    SetConfig

    print "Your account configuration has been initialized as follows:"
    ShowConfig
    print \
"To change these values or read a description of the
configuration options, use \"$name\" without arguments."
    exit 0
}

function List {
    GetConfig c || exit 1
    ShowConfig
    exit 0
}

# @(#) getpwnam.ksh 1.0 94/03/05
# Usage: getpwnam <name> [<pre>]
# Reads the passwd file entry for account <name>
# Sets 7 global variables from fields in the passwd entry:
# <pre>name	Account name
# <pre>password	Password field
# <pre>uid	Login user ID
# <pre>gid	Login group ID
# <pre>gcos	GCOS (comment) field
# <pre>home	Home directory
# <pre>shell	Login shell
# Returns 0 on success, 1 on error.
# If <pre> is not given, pw_ is used.
# Example: getpwnam spcecdt PW_
# sets PW_name, PW_password, etc. with fields from spcecdt's passwd entry
function getpwnam {
    typeset IFS=: user=$1 p=$2

    [ $# -lt 1 ] && return 1
    [ $# -eq 1 ] && p=p_

    set -- $(grep "^$user:" /etc/passwd 2>/dev/null) || return 1
    eval $(print -r \
"${p}name=$1
${p}password=$2
${p}uid=$3
${p}gid=$4
${p}gcos=$5
${p}home=$6
${p}shell=$7" | set_vars)
}

# Start of main program

name=${0##*/}
Usage="Usage: $name [-bcdhil] [-f filename] [option[=value]] ..."
PrefFile=$HOME/.config
typeset -i u=0 debug=0

while getopts :bcdf:hilu: opt; do
    case $opt in
    b|c)
	action="Setup $opt";;
    d)
	debug=1;;
    f)
	PrefFile=$OPTARG;;
    u)
	getpwnam "$OPTARG"
	PrefFile=$p_home/.config
#	if [ ! -f $PrefFile ]; then
#	    print -u2 "WARNING: This user has no config file.
#	fi
	HOME=$p_home
	USER=$p_name
	NAME=$p_gcos
	u=1
	;;
    h)
	echo \
"$name: Display or modify account configuration.
$Usage
Options:
-b: Process config file & emit sh/ksh/bash initialization strings.  This is
    typically used in a .profile or /etc/profile like this: eval "`config -b`"
-c: Process config file & emit csh/tcsh initialization strings.  This is
    typically used in a .cshrc or /etc/cshrc like this:
    set tmpfile=/tmp/login.$$
    config -c > $tmpfile
    source $tmpfile
    rm $tmpfile
-d: Debug configuration.  Use with -b or -c option to check configuration
    without modifying login session.  Actions that would modify login
    session are described without being taken.
-f <filename>: Read/write <filename> instead of \$HOME/.config.
-h: Print this help.
-i: Initialize account configuration.
-l: List current account configuration.
-u <user>: Process configuration file for <user>."
       exit 0
       ;;
    l)
	action=List;;
    i)
	action=Initialize;;
    +?)
	print -u2 "$name: options should not be preceded by a '+'."
	exit 1
	;;
    ?) 
	print -u2 "$name: $OPTARG: bad option.  Use -h for help."
	exit 1
	;;
    esac
done
 
# Configuration file variable names
set -A Vars "" mesg NAME TERM askterm PAGER EDITOR interrupt erase userpath

typeset -i NumPref=$((${#Vars[*]}-1))

InitArrs

# Execute action, if any
eval $action

# remove args that were options
let OPTIND=OPTIND-1
shift $OPTIND

if [ $# -lt 1 ]; then
    Interactive
else
    GetConfig c || exit 1
    for arg; do
	if [[ "$arg" = *=* ]]; then
	    AssignVal "$arg"
	else
	    ShowValue "$arg"
	fi
    done
fi

SetConfig
print -u2 "Configuration changes will take effect next time you log in."
