4
\$\begingroup\$

I'm using the following Bash CGI to upload a file:

#!/bin/bash
echo "Content-Type: text/plain"
echo

if [ "$REQUEST_METHOD" = "POST" ]; then
    TMPOUT=hello
    cat >$TMPOUT

    # Get the line count
    LINES=$(wc -l $TMPOUT | cut -d ' ' -f 1)

    # Remove the first four lines
    tail -$((LINES - 4)) $TMPOUT >$TMPOUT.1

    # Remove the last line
    head -$((LINES - 5)) $TMPOUT.1 >$TMPOUT

    # Copy everything but the new last line to a temporary file
    head -$((LINES - 6)) $TMPOUT >$TMPOUT.1

    # Copy the new last line but remove trailing \r\n
    tail -1 $TMPOUT | tr -d '\r\n' >> $TMPOUT.1
fi

This is for a uClinux/Busybox server. When a file is passed this way, the original $TMPOUT will contain a four line head and one line tail that need to be removed to end up with the same file. The resulting file's hash is identical to the original.

It works but it seems pretty ugly, creating two files and such. I'm by no means a pro in bash, can this be made prettier?

Keep in mind that the target is a little embedded device and has no Perl/Python or anything on it. It needs to be pure bash.

\$\endgroup\$
  • \$\begingroup\$ That's an unusual protocol. Could you give us an idea of what the four header lines and one footer line look contain? \$\endgroup\$ – 200_success Feb 4 '15 at 19:39
5
\$\begingroup\$

Since your script has no content to return, a status code of 204 No Content would be more desirable than 200 Success. For that, you should echo "Status: 204 No Content" (RFC 3875 Sec 6.3.3). Also consider returning using status code 405 Method Not Allowed for anything other than a POST request.

$TMPOUT is a misnomer. The file is not temporary at all — $TMPOUT.1 contains the final output of your script.

If your goal is to redirect the input to a file, discarding the first four lines, the last line, and the trailing newline of the penultimate line, you don't need to execute any external commands. Bash is fully capable of doing all of the work itself. The script isn't pretty, but I still find it easier to understand than copying the data back and forth, extracting lines here and there each time.

#!/bin/bash

case "$REQUEST_METHOD" in
  POST)
    (
        # Discard first four lines
        read && read && read && read &&

        # Read and echo, buffering two lines
        read line1 &&
        read line2 &&
        while read nextline ; do
            echo "$line1"
            line1="$line2"
            line2="$nextline"
        done

        # Echo penultimate line with no trailing newline.
        echo -n "$line1"

        # Discard last line ($line2)
    ) > hello

    echo 'Status: 204 No Content'
    echo
    ;;

  *)
    echo 'Status: 405 Method Not Allowed'
    echo
esac
\$\endgroup\$
  • \$\begingroup\$ The solution also works with Busybox ash instead of bash. \$\endgroup\$ – 200_success Feb 4 '15 at 20:36
3
\$\begingroup\$

Your script is doing a number of unnecessary file copies and scans. I tried to streamline the process a chunk, and came up with the following to replace the line-stripping.

Your code does:

TMPOUT=hello
cat >$TMPOUT

# Get the line count
LINES=$(wc -l $TMPOUT | cut -d ' ' -f 1)

# Remove the first four lines
tail -$((LINES - 4)) $TMPOUT >$TMPOUT.1

# Remove the last line
head -$((LINES - 5)) $TMPOUT.1 >$TMPOUT

This effectively copies the STDIN to a file, copies part to the .1 file, and copies another part back.

This can be replaced with:

TMPOUT=hello

#Save the important contents (all but the first 4 and last lines)
sed -e '1,4d' -e '$d' >$TMPOUT

Now, all that's left to do is strip the last line's end-of-line marker. I struggled with this, and while your solution may be more reliable, I was tempted to suggest just stripping the final bytes with something like:

#Count the characters in the file
BYTES=$( wc -m $TMPOUT | cut -d" " -f1 )

#how many chars to keep.
BYTES=$(( $BYTES - 1 ))
head -c $BYTES $TMPOUT >$TMPOUT.1

Still, that is potentially buggy if the last line has a \r\n terminator, since it only strips 1 char.

Your version may be better, but it's still simpler with the pre-stripped input file:

LINES=$( wc -l $TMPOUT | cut -d" " -f1 )
LINES=$(( $LINES - 1 ))
head -n $LINES $TMPOUT >$TMPOUT.1
tail -n 1 $TMPOUT | tr -d '\r\n' >> $TMPOUT.1

The above commands all work on busybox as installed on my Ubuntu 12.04 box.

Just thinking through this a bit further, you can use the tee command to save the output at the same time as you count the lines:

LINES=$( sed -e '1,4d' -e '$d' | tee $TMPOUT | wc -l | cut -d" " -f1 )

Hmmm, that saves a file-scan.

\$\endgroup\$

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.