3

I want to run a series of tests (each on a different PID), and derive a successful status only if all tests exit successfully. Something like

check $PID1 && check $PID2 && check $PID3

but for an indeterminate number of tests. How can I accomplish this?

ilkkachu
  • 138,973
alexis
  • 5,759

4 Answers4

8

That shouldn't be too hard to write out as a loop:

pids=(1025 3425 6474)
check_all() {
    for pid in "$@"; do
        if ! check "$pid"; then
            return 1
        fi
    done
}
check_all "${pids[@]}"

Like the chain of commands linked with &&, the function will stop on the first failing check.

Though do note that I replaced your variables PID1, PID2 etc. with a single array. Bash could iterate over variables whose names start with a particular string, but arrays are just more convenient. (Unless those variables come from the outside of the script through the environment where you can't pass proper arrays.)

Also, I hard-coded the check command in the loop here. You could pass distinct commands for the function to run, but that's ripe with issues with word splitting and quote handling. (See here and here.)

ilkkachu
  • 138,973
  • You could also use a subshell declaration for the function: check_all() ( ... ) and then add set -e, then you can get rid of the whole if ! ... ; then return 1; fi part and just check $pid as only command in the for-loop. (see). – pLumo Oct 16 '19 at 15:49
  • @pLumo, nah, I like being explicit about the test and exit/return. And the subshell isn't needed for anything else so I just like to avoid it on principle. :P check "$pid" || return 1 would be one alternative, of course. – ilkkachu Oct 16 '19 at 16:58
  • Thank you, putting the PIDs in the array and returning from a function cleans up the control flow a lot. My loop was getting the PIDs from a file with read (left this out of the question for obvious reasons), and I was trying to exit the loop with the success status in $?... much too complicated. – alexis Oct 17 '19 at 09:58
  • @alexis, yeah, getting a sane status out of a loop is hard since return just sets the exit status to zero... But you could wrap the while read... inside a function too and bypass the whole array. That is, if you only need the list of PIDs once. – ilkkachu Oct 17 '19 at 10:38
  • I hadn't realized read has an exit status! Thanks for that tip too :-) – alexis Oct 17 '19 at 10:44
  • @alexis, eh? I wonder how you did read the PIDs then. :) Anyway, about the exit status of read, note that it's picky about newlines, see: https://unix.stackexchange.com/a/478732/170373 – ilkkachu Oct 17 '19 at 10:52
  • If you run this intensely the expansion is a weak point. Can get very slow. If check is some tiny check, and check_all is frequently called with any number of pids, this really matters. –  Oct 17 '19 at 12:14
  • @rastafile, what expansion? What do you mean with "running intensely"? Also, if you have a better way, not prone to "getting very slow", you're free to suggest one, or write an answer of your own. – ilkkachu Oct 17 '19 at 12:21
  • But i did write one, have you not seen? 22h ago -- I mean this for pid in "$@" expansion. Look at the Q. An "endless" row of PIDs going through only one single test. COULD be very high turnover. I did a prime numbers program just lately in bash. Tried different "tricks" but it gets very slow -- must be the loop-the-array-elements thing. Benchmark result is about 20s to 0.05s (bash vs perl, up to 30'000) –  Oct 17 '19 at 12:28
  • @rastafile, yes, if you pass a very large number of arguments to a function, it will take some time to process them all. But if you do have a large amount of items to process, a shell script will be slow just because of the shell, too, and you probably should implement your program using some other tool. Doubly so if the task at hand (the check command) involves launching external programs, and some other tool lets you avoid that. Even so, I doubt the expansion of an array is going to be the slow part. – ilkkachu Oct 17 '19 at 12:36
  • hmm...I think you mix a few things up. These are two extremes (mass testing of PIDs, "elaborate" array puhing-and-looping of found prime numbers). My solution is perfect for the first: input stream, no additional process, even more usable as it is a one liner. You made a standerd loop with exit status. Not tailor made. But then again, this Q is not well specified. At least we all agree on that. –  Oct 17 '19 at 13:14
  • @rastafile, yes, I made a standard loop, which, in my opinion, fits the issue presented in the question nicely. I don't see any relevant problems with the solution in sensible use cases. I also addressed your concern of slow running by proposing the use of a another tool, which again, in my opinion, would be the best solution. Any any case, I don't expect slowness to be an issue since the question presents a case of only three PIDs. Also, I find the question as presented clear enough on the relevant parts. Please do not presume to know my opinions on matters like that, or otherwise. – ilkkachu Oct 17 '19 at 13:21
  • @ilkkachu "I wonder how you did read the PIDs then." Sort of like this: while true; do; read PID ; ... done < pid.file (with some conditionals leading to a break in the ...). This reads the input line by line. read will assign an empty value after the inputs run out, so I had to check for that, etc. etc. – alexis Oct 17 '19 at 16:29
  • @alexis, yep. As long as it works, it's not wrong. – ilkkachu Oct 17 '19 at 18:29
  • 1
    Well it didn't quite work, hence this question! :-D – alexis Oct 17 '19 at 19:59
4

You could put it in a subshell with exit-on-error ( -e ):

pids=(1025 3425 6474)
(
    set -e
    for pid in "${pids[@]}"; do
        check "$pid"
    done
)
echo $?

Alternatively, you can use || exit 1 instead of set -e:

pids=(1025 3425 6474)
(
    for pid in "${pids[@]}"; do
        check "$pid" || exit 1
    done
)
echo $?
pLumo
  • 22,565
2

Maybe:

parallel -j0 check ::: $pid1 $pid2 $pidN &&
  echo all succeeded
parallel -j0  '! check' ::: $pid1 $pid2 $pidN &&
  echo all failed
parallel -j0 --halt soon,success=1 check ::: $pid1 $pid2 $pidN &&
  echo one succeeded
parallel -j0 --halt soon,fail=1 check ::: $pid1 $pid2 $pidN ||
  echo one failed

It will run the checks in parallel. Replace soon with now if you want running checks to be killed as soon as we know the result.

If you have the PIDs as output from a pipe (one per line):

pid_generator | parallel -j0 check && echo all succeeded

parallel gives one value to check and runs as many check as possible in parallel (-j0).

If the server does not have parallel installed, run this on a machine that has parallel installed:

parallel --embed > new_script

Edit new_script (the last 5 lines) and copy the script to the server.

Ole Tange
  • 35,514
  • And what about the "indeterminate" input? And what if check takes only one argument? –  Oct 17 '19 at 02:29
  • 1
    Wow, parallel. I'd probably go for this solution if I was already using parallel, but in this case a pure-bash approach is better (don't want extra dependencies on the target server...) – alexis Oct 17 '19 at 10:01
  • @alexis Try parallel --embed for that. – Ole Tange Oct 17 '19 at 11:44
  • @rastafile check is only called with one input. You can put as many $pids as you want. – Ole Tange Oct 17 '19 at 11:46
  • @OleTange I do not know how many PIDs there are. Question of not knowing, not of not being able to put enough. "one input" is too monolithic. An input stream that finishes or breaks is more flexible. Up to the user to control the "finishing" (error or EOF). Your solution actually needs a helper loop or extension or xargs. –  Oct 17 '19 at 11:58
  • @rastafile How do you receive the PIDs? – Ole Tange Oct 17 '19 at 12:01
  • @OleTange o I see you mean parallel feeds the PIDs to check one by one. Normally it's the other way round isn't it? "run the checks in parallel" --- this is VERY ambiguous: here check is run serially in parallel –  Oct 17 '19 at 12:03
  • How? In a very very unix way. File with newlines and ASCII integers. But you can plug my bare compound command (a while loop) to any INPUT stream, up to a process substitution from the right: WHILE < <(pid-generetor-indet.cmd) –  Oct 17 '19 at 12:08
0

A simple function could test each argument; if any fail, return from the function with a non-zero code:

#!/bin/sh

testall() {
  for test
  do
    sh -c "$test" || return 1
  done
}

With some example tests:

$ testall "echo 1" "echo 2" "false" && echo all OK
1
2
$ testall "echo 1" "echo 2" "echo 3" && echo all OK
1
2
3
all OK
$ testall && echo all OK
all OK
Jeff Schaller
  • 67,283
  • 35
  • 116
  • 255