Husk zeropad på hex-verdier
[einar-bin] / prettyping.sh
1 #!/bin/bash
2 #
3 # Written by Denilson Figueiredo de Sá <denilsonsa@gmail.com>
4 # MIT license
5 #
6 # Requirements:
7 # * bash (tested on 4.20, should work on older versions too)
8 # * gawk (GNU awk, tested on 4.0.1, should work on older versions too)
9 # * ping (from iputils)
10
11 # TODO: Detect the following kind of message and avoid printing it repeatedly.
12 # From 192.168.1.11: icmp_seq=4 Destination Host Unreachable
13 #
14 # TODO: print the destination (also) at the bottom bar. Useful after leaving
15 # the script running for quite some time.
16 #
17 # TODO: Implement audible ping.
18 #
19 # TODO: Autodetect the width of printf numbers, so they will always line up correctly.
20 #
21 # TODO: Test the behavior of this script upon receiving out-of-order packets, like these:
22 # http://www.blug.linux.no/rfc1149/pinglogg.txt
23 #
24 # TODO? How will prettyping behave if it receives a duplicate response?
25
26 print_help() {
27 cat << EOF
28 Usage: $MYNAME [prettyping parameters] <standard ping parameters>
29
30 This script is a wrapper around the system's "ping" tool. It will substitute
31 each ping response line by a colored character, giving a very compact overview
32 of the ping responses.
33
34 prettyping parameters:
35 --[no]color Enable/disable color output. (default: enabled)
36 --[no]multicolor Enable/disable multi-color unicode output. Has no effect if
37 either color or unicode is disabled. (default: enabled)
38 --[no]unicode Enable/disable unicode characters. (default: enabled)
39 --[no]terminal Force the output designed to a terminal. (default: auto)
40 --last <n> Use the last "n" pings at the statistics line. (default: 60)
41 --columns <n> Override auto-detection of terminal dimensions.
42 --lines <n> Override auto-detection of terminal dimensions.
43 --rttmin <n> Minimum RTT represented in the unicode graph. (default: auto)
44 --rttmax <n> Maximum RTT represented in the unicode graph. (default: auto)
45
46 ping parameters handled by prettyping:
47 -a Audible ping is not implemented yet.
48 -f Flood mode is not allowed in prettyping.
49 -q Quiet output is not allowed in prettyping.
50 -R Record route mode is not allowed in prettyping.
51 -v Verbose output seems to be the default mode in ping.
52
53 Tested with Linux ping tool from "iputils" package:
54 http://www.linuxfoundation.org/collaborate/workgroups/networking/iputils
55 EOF
56 }
57
58 # Thanks to people at #bash who pointed me at
59 # http://bash-hackers.org/wiki/doku.php/scripting/posparams
60 parse_arguments() {
61 USE_COLOR=1
62 USE_MULTICOLOR=1
63 USE_UNICODE=1
64
65 if [ -t 1 ]; then
66 IS_TERMINAL=1
67 else
68 IS_TERMINAL=0
69 fi
70
71 LAST_N=60
72 OVERRIDE_COLUMNS=0
73 OVERRIDE_LINES=0
74 RTT_MIN=auto
75 RTT_MAX=auto
76
77 PING_PARAMS=( )
78
79 while [[ $# != 0 ]] ; do
80 case "$1" in
81 -h | -help | --help )
82 print_help
83 exit
84 ;;
85
86 # Forbidden ping parameters within prettyping:
87 -f )
88 echo "${MYNAME}: You can't use the -f (flood) option."
89 exit 1
90 ;;
91 -R )
92 # -R prints extra information at each ping response.
93 echo "${MYNAME}: You can't use the -R (record route) option."
94 exit 1
95 ;;
96 -q )
97 echo "${MYNAME}: You can't use the -q (quiet) option."
98 exit 1
99 ;;
100 -v )
101 # -v enables verbose output. However, it seems the output with
102 # or without this option is the same. Anyway, prettyping will
103 # strip this parameter.
104 ;;
105 # Note:
106 # Small values for -s parameter prevents ping from being able to
107 # calculate RTT.
108
109 # New parameters:
110 -a )
111 # TODO: Implement audible ping for responses or for missing packets
112 ;;
113
114 -color | --color ) USE_COLOR=1 ;;
115 -nocolor | --nocolor ) USE_COLOR=0 ;;
116 -multicolor | --multicolor ) USE_MULTICOLOR=1 ;;
117 -nomulticolor | --nomulticolor ) USE_MULTICOLOR=0 ;;
118 -unicode | --unicode ) USE_UNICODE=1 ;;
119 -nounicode | --nounicode ) USE_UNICODE=0 ;;
120 -terminal | --terminal ) IS_TERMINAL=1 ;;
121 -noterminal | --noterminal ) IS_TERMINAL=0 ;;
122
123 #TODO: Check if these parameters are numbers.
124 -last | --last ) LAST_N="$2" ; shift ;;
125 -columns | --columns ) OVERRIDE_COLUMNS="$2" ; shift ;;
126 -lines | --lines ) OVERRIDE_LINES="$2" ; shift ;;
127 -rttmin | --rttmin ) RTT_MIN="$2" ; shift ;;
128 -rttmax | --rttmax ) RTT_MAX="$2" ; shift ;;
129
130 * )
131 PING_PARAMS+=("$1")
132 ;;
133 esac
134 shift
135 done
136
137 if [[ "${RTT_MIN}" -gt 0 && "${RTT_MAX}" -gt 0 && "${RTT_MIN}" -ge "${RTT_MAX}" ]] ; then
138 echo "${MYNAME}: Invalid --rttmin and -rttmax values."
139 exit 1
140 fi
141
142 if [[ "${#PING_PARAMS[@]}" = 0 ]] ; then
143 echo "${MYNAME}: Missing parameters, use --help for instructions."
144 exit 1
145 fi
146 }
147
148 MYNAME=`basename "$0"`
149 parse_arguments "$@"
150
151
152 export LC_ALL=C
153
154 # Warning! Ugly code ahead!
155 # The code is so ugly that the comments explaining it are
156 # bigger than the code itself!
157 #
158 # Suppose this:
159 #
160 # cmd_a | cmd_b &
161 #
162 # I need the PID of cmd_a. How can I get it?
163 # In bash, $! will give me the PID of cmd_b.
164 #
165 # So, I came up with this ugly solution: open a subshell, like this:
166 #
167 # (
168 # cmd_a &
169 # echo "This is the PID I want $!"
170 # wait
171 # ) | cmd_b
172
173
174 # Ignore Ctrl+C here.
175 # If I don't do this, this shell script is killed before
176 # ping and gawk can finish their work.
177 trap '' 2
178
179 # Now the ugly code.
180 (
181 ping "${PING_PARAMS[@]}" &
182 # Commented out, because it looks like this line is not needed
183 #trap "kill -2 $! ; exit 1" 2 # Catch Ctrl+C here
184 wait
185 ) 2>&1 | gawk '
186 # Weird that awk does not come with abs(), so I need to implement it.
187 function abs(x) {
188 return ( (x < 0) ? -x : x )
189 }
190
191 # Ditto for ceiling function.
192 function ceil(x) {
193 return (x == int(x)) ? x : int(x) + 1
194 }
195
196 # Currently, this function is called once, at the beginning of this
197 # script, but it is also possible to call this more than once, to
198 # handle window size changes while this program is running.
199 #
200 # Local variables MUST be declared in argument list, else they are
201 # seen as global. Ugly, but that is how awk works.
202 function get_terminal_size(SIZE,SIZEA) {
203 if( HAS_STTY ) {
204 if( (STTY_CMD | getline SIZE) == 1 ) {
205 split(SIZE, SIZEA, " ")
206 LINES = SIZEA[1]
207 COLUMNS = SIZEA[2]
208 } else {
209 HAS_STTY = 0
210 }
211 close(STTY_CMD)
212 }
213 if ( int('"${OVERRIDE_COLUMNS}"') ) { COLUMNS = int('"${OVERRIDE_COLUMNS}"') }
214 if ( int('"${OVERRIDE_LINES}"') ) { LINES = int('"${OVERRIDE_LINES}"') }
215 }
216
217 ############################################################
218 # Functions related to cursor handling
219
220 # Function called whenever a non-dotted line is printed.
221 #
222 # It will move the cursor to the line next to the statistics and
223 # restore the default color.
224 function other_line_is_printed() {
225 if( IS_PRINTING_DOTS ) {
226 if( '"${IS_TERMINAL}"' ) {
227 printf( ESC_DEFAULT ESC_NEXTLINE ESC_NEXTLINE "\n" )
228 } else {
229 printf( ESC_DEFAULT "\n" )
230 print_statistics_bar()
231 }
232 }
233 IS_PRINTING_DOTS = 0
234 CURR_COL = 0
235 }
236
237 # Function called whenever a non-dotted line is repeated.
238 function other_line_is_repeated() {
239 if (other_line_times < 2) {
240 return
241 }
242 if( '"${IS_TERMINAL}"' ) {
243 printf( ESC_DEFAULT ESC_ERASELINE "\r" )
244 }
245 printf( "Last message repeated %d times.", other_line_times )
246 if( ! '"${IS_TERMINAL}"' ) {
247 printf( "\n" )
248 }
249 }
250
251 # Prints the newlines required for the live statistics.
252 #
253 # I need to print some newlines and then return the cursor back to its position
254 # to make sure the terminal will scroll.
255 #
256 # If the output is not a terminal, break lines on every LAST_N dots.
257 function print_newlines_if_needed() {
258 if( '"${IS_TERMINAL}"' ) {
259 # COLUMNS-1 because I want to avoid bugs with the cursor at the last column
260 if( CURR_COL >= COLUMNS-1 ) {
261 CURR_COL = 0
262 }
263 if( CURR_COL == 0 ) {
264 if( IS_PRINTING_DOTS ) {
265 printf( "\n" )
266 }
267 #printf( "\n" "\n" ESC_PREVLINE ESC_PREVLINE ESC_ERASELINE )
268 printf( ESC_DEFAULT "\n" "\n" ESC_CURSORUP ESC_CURSORUP ESC_ERASELINE )
269 }
270 } else {
271 if( CURR_COL >= LAST_N ) {
272 CURR_COL = 0
273 printf( ESC_DEFAULT "\n" )
274 print_statistics_bar()
275 }
276 }
277 CURR_COL++
278 IS_PRINTING_DOTS = 1
279 }
280
281 ############################################################
282 # Functions related to the data structure of "Last N" statistics.
283
284 # Clears the data structure.
285 function clear(d) {
286 d["index"] = 0 # The next position to store a value
287 d["size"] = 0 # The array size, goes up to LAST_N
288 }
289
290 # This function stores the value to the passed data structure.
291 # The data structure holds at most LAST_N values. When it is full,
292 # a new value overwrite the oldest one.
293 function store(d, value) {
294 d[d["index"]] = value
295 d["index"]++
296 if( d["index"] >= d["size"] ) {
297 if( d["size"] < LAST_N ) {
298 d["size"]++
299 } else {
300 d["index"] = 0
301 }
302 }
303 }
304
305 ############################################################
306 # Functions related to processing the received response
307
308 function process_rtt(rtt) {
309 # Overall statistics
310 last_rtt = rtt
311 total_rtt += rtt
312 if( last_seq == 0 ) {
313 min_rtt = max_rtt = rtt
314 } else {
315 if( rtt < min_rtt ) min_rtt = rtt
316 if( rtt > max_rtt ) max_rtt = rtt
317 }
318
319 # "Last N" statistics
320 store(lastn_rtt,rtt)
321 }
322
323 ############################################################
324 # Functions related to printing the fancy ping response
325
326 # block_index is just a local variable.
327 function print_response_legend(i) {
328 if( '"${USE_UNICODE}"' ) {
329 printf( BLOCK[0] ESC_DEFAULT "%4d ", 0)
330 for( i=1 ; i<BLOCK_LEN ; i++ ) {
331 printf( BLOCK[i] ESC_DEFAULT "%4d ",
332 BLOCK_RTT_MIN + ceil((i-1) * BLOCK_RTT_RANGE / (BLOCK_LEN - 2)) )
333 }
334 printf( "\n" )
335 }
336
337 # Useful code for debugging.
338 #for( i=0 ; i<=BLOCK_RTT_MAX ; i++ ) {
339 # print_received_response(i)
340 # printf( ESC_DEFAULT "%4d\n", i )
341 #}
342 }
343
344 # block_index is just a local variable.
345 function print_received_response(rtt, block_index) {
346 if( '"${USE_UNICODE}"' ) {
347 if( rtt < BLOCK_RTT_MIN ) {
348 block_index = 0
349 } else if( rtt >= BLOCK_RTT_MAX ) {
350 block_index = BLOCK_LEN - 1
351 } else {
352 block_index = 1 + int((rtt - BLOCK_RTT_MIN) * (BLOCK_LEN - 2) / BLOCK_RTT_RANGE)
353 }
354 printf( BLOCK[block_index] )
355 } else {
356 printf( ESC_GREEN "." )
357 }
358 }
359
360 function print_missing_response(rtt) {
361 printf( ESC_RED "!" )
362 }
363
364 ############################################################
365 # Functions related to printing statistics
366
367 function print_overall() {
368 if( '"${IS_TERMINAL}"' ) {
369 printf( "%2d/%3d (%2d%%) lost; %4.0f/" ESC_BOLD "%4.0f" ESC_DEFAULT "/%4.0fms; last: " ESC_BOLD "%4.0f" ESC_DEFAULT "ms",
370 lost,
371 lost+received,
372 (lost*100/(lost+received)),
373 min_rtt,
374 (total_rtt/received),
375 max_rtt,
376 last_rtt )
377 } else {
378 printf( "%2d/%3d (%2d%%) lost; %4.0f/" ESC_BOLD "%4.0f" ESC_DEFAULT "/%4.0fms",
379 lost,
380 lost+received,
381 (lost*100/(lost+received)),
382 min_rtt,
383 (total_rtt/received),
384 max_rtt )
385 }
386 }
387
388 function print_last_n(i, sum, min, avg, max, diffs) {
389 # Calculate and print the lost packets statistics
390 sum = 0
391 for( i=0 ; i<lastn_lost["size"] ; i++ ) {
392 sum += lastn_lost[i]
393 }
394 printf( "%2d/%3d (%2d%%) lost; ",
395 sum,
396 lastn_lost["size"],
397 sum*100/lastn_lost["size"] )
398
399 # Calculate the min/avg/max rtt times
400 sum = diffs = 0
401 min = max = lastn_rtt[0]
402 for( i=0 ; i<lastn_rtt["size"] ; i++ ) {
403 sum += lastn_rtt[i]
404 if( lastn_rtt[i] < min ) min = lastn_rtt[i]
405 if( lastn_rtt[i] > max ) max = lastn_rtt[i]
406 }
407 avg = sum/lastn_rtt["size"]
408
409 # Calculate mdev (mean absolute deviation)
410 for( i=0 ; i<lastn_rtt["size"] ; i++ ) {
411 diffs += abs(lastn_rtt[i] - avg)
412 }
413 diffs /= lastn_rtt["size"]
414
415 # Print the rtt statistics
416 printf( "%4.0f/" ESC_BOLD "%4.0f" ESC_DEFAULT "/%4.0f/%4.0fms (last %d)",
417 min,
418 avg,
419 max,
420 diffs,
421 lastn_rtt["size"] )
422 }
423
424 function print_statistics_bar() {
425 if( '"${IS_TERMINAL}"' ) {
426 printf( ESC_SAVEPOS ESC_DEFAULT )
427
428 printf( ESC_NEXTLINE ESC_ERASELINE )
429 print_overall()
430 printf( ESC_NEXTLINE ESC_ERASELINE )
431 print_last_n()
432
433 printf( ESC_UNSAVEPOS )
434 } else {
435 print_overall()
436 printf( "\n" )
437 print_last_n()
438 printf( "\n" )
439 }
440 }
441
442 ############################################################
443 # Initializations
444 BEGIN {
445 # Easy way to get each value from ping output
446 FS = "="
447
448 ############################################################
449 # General internal variables
450
451 # This is needed to keep track of lost packets
452 last_seq = 0
453
454 # The previously printed non-ping-response line
455 other_line = ""
456 other_line_times = 0
457
458 # Variables to keep the screen clean
459 IS_PRINTING_DOTS = 0
460 CURR_COL = 0
461
462 ############################################################
463 # Variables related to "overall" statistics
464 received = 0
465 lost = 0
466 total_rtt = 0
467 min_rtt = 0
468 max_rtt = 0
469 last_rtt = 0
470
471 ############################################################
472 # Variables related to "last N" statistics
473 LAST_N = int('"${LAST_N}"')
474
475 # Data structures for the "last N" statistics
476 clear(lastn_lost)
477 clear(lastn_rtt)
478
479 ############################################################
480 # Terminal height and width
481
482 # These are sane defaults, in case we cannot query the actual terminal size
483 LINES = 24
484 COLUMNS = 80
485
486 # Auto-detecting the terminal size
487 HAS_STTY = 1
488 STTY_CMD = "stty size --file=/dev/tty 2> /dev/null"
489 get_terminal_size()
490 if( '"${IS_TERMINAL}"' && COLUMNS <= 50 ) {
491 print "Warning: terminal width is too small."
492 }
493
494 ############################################################
495 # ANSI escape codes
496
497 # Color escape codes.
498 # Fortunately, awk defaults any unassigned variable to an empty string.
499 if( '"${USE_COLOR}"' ) {
500 ESC_DEFAULT = "\033[0m"
501 ESC_BOLD = "\033[1m"
502 #ESC_BLACK = "\033[0;30m"
503 #ESC_GRAY = "\033[1;30m"
504 ESC_RED = "\033[0;31m"
505 ESC_GREEN = "\033[0;32m"
506 ESC_YELLOW = "\033[0;33m"
507 ESC_BLUE = "\033[0;34m"
508 ESC_MAGENTA = "\033[0;35m"
509 ESC_CYAN = "\033[0;36m"
510 ESC_WHITE = "\033[0;37m"
511 ESC_YELLOW_ON_GREEN = "\033[42;33m"
512 ESC_RED_ON_YELLOW = "\033[43;31m"
513 }
514 # Other escape codes, see:
515 # http://en.wikipedia.org/wiki/ANSI_escape_code
516 # http://invisible-island.net/xterm/ctlseqs/ctlseqs.html
517 ESC_NEXTLINE = "\n"
518 ESC_CURSORUP = "\033[A"
519 ESC_SCROLLUP = "\033[S"
520 ESC_SCROLLDOWN = "\033[T"
521 ESC_ERASELINE = "\033[2K"
522 ESC_SAVEPOS = "\0337"
523 ESC_UNSAVEPOS = "\0338"
524
525 # I am avoiding these escapes as they are not listed in:
526 # http://vt100.net/docs/vt100-ug/chapter3.html
527 #ESC_PREVLINE = "\033[F"
528 #ESC_SAVEPOS = "\033[s"
529 #ESC_UNSAVEPOS = "\033[u"
530
531 # I am avoiding this to improve compatibility with (older versions of) tmux
532 #ESC_NEXTLINE = "\033[E"
533
534 ############################################################
535 # Unicode characters (based on https://github.com/holman/spark )
536 if( '"${USE_UNICODE}"' ) {
537 BLOCK[ 0] = ESC_GREEN "▁"
538 BLOCK[ 1] = ESC_GREEN "▂"
539 BLOCK[ 2] = ESC_GREEN "▃"
540 BLOCK[ 3] = ESC_GREEN "▄"
541 BLOCK[ 4] = ESC_GREEN "▅"
542 BLOCK[ 5] = ESC_GREEN "▆"
543 BLOCK[ 6] = ESC_GREEN "▇"
544 BLOCK[ 7] = ESC_GREEN "█"
545 BLOCK[ 8] = ESC_YELLOW_ON_GREEN "▁"
546 BLOCK[ 9] = ESC_YELLOW_ON_GREEN "▂"
547 BLOCK[10] = ESC_YELLOW_ON_GREEN "▃"
548 BLOCK[11] = ESC_YELLOW_ON_GREEN "▄"
549 BLOCK[12] = ESC_YELLOW_ON_GREEN "▅"
550 BLOCK[13] = ESC_YELLOW_ON_GREEN "▆"
551 BLOCK[14] = ESC_YELLOW_ON_GREEN "▇"
552 BLOCK[15] = ESC_YELLOW_ON_GREEN "█"
553 BLOCK[16] = ESC_RED_ON_YELLOW "▁"
554 BLOCK[17] = ESC_RED_ON_YELLOW "▂"
555 BLOCK[18] = ESC_RED_ON_YELLOW "▃"
556 BLOCK[19] = ESC_RED_ON_YELLOW "▄"
557 BLOCK[20] = ESC_RED_ON_YELLOW "▅"
558 BLOCK[21] = ESC_RED_ON_YELLOW "▆"
559 BLOCK[22] = ESC_RED_ON_YELLOW "▇"
560 BLOCK[23] = ESC_RED_ON_YELLOW "█"
561 if( '"${USE_MULTICOLOR}"' && '"${USE_COLOR}"' ) {
562 # Multi-color version:
563 BLOCK_LEN = 24
564 BLOCK_RTT_MIN = 10
565 BLOCK_RTT_MAX = 230
566 } else {
567 # Simple version:
568 BLOCK_LEN = 8
569 BLOCK_RTT_MIN = 25
570 BLOCK_RTT_MAX = 175
571 }
572
573 if( int('"${RTT_MIN}"') > 0 && int('"${RTT_MAX}"') > 0 ) {
574 BLOCK_RTT_MIN = int('"${RTT_MIN}"')
575 BLOCK_RTT_MAX = int('"${RTT_MAX}"')
576 } else if( int('"${RTT_MIN}"') > 0 ) {
577 BLOCK_RTT_MIN = int('"${RTT_MIN}"')
578 BLOCK_RTT_MAX = BLOCK_RTT_MIN * (BLOCK_LEN - 1)
579 } else if( int('"${RTT_MAX}"') > 0 ) {
580 BLOCK_RTT_MAX = int('"${RTT_MAX}"')
581 BLOCK_RTT_MIN = int(BLOCK_RTT_MAX / (BLOCK_LEN - 1))
582 }
583
584 BLOCK_RTT_RANGE = BLOCK_RTT_MAX - BLOCK_RTT_MIN
585 print_response_legend()
586 }
587 }
588
589 ############################################################
590 # Main loop
591 {
592 # Sample line:
593 # 64 bytes from 8.8.8.8: icmp_seq=1 ttl=49 time=184 ms
594 if( $0 ~ /^[0-9]+ bytes from .*: icmp_[rs]eq=[0-9]+ ttl=[0-9]+ time=[0-9.]+ *ms/ ) {
595 if( other_line_times >= 2 ) {
596 if( '"${IS_TERMINAL}"' ) {
597 printf( "\n" )
598 } else {
599 other_line_is_repeated()
600 }
601 }
602 other_line = ""
603 other_line_times = 0
604
605 # $1 = useless prefix string
606 # $2 = icmp_seq
607 # $3 = ttl
608 # $4 = time
609
610 # This must be called before incrementing the last_seq variable!
611 rtt = int($4)
612 process_rtt(rtt)
613
614 seq = int($2)
615
616 while( last_seq < seq - 1 ) {
617 # Lost a packet
618 print_newlines_if_needed()
619 print_missing_response()
620
621 last_seq++
622 lost++
623 store(lastn_lost, 1)
624 }
625
626 # Received a packet
627 print_newlines_if_needed()
628 print_received_response(rtt)
629
630 last_seq++
631 received++
632 store(lastn_lost, 0)
633
634 if( '"${IS_TERMINAL}"' ) {
635 print_statistics_bar()
636 }
637 } else if ( $0 == "" ) {
638 # Do nothing on blank lines.
639 } else {
640 other_line_is_printed()
641 if ( $0 == other_line ) {
642 other_line_times++
643 if( '"${IS_TERMINAL}"' ) {
644 other_line_is_repeated()
645 }
646 } else {
647 other_line = $0
648 other_line_times = 1
649 printf( "%s\n", $0 )
650 }
651 }
652
653 # Not needed when the output is a terminal, but does not hurt either.
654 fflush()
655 }'