2

I am trying to ping several IP addresses / hostnames, and determine whether any of them respond.

Starting from this question, I'm using the echo host1 host2 host3 | xargs -n1 -P0 ping -c1 -w2 approach.

My problem is that ping will return zero if the ping succeeds and non-zero (actually 1) if the ping fails.

This means, that the overall command returns 123 if any of the pings fail, and 0 if all pings succeed, which isn't what I want.

I can write a hacky script like:

#!/bin/bash
ping "$@"
exit $(( 1 - $? ))

And then use that script in the xargs parameters instead of the real ping, which achieves what I want, but feels very messy.

Is there a better way to achieve what I need, without having to install non-standard ping tools, or have hacky external scripts.

As xargs is a command, I don't think I can use a shell function in place of my external script either, which would have given a slightly more elegant option (allowing one script to hold all the code, rather than needing a second script to call from the script where I am calling xargs from.

1
  • 5
    Did you ever consider using something like fping instead of ping? Commented Nov 26 at 11:35

6 Answers 6

7

You don't need the separate script (or the bash shell). Shell code can also be given as argument with the -c option (that's what the system("shell code") function of most languages use):

if
  echo host1 host2 host3 |
    xargs -n1 -P0 sh -c '! "$0" "$@"' ping -c1 -w2
then
  echo 'They all failed'
else
  echo 'At least one succeeded'
fi

In:

sh -u -ce -o xtrace -- 'shell code' 'script name' 'arg 1' 'arg 2'

(here -u, e and -o xtrace to show you can have more options before or after -c)

  • the first non-option argument (here shell code) is the code to interpret
  • the second (script name) a name you want to give to that inline script (the equivalent of the file name for a script in a file). It has these effects:
    • it's used for instance in error messages that the shell generates to tell the user the error is coming from that script.
    • it's what the $0 special parameter expands to.
  • and the remaining arguments make up the arguments of the inline script, so are available in the shell code as $1, $2... with "$@" the verbatim list of them all.

So, here with sh -c '! "$0" "$@"' ping -c1 -w2, we're creating an inline script which we call ping. xargs will pass -c1, -w2 and one word read from stdin at a time as arguments to that script, and that script runs the command called ping with those arguments and the exit status reversed with !.

You may argue that shell error messages if any showing as "ping" error messages, may be confusing to the user, so you could change it to sh -c '! "$@"' sh ping -c1 -w2 for the inline script's name to be sh instead.

But here I'd argue that to scan networks, you'd rather use things like nmap than ping in a loop.

0
7

I don't think I've ever seen a ping tool that allows multiple targets on the command line, and it's mostly aimed for interactive use anyway.

On the other hand, fping is pretty much made for scripting and allows pinging multiple targets in parallel. Of course, it also exits with a falsy status if any of the targets fail to answer, but it can also give you a list of the ones that did answer as output. If there's any output, someone answered.

So, e.g.

ping_any() {
    # truthy if the output is not empty
    [ "$(fping -a -r1 "$@")" ]
}
if ping_any somehost otherhost...; then
    echo someone answered
fi

(adjust the retries and time limits with -t, -r and -B to taste)

1
  • I wasn't wanting to install any extra packages, but this is a good solution if installing fping is OK. Commented 2 days ago
2

If you're doing network analysis on that machine, you might as well use a bit more network-analystic tools. Install mtr-packet (often in the mtr software package). You need to resolve your hostnames first, but that's a good idea anyways, because if you want to check whether any host succeeds, and a single hostname has multiple A records, then ping only tries one, and not the others, too, even if the first fails.

So, here goes:

#!/bin/bash
# let's assume we have all the hostnames we care about in
# a file called names.hosts
parallel -j1 -m dig +short :::: names.hosts > addresses.hosts

counter=1
while IFS=$'\n' read -r addr; do
  printf '%d send-probe ip-4 %s timeout 2\n' ${counter} "${addr}"
  counter=$((counter + 1))
done < addresses.hosts \
  | mtr-packet \
  | grep '^[0-9]* reply'

You'll really to read mtr-packet's man page, as aside from icmp (which is what ping does), it also supports other probe protocols, and can set quite some other options.

For compactness, the whole while… thing could be replaced with awk or sed = | sed 'N;s/\n/ /' rather straightforwardly. I just didn't feel like discussing the more obscure sed commands.

parallel -j1 -m dig +short :::: names.hosts \
  | sed = \
  | sed 'N;s/\n/ send-probe timeout 2 ip-4 /' \
  | mtr-packet
3
  • 1
    This isn't for network analysis, just verifying that creating a (group of) VMs won't create an IP conflict. Hence not wanting to install custom tools. Commented Nov 26 at 15:46
  • absolutely understandable! Do note that this sounds like a pretty bad way of avoiding conflicts, because not all other machines have to be running all the time, nor do all machines react to pings; so you might still cause a conflict, once one of these machines comes back online. If these other hosts are all just your local VMs, then there's an argument to be made that you should assign IP addresses exclusively, instead of trying to avoid conflicts "in hindsight". Typical VM orchestration solves that issue either through things like DHCP or other lease logic. Commented Nov 26 at 16:00
  • I agree it is imperfect, but it will detect a reasonable percentage of cases. The environment this is used in doesn't support the better ways of doing this and isn't completely within my control. Commented 2 days ago
1

My problem is that ping will return zero if the ping succeeds and non-zero (actually 1) if the ping fails.

That's usual UNIX shell command behaviour! a return value of 0 signals success, anything else failure. So, that seems right?

This means, that the overall command returns 123 if any of the pings fail, and 0 if all pings succeed, which isn't what I want.

You could instead of xargs use parallel (thanks @OleTange!), it supports cancelling on a count of failed or successful tasks with the --halt (short version of --halt-on-error) option:

echo host1 host2 host… | parallel -P 0 -n 1 --halt now,success=1 --joblog=jobs.log ping -c1 -w2
# or, simpler without the echo & pipe
parallel -P 0 -n 1 --halt now,success=1 --joblog=jobs.log ping -c1 -w2 ::: host1 host2 host…

now means that when the condition (after the comma) is achieved to stop all other processes immediately (alternative is soon, which lets them finish but doesn't spawn new ones). success=1 means the condition is the successful completion of at least 1 started process.

Now you have a joblog, which is a tab-separated file. So,

# strip off first line and get seventh, ninth column
# then look for columns that start with 0 and 
<jobs.log tail -n+2 \
  | cut -f7,9 \
  | grep -q '^0'

This returns success if there's any line with a 0 exit status, and failure if there isn't. You could of course also do more fancy stuff with the joblog and see which hosts actually responded.

1
  • 1
    My systems don't have parallel installed, so sticking with xargs works better for me. Commented Nov 26 at 15:46
0

How about a simple for loop (I'm assuming bash, I am unfamiliar with other shells):

for x in host1 host2 host3
do
    ping -c1 -w2 -q $x && echo $x OK || echo $x Failed
done

Which pings each host and reports success or failure.

This answer shows how to embed the loop in an xargs command.

4
  • 2
    That's more zsh syntax. In bash, leaving a variable unquoted is asking the shell to split it on values of $IFS (and perform globbing on the result) which doesn't make sense here. Commented Nov 26 at 14:03
  • Using && || in place of a proper if statement is also generally considered bad practice. Her echo failed will also be run if echo OK fails. Commented Nov 26 at 14:06
  • 3
    The problem with sequential testing is it can take a long time even with a relatively small number of hosts. '-w2' means that for 10 hosts the script will wait at least 20s, versus 2s if doing them in parallel Commented Nov 26 at 14:57
  • 1
    xargs -P0 runs commands in parallel with unlimited parallelism. The question didn't mention this, but is an important part of the behaviour they want to replicate. I didn't see the point of overcomplicating things with xargs sh -c either until I saw this comment and looked more closely at @MichaelFirth's original commend. GNU parallel is an easier-to-use tool which is worth installing if you do lots of this kind of thing, or there's make -j 0 if you can turn your task into a Makefile with parallel recipes... But xargs -P0 is fairly useful. Commented yesterday
0

Sequential testing rather than parallel

#!/bin/bash
#
hosts=( 192.0.2.1  198.51.100.2  1.1.1.1  203.0.113.3 )

for host in "${hosts[@]}"
do
    ping -c1 -i0.3 -w1 "$host" && break
done

if [ "$?" -eq 0 ]
then
    echo "One of the hosts ($host) successfully responded" >&2
fi

You can encapsulate this in a function, and it wouldn't be too hard to write the first responding host name to stdout if required.

pingAny()
{
    local host
    for host in "$@"
    do
        ping -q -c1 -i0.3 -w1 "$host" >/dev/null 2>&1 && break
    done
}

if pingAny 192.0.2.1 198.51.100.2 1.1.1.1 203.0.113.3
then
    echo "One of the hosts successfully responded" >&2
fi
1
  • 2
    The problem with sequential testing is it can take a long time even with a relatively small number of hosts. '-w1' means that for 10 hosts the script will wait at least 10s Commented Nov 26 at 14:56

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.