38

Is there any variable that cron sets when it runs a program ? If the script is run by cron, I would like to skip some parts; otherwise invoke those parts.

How can I know if the Bash script is started by cron ?

3
  • Why don't you just us ps?
    – terdon
    Aug 31, 2012 at 9:20
  • see: serverfault.com/questions/146745/… Aug 31, 2012 at 11:25
  • @terdon: probably because ps is fairly badly documented (especially Linux's version which supports several different syntax styles) and the man page is even more dense and cryptic than most tools. I suspect most people don't even realise just how useful and versatile a tool ps can be.
    – cas
    Aug 31, 2012 at 12:55

11 Answers 11

44

I'm not aware that cron does anything to its environment by default that can be of use here, but there are a couple of things you could do to get the desired effect.

1) Make a hard or soft link to the script file, so that, for example, myscript and myscript_via_cron point to the same file. You can then test the value of $0 inside the script when you want to conditionally run or omit certain parts of the code. Put the appropriate name in your crontab, and you're set.

2) Add an option to the script, and set that option in the crontab invocation. For example, add an option -c, which tells the script to run or omit the appropriate parts of the code, and add -c to the command name in your crontab.

And of course, cron can set arbitrary environment variables, so you could just put a line like RUN_BY_CRON="TRUE" in your crontab, and check its value in your script.

0
23

Scripts run from cron are not run in interactive shells. Neither are startup scripts. The differentiation is that interactive shells have STDIN and STDOUT attached to a tty.

Method 1: check if $- includes the i flag. i is set for interactive shells.

case "$-" in
    *i*)
        interactive=1
        ;;
    *)
        not_interactive=1
        ;;
esac

Method 2: check is $PS1 is empty.

if [ -z "$PS1" ]; then
    not_interactive=1 
else
    interactive=1
fi

references:

Method 3: test your tty. it's not as reliable, but for simple cron jobs you should be ok, as cron does not by default allocate a tty to a script.

if [ -t 0 ]; then
    interactive=1
else
    non_interactive=1
fi

Keep in mind that you can however force an interactive shell using -i, but you'd probably be aware if you were doing this...

9
  • 1
    Note that the $PS1 command does not work when checking if script is started by systemd or not. the $- one does
    – mveroone
    Nov 17, 2015 at 10:08
  • 4
    'case "$-" in' doesn't appear to work in bash scripts.
    – Hobadee
    May 17, 2017 at 23:13
  • 1
    case "$-" in *i*) echo true ;; *) echo false ;; esac says true when I run it directly on the prompt. However, when putting it in a script.sh and running it with bash script.sh it says false. So this does not work for me to detect whether the script is being run from cron or invoked directly on an interactive shell.
    – saraedum
    Apr 16, 2020 at 14:19
  • 1
    So... this answer is wrong. Script invoked manually (./test.sh) says that this is not interactive shell. So I cannot determine if this is cron job or script started manually.
    – Kamil
    Dec 15, 2022 at 1:07
  • 1
    @Kamil you're right. I just tested it, too, and case "$-" in tests for interactive vs script, not "script run from bash prompt" vs "script run from cron job".
    – RonJohn
    Apr 14, 2023 at 16:41
13

First, get cron's PID, then get the current process's parent PID (PPID), and compare them:

CRONPID=$(ps ho %p -C cron)
PPID=$(ps ho %P -p $$)
if [ $CRONPID -eq $PPID ] ; then echo Cron is our parent. ; fi

If your script is started by another process that might have been started by cron, then you can walk your way back up the parent PIDs until you get to either $CRONPID or 1 (init's PID).

something like this, maybe (Untested-But-It-Might-Work<TM>):

PPID=$$   # start from current PID
CRON_IS_PARENT=0
CRONPID=$(ps ho %p -C cron)
while [ $CRON_IS_PARENT -ne 1 ] && [ $PPID -ne 1 ] ; do
  PPID=$(ps ho %P -p $PPID)
  [ $CRONPID -eq $PPID ] && CRON_IS_PARENT=1
done

From Deian: This is a version tested on RedHat Linux

# start from current PID
MYPID=$$
CRON_IS_PARENT=0
# this might return a list of multiple PIDs
CRONPIDS=$(ps ho %p -C crond)

CPID=$MYPID
while [ $CRON_IS_PARENT -ne 1 ] && [ $CPID -ne 1 ] ; do
        CPID_STR=$(ps ho %P -p $CPID)
        # the ParentPID came up as a string with leading spaces
        # this will convert it to int
        CPID=$(($CPID_STR))
        # now loop the CRON PIDs and compare them with the CPID
        for CRONPID in $CRONPIDS ; do
                [ $CRONPID -eq $CPID ] && CRON_IS_PARENT=1
                # we could leave earlier but it's okay like that too
        done
done

# now do whatever you want with the information
if [ "$CRON_IS_PARENT" == "1" ]; then
        CRON_CALL="Y"
else
        CRON_CALL="N"
fi

echo "CRON Call: ${CRON_CALL}"
1
  • 2
    On Solaris cron starts a shell and the shell runs the script, which itself starts another shell. So the parent pid in the script is not the pid of cron.
    – ceving
    Dec 15, 2016 at 17:17
5

If your script file is invoked by cron and it contains a shell in the first line like #!/bin/bash you need to find the parent-parent name for your purpose.

1) cron is invoked at the given time in your crontab, executing a shell 2) shell executes your script 3) your script is running

The parent PID is available in bash as variable $PPID. The ps command to get the parent PID of the parent PID is:

PPPID=`ps h -o ppid= $PPID`

but we need the name of the command, not the pid, so we call

P_COMMAND=`ps h -o %c $PPPID`

now we just need to test the result for "cron"

if [ "$P_COMMAND" == "cron" ]; then
  RUNNING_FROM_CRON=1
fi

Now you can test anywhere in your script

if [ "$RUNNING_FROM_CRON" == "1" ]; then
  ## do something when running from cron
else
  ## do something when running from shell
fi

Good luck!

1
  • This works only for Linux ps. For MacOS (as well as Linux, maybe *BSD too), you can use the following P_COMMAND: P_COMMAND=$(basename -a $(ps h -o comm $PPPID))
    – mdd
    Nov 11, 2019 at 16:58
3

There is no authoritative answer. Some other answers here actually try to match the cron environment, which is a hefty task.

Personally, I just set a variable in my crontab, e.g. CRON='in_cron' at the top. Then you can test it like this:

if [ "$CRON" != "in_cron" ]; then
  echo "This is not a cron job"
fi

This will authoritatively differentiate between interactive/nohup/ssh and cron, but it does require that variable declaration in the relevant user's crontab. For /etc/cron.d scripts, you should be able to put it in /etc/crontab (which works at least on my Debian 12 system).


The terminal ($TERM) variable is pretty decent here, as is the shell's "interactive" option flag (special parameter $- containing i). Some systems set TERM=dumb while most leave it empty, so we'll just check for either and also check for the interactive option flag:

if [ "${TERM:-dumb}$-" != "dumb${-#*i}" ]; then
  echo "This is not a cron job"
fi

The above code substitutes the word "dumb" when there is no value for $TERM. Therefore, the conditional fires when there is no $TERM or $TERM is set to "dumb" or if the $PS1 variable is not empty or if $- does not match itself when removing all characters up to the first i (this removes nothing when there is no i).

I've tested this on Debian 9, 11, & 12 (TERM=), CentOS 6.4 & 7.4 (TERM=dumb), and FreeBSD 7.3 && 11.2 (TERM=). This used to additionally include $PS1, but my Debian 12 system's cron now sets that variable somehow.

On at least Debian 12, this will differentiate between interactive/nohup and cron/ssh.


You can alternatively check the terminal name. Cron usually (verify this!) doesn't allocate a terminal, so you could run tty and the POSIX standard mandates that (if it really has no terminal) it must say not a tty in its output:

if [ "$(tty)" != "not a tty" ]; then
  echo "This is not a cron job"
fi

On at least Debian 12, this will differentiate between interactive and cron/ssh/nohup.


Another answer mentioned ( : > /dev/tty) 2>/dev/null to match interactivity.

On at least Debian 12, this will differentiate between interactive/nohup vs cron/ssh.


A fourth way to take advantage of cron not allocating a terminal would simply be to test for whether standard input is open. You can do this with ! [ -t 0 ], but this will give you an improper answer if you're piping data into the script.

1
  • This is the correct answer, for cron, the $(tty) is perfect and simple
    – higuita
    May 17, 2023 at 5:11
2

A simple echo $TERM | mail [email protected] in cron showed me that on both Linux and AIX, cron seems to set $TERM to 'dumb'.

Now theoretically there may still be actual dumb terminals around, but I suspect that for most occasions, that should suffice...

2
  • THIS is the answer. A shell script in my environment with echo $TERM run from a $ prompt says "linux", but says "dumb" when run from cron.
    – RonJohn
    Apr 14, 2023 at 16:50
  • In cron, $TERM is either empty or set to "dumb" depending on your distribution. See my answer for further detail, including a way to use $TERM that works for either of those two values that also adds $- for further safety.
    – Adam Katz
    May 17, 2023 at 18:50
1

Works on FreeBSD or on Linux:

if [ "Z$(ps o comm="" -p $(ps o ppid="" -p $$))" == "Zcron" -o \
     "Z$(ps o comm="" -p $(ps o ppid="" -p $(ps o ppid="" -p $$)))" == "Zcron" ]
then
    echo "Called from cron"
else
    echo "Not called from cron"
fi

You can go as far up the process tree as you wish.

1

This bash function should work on systems with either cron or crond. Tested under Debian bullseye.

## Returns 0 (success) if we are running under Cron
function undercron ()
{
    local cronpid=$(pgrep --uid=root --oldest --exact '^crond?$')
    for ((ppid=PPID; ppid > 1; ppid=$(ps ho %P -p $ppid))); do
        if ((ppid == cronpid)); then
            return 0
        fi
    done
    return 1
}
1

A generic solution to the question "is my output a terminal or am I running from a script" is:

( : > /dev/tty ) 2>/dev/null && dev_tty_good=y || dev_tty_good=n
1
  • Interesting, this exits 0 interactively and 2 from cron. [ -w /dev/tty ] exits 0 in both scenarios.
    – Adam Katz
    May 17, 2023 at 15:14
0

There is a problem I do not see anybody mentioning. Most test fail when I run my script from a remote computer using ssh!

ssh [email protected] ./SmartHome.sh

Then (say) [ -t 0 ] and ( : > /dev/tty) and $TERM all indicate wrong (OK, they are all actually right, but are not indicating what we need).

So, the simplest universal solution is to call the script from the cron with an argument, as mentioned in the most upvoted answer under 2). Then, ANY other invocation will be seen as manual run. I use - (but can be anything) as the first argument to cron call and then check for it in my script:

[ "$1" == "-" ] && shift || Run_Manually=1
1
  • Aside from the answers that track the cron PID, there is no way to determine you're in cron or some other headless script (be it via ssh or nohup). I simply set a variable in my crontab (at the top, something like CRON=in_cron will do), then check for that.
    – Adam Katz
    May 17, 2023 at 15:41
0

I've tested several OS (Ubuntu, CentOS, SUSE) and executed a script which includes printenv through SSH and local terminals and it seems these variables are never available if crond executes a script, but available in all other cases:

LESSOPEN
LESSCLOSE

So checking if this variable is not set, should work:

if [[ -z ${LESSOPEN+x} ]]; then
  echo "Script is executed by cron"
else
  echo "Script is executed manually"
fi
5
  • I can't see those environment variables on any Unix I have interactive access to. But what you are basically saying is the inverse of the suggestion at the end of the accepted answer: Test for the absense of some variable that is usually declared in an interactive shell.
    – Kusalananda
    Feb 24, 2022 at 10:54
  • @they Thank you for testing. Does SSH_CONNECTION exist in Unix if you connect to the Terminal through SSH? That's an additional variable which could be used in Linux, but it does not exist if the Terminal on the machine itself is used of course.
    – mgutt
    Feb 26, 2022 at 8:49
  • You could test for PS1, the interactive prompt. This is essentially what another answer already suggests, though.
    – Kusalananda
    Feb 26, 2022 at 8:54
  • Yes, but this does not cover situations when you execute a script by passing the SSH command as a parameter as mentioned in this answer.
    – mgutt
    Feb 26, 2022 at 10:11
  • Yes, as the answer that you link to mentions, it is safer to give the script an indication that it is running from cron (which is the specific situation that we want to test for) than relying on what the environment usually looks like. Personally, I would run the script with FROM_CRON=true ./myscript in the cron schedule, and then test for FROM_CRON in the script (if [ "${FROM_CRON-false}" = true ]; then ...; fi), rather than abusing the command line options. It's unclear why this has not been suggested so far. (EDIT: Oh, yes, it's in the accepted answer).
    – Kusalananda
    Feb 26, 2022 at 10:22

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .