├── 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: [](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 |
--------------------------------------------------------------------------------