0

I want to detect whether my script is already running, so I use this:

pidof -o %PPID -x "$0" >/dev/null && echo 'already running!' && exit 1

That works. But I want it in a function:

#!bin/bash
set -eu
set -o pipefail

checkIfRunning() { pidof -o %PPID -x "$0" >/dev/null && echo 'already running!' && exit 1 }

#...

checkIfRunning

That doesn't work: it always exits even if only one instance of the script is running.

I thought that if pidof were running in a subshell that it would detect the subshell and the script, and thus explain the result - but I don't see how a subshell is created here?

What is the reason for this, and how do I fix it?

lonix
  • 1,723
  • How are you starting the script? – Kusalananda Jul 16 '22 at 05:59
  • @Kusalananda ./myscript.sh. Are you implying the above should work and the problem is elsewhere? – lonix Jul 16 '22 at 07:29
  • 2
    Beware that in several shells, $0 inside a function expands to the name of the function. Something to bear in mind if you intend to switch to another shell in the future. Having SCRIPTNAME=$0 at the start of the script and refer to that instead would make your script more portable. – Stéphane Chazelas Jul 16 '22 at 08:58
  • @StéphaneChazelas Something new I didn't know - do you recall which shells that applies to? I only use "common" ones, but it's good to beware. – lonix Jul 16 '22 at 09:01
  • @lonix zsh (when not in sh emulation) and ksh at least though in ksh (ksh93 and mksh at least), that only applies to functions declared with the function f { ...;} syntax. – Stéphane Chazelas Jul 16 '22 at 09:04
  • man set says that The -e setting shall be ignored when executing the compound list following the while, until, if, or elif reserved word (...) or any command of an AND-OR list other than the last. -- I read this as: "set -e will not react to anything in a &&-chain except the last command", which in this case simply exits 1 (successfully). – pzkpfw Jul 16 '22 at 09:56
  • @pzkpfw, there's no standalone set command. It seems on your system set man page documents the set special builtin of some POSIX-like shell, or maybe it's actually the POSIX specification of the special set sh builtin. Here, the OP would want to look at the documentation of their own shell. Like info bash set for bash or info zsh errexit for zsh – Stéphane Chazelas Jul 16 '22 at 14:58

1 Answers1

2

In your code with set -e (short for set -o errexit) unhandled command failures cause the shell to exit.

pidof returns with a failure exit status when no matching process is found, but you do handle that failure. pidof is used as part of a and-or list, so its failure does not trigger errexit.

However because you used an and-or list where you should have used a if construct, the pidof && othercommand itself exits with a non-zero exit status and because that and-or list is the last thing run in the checkIfRunning function, the checkIfRunning function itself will also return with a failure exit status.

And that failure is not handled, so errexit is triggered. errexit is triggered not because pidof fails but because checkIfRunning failed.

Here, you should have written:

checkIfRunning() {
  if pidof -o %PPID -x -- "$0" >/dev/null; then
    echo >&2 'Already running!'
    exit 1
  fi
}

The exit status of a if construct is that of the last command run in the then or else part or 0 if none was run, not the exit status of the condition part.

Generally, replacing if constructs with and-or lists is wrong. Besides the problem of the overall exit status, if echo fails in your pidof && echo && exit 1, the script doesn't exit.

I'd argue that as soon as you use functions or anything other than the simplest script, errexit should be avoided and proper error handling be done.

See the corresponding section about set -euo pipefail in the bash wiki (short for set -o errexit -o nounset -o pipefail) or this FAQ entry.

Regarding your (now deleted) answer which tried to address the problem with:

checkIfRunning() {
  set +e                              # <---
  pidof -o %PPID -x $0 >/dev/null \
    && echo "running!" && exit 1
  set -e                              # <---
}

The only reason it works is because set -e happens to exit with a success status, so checkIfRunning does as well, the set +e is irrelevant and not needed, set -e could have been replaced with true or return 0.