4

I am writing an automated homework grader in bash. The grader compiles a program and runs it. If the program fails to compile or fails to run (e.g. due to a segmentation fault) the grade should be a fixed small number, e.g. 5. Otherwise, the grade is the last line output by the program.

To get the last line, I can do:

grade=$( ./a.out | tail -1 )

But this always gives an exit code of 0, even if a.out fails to run (e.g. not found), so I cannot tell whether the program failed to exist.

Another option is to use a temporary file:

./a.out > temp
if [ $? -ne 0 ]
then
  grade=0
else
  grade=$( tail -1 temp )
fi

However, this might be problematic if there are many different processes doing the same simultaneously. Even with one process, it is wasteful to keep all output in a file (the output might be large) when I only need the last line.

Is there a solution that does not use a temporary file?

Rich
  • 823
  • Any objections to running it twice? Once to ensure a zero exit code, subsequently for the output? – Jeff Schaller Mar 31 '19 at 16:14
  • @JeffSchaller This is also an option, but it is quite inefficient, especially when there are many simultanous submissions. Consider 100 students submitting simultaneously. There is a noticeable difference between 100 runs and 200 runs. – Erel Segal-Halevi Mar 31 '19 at 16:15
  • 1
    In bash, you can use PIPESTATUS instead of $?: exit 13 | tail -n1; echo ${PIPESTATUS[0]} –  Mar 31 '19 at 17:14
  • @mosvy strangely, this does not work when I capture the output into a variable: *** $ grade=$( ./a.out | tail -1 ) *** bash: ./a.out: No such file or directory *** $ echo ${PIPESTATUS[0]} *** 0 – Erel Segal-Halevi Mar 31 '19 at 18:53
  • 1
    command substitutions are run in a subshell; you should check the PIPESTATUS inside that subshell: grade=$(./a.out | tail -1; test ${PIPESTATUS[0]} = 0 || echo 5) –  Mar 31 '19 at 18:57
  • I found a different solution: "set -o pipefail" at the start of the script (as recommended here: https://unix.stackexchange.com/a/267031/16569 ) makes the return value of the pipe nonzero if one part of the pipe fails. This works even in the command substitution. – Erel Segal-Halevi Mar 31 '19 at 19:00
  • Or even better, do the test before the tail ;-) grade=$({ ./a.out || echo 5; } | tail -n1) –  Mar 31 '19 at 19:02

2 Answers2

3
grade=$( { ./a.out 2>/dev/null || echo 0; } | tail -n 1 )

This would try to execute ./a.out and then add a line with a single 0 to its output if that program exited with a non-zero exit status or failed to execute at all. The 0 would be caught by tail -n 1 and placed in $grade.

If ./a.out executed correctly and terminated with a zero exit status, the echo would not be triggered.

Remove the redirection of standard error to /dev/null if you are interested in seeing diagnostic messages related to running ./a.out.

Change the 0 to "$?" to get the exit code instead. To be able to differentiate a number from an error, you may want to use NaN instead, or some error string.

Kusalananda
  • 333,661
2

Your problem boils down to: "I want to get the last line of output AND detect abnormal exit without using temporary files". In that case, set -o pipefail is your friend.

Here's a simple script that executes its arguments and records the last line of the output on normal exit:

#!/bin/bash

[[ -x $1 ]] || {
    echo >&2 "Usage ${0##*/} <PROGRAM> [ARG1] ..."
    exit 1
}

set -o pipefail
program=$1
shift;
x=$($program "$@" | tail -1)

if [[ $? -eq 0 ]]; then
    echo "Pass: $x"
else
    echo "Fail: $?"
fi

I'll leave the problem of assigning a grade to you.