10

I've written a small tool in C++, compiled it, made it executable, and move it to /opt/tools/bin. The latter is a directory I created to store small, custom programs. I export it to $PATH in my .zprofile, which let's me use tool in a terminal, and the program runs fine.

However, when I try setting up a cron job to run tool a couple of times a day, cron always complains about "/bin/sh: ./tool: No such file or directory".

I've tried both these cron commands with the same result:

  • 0 8,12,16,20 * * * zhrdct >> /opt/tools/var/log/zhrdct.log 2>&1
  • 0 8,12,16,20 * * * ./zhrdct >> /opt/tools/var/log/zhrdct.log 2>&1

What could be the issue here?

I know that I can probably use the absolute path to tool but if possible I'd like it work with the shorthand.

1
  • 1
    For starters, cron uses /bin/sh, not /usr/bin/zsh.
    – OrangeDog
    Commented May 17, 2023 at 9:04

5 Answers 5

47

You should use in cron ALWAYS the full path to the executables. It is run in quite different environment and a lot of executables are not "reachable"

0 8,12,16,20 * * * /path/to/zhrdct >> /opt/tools/var/log/zhrdct.log 2>&1
0
15

Cron runs in a non-interactive shell, so a lot of the features you'd see from a regular bash prompt (configured PATH, envvars) will be different or missing entirely.

If you want to omit absolute paths in cron, you'll need to configure PATH from scratch, as there is no implicit PATH variable to work with:

PATH=/opt/tools/bin:/usr/bin:/bin
0 8,12,16,20 * * * zhrdct >> /opt/tools/var/log/zhrdct.log 2>&1

N.B.: if your script contains any other shorthand paths to binaries, you'll need to ensure those paths are added to PATH as well.

1
  • 11
    In general, you don't want to replicate any of the environment configuration in CRON because you'll be updating it regularly. Just use absolute paths in CRON jobs and the moment you see one sample entry, you would recognize what's happening.
    – Nelson
    Commented May 15, 2023 at 6:02
7

There are two issues here:

  1. The directory /opt/tools/bin won't be in the default $PATH for cron jobs. You can add it in the crontab like this:

    PATH=/opt/tools/bin:/usr/local/bin:/bin:/usr/bin:/sbin:/usr/sbin
    
    0 8,12,16,20 * * * zhrdct >> /opt/tools/var/log/zhrdct.log 2>&1
    
  2. Generally, and specifically here when using crontab, when you run zhrdct the system searches the directories listed in $PATH for the specified command. If it can't be found then it returns the command not found error you're encountering. HOWEVER, if you specify a path to the command, as in ./zhrdct or /opt/tools/bin/zhrdct this searching is disabled and the command is executed directly as specified.

    In the case of ./zhrdct the dot represents "the current directory". For crontab if you're lucky that will happen to be your home directory ($HOME) but it's quite possible that it might be some other unspecified place. You have three choices here

    • Set the current directory (cd "$HOME" && …commands to run from HOME…)
    • Set the search $PATH was already described and run zhrdct
    • Use an absolute path to the command such as /opt/tools/bin/zhrdct.

Note that in any situation, ./zhrdct will fail if the command is in /opt/tools/bin and cron's current directory is anything other that /opt/tools/bin itself.

1

Commands run by cronjobs do not enjoy the various initialisations made by your login shell, such as setting up the PATH, loading /etc/bash.bashrc and ~/.bashrc and ~/.bash_profile

It is possible to configure HOME and PATH variables at the top of your crontab.

However, one trick I have found helpful is to simply put bash -i before the command, so that your command will have all that nice setup ready when it runs.

0 8,12,16,20 * * * bash -i zhrdct >> /opt/tools/var/log/zhrdct.log 2>&1

You can even pass arguments to the script, e.g.:

* * * * *        bash -i my-test-script --opt1 arg1 arg2

Hopefully zsh -i will work similarly, since that is the shell the poster is using.

Caveat: It's working fine here on Manjaro, but I think maybe on some systems this doesn't always work (CentOS?). So your mileage may vary.

Caveat: If your command is doing some | piping, then the command after the pipe might also want the bash -i which can become unwieldy!

-1

Another option is to store your compiled binary in the example location, and to also append version numbers

That gives you a series of versions - for example:

/opt/tools/bin/zhrdct-1.0.0
/opt/tools/bin/zhrdct-1.0.1
/opt/tools/bin/zhrdct-1.2.0

Then for convenience you symlink from /usr/local/bin/tool to whatever version should be run by default with

sudo ln -s /opt/tools/bin/zhrdct-1.0.1  /usr/local/bin/tool

In a directory listing this will appear as

lrwxrwxrwx   1 root root    11 May 16 14:35 tool -> /opt/tools/bin/zhrdct-1.0.1

And you can simply run tool at the prompt or called from cron because /usr/local/bin should be in your path already.

You should be able to add this to your deployment system too, whether that's a Makefile or a shell script, or something more complex like salt or ansible or ...

Another benefit is that if something still requires version1 of the tool, you can call that one directly, or use a second symlink called tool-1.

2
  • 4
    The version number suggestion seems entirely orthogonal to where the binary is stored / what path is set
    – abligh
    Commented May 17, 2023 at 5:32
  • @abligh true, but it is related and only a slight variation. OP writes their own code and I've occasionally found it useful to keep the old versions around ready to run, if just for comparison with the latest version, unit testing etc.
    – Criggie
    Commented May 17, 2023 at 7:33

You must log in to answer this question.

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