#!/bin/bash # # Written by Denilson Figueiredo de Sá # MIT license # # Requirements: # * bash (tested on 4.20, should work on older versions too) # * gawk (GNU awk, tested on 4.0.1, should work on older versions too) # * ping (from iputils) # TODO: Detect the following kind of message and avoid printing it repeatedly. # From 192.168.1.11: icmp_seq=4 Destination Host Unreachable # # TODO: print the destination (also) at the bottom bar. Useful after leaving # the script running for quite some time. # # TODO: Implement audible ping. # # TODO: Autodetect the width of printf numbers, so they will always line up correctly. # # TODO: Test the behavior of this script upon receiving out-of-order packets, like these: # http://www.blug.linux.no/rfc1149/pinglogg.txt # # TODO? How will prettyping behave if it receives a duplicate response? print_help() { cat << EOF Usage: $MYNAME [prettyping parameters] This script is a wrapper around the system's "ping" tool. It will substitute each ping response line by a colored character, giving a very compact overview of the ping responses. prettyping parameters: --[no]color Enable/disable color output. (default: enabled) --[no]multicolor Enable/disable multi-color unicode output. Has no effect if either color or unicode is disabled. (default: enabled) --[no]unicode Enable/disable unicode characters. (default: enabled) --[no]terminal Force the output designed to a terminal. (default: auto) --last Use the last "n" pings at the statistics line. (default: 60) --columns Override auto-detection of terminal dimensions. --lines Override auto-detection of terminal dimensions. --rttmin Minimum RTT represented in the unicode graph. (default: auto) --rttmax Maximum RTT represented in the unicode graph. (default: auto) ping parameters handled by prettyping: -a Audible ping is not implemented yet. -f Flood mode is not allowed in prettyping. -q Quiet output is not allowed in prettyping. -R Record route mode is not allowed in prettyping. -v Verbose output seems to be the default mode in ping. Tested with Linux ping tool from "iputils" package: http://www.linuxfoundation.org/collaborate/workgroups/networking/iputils EOF } # Thanks to people at #bash who pointed me at # http://bash-hackers.org/wiki/doku.php/scripting/posparams parse_arguments() { USE_COLOR=1 USE_MULTICOLOR=1 USE_UNICODE=1 if [ -t 1 ]; then IS_TERMINAL=1 else IS_TERMINAL=0 fi LAST_N=60 OVERRIDE_COLUMNS=0 OVERRIDE_LINES=0 RTT_MIN=auto RTT_MAX=auto PING_PARAMS=( ) while [[ $# != 0 ]] ; do case "$1" in -h | -help | --help ) print_help exit ;; # Forbidden ping parameters within prettyping: -f ) echo "${MYNAME}: You can't use the -f (flood) option." exit 1 ;; -R ) # -R prints extra information at each ping response. echo "${MYNAME}: You can't use the -R (record route) option." exit 1 ;; -q ) echo "${MYNAME}: You can't use the -q (quiet) option." exit 1 ;; -v ) # -v enables verbose output. However, it seems the output with # or without this option is the same. Anyway, prettyping will # strip this parameter. ;; # Note: # Small values for -s parameter prevents ping from being able to # calculate RTT. # New parameters: -a ) # TODO: Implement audible ping for responses or for missing packets ;; -color | --color ) USE_COLOR=1 ;; -nocolor | --nocolor ) USE_COLOR=0 ;; -multicolor | --multicolor ) USE_MULTICOLOR=1 ;; -nomulticolor | --nomulticolor ) USE_MULTICOLOR=0 ;; -unicode | --unicode ) USE_UNICODE=1 ;; -nounicode | --nounicode ) USE_UNICODE=0 ;; -terminal | --terminal ) IS_TERMINAL=1 ;; -noterminal | --noterminal ) IS_TERMINAL=0 ;; #TODO: Check if these parameters are numbers. -last | --last ) LAST_N="$2" ; shift ;; -columns | --columns ) OVERRIDE_COLUMNS="$2" ; shift ;; -lines | --lines ) OVERRIDE_LINES="$2" ; shift ;; -rttmin | --rttmin ) RTT_MIN="$2" ; shift ;; -rttmax | --rttmax ) RTT_MAX="$2" ; shift ;; * ) PING_PARAMS+=("$1") ;; esac shift done if [[ "${RTT_MIN}" -gt 0 && "${RTT_MAX}" -gt 0 && "${RTT_MIN}" -ge "${RTT_MAX}" ]] ; then echo "${MYNAME}: Invalid --rttmin and -rttmax values." exit 1 fi if [[ "${#PING_PARAMS[@]}" = 0 ]] ; then echo "${MYNAME}: Missing parameters, use --help for instructions." exit 1 fi } MYNAME=`basename "$0"` parse_arguments "$@" export LC_ALL=C # Warning! Ugly code ahead! # The code is so ugly that the comments explaining it are # bigger than the code itself! # # Suppose this: # # cmd_a | cmd_b & # # I need the PID of cmd_a. How can I get it? # In bash, $! will give me the PID of cmd_b. # # So, I came up with this ugly solution: open a subshell, like this: # # ( # cmd_a & # echo "This is the PID I want $!" # wait # ) | cmd_b # Ignore Ctrl+C here. # If I don't do this, this shell script is killed before # ping and gawk can finish their work. trap '' 2 # Now the ugly code. ( ping "${PING_PARAMS[@]}" & # Commented out, because it looks like this line is not needed #trap "kill -2 $! ; exit 1" 2 # Catch Ctrl+C here wait ) 2>&1 | gawk ' # Weird that awk does not come with abs(), so I need to implement it. function abs(x) { return ( (x < 0) ? -x : x ) } # Ditto for ceiling function. function ceil(x) { return (x == int(x)) ? x : int(x) + 1 } # Currently, this function is called once, at the beginning of this # script, but it is also possible to call this more than once, to # handle window size changes while this program is running. # # Local variables MUST be declared in argument list, else they are # seen as global. Ugly, but that is how awk works. function get_terminal_size(SIZE,SIZEA) { if( HAS_STTY ) { if( (STTY_CMD | getline SIZE) == 1 ) { split(SIZE, SIZEA, " ") LINES = SIZEA[1] COLUMNS = SIZEA[2] } else { HAS_STTY = 0 } close(STTY_CMD) } if ( int('"${OVERRIDE_COLUMNS}"') ) { COLUMNS = int('"${OVERRIDE_COLUMNS}"') } if ( int('"${OVERRIDE_LINES}"') ) { LINES = int('"${OVERRIDE_LINES}"') } } ############################################################ # Functions related to cursor handling # Function called whenever a non-dotted line is printed. # # It will move the cursor to the line next to the statistics and # restore the default color. function other_line_is_printed() { if( IS_PRINTING_DOTS ) { if( '"${IS_TERMINAL}"' ) { printf( ESC_DEFAULT ESC_NEXTLINE ESC_NEXTLINE "\n" ) } else { printf( ESC_DEFAULT "\n" ) print_statistics_bar() } } IS_PRINTING_DOTS = 0 CURR_COL = 0 } # Function called whenever a non-dotted line is repeated. function other_line_is_repeated() { if (other_line_times < 2) { return } if( '"${IS_TERMINAL}"' ) { printf( ESC_DEFAULT ESC_ERASELINE "\r" ) } printf( "Last message repeated %d times.", other_line_times ) if( ! '"${IS_TERMINAL}"' ) { printf( "\n" ) } } # Prints the newlines required for the live statistics. # # I need to print some newlines and then return the cursor back to its position # to make sure the terminal will scroll. # # If the output is not a terminal, break lines on every LAST_N dots. function print_newlines_if_needed() { if( '"${IS_TERMINAL}"' ) { # COLUMNS-1 because I want to avoid bugs with the cursor at the last column if( CURR_COL >= COLUMNS-1 ) { CURR_COL = 0 } if( CURR_COL == 0 ) { if( IS_PRINTING_DOTS ) { printf( "\n" ) } #printf( "\n" "\n" ESC_PREVLINE ESC_PREVLINE ESC_ERASELINE ) printf( ESC_DEFAULT "\n" "\n" ESC_CURSORUP ESC_CURSORUP ESC_ERASELINE ) } } else { if( CURR_COL >= LAST_N ) { CURR_COL = 0 printf( ESC_DEFAULT "\n" ) print_statistics_bar() } } CURR_COL++ IS_PRINTING_DOTS = 1 } ############################################################ # Functions related to the data structure of "Last N" statistics. # Clears the data structure. function clear(d) { d["index"] = 0 # The next position to store a value d["size"] = 0 # The array size, goes up to LAST_N } # This function stores the value to the passed data structure. # The data structure holds at most LAST_N values. When it is full, # a new value overwrite the oldest one. function store(d, value) { d[d["index"]] = value d["index"]++ if( d["index"] >= d["size"] ) { if( d["size"] < LAST_N ) { d["size"]++ } else { d["index"] = 0 } } } ############################################################ # Functions related to processing the received response function process_rtt(rtt) { # Overall statistics last_rtt = rtt total_rtt += rtt if( last_seq == 0 ) { min_rtt = max_rtt = rtt } else { if( rtt < min_rtt ) min_rtt = rtt if( rtt > max_rtt ) max_rtt = rtt } # "Last N" statistics store(lastn_rtt,rtt) } ############################################################ # Functions related to printing the fancy ping response # block_index is just a local variable. function print_response_legend(i) { if( '"${USE_UNICODE}"' ) { printf( BLOCK[0] ESC_DEFAULT "%4d ", 0) for( i=1 ; i= BLOCK_RTT_MAX ) { block_index = BLOCK_LEN - 1 } else { block_index = 1 + int((rtt - BLOCK_RTT_MIN) * (BLOCK_LEN - 2) / BLOCK_RTT_RANGE) } printf( BLOCK[block_index] ) } else { printf( ESC_GREEN "." ) } } function print_missing_response(rtt) { printf( ESC_RED "!" ) } ############################################################ # Functions related to printing statistics function print_overall() { if( '"${IS_TERMINAL}"' ) { printf( "%2d/%3d (%2d%%) lost; %4.0f/" ESC_BOLD "%4.0f" ESC_DEFAULT "/%4.0fms; last: " ESC_BOLD "%4.0f" ESC_DEFAULT "ms", lost, lost+received, (lost*100/(lost+received)), min_rtt, (total_rtt/received), max_rtt, last_rtt ) } else { printf( "%2d/%3d (%2d%%) lost; %4.0f/" ESC_BOLD "%4.0f" ESC_DEFAULT "/%4.0fms", lost, lost+received, (lost*100/(lost+received)), min_rtt, (total_rtt/received), max_rtt ) } } function print_last_n(i, sum, min, avg, max, diffs) { # Calculate and print the lost packets statistics sum = 0 for( i=0 ; i max ) max = lastn_rtt[i] } avg = sum/lastn_rtt["size"] # Calculate mdev (mean absolute deviation) for( i=0 ; i 0 && int('"${RTT_MAX}"') > 0 ) { BLOCK_RTT_MIN = int('"${RTT_MIN}"') BLOCK_RTT_MAX = int('"${RTT_MAX}"') } else if( int('"${RTT_MIN}"') > 0 ) { BLOCK_RTT_MIN = int('"${RTT_MIN}"') BLOCK_RTT_MAX = BLOCK_RTT_MIN * (BLOCK_LEN - 1) } else if( int('"${RTT_MAX}"') > 0 ) { BLOCK_RTT_MAX = int('"${RTT_MAX}"') BLOCK_RTT_MIN = int(BLOCK_RTT_MAX / (BLOCK_LEN - 1)) } BLOCK_RTT_RANGE = BLOCK_RTT_MAX - BLOCK_RTT_MIN print_response_legend() } } ############################################################ # Main loop { # Sample line: # 64 bytes from 8.8.8.8: icmp_seq=1 ttl=49 time=184 ms if( $0 ~ /^[0-9]+ bytes from .*: icmp_[rs]eq=[0-9]+ ttl=[0-9]+ time=[0-9.]+ *ms/ ) { if( other_line_times >= 2 ) { if( '"${IS_TERMINAL}"' ) { printf( "\n" ) } else { other_line_is_repeated() } } other_line = "" other_line_times = 0 # $1 = useless prefix string # $2 = icmp_seq # $3 = ttl # $4 = time # This must be called before incrementing the last_seq variable! rtt = int($4) process_rtt(rtt) seq = int($2) while( last_seq < seq - 1 ) { # Lost a packet print_newlines_if_needed() print_missing_response() last_seq++ lost++ store(lastn_lost, 1) } # Received a packet print_newlines_if_needed() print_received_response(rtt) last_seq++ received++ store(lastn_lost, 0) if( '"${IS_TERMINAL}"' ) { print_statistics_bar() } } else if ( $0 == "" ) { # Do nothing on blank lines. } else { other_line_is_printed() if ( $0 == other_line ) { other_line_times++ if( '"${IS_TERMINAL}"' ) { other_line_is_repeated() } } else { other_line = $0 other_line_times = 1 printf( "%s\n", $0 ) } } # Not needed when the output is a terminal, but does not hurt either. fflush() }'