├── LICENSE ├── README.md ├── mockping.sh └── prettyping /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Denilson Figueiredo de Sá 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | prettyping 2 | ========== 3 | 4 | [Donation - buy me a coffee](https://denilson.sa.nom.br/donate.html) 5 | 6 | `prettyping` is a wrapper around the standard `ping` tool with the objective of 7 | making the output prettier, more colorful, more compact, and easier to read. 8 | 9 | `prettyping` runs the standard `ping` in the background and parses its output, 10 | showing the ping responses in a *graphical* way at the terminal (by using 11 | colors and Unicode characters). 12 | 13 | `prettyping` is written in `bash` and `awk`, and is reported to work on many 14 | different systems (Linux, Mac OS X, BSD…), as well as running on different 15 | versions of `awk` (`gawk`, `mawk`, `nawk`, `busybox awk`). 16 | 17 | Read about the history of this project, as well as detailed information, 18 | screenshots, videos at: 19 | 20 | Requirements 21 | ------------ 22 | 23 | * `bash` (tested on 4.20, should work on versions as old as 2008) 24 | * `awk` (either [gawk][], [mawk][], [nawk][] or [busybox awk][]; should work on 25 | `gawk` versions as old as 2008; should probably work on any other awk 26 | implementation) 27 | * `ping` (from `iputils`, or any other version that prints essentially the same 28 | output, like Mac OS X ping or [oping][]) 29 | * Optional dependency on `stty` or `tput` to auto-detect the terminal size. 30 | 31 | Installation 32 | ------------ 33 | 34 | 1. Download [prettyping][] script and save it anywhere. 35 | 2. Make it executable: `chmod +x prettyping` 36 | 37 | That's all! No root permission is required. You can save and run it from any 38 | directory. As long as your user can run `ping`, `bash` and `awk`, then 39 | `prettyping` will work. 40 | 41 | Alternatively, you can download the latest tarball from GitHub: [![Latest release](https://img.shields.io/github/release/denilsonsa/prettyping.svg)](https://github.com/denilsonsa/prettyping/releases/latest) 42 | 43 | For people building a `prettyping` package (for any Linux distro or for Mac OS 44 | X), just install the `prettyping` script into `/usr/bin/`, or whatever 45 | directory is appropriate. No other file is necessary. 46 | 47 | [gawk]: https://www.gnu.org/software/gawk/ 48 | [mawk]: https://invisible-island.net/mawk/ 49 | [nawk]: https://github.com/onetrueawk/awk 50 | [busybox awk]: https://www.busybox.net/downloads/BusyBox.html#awk 51 | [oping]: http://verplant.org/liboping/ 52 | [prettyping]: https://raw.githubusercontent.com/denilsonsa/prettyping/master/prettyping 53 | -------------------------------------------------------------------------------- /mockping.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This is just a mock ping program that reproduces the same output all the 4 | # time. It is used for testing/developing prettyping. 5 | 6 | sample_output() { 7 | cat << EOF 8 | PING registro.br (200.160.2.3) 56(84) bytes of data. 9 | Request timeout for icmp_seq 1 10 | 64 bytes from registro.br (200.160.2.3): icmp_seq=2 ttl=56 time=25.5 ms 11 | 64 bytes from registro.br (200.160.2.3): icmp_seq=3 ttl=56 time=55.7 ms 12 | 64 bytes from registro.br (200.160.2.3): icmp_seq=4 ttl=56 time=75.2 ms 13 | ping: sendto: Network is down 14 | ping: sendto: No route to host 15 | ping: sendto: No route to host 16 | ping: sendto: No route to host 17 | ping: sendto: No route to host 18 | ping: sendto: No route to host 19 | Request timeout for icmp_seq 5 20 | Request timeout for icmp_seq 6 21 | Request timeout for icmp_seq 7 22 | 64 bytes from registro.br (200.160.2.3): icmp_seq=8 ttl=56 time=123 ms 23 | 64 bytes from registro.br (200.160.2.3): icmp_seq=9 ttl=56 time=149 ms 24 | 64 bytes from registro.br (200.160.2.3): icmp_seq=10 ttl=56 time=183 ms 25 | Request timeout for icmp_seq 11 26 | Request timeout for icmp_seq 12 27 | Request timeout for icmp_seq 13 28 | 64 bytes from registro.br (200.160.2.3): icmp_seq=14 ttl=56 time=123 ms 29 | 64 bytes from registro.br (200.160.2.3): icmp_seq=15 ttl=56 time=149 ms 30 | 64 bytes from registro.br (200.160.2.3): icmp_seq=16 ttl=56 time=183 ms 31 | 64 bytes from registro.br (200.160.2.3): icmp_seq=19 ttl=56 time=183 ms 32 | 64 bytes from registro.br (200.160.2.3): icmp_seq=20 ttl=56 time=183 ms 33 | 64 bytes from registro.br (200.160.2.3): icmp_seq=21 ttl=56 time=183 ms 34 | From 10.1.1.160 icmp_seq=22 Destination Host Unreachable 35 | From 10.1.1.160 icmp_seq=23 Destination Host Unreachable 36 | From 10.1.1.160 icmp_seq=24 Destination Host Unreachable 37 | From 10.1.1.160 icmp_seq=25 Destination Host Unreachable 38 | From 10.1.1.160 icmp_seq=26 Destination Host Unreachable 39 | From 10.1.1.160 icmp_seq=27 Destination Host Unreachable 40 | From 10.1.1.160 icmp_seq=28 Destination Host Unreachable 41 | 64 bytes from registro.br (200.160.2.3): icmp_seq=29 ttl=56 time=183 ms 42 | 64 bytes from registro.br (200.160.2.3): icmp_seq=30 ttl=56 time=183 ms 43 | 64 bytes from registro.br (200.160.2.3): icmp_seq=31 ttl=56 time=183 ms 44 | 64 bytes from registro.br (200.160.2.3): icmp_seq=32 ttl=56 time=183 ms 45 | 46 | --- registro.br ping statistics --- 47 | 3 packets transmitted, 3 received, 0% packet loss, time 2000ms 48 | rtt min/avg/max/mdev = 36.750/38.535/40.048/1.360 ms 49 | EOF 50 | } 51 | 52 | sample_output | while read line; do 53 | echo -E "$line" 54 | sleep 0.25s 55 | done 56 | -------------------------------------------------------------------------------- /prettyping: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # 3 | # Written by Denilson Figueiredo de Sá 4 | # MIT license 5 | # 6 | # Requirements: 7 | # * bash (tested on 4.20, should work on older versions too) 8 | # * awk (works with GNU awk, nawk, busybox awk, mawk) 9 | # * ping (from iputils) 10 | # 11 | # More information: 12 | # https://denilsonsa.github.io/prettyping/ 13 | # https://github.com/denilsonsa/prettyping 14 | # https://old.reddit.com/r/linux/comments/1op98a/prettypingsh_a_better_ui_for_watching_ping/ 15 | # Third-party demonstration video: https://www.youtube.com/watch?v=ziEMY1BcikM 16 | 17 | # TODO: Test in original-awk: 18 | # https://aur.archlinux.org/packages/original-awk 19 | # https://sources.debian.org/src/original-awk/ 20 | # TODO: Adjust how many items in the legend are printed based on the terminal width. 21 | # 22 | # TODO: Detect the following kinds of message and avoid printing it repeatedly. 23 | # From 192.168.1.11: icmp_seq=4 Destination Host Unreachable 24 | # Request timeout for icmp_seq 378 25 | # 26 | # TODO: Handle when a single message is spread over multiple lines. Specially, 27 | # like in this case: https://bitbucket.org/denilsonsa/small_scripts/issue/5 28 | # 29 | # TODO: Print the destination (also) at the bottom bar. Useful after leaving 30 | # the script running for quite some time. 31 | # 32 | # TODO: Print the destination as escape codes to xterm title. 33 | # 34 | # TODO: Print the current time in the beginning of each line. 35 | # 36 | # TODO: Implement audible ping. 37 | # 38 | # TODO: Autodetect the width of printf numbers, so they will always line up correctly. 39 | # 40 | # TODO: Test the behavior of this script upon receiving out-of-order packets, like these: 41 | # https://web.archive.org/web/20130531083142/http://www.blug.linux.no/rfc1149/pinglogg.txt 42 | # 43 | # TODO? How will prettyping behave if it receives a duplicate response? 44 | 45 | print_help() { 46 | cat << EOF 47 | Usage: $MYNAME [prettyping parameters] 48 | 49 | This script is a wrapper around the system's "ping" tool. It will substitute 50 | each ping response line by a colored character, giving a very compact overview 51 | of the ping responses. 52 | 53 | prettyping parameters: 54 | --[no]color Enable/disable color output. (default: enabled) 55 | --[no]multicolor Enable/disable multi-color unicode output. Has no effect if 56 | either color or unicode is disabled. (default: enabled) 57 | --[no]unicode Enable/disable unicode characters. (default: enabled) 58 | --[no]legend Enable/disable the latency legend. (default: enabled) 59 | --[no]globalstats Enable/disable the global statistics line. (default: enabled) 60 | --[no]recentstats Enable/disable the "last n" statistics line. (default: enabled) 61 | --[no]terminal Force the output designed to a terminal. (default: auto) 62 | --last Use the last "n" pings at the statistics line. (default: 60) 63 | --columns Override auto-detection of terminal dimensions. 64 | --lines Override auto-detection of terminal dimensions. 65 | --rttmin Minimum RTT represented in the unicode graph. (default: auto) 66 | --rttmax Maximum RTT represented in the unicode graph. (default: auto) 67 | --awkbin Override the awk interpreter. (default: awk) 68 | --pingbin Override the ping tool. (default: ping) 69 | 70 | ping parameters handled by prettyping: 71 | -a Audible ping is not implemented yet. 72 | -f Flood mode is not allowed in prettyping. 73 | -q Quiet output is not allowed in prettyping. 74 | -R Record route mode is not allowed in prettyping. 75 | -v Verbose output seems to be the default mode in ping. 76 | 77 | All other parameters are passed directly to ping. 78 | EOF 79 | } 80 | 81 | # Thanks to people at #bash who pointed me at 82 | # https://web.archive.org/web/20100301171512/https://bash-hackers.org/wiki/doku.php/scripting/posparams 83 | parse_arguments() { 84 | USE_COLOR=1 85 | USE_MULTICOLOR=1 86 | USE_UNICODE=1 87 | USE_LEGEND=1 88 | USE_GLOBALSTATS=1 89 | USE_RECENTSTATS=1 90 | 91 | if [ -t 1 ]; then 92 | IS_TERMINAL=1 93 | else 94 | IS_TERMINAL=0 95 | fi 96 | 97 | LAST_N=60 98 | OVERRIDE_COLUMNS=0 99 | OVERRIDE_LINES=0 100 | RTT_MIN=auto 101 | RTT_MAX=auto 102 | 103 | PING_BIN="ping" 104 | #PING_BIN="./mockping.awk" 105 | PING_PARAMS=( ) 106 | 107 | AWK_BIN="awk" 108 | AWK_PARAMS=( ) 109 | 110 | while [[ $# != 0 ]] ; do 111 | case "$1" in 112 | -h | -help | --help ) 113 | print_help 114 | exit 115 | ;; 116 | 117 | # Forbidden ping parameters within prettyping: 118 | -f ) 119 | echo "${MYNAME}: You can't use the -f (flood) option." 120 | exit 1 121 | ;; 122 | -R ) 123 | # -R prints extra information at each ping response. 124 | echo "${MYNAME}: You can't use the -R (record route) option." 125 | exit 1 126 | ;; 127 | -q ) 128 | echo "${MYNAME}: You can't use the -q (quiet) option." 129 | exit 1 130 | ;; 131 | -v ) 132 | # -v enables verbose output. However, it seems the output with 133 | # or without this option is the same. Anyway, prettyping will 134 | # strip this parameter. 135 | ;; 136 | # Note: 137 | # Small values for -s parameter prevents ping from being able to 138 | # calculate RTT. 139 | 140 | # New parameters: 141 | -a ) 142 | # TODO: Implement audible ping for responses or for missing packets 143 | ;; 144 | 145 | -color | --color ) USE_COLOR=1 ;; 146 | -nocolor | --nocolor ) USE_COLOR=0 ;; 147 | -multicolor | --multicolor ) USE_MULTICOLOR=1 ;; 148 | -nomulticolor | --nomulticolor ) USE_MULTICOLOR=0 ;; 149 | -unicode | --unicode ) USE_UNICODE=1 ;; 150 | -nounicode | --nounicode ) USE_UNICODE=0 ;; 151 | -legend | --legend ) USE_LEGEND=1 ;; 152 | -nolegend | --nolegend ) USE_LEGEND=0 ;; 153 | -globalstats | --globalstats ) USE_GLOBALSTATS=1 ;; 154 | -noglobalstats | --noglobalstats ) USE_GLOBALSTATS=0 ;; 155 | -recentstats | --recentstats ) USE_RECENTSTATS=1 ;; 156 | -norecentstats | --norecentstats ) USE_RECENTSTATS=0 ;; 157 | -terminal | --terminal ) IS_TERMINAL=1 ;; 158 | -noterminal | --noterminal ) IS_TERMINAL=0 ;; 159 | 160 | -awkbin | --awkbin ) AWK_BIN="$2" ; shift ;; 161 | -pingbin | --pingbin ) PING_BIN="$2" ; shift ;; 162 | 163 | #TODO: Check if these parameters are numbers. 164 | -last | --last ) LAST_N="$2" ; shift ;; 165 | -columns | --columns ) OVERRIDE_COLUMNS="$2" ; shift ;; 166 | -lines | --lines ) OVERRIDE_LINES="$2" ; shift ;; 167 | -rttmin | --rttmin ) RTT_MIN="$2" ; shift ;; 168 | -rttmax | --rttmax ) RTT_MAX="$2" ; shift ;; 169 | 170 | * ) 171 | PING_PARAMS+=("$1") 172 | ;; 173 | esac 174 | shift 175 | done 176 | 177 | if [[ "${RTT_MIN}" -gt 0 && "${RTT_MAX}" -gt 0 && "${RTT_MIN}" -ge "${RTT_MAX}" ]] ; then 178 | echo "${MYNAME}: Invalid --rttmin and -rttmax values." 179 | exit 1 180 | fi 181 | 182 | if [[ "${#PING_PARAMS[@]}" = 0 ]] ; then 183 | echo "${MYNAME}: Missing parameters, use --help for instructions." 184 | exit 1 185 | fi 186 | 187 | # Workaround for mawk: 188 | # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=593504 189 | local version="$(echo | "${AWK_BIN}" -W version 2>&1)" 190 | if [[ "${version}" == mawk* ]] ; then 191 | AWK_PARAMS+=(-W interactive) 192 | fi 193 | } 194 | 195 | MYNAME=`basename "$0"` 196 | parse_arguments "$@" 197 | 198 | 199 | export LC_ALL=C 200 | 201 | # Warning! Ugly code ahead! 202 | # The code is so ugly that the comments explaining it are 203 | # bigger than the code itself! 204 | # 205 | # Suppose this: 206 | # 207 | # cmd_a | cmd_b & 208 | # 209 | # I need the PID of cmd_a. How can I get it? 210 | # In bash, $! will give me the PID of cmd_b. 211 | # 212 | # So, I came up with this ugly solution: open a subshell, like this: 213 | # 214 | # ( 215 | # cmd_a & 216 | # echo "This is the PID I want $!" 217 | # wait 218 | # ) | cmd_b 219 | 220 | 221 | # Ignore Ctrl+C here. 222 | # If I don't do this, this shell script is killed before 223 | # ping and gawk can finish their work. 224 | trap '' 2 225 | 226 | # Now the ugly code. 227 | ( 228 | "${PING_BIN}" "${PING_PARAMS[@]}" & 229 | PING_PID="$!" 230 | 231 | # Commented out, because it looks like this line is not needed 232 | #trap "kill -2 $PING_PID ; exit 1" 2 # Catch Ctrl+C here 233 | 234 | wait 235 | ) 2>&1 | ( 236 | if [ "${IS_TERMINAL}" = 1 ]; then 237 | # Print a message to notify the awk script about terminal size change. 238 | trap "echo SIGWINCH" 28 239 | fi 240 | 241 | # The trap must be in another subshell because otherwise it will interrupt 242 | # the "wait" commmand. 243 | while read line; do 244 | echo -E "$line" 245 | done 246 | ) 2>&1 | "${AWK_BIN}" "${AWK_PARAMS[@]}" ' 247 | # Weird that awk does not come with abs(), so I need to implement it. 248 | function abs(x) { 249 | return ( (x < 0) ? -x : x ) 250 | } 251 | 252 | # Ditto for ceiling function. 253 | function ceil(x) { 254 | return (x == int(x)) ? x : int(x) + 1 255 | } 256 | 257 | # Local variables MUST be declared in argument list, else they are 258 | # seen as global. Ugly, but that is how awk works. 259 | function get_terminal_size(SIZE, SIZEA, HAS_DETECTED, CMD) { 260 | HAS_DETECTED = 0 261 | 262 | CMD = "stty -f /dev/tty size 2> /dev/null" 263 | 264 | if ( (CMD | getline SIZE) == 1 ) { 265 | split(SIZE, SIZEA, " ") 266 | LINES = int(SIZEA[1]) 267 | COLUMNS = int(SIZEA[2]) 268 | HAS_DETECTED = 1 269 | } 270 | close(CMD) 271 | 272 | if ( HAS_DETECTED == 0 ) { 273 | CMD = "tput lines" 274 | if ( (CMD | getline SIZE) == 1 ) { 275 | LINES = int(SIZE) 276 | HAS_DETECTED = 1 277 | } 278 | close(CMD) 279 | 280 | CMD = "tput cols" 281 | if ( (CMD | getline SIZE) == 1 ) { 282 | COLUMNS = int(SIZE) 283 | HAS_DETECTED = 1 284 | } 285 | close(CMD) 286 | } 287 | 288 | if ( int('"${OVERRIDE_COLUMNS}"') ) { COLUMNS = int('"${OVERRIDE_COLUMNS}"') } 289 | if ( int('"${OVERRIDE_LINES}"') ) { LINES = int('"${OVERRIDE_LINES}"') } 290 | } 291 | 292 | ############################################################ 293 | # Functions related to cursor handling 294 | 295 | # Function called whenever a non-dotted line is printed. 296 | # 297 | # It will move the cursor to the line next to the statistics and 298 | # restore the default color. 299 | function other_line_is_printed() { 300 | if ( IS_PRINTING_DOTS ) { 301 | if ( '"${IS_TERMINAL}"' ) { 302 | printf( ESC_DEFAULT ESC_NEXTLINE ESC_NEXTLINE "\n" ) 303 | } else { 304 | printf( ESC_DEFAULT "\n" ) 305 | print_statistics_bar() 306 | } 307 | } 308 | IS_PRINTING_DOTS = 0 309 | CURR_COL = 0 310 | } 311 | 312 | # Function called whenever a non-dotted line is repeated. 313 | function other_line_is_repeated() { 314 | if ( other_line_times < 2 ) { 315 | return 316 | } 317 | if ( '"${IS_TERMINAL}"' ) { 318 | printf( ESC_DEFAULT ESC_ERASELINE "\r" ) 319 | } 320 | printf( "Last message repeated %d times.", other_line_times ) 321 | if ( ! '"${IS_TERMINAL}"' ) { 322 | printf( "\n" ) 323 | } 324 | } 325 | 326 | # Function called whenever the repeating line has changed. 327 | function other_line_finished_repeating() { 328 | if ( other_line_times >= 2 ) { 329 | if ( '"${IS_TERMINAL}"' ) { 330 | printf( "\n" ) 331 | } else { 332 | other_line_is_repeated() 333 | } 334 | } 335 | other_line = "" 336 | other_line_times = 0 337 | } 338 | 339 | # Prints the newlines required for the live statistics. 340 | # 341 | # I need to print some newlines and then return the cursor back to its position 342 | # to make sure the terminal will scroll. 343 | # 344 | # If the output is not a terminal, break lines on every LAST_N dots. 345 | function print_newlines_if_needed() { 346 | if ( '"${IS_TERMINAL}"' ) { 347 | # COLUMNS-1 because I want to avoid bugs with the cursor at the last column 348 | if ( CURR_COL >= COLUMNS-1 ) { 349 | CURR_COL = 0 350 | } 351 | if ( CURR_COL == 0 ) { 352 | if ( IS_PRINTING_DOTS ) { 353 | printf( "\n" ) 354 | } 355 | 356 | # Reserve the extra lines for printing the statistics. 357 | if ( '"${USE_GLOBALSTATS}"' ) { printf( ESC_DEFAULT "\n" ) } 358 | if ( '"${USE_RECENTSTATS}"' && LAST_N > 0 ) { printf( ESC_DEFAULT "\n" ) } 359 | if ( '"${USE_RECENTSTATS}"' && LAST_N > 0 ) { printf( ESC_CURSORUP ) } 360 | if ( '"${USE_GLOBALSTATS}"' ) { printf( ESC_CURSORUP ) } 361 | 362 | # Erase the (new) current line, to make it clean for the next response dots. 363 | printf( ESC_ERASELINE ) 364 | } 365 | } else { 366 | if ( CURR_COL >= LAST_N && LAST_N > 0 ) { 367 | CURR_COL = 0 368 | printf( ESC_DEFAULT "\n" ) 369 | print_statistics_bar() 370 | } 371 | } 372 | IS_PRINTING_DOTS = 1 373 | } 374 | 375 | ############################################################ 376 | # Functions related to the data structure of "Last N" statistics. 377 | 378 | # Clears the data structure. 379 | function clear(d) { 380 | d["index"] = 0 # The next position to store a value 381 | d["size"] = 0 # The array size, goes up to LAST_N 382 | } 383 | 384 | # This function stores the value to the passed data structure. 385 | # The data structure holds at most LAST_N values. When it is full, 386 | # a new value overwrite the oldest one. 387 | function store(d, value) { 388 | d[d["index"]] = value 389 | d["index"]++ 390 | if ( d["index"] >= d["size"] ) { 391 | if ( d["size"] < LAST_N ) { 392 | d["size"]++ 393 | } else { 394 | d["index"] = 0 395 | } 396 | } 397 | } 398 | 399 | ############################################################ 400 | # Functions related to processing the received response 401 | 402 | function process_rtt(rtt) { 403 | # Overall statistics 404 | last_rtt = rtt 405 | total_rtt += rtt 406 | if ( last_seq == 0 ) { 407 | min_rtt = max_rtt = rtt 408 | } else { 409 | if ( rtt < min_rtt ) min_rtt = rtt 410 | if ( rtt > max_rtt ) max_rtt = rtt 411 | } 412 | 413 | # "Last N" statistics 414 | store(lastn_rtt,rtt) 415 | } 416 | 417 | function lost_a_packet() { 418 | print_newlines_if_needed() 419 | print_missing_response() 420 | 421 | last_seq++ 422 | lost++ 423 | store(lastn_lost, 1) 424 | } 425 | 426 | ############################################################ 427 | # Functions related to printing the fancy ping response 428 | 429 | # block_index, n, w are just local variables. 430 | function print_response_legend(i, n, w) { 431 | if ( ! '"${USE_LEGEND}"' ) { 432 | return 433 | } 434 | if ( BLOCK_LEN > 1 ) { 435 | # w counts the cursor position in the current line. Because of the 436 | # escape codes, I need to jump through some hoops in order to count the 437 | # position correctly. 438 | w = 0 439 | n = "0 " 440 | w += length(n) + 1 441 | 442 | printf( n BLOCK[0] ESC_DEFAULT ) 443 | 444 | for ( i=1 ; i= COLUMNS ) { 453 | printf( "\n" ) 454 | w = length(n) + 1 455 | } else { 456 | printf( " " ) 457 | w += 1 458 | } 459 | 460 | printf( n BLOCK[i] ESC_DEFAULT ) 461 | } 462 | printf( " ∞\n" ) 463 | } 464 | 465 | # Useful code for debugging. 466 | #for ( i=0 ; i<=BLOCK_RTT_MAX ; i++ ) { 467 | # print_received_response(i) 468 | # printf( ESC_DEFAULT "%4d\n", i ) 469 | #} 470 | } 471 | 472 | # block_index is just a local variable. 473 | function print_received_response(rtt, block_index) { 474 | if ( rtt < BLOCK_RTT_MIN ) { 475 | block_index = 0 476 | } else if ( rtt >= BLOCK_RTT_MAX ) { 477 | block_index = BLOCK_LEN - 1 478 | } else { 479 | block_index = 1 + int((rtt - BLOCK_RTT_MIN) * (BLOCK_LEN - 2) / BLOCK_RTT_RANGE) 480 | } 481 | printf( BLOCK[block_index] ) 482 | CURR_COL++ 483 | } 484 | 485 | function print_missing_response(rtt) { 486 | printf( ESC_RED "!" ) 487 | CURR_COL++ 488 | } 489 | 490 | ############################################################ 491 | # Functions related to printing statistics 492 | 493 | # All arguments are just local variables. 494 | function print_global_stats(percentage_lost, avg_rtt) { 495 | # Handling division by zero. 496 | # Note that mawk does not consider division by zero an error, while all 497 | # other awk implementations abort in such case. 498 | # https://stackoverflow.com/questions/14581966/why-does-awk-produce-different-results-for-division-by-zero 499 | 500 | avg_rtt = ( received > 0 ) ? (total_rtt/received) : 0 501 | percentage_lost = ( lost+received > 0 ) ? (lost*100/(lost+received)) : 0 502 | 503 | if ( '"${IS_TERMINAL}"' ) { 504 | printf( "%2d/%3d (%2d%%) lost; %4.0f/" ESC_BOLD "%4.0f" ESC_DEFAULT "/%4.0fms; last: " ESC_BOLD "%4.0f" ESC_DEFAULT "ms", 505 | lost, 506 | lost+received, 507 | percentage_lost, 508 | min_rtt, 509 | avg_rtt, 510 | max_rtt, 511 | last_rtt ) 512 | } else { 513 | printf( "%2d/%3d (%2d%%) lost; %4.0f/" ESC_BOLD "%4.0f" ESC_DEFAULT "/%4.0fms", 514 | lost, 515 | lost+received, 516 | percentage_lost, 517 | min_rtt, 518 | avg_rtt, 519 | max_rtt ) 520 | } 521 | } 522 | 523 | # All arguments are just local variables. 524 | function print_recent_stats(i, percentage_lost, sum, min, avg, max, diffs) { 525 | # Calculate and print the lost packets statistics 526 | sum = 0 527 | for ( i=0 ; i 0) ? (sum*100/lastn_lost["size"]) : 0 531 | printf( "%2d/%3d (%2d%%) lost; ", 532 | sum, 533 | lastn_lost["size"], 534 | percentage_lost ) 535 | 536 | # Calculate the min/avg/max rtt times 537 | sum = diffs = 0 538 | min = max = lastn_rtt[0] 539 | for ( i=0 ; i max ) max = lastn_rtt[i] 543 | } 544 | avg = (lastn_rtt["size"]) ? (sum/lastn_rtt["size"]) : 0 545 | 546 | # Calculate mdev (mean absolute deviation) 547 | for ( i=0 ; i 0 ) { 551 | diffs /= lastn_rtt["size"] 552 | } 553 | 554 | # Print the rtt statistics 555 | printf( "%4.0f/" ESC_BOLD "%4.0f" ESC_DEFAULT "/%4.0f/%4.0fms (last %d)", 556 | min, 557 | avg, 558 | max, 559 | diffs, 560 | lastn_rtt["size"] ) 561 | } 562 | 563 | function print_statistics_bar() { 564 | if ( '"${IS_TERMINAL}"' ) { 565 | printf( ESC_SAVEPOS ESC_DEFAULT ) 566 | 567 | if ( '"${USE_GLOBALSTATS}"' ) { 568 | printf( ESC_NEXTLINE ESC_ERASELINE ) 569 | print_global_stats() 570 | } 571 | if ( '"${USE_RECENTSTATS}"' && LAST_N > 0 ) { 572 | printf( ESC_NEXTLINE ESC_ERASELINE ) 573 | print_recent_stats() 574 | } 575 | 576 | printf( ESC_UNSAVEPOS ) 577 | } else { 578 | if ( '"${USE_GLOBALSTATS}"' ) { 579 | print_global_stats() 580 | printf( "\n" ) 581 | } 582 | if ( '"${USE_RECENTSTATS}"' && LAST_N > 0 ) { 583 | print_recent_stats() 584 | printf( "\n" ) 585 | } 586 | } 587 | } 588 | 589 | function print_statistics_bar_if_terminal() { 590 | if ( '"${IS_TERMINAL}"' ) { 591 | print_statistics_bar() 592 | } 593 | } 594 | 595 | ############################################################ 596 | # Initializations 597 | BEGIN { 598 | # Easy way to get each value from ping output 599 | FS = "=" 600 | 601 | ############################################################ 602 | # General internal variables 603 | 604 | # This is needed to keep track of lost packets 605 | last_seq = 0 606 | 607 | # The previously printed non-ping-response line 608 | other_line = "" 609 | other_line_times = 0 610 | 611 | # Variables to keep the screen clean 612 | IS_PRINTING_DOTS = 0 613 | CURR_COL = 0 614 | 615 | ############################################################ 616 | # Variables related to "overall" statistics 617 | received = 0 618 | lost = 0 619 | total_rtt = 0 620 | min_rtt = 0 621 | max_rtt = 0 622 | last_rtt = 0 623 | 624 | ############################################################ 625 | # Variables related to "last N" statistics 626 | LAST_N = int('"${LAST_N}"') 627 | 628 | # Data structures for the "last N" statistics 629 | clear(lastn_lost) 630 | clear(lastn_rtt) 631 | 632 | ############################################################ 633 | # Terminal height and width 634 | 635 | # These are sane defaults, in case we cannot query the actual terminal size 636 | LINES = 24 637 | COLUMNS = 80 638 | 639 | # Auto-detecting the terminal size 640 | get_terminal_size() 641 | if ( '"${IS_TERMINAL}"' && COLUMNS <= 50 ) { 642 | print "Warning: terminal width is too small." 643 | } 644 | 645 | ############################################################ 646 | # ANSI escape codes 647 | 648 | # Color escape codes. 649 | # Fortunately, awk defaults any unassigned variable to an empty string. 650 | if ( '"${USE_COLOR}"' ) { 651 | ESC_DEFAULT = "\033[0m" 652 | ESC_BOLD = "\033[1m" 653 | #ESC_BLACK = "\033[0;30m" 654 | #ESC_GRAY = "\033[1;30m" 655 | ESC_RED = "\033[0;31m" 656 | ESC_GREEN = "\033[0;32m" 657 | ESC_YELLOW = "\033[0;33m" 658 | ESC_BLUE = "\033[0;34m" 659 | ESC_MAGENTA = "\033[0;35m" 660 | ESC_CYAN = "\033[0;36m" 661 | ESC_WHITE = "\033[0;37m" 662 | ESC_YELLOW_ON_GREEN = "\033[42;33m" 663 | ESC_RED_ON_YELLOW = "\033[43;31m" 664 | } 665 | # Other escape codes, see: 666 | # https://en.wikipedia.org/wiki/ANSI_escape_code 667 | # https://invisible-island.net/xterm/ctlseqs/ctlseqs.html 668 | ESC_NEXTLINE = "\n" 669 | ESC_CURSORUP = "\033[A" 670 | ESC_CURSORDOWN = "\033[B" 671 | ESC_SCROLLUP = "\033[S" 672 | ESC_SCROLLDOWN = "\033[T" 673 | ESC_ERASELINEEND = "\033[0K" 674 | ESC_ERASELINE = "\033[2K" 675 | ESC_SAVEPOS = "\0337" 676 | ESC_UNSAVEPOS = "\0338" 677 | 678 | # I am avoiding these escapes as they are not listed in: 679 | # https://vt100.net/docs/vt100-ug/chapter3.html 680 | #ESC_PREVLINE = "\033[F" 681 | #ESC_SAVEPOS = "\033[s" 682 | #ESC_UNSAVEPOS = "\033[u" 683 | 684 | # I am avoiding this to improve compatibility with (older versions of) tmux 685 | #ESC_NEXTLINE = "\033[E" 686 | 687 | ############################################################ 688 | # Unicode characters (based on https://github.com/holman/spark ) 689 | if ( '"${USE_UNICODE}"' ) { 690 | BLOCK[ 0] = ESC_GREEN "▁" 691 | BLOCK[ 1] = ESC_GREEN "▂" 692 | BLOCK[ 2] = ESC_GREEN "▃" 693 | BLOCK[ 3] = ESC_GREEN "▄" 694 | BLOCK[ 4] = ESC_GREEN "▅" 695 | BLOCK[ 5] = ESC_GREEN "▆" 696 | BLOCK[ 6] = ESC_GREEN "▇" 697 | BLOCK[ 7] = ESC_GREEN "█" 698 | BLOCK[ 8] = ESC_YELLOW_ON_GREEN "▁" 699 | BLOCK[ 9] = ESC_YELLOW_ON_GREEN "▂" 700 | BLOCK[10] = ESC_YELLOW_ON_GREEN "▃" 701 | BLOCK[11] = ESC_YELLOW_ON_GREEN "▄" 702 | BLOCK[12] = ESC_YELLOW_ON_GREEN "▅" 703 | BLOCK[13] = ESC_YELLOW_ON_GREEN "▆" 704 | BLOCK[14] = ESC_YELLOW_ON_GREEN "▇" 705 | BLOCK[15] = ESC_YELLOW_ON_GREEN "█" 706 | BLOCK[16] = ESC_RED_ON_YELLOW "▁" 707 | BLOCK[17] = ESC_RED_ON_YELLOW "▂" 708 | BLOCK[18] = ESC_RED_ON_YELLOW "▃" 709 | BLOCK[19] = ESC_RED_ON_YELLOW "▄" 710 | BLOCK[20] = ESC_RED_ON_YELLOW "▅" 711 | BLOCK[21] = ESC_RED_ON_YELLOW "▆" 712 | BLOCK[22] = ESC_RED_ON_YELLOW "▇" 713 | BLOCK[23] = ESC_RED_ON_YELLOW "█" 714 | if ( '"${USE_MULTICOLOR}"' && '"${USE_COLOR}"' ) { 715 | # Multi-color version: 716 | BLOCK_LEN = 24 717 | BLOCK_RTT_MIN = 10 718 | BLOCK_RTT_MAX = 230 719 | } else { 720 | # Simple version: 721 | BLOCK_LEN = 8 722 | BLOCK_RTT_MIN = 25 723 | BLOCK_RTT_MAX = 175 724 | } 725 | } else { 726 | BLOCK[ 0] = ESC_GREEN "_" 727 | BLOCK[ 1] = ESC_GREEN "." 728 | BLOCK[ 2] = ESC_GREEN "o" 729 | BLOCK[ 3] = ESC_GREEN "O" 730 | BLOCK[ 4] = ESC_YELLOW "_" 731 | BLOCK[ 5] = ESC_YELLOW "." 732 | BLOCK[ 6] = ESC_YELLOW "o" 733 | BLOCK[ 7] = ESC_YELLOW "O" 734 | BLOCK[ 8] = ESC_RED "_" 735 | BLOCK[ 9] = ESC_RED "." 736 | BLOCK[10] = ESC_RED "o" 737 | BLOCK[11] = ESC_RED "O" 738 | if ( '"${USE_MULTICOLOR}"' && '"${USE_COLOR}"' ) { 739 | # Multi-color version: 740 | BLOCK_LEN = 12 741 | BLOCK_RTT_MIN = 20 742 | BLOCK_RTT_MAX = 220 743 | } else { 744 | # Simple version: 745 | BLOCK_LEN = 4 746 | BLOCK_RTT_MIN = 75 747 | BLOCK_RTT_MAX = 225 748 | } 749 | } 750 | 751 | if ( int('"${RTT_MIN}"') > 0 && int('"${RTT_MAX}"') > 0 ) { 752 | BLOCK_RTT_MIN = int('"${RTT_MIN}"') 753 | BLOCK_RTT_MAX = int('"${RTT_MAX}"') 754 | } else if ( int('"${RTT_MIN}"') > 0 ) { 755 | BLOCK_RTT_MIN = int('"${RTT_MIN}"') 756 | BLOCK_RTT_MAX = BLOCK_RTT_MIN * (BLOCK_LEN - 1) 757 | } else if ( int('"${RTT_MAX}"') > 0 ) { 758 | BLOCK_RTT_MAX = int('"${RTT_MAX}"') 759 | BLOCK_RTT_MIN = int(BLOCK_RTT_MAX / (BLOCK_LEN - 1)) 760 | } 761 | 762 | BLOCK_RTT_RANGE = BLOCK_RTT_MAX - BLOCK_RTT_MIN 763 | print_response_legend() 764 | } 765 | 766 | ############################################################ 767 | # Main loop 768 | { 769 | if ( $0 ~ /^[0-9]+ bytes from .*[:,] icmp_[rs]eq=[0-9]+ (ttl|hlim)=[0-9]+ time=[0-9.]+ *ms/ ) { 770 | # Sample line from ping: 771 | # 64 bytes from 8.8.8.8: icmp_seq=1 ttl=49 time=184 ms 772 | if ( other_line_times >= 2 ) { 773 | other_line_finished_repeating() 774 | } 775 | 776 | # $1 = useless prefix string 777 | # $2 = icmp_seq 778 | # $3 = ttl/hlim 779 | # $4 = time 780 | 781 | # This must be called before incrementing the last_seq variable! 782 | rtt = int($4) 783 | process_rtt(rtt) 784 | 785 | seq = int($2) 786 | 787 | while ( last_seq < seq - 1 ) { 788 | lost_a_packet() 789 | } 790 | 791 | # Received a packet 792 | print_newlines_if_needed() 793 | print_received_response(rtt) 794 | 795 | # In case of receiving multiple responses with the same seq number, it 796 | # is better to use "last_seq = seq" than to increment last_seq. 797 | last_seq = seq 798 | 799 | received++ 800 | store(lastn_lost, 0) 801 | 802 | print_statistics_bar_if_terminal() 803 | } else if ( $0 ~ /^.*onnected to.*, seq=[0-9]+ time= *[0-9.]+ *ms/ ) { 804 | # Sample line from httping: 805 | # connected to 200.149.119.168:80 (273 bytes), seq=0 time=129.86 ms 806 | if ( other_line_times >= 2 ) { 807 | other_line_finished_repeating() 808 | } 809 | 810 | seq = $0 811 | sub(/.* seq=/, "", seq) 812 | seq = int(seq) 813 | 814 | rtt = $0 815 | sub(/.* time=/, "", rtt) 816 | rtt = rtt + 0.0 817 | 818 | process_rtt(rtt) 819 | 820 | while ( last_seq < seq - 1 ) { 821 | lost_a_packet() 822 | } 823 | 824 | # Received a packet 825 | print_newlines_if_needed() 826 | print_received_response(rtt) 827 | 828 | # In case of receiving multiple responses with the same seq number, it 829 | # is better to use "last_seq = seq" than to increment last_seq. 830 | last_seq = seq 831 | 832 | received++ 833 | store(lastn_lost, 0) 834 | 835 | print_statistics_bar_if_terminal() 836 | } else if ( $0 == "" ) { 837 | # Do nothing on blank lines. 838 | } else if ( $0 == "error shutting down ssl" ) { 839 | # Common error message when using httping, ignore it. 840 | } else if ( $0 ~ /^Request timeout for icmp_seq [0-9]+/ ) { 841 | # Reply timeout is printed on Mac OS X. 842 | 843 | if ( other_line_times >= 2 ) { 844 | other_line_finished_repeating() 845 | } 846 | 847 | lost_a_packet() 848 | 849 | # Making sure the last_seq number is correct. 850 | gsub(/.* icmp_seq /, "") 851 | seq = int($0) 852 | last_seq = seq 853 | 854 | print_newlines_if_needed() 855 | print_statistics_bar_if_terminal() 856 | } else if ( $0 ~ /^SIGWINCH$/ ) { 857 | get_terminal_size() 858 | 859 | if ( IS_PRINTING_DOTS ) { 860 | if ( CURR_COL >= COLUMNS-1 ) { 861 | # Not enough space anyway. 862 | } else { 863 | # Making up room in case the number of lines has changed. 864 | printf( ESC_NEXTLINE ESC_NEXTLINE ESC_CURSORUP ESC_CURSORUP ) 865 | # Moving to the correct column and erasing the rest of the line. 866 | printf( "\033[" (CURR_COL+1) "G" ESC_DEFAULT ESC_ERASELINEEND ) 867 | } 868 | 869 | print_newlines_if_needed() 870 | print_statistics_bar_if_terminal() 871 | } 872 | } else { 873 | other_line_is_printed() 874 | original_line = $0 875 | gsub(/icmp_seq[= ][0-9]+/, "") 876 | if ( $0 == other_line ) { 877 | other_line_times++ 878 | if ( '"${IS_TERMINAL}"' ) { 879 | other_line_is_repeated() 880 | } 881 | } else { 882 | other_line_finished_repeating() 883 | other_line = $0 884 | other_line_times = 1 885 | printf( "%s\n", original_line ) 886 | } 887 | } 888 | 889 | # Not needed when the output is a terminal, but does not hurt either. 890 | fflush() 891 | }' 892 | --------------------------------------------------------------------------------