#!/bin/sh -e

# led_ctrl: allow to configure LEDs using the LED sysfs API

# Copyright (C) 2010 Simon Guinot <sguinot@lacie.com>
# Copyright (C) 2010 Thomas Monjalon <thomas@monjalon.net>
# Copyright (C) 2010 Loic Dachary <loic@dachary.org>

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

SYSPATH=/sys/class/leds
CONFILE=/etc/led_ctrl.conf
DEBUG=false
TESTS=false
HELP=false
unset MODE

log_and_exec()
{
	$DEBUG && echo "$@" >&2
	eval "$@"
}

log_and_die()
{
	printf "$@\n" >&2
	exit 1
}

set_led_attr() # <led> <attribute> <value>
{
	local attr err=0

	IFS_ORIG="$IFS"
	# Note that '\t' is added to prevent a standard POSIX shell
	# from removing the trailing newline characters.
	IFS=$(printf '\n\t')

	for attr in $(ls -1 $SYSPATH/$1/$2); do
		log_and_exec "echo $3 > \"$attr\"" || { err=1; break; }
	done
	IFS="$IFS_ORIG"

	return $err
}

set_led_mode() # <mode>
{
	local section leds settings setting attr value

	section=$(sed -n -e "s/#.*//" -e "/{$1}/,/{.*}/p" $CONFILE)
	[ -n "$section" ] || \
		log_and_die "no mode $1 in $CONFILE"

	echo "$section" | grep '=' | while read leds settings; do
		for setting in $settings; do
			attr=$(echo $setting | cut -d'=' -f1)
			value=$(echo $setting | cut -d'=' -f2)
			[ -n "$attr" -a -n "$value" ] || \
				log_and_die "$CONFILE: mode=$1 leds=$leds, invalid setting $setting"
			set_led_attr "$leds" $attr $value || \
				log_and_die "$CONFILE: unable to apply $attr=$value for $leds"
		done
	done || return $?
}

list_led_modes()
{
	sed -n 's/{\(.*\)}/\1/p' $CONFILE
}

run_test() # <message> <test>
{
	eval "$2" && echo "$1 : OK" || { echo "$1 : FAIL"; TEST_ERR=1; }
}

run_test_suite()
{
	TEST_ERR=0
	TEST_DIR=/tmp/$(basename $0)-test-$$

	SYSPATH=$TEST_DIR/leds
	mkdir -p $SYSPATH/led1
	touch $SYSPATH/led1/attr1
	touch $SYSPATH/led1/attr2
	touch $SYSPATH/led1/attr3
	mkdir -p $SYSPATH/led2
	touch $SYSPATH/led2/attr1
	touch $SYSPATH/led2/attr2
	mkdir -p $SYSPATH/led\ 3
	touch $SYSPATH/led\ 3/attr1
	mkdir -p $SYSPATH/led\ 4
	touch $SYSPATH/led\ 4/attr1
	chmod 500 $SYSPATH/*
	chmod 500 $SYSPATH

	CONFILE=$TEST_DIR/conf
	cat > $CONFILE <<- EOF
		{mode1}
		led1		=value

		{mode2}
		led1		attr1=

		{mode3}
		*		attr1=3
		led[0-9]	attr2=3

		{mode4}
		led\ [0-9]	attr1=4

		{mode5}
		#{mode6}
		led1		attr1=5 attr2=5 attr3=5
		led2		attr1=5 attr2=5 # some comments
		#led3		attr1=5
	EOF

	run_test 'non-defined led mode' \
		'set_led_mode nomatch 2>&1 | grep -q "no mode"'

	run_test 'match empty attribute name' \
		'set_led_mode mode1 2>&1 | grep -q "invalid setting"'

	run_test 'match empty attribute value' \
		'set_led_mode mode2 2>&1 | grep -q "invalid setting"'

	run_test 'led name with wildcard and globbing' 'set_led_mode mode3'
	run_test '  led1, set attr1=3' '[ "$(cat $SYSPATH/led1/attr1)" = 3 ]'
	run_test '  led2, set attr1=3' '[ "$(cat $SYSPATH/led2/attr1)" = 3 ]'
	run_test '  led 3, set attr1=3' '[ "$(cat $SYSPATH/led\ 3/attr1)" = 3 ]'
	run_test '  led1, set attr2=3' '[ "$(cat $SYSPATH/led1/attr2)" = 3 ]'
	run_test '  led2, set attr2=3' '[ "$(cat $SYSPATH/led2/attr2)" = 3 ]'

	run_test 'led name with whitespace and globbing' 'set_led_mode mode4'
	run_test '  led 3, set attr1=4' '[ "$(cat $SYSPATH/led\ 3/attr1)" = 4 ]'
	run_test '  led 4, set attr1=4' '[ "$(cat $SYSPATH/led\ 4/attr1)" = 4 ]'

	run_test 'multiple attributes and discarded comments' \
		'set_led_mode mode5'
	run_test '  led1, set attr1=5' '[ "$(cat $SYSPATH/led1/attr1)" = 5 ]'
	run_test '  led1, set attr2=5' '[ "$(cat $SYSPATH/led1/attr2)" = 5 ]'
	run_test '  led1, set attr3=5' '[ "$(cat $SYSPATH/led1/attr3)" = 5 ]'
	run_test '  led2, set attr1=5' '[ "$(cat $SYSPATH/led2/attr1)" = 5 ]'
	run_test '  led2, set attr2=5' '[ "$(cat $SYSPATH/led2/attr1)" = 5 ]'

	run_test 'commented-out led mode' \
		'set_led_mode mode6 2>&1 | grep -q "no mode"'

	chmod -R 700 $SYSPATH
	rm -fr $TEST_DIR

	return $TEST_ERR
}

usage()
{
	cat <<- EOF
		Usage: $(basename $0) [options] [mode]
		  Configure LEDs for specified mode as defined in $CONFILE.
		  List all modes if none is provided.

		Options:
		  --config|-c  use an alternate configuration file
		  --debug |-d  print debug messages
		  --tests |-t  run non destructive internal tests
		  --help  |-h  display this help
	EOF
}

#
# Main
#

while [ $# -gt 0 ]; do
	case $1 in
		--config|-c) CONFILE=$2; shift ;;
		--debug |-d) DEBUG=true ;;
		--tests |-t) TESTS=true ;;
		--help  |-h) HELP=true ;;
		-*         ) log_and_die "unknown argument: $1\n\n$(usage)" ;;
		*          ) MODE=$1 ;;
	esac
	shift
done

$HELP && usage
$TESTS && run_test_suite
{ $HELP || $TESTS ;} && exit 0

[ -n "$(ls -A $SYSPATH 2>/dev/null)" ] || \
	log_and_die "missing or empty sysfs LED directory: $SYSPATH"
[ -e $CONFILE ] || log_and_die "no configuration file: $CONFILE\n\n$(usage)"

if [ -n "$MODE" ]; then
	set_led_mode $MODE
else
	list_led_modes
fi
