Problem
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 rn
tail -1 $TMPOUT | tr -d 'rn' >> $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.
Solution
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
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 rn
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 'rn' >> $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.