I have the following output:

2015/1/7    8
2015/1/8    49
2015/1/9    40
2015/1/10   337
2015/1/11   11
2015/1/12   3
2015/1/13   9
2015/1/14   102
2015/1/15   62
2015/1/16   10
2015/1/17   30
2015/1/18   30
2015/1/19   1
2015/1/20   3
2015/1/21   23
2015/1/22   12
2015/1/24   6
2015/1/25   3
2015/1/27   2
2015/1/28   16
2015/1/29   1
2015/2/1    12
2015/2/2    2
2015/2/3    1
2015/2/4    10
2015/2/5    13
2015/2/6    2
2015/2/9    2
2015/2/10   25
2015/2/11   1
2015/2/12   6
2015/2/13   12
2015/2/14   2
2015/2/16   8
2015/2/17   8
2015/2/20   1
2015/2/23   1
2015/2/27   1
2015/3/2    3
2015/3/3    2

And I'd like to draw an histogram

2015/1/7  ===
2015/1/8  ===========
2015/1/9  ==========
2015/1/10 ====================================================================
2015/1/11 ===
2015/1/11 =
...

Do you know if there is a bash command that would let me do that?

migrated from serverfault.com Jan 6 '15 at 16:50

This question came from our site for system and network administrators.

up vote 9 down vote accepted

Try this in :

perl -lane 'print $F[0], "\t", "=" x ($F[1] / 5)' file

EXPLANATIONS:

  • -a is an explicit split() in @F array, we get the values with $F[n]
  • x is to tell perl to print a character N times
  • ($F[1] / 5) : here we get the number and divide it by 5 for a pretty print output
  • 1
    perl -lane 'print $F[0], "\t", $F[1], "\t", "=" x ($F[1] / 3 + 1)' It looks really great :) thanks – Natim Jan 7 '15 at 9:05

In perl:

perl -pe 's/ (\d+)$/"="x$1/e' file
  • e causes the expression to be evaluated, so I get = repeated using the value of $1 (the number matched by (\d+)).
  • You could do "="x($1\/3) instead of "="x$1 to get shorter lines. (The / is escaped since we're in the middle of a substitution command.)

In bash (inspired from this SO answer):

while read d n 
do 
    printf "%s\t%${n}s\n" "$d" = | tr ' ' '=' 
done < test.txt
  • printf pads the second string using spaces to get a width of $n (%${n}s), and I replace the spaces with =.
  • The columns are delimited using a tab (\t), but you can make it prettier by piping to column -ts'\t'.
  • You could use $((n/3)) instead of ${n} to get shorter lines.

Another version:

unset IFS; printf "%s\t%*s\n" $(sed 's/$/ =/' test.txt) | tr ' ' =

The only drawback I can see is that you'll need to pipe sed's output to something if you want to scale down, otherwise this is the cleanest option. If there is a chance of your input file containing one of [?* you should lead the command w/ set -f;.

  • 2
    Bravo for showing a shell solution too. Your Perl solution is very clean as well. – chicks Jan 6 '15 at 17:38
  • @mikeserv Wonderful! I always forget %*s even though it was the first printf-related trick I learnt in C programming. – muru Jan 7 '15 at 2:22
  • The printf(sed) | tr version doesn't works here as far as I can tell. – Natim Jan 7 '15 at 9:03
  • @Natim here being where? – muru Jan 7 '15 at 10:42
  • @mikeserv limitations in argument length perhaps? – muru Jan 7 '15 at 11:55

Easy with awk

awk '{$2=sprintf("%-*s", $2, ""); gsub(" ", "=", $2); printf("%-10s%s\n", $1, $2)}' file

2015/1/7 ========
2015/1/8 =================================================
2015/1/9 ========================================
..
..

Or with my favourite programming language

python3 -c 'import sys
for line in sys.stdin:
  data, width = line.split()
  print("{:<10}{:=<{width}}".format(data, "", width=width))' <file

How about:

#! /bin/bash
histo="======================================================================+"

read datewd value

while [ -n "$datewd" ] ; do
   # Use a default width of 70 for the histogram
   echo -n "$datewd      "
   echo ${histo:0:$value}

   read datewd value
done

Which produces:

~/bash $./histogram.sh < histdata.txt
2015/1/7    ========
2015/1/8    =================================================
2015/1/9    ========================================
2015/1/10   ======================================================================+
2015/1/11   ===========
2015/1/12   ===
2015/1/13   =========
2015/1/14   ======================================================================+
2015/1/15   ==============================================================
2015/1/16   ==========
2015/1/17   ==============================
2015/1/18   ==============================
2015/1/19   =
2015/1/20   ===
2015/1/21   =======================
2015/1/22   ============
2015/1/24   ======
2015/1/25   ===
2015/1/27   ==
2015/1/28   ================
2015/1/29   =
2015/2/1    ============
2015/2/2    ==
2015/2/3    =
2015/2/4    ==========
2015/2/5    =============
2015/2/6    ==
2015/2/9    ==
2015/2/10   =========================
2015/2/11   =
2015/2/12   ======
2015/2/13   ============
2015/2/14   ==
2015/2/16   ========
2015/2/17   ========
2015/2/20   =
2015/2/23   =
2015/2/27   =
2015/3/2    ===
2015/3/3    ==
~/bash $

This struck me as a fun traditional command line problem. Here's my bash script solution:

awk '{if (count[$1]){count[$1] += $2} else {count[$1] = $2}} \
        END{for (year in count) {print year, count[year];}}' data |
sed -e 's/\// /g' | sort -k1,1n -k2,2n -k3,3n |
awk '{printf("%d/%d/%d\t", $1,$2,$3); for (i=0;i<$4;++i) {printf("=")}; printf("\n");}'

The little script above assumes the data is in a file imaginatively named "data".

I'm not too happy with the "run it through sed and sort" line - it would be unnecessary if your month and day-of-month always had 2 digits, but that's life.

Also, as a historical note, traditional Unixes used to come with a command line plotting utility that could do fairly ugly ASCII graphs and plots. I can't remember the name, but it looks like GNU plotutils replace the old traditional utility.

  • Shouldn't that be if ($1 in count) ...? – muru Jan 6 '15 at 18:58
  • 1
    @muru - seems to work either way. However, I did find a typo in the "else" clause. Thanks. – Bruce Ediger Jan 6 '15 at 19:19

Nice exercise here. I dumped the data in a file called "data" because I am very imaginative.

Well, you asked for it in bash... here it is in pure bash.

cat data | while read date i; do printf "%-10s " $date; for x in $(seq 1 $i); do echo -n "="; done; echo; done

awk is a better option.

awk '{ s=" ";while ($2-->0) s=s"=";printf "%-10s %s\n",$1,s }' data
  • Can you pipe the data through awk instead of using a file? – Natim Jan 7 '15 at 8:52
  • Yes, it's the same thing either way. Just add a "cat data |" at the beginning like I had for the bash bits, or a "<data" at the end. Or you can even just have the awk part without a file specified, paste in the data and hit ctrl-D at the end. Specifying the file just treats that file as stdin, and I didn't want to keep copying and pasting the datafile because I'm lazy. – Falsenames Jan 7 '15 at 16:47
  • 1
    Actually, I just reread the question while linking this to a coworker... you said you had "output", not a data file. So you can just run whatever is creating that report, then pipe it to awk, and you're done. Pipes just direct output of the last command as the source of input for the next command. – Falsenames Jan 7 '15 at 17:06

Try this:

while read value count; do
    printf '%s:\t%s\n' "${value}" "$(printf "%${count}s" | tr ' ' '=')"
done <path/to/my-output

The only tricky part is the construction of the bar. I do it here by delegating to printf and tr like this SO answer.

As a bonus, it's POSIX-sh-compliant.

References:

Your Answer

 

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

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