Bourne Shell - Problem with while loop variable scope.

Hello

I am having issues with a script I'm working on developing on a Solaris machine.

The script is intended to find out how many times a particular user (by given userid) has logged into the local system for more than one hour today.

Here is my while loop:

last $user | grep -v 'sshd' | grep -v 'still logged in' | grep "`date | cut -d' ' -f2-4`" | cut -d\( -f2 | cut -d: -f1 | while read numHours
do
        if [ $numHours -gt 0 ]; then
                numLogins=$(( $numLogins + 1 ))
                echo "$user logged in once for more than an hour ($numHours hours)." #DEBUG
        fi
done

I have declared numLogins before the loop, and the problem is after the loop I try and "echo $numLogins" but numLogins always contains 0. Even when my debug line prints out showing that the user has logged in for at least an hour.

I have determined that I believe this is because the while loop (when used in a pipeline) has a seperate variable scope, but I have tried rewriting my loop like:

while loop=read $line
do
        numHours=`echo $line | grep -v 'sshd' | grep -v 'still logged in' | grep "\`date | cut -d' ' -f2-4`" | cut -d\( -f2 | cut -d: -f1
        if [ $numHours -gt 0 ]; then
                numLogins=$(( $numLogins + 1 ))
                echo "$user logged in once for more than an hour ($numHours hours)." #DEBUG
        fi
done < `last $user`

I also thought it may be with the way I was incrementing my numLogins variable,
I have tried to use:

(( numLogins++ ))
using expr,
numLogins=`echo $numLogins + 1 | bc`

and a few other methods I can't quite remember.

Any help on this would be extremely appreciated.
Thanks a lot!

You can't use read behind a chain of pipes because commands behind pipes will be in a seperate subshell.

When your pipe chain's wider than the screen it's time to rethink what you're doing, anyway. What's the exact output from last and what's the output you want from your program?

My last output is as follows:

richards  pts/4        s0106002215952e5 Wed Dec  1 14:04   still logged in
richards  sshd         s0106002215952e5 Wed Dec  1 14:04   still logged in
richards  pts/4        s0106002215952e5 Wed Dec  1 12:02 - 13:03  (01:01)
richards  sshd         s0106002215952e5 Wed Dec  1 12:02 - 13:03  (01:01)
richards  pts/3        s0106002215952e5 Tue Nov 30 21:29 - 23:28  (01:58)
richards  sshd         s0106002215952e5 Tue Nov 30 21:29 - 23:28  (01:58)
....

And I would like my program to output (in this case):

"The user ******** with a user id of *** has logged in 1 time(s) today for over an hour.
#!/bin/sh

# substitute with last $username > /tmp/$$ on your system
cat > /tmp/$$ <<EOF
richards pts/4 s0106002215952e5 Wed Dec 1 14:04 still logged in
richards sshd s0106002215952e5 Wed Dec 1 14:04 still logged in
richards pts/4 s0106002215952e5 Wed Dec 1 12:02 - 13:03 (01:01)
richards sshd s0106002215952e5 Wed Dec 1 12:02 - 13:03 (01:01)
richards pts/3 s0106002215952e5 Tue Nov 30 21:29 - 23:28 (01:58)
richards sshd s0106002215952e5 Tue Nov 30 21:29 - 23:28 (01:58)
EOF

LOGINS=0
TOTALTIME=0

while read NAME T SESSION DAYNAME MON DAY TIMESTART DASH TIMEEND DURATION
do
	# Don't count SSH sessions
	[ "$T" == "sshd" ] && continue
	# Skip logged-in sessions
	[ "$DASH" == "still" ] && continue

	((LOGINS++))

	DURATION=${DURATION/(/}
	DURATION=${DURATION/)/}
	IFS=":" read HOURS MINS <<< "$DURATION"

	# Turn 05 into 105, so it doesn't think it's octal, ugh
	HOURS="1${HOURS}"
	MINS="1${MINS}"

	(( TOTALTIME += ((HOURS-100)*60)+(MINS-100) ))
done < /tmp/$$

HOURS=$((TOTALTIME/60))
MINS=$((TOTALTIME%60))

if [ "${HOURS}" -ge 1 ]
then
        echo "user logged in ${LOGINS} times for over an hour"
else
        echo "user logged in for ${MINS} minutes"
fi

rm -f /tmp/$$

Would there be anyway to do this without using temporary files?

You might try

while read ...
do
...
done <<< $(last $user)

if your shell supports it.

What do the 3 <'s mean?

I understand < is redirect, and << is append.

It does what it looks like, puts a string into stdin, instead of a file.