4

I have to write a bash script which will count symbols like "<" and ">" from stdin.

For example:

$ ./myscript.sh <example.html
> - 20
< - 21
Found mismatching brackets!

I did this:

x=`grep -o '>' example.html | wc -l`
y=`grep -o '<' example.html | wc -l`
if [ "$x" -ne "$y" ]; then
  echo "Mismatch!"
fi
echo $x
echo $y

Is this good way? I don't know how to get file name "example.html" from stdin.

1
  • 1
    side note: counting the signs is not the best way to check if a file is valid. While it is recommended to encode a greater than sign as &gt; the w3c validator doesn't even generate a warning if you don't. So if you are trying to validate XHTML/XML/SGML/etc. you need to look into different tests.
    – Bram
    Sep 29, 2013 at 11:02

3 Answers 3

15

The whole point of stdin is that it can be anything, it can for instance be a pipe, a network socket a regular file, a device, it can be half way through a regular file when your script is started... If you can't process the data in one pass, then you limit yourself to seekable files, that is regular files and a few device files, or will have to store the information somehow (in a temporary file or memory...). It's possible here to get all the information at once though.

For instance, you could do:

$ grep -o '[<>]' < a.html | sort | uniq -c
     82 <
     82 >

POSIXly:

fold -w 1 a.html | grep '[<>]' | sort | uniq -c

To detect mismatch:

if fold -w 1 a.html | awk '{n[$0]++}
     END{exit(n["<"]!=n[">"])}'
then
  echo match
else
  echo mismatch
fi

Now, to answer the question in the subject, on Linux, you can find a "name" for stdin with:

readlink -f /dev/stdin

Example:

$ readlink -f /dev/stdin < a
/home/chazelas/a
$ : | readlink -f /dev/stdin
/proc/20238/fd/pipe:[758683]

(20238 above is the pid of readlink, so that path won't be of much use after readlink has exited, and it wouldn't be anyway, that pipe:[758683] is just informational, it can't be opened).

And more generally if lsof is available:

lsof -ad0 -p "$$" -Fn 2> /dev/null | sed -n 'n;s/^n//p'

(Though, $$ being the pid of the process that executed the shell, it would not work in subshells that have had their stdin redirected)

Now, you won't necessarily be able to reopen that file for reading, and even when you do, reading from that file might not give you the same data again (think of pipes for instance).

$ seq 3 > a
$ { cat; cat /dev/stdin; } < a
1
2
3
1
2
3
$ cat a | { cat; cat /dev/stdin; }
1
2
3

On Linux, opening /dev/stdin if stdin is a regular file, will read the file from the start again while on other systems, opening /dev/stdin is more like a dup(0), that is it does not rewind the file to beginning (on the first example above, it would output 1\n2\n3\n once instead of twice).

0
7

You will have to store the file contents somehow. You can use a variable.

content=`cat`
x=`echo "$content" | grep -o '>' | wc -l`
y=`echo "$content" | grep -o '<' | wc -l`
if [ "$x" -ne "$y" ]; then
  echo "Mismatch!"
fi
echo $x
echo $y

Or a temporary file (necessary if example.html contains null bytes).

tmp=`mktemp`
trap "rm $tmp" EXIT
x=`grep -o '>' "$tmp" | wc -l`
y=`grep -o '<' "$tmp" | wc -l`
if [ "$x" -ne "$y" ]; then
  echo "Mismatch!"
fi
echo $x
echo $y

If reading the file contents from stdin is not a requirement, you can pass the filename as an argument to the script.

x=`grep -o '>' "$1" | wc -l`
y=`grep -o '<' "$1" | wc -l`
if [ "$x" -ne "$y" ]; then
  echo "Mismatch!"
fi
echo $x
echo $y

Call the script like so:

$ ./myscript.sh example.html
1
  • +1 because this answers the general question of how to pretend stdin is a file, and not trying to specifically count brackets. It also basically hits all 3 ways of doing it (save stdin to variable, save stdin to file, don't use stdin). Sep 29, 2013 at 5:30
1

One possibility for your task is:

#!/bin/bash

if [[ -n $1 ]]; then
   if [[ ! -f $1 ]] || [[ ! -r $1 ]]; then
      echo >&2 "File \`$1' not found or not readable."
      exit 1
   fi
   exec "$0" < "$1"
fi

declare -A hary
while read c; do
   (( ++hary[$c] ))
done < <(grep -o '[<>]')

echo "> ${hary[>]}"
echo "< ${hary[<]}"

If you call this script countmismatched (you can choose a shorter name), you'll be able to use it with or without a filename. A few possiblities:

$ countmismatched example.html
$ countmismatched < example.html
$ cat example.html | countmismatched

The output will be something like:

> 41
< 42

If you need to detect mismatched, add at the end of the script:

if (( hary[<]} != hary[>] )); then
    echo "Mismatched brackets"
else
    echo "It's all good"
fi

or something more explicit:

((difference=hary[<]-hary[>]))
if (( difference>0 )); then
    echo "Mismatched brackets: you have $difference more <'s than >'s"
elif (( difference<0 )); then
    echo "Mismatched brackets: you have $((-difference)) more >'s than <'s"
else
    echo "It's all good"
fi
0

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.