3

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?

  • Any objections to running it twice? Once to ensure a zero exit code, subsequently for the output? – Jeff Schaller Mar 31 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 at 16:15
  • 1
    In bash, you can use PIPESTATUS instead of $?: exit 13 | tail -n1; echo ${PIPESTATUS[0]} – mosvy Mar 31 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 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) – mosvy Mar 31 at 18:57
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.

2
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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.