3

I have a bash script that I need to be able to start from either:

  1. cron
  2. an interactive (logon) shell

This script needs to know whether it was started from cron, or started from an interactive shell. I thought I had resolved this issue when I declared an environment variable in the root crontab:

RUN_BY_CRON="TRUE"

In the script, I test RUN_BY_CRON and use the result to set another variable:

if [ "$RUN_BY_CRON" = "TRUE" ]; then            
    ((wait_time=DELAY_HALT*60))
fi

This worked until I added set -u to my script (as a "common defensive programming strategy"). Since then, when I run the script from the command line, set -u flags RUN_BY_CRON as an "unbound variable" error:

$ sudo ./skedrtc.sh
./skedrtc.sh: line 24: RUN_BY_CRON: unbound variable

FWIW, I ran shellcheck on this script, and got no warning or error.

I tried adding a test for RUN_BY_CRON, but got the same error. I tried testing for an interactive shell, but testing from within the script itself isn't helpful:

...
if [ -z "$RUN_BY_CRON" ]; then        # test for null string
    RUN_BY_CRON="FALSE"
fi
...

if [[ $- == *i* ]]; then              # test for interactive shell
    RUN_BY_CRON="FALSE"
fi

This feels like a "catch 22" situation. I've looked for ways to create a try-catch block, but AIUI there's nothing like that in bash.

Q: How can I avoid this "unbound variable" error without removing the set -u ?

2

2 Answers 2

8

Potentially you could use the standard ${var-string} syntax¹ which means that if $var is unset then it expands to string otherwise it expands to the value of the variable and (like the other related variants: ${var+string}, ${var=string}, ${var?string}, ${var:-string}, ${var:+string}, ${var:?string}¹) bypasses the effect of the nounset option.

e.g

#!/bin/bash

set -u # same as, shorter, though less legible than set -o nounset

printf '%s\n' "${NOSUCHVARIABLE-FALSE}"

Now if I run this normally:

% ./x
FALSE

So there's no error, and the value is expanded to a "default" value.

But if the value it set:

% NOSUCHVARIABLE=hello ./x
hello

This even works if the variable is set but is empty

% NOSUCHVARIABLE= ./x


If you also want it to replace the empty string then use :- instead;

#!/bin/bash

set -u

printf '%s\n' "${NOSUCHVARIABLE:-FALSE}"

% NOSUCHVARIABLE="" ./x
FALSE

A not uncommon pattern is to have code do something like:

NOSUCHVARIABLE="${NOSUCHVARIABLE-default value}"

Or:

: "${NOSUCHVARIABLE=default value}"

at the beginning to ensure there's a default value set.

In ksh, bash or zsh, you can also the the -v predicate of their builtin [ utility or of the [[ ... ]] construct to test whether a variable is set³ which would also allow bypassing nounset:

if [[ ! -v RUN_BY_CRON || -z "$RUN_BY_CRON" || $- = *i* ]]; then
  RUN_BY_CRON=FALSE
fi

The standard sh equivalent of ksh's [ -v var ] being [ -n "${var+set}" ].


¹ Originally from the Bourne shell, like the -u option, aliases to -o nounset in ksh. All of that specified by POSIX for sh

² Also in zsh: ${var::=string} for unconditional assignment and ${+var} which expands to 1 or 0 depending on whether $var is set or not.

³ With some variations between shells for variables that are declared but not set, or set to an empty list for arrays or associative arrays.

5
  • I'm guessing that your NOSUCHVARIABLE is supposed to represent the environment variable I set in the crontab: RUN_BY_CRON ???
    – Seamus
    Commented Jan 20 at 3:27
  • Yes; it's an example of a variable that wouldn't normally exist. Commented Jan 20 at 3:49
  • OK - thanks. I've just done a quick test using RUN_BY_CRON=${RUN_BY_CRON-""}. That seems to be all I need in my case. AFAIC, this is a correct answer --- and the accepted answer :)
    – Seamus
    Commented Jan 20 at 3:53
  • Just one other thing: Do you have a link to the GNU docs on this technique?... Is this it?
    – Seamus
    Commented Jan 20 at 4:22
  • 1
    @Seamus It's in POSIX, so you can look there.
    – Kusalananda
    Commented Jan 20 at 5:15
0

The genesis of the Question here concerns how to make a reliable determination as to whether a script was started from a cron job - or from the command line (or some other method; e.g. systemd). In fact, it was my attempt to implement a suggestion in another answer here on SE that led to the question above.

In thinking about this problem, I developed another approach that also seems to work. It's shown below as a bash function; it utilizes the ability of pstree to provide the entire process tree for the PID of the current shell ($$). Also note that usage of the regex [c]ron, prevents grep from including its own process in the output. I'll share it here as it may be of interest to some:

cron_ancestry() {
    if [[ -n $(pstree -aps $(echo $$) | grep -m 1 "[c]ron,") ]]; then
        echo "cron is your daddy"
    else
        echo "spawned by unknown"
    fi
}

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.