5

I want to execute a Bash function at a scheduled time. I think the right tool for this is the at command.

However, it doesn't seem to be working.

function stupid () {
    date
}

export -f stupid

cat << EOM | at now + 1 minute
stupid
EOM

After waiting the minute, I check my mail and this is the error I see:

sh: line 41: syntax error near unexpected token `=\(\)\ {\ \ date"
"}'
sh: line 41: `"}; export BASH_FUNC_stupid()'

I don't understand what's going wrong here, especially since I know the function works.

$ stupid
Fri May 29 21:05:38 UTC 2015

Looking at the error, I think the incorrect shell is being used to execute the function (sh as opposed to bash), but if I check $SHELL I see it points to /bin/bash, and the man page for at says:

$ echo $SHELL
/bin/bash
$ man at

    ...

    SHELL   The value of the SHELL environment variable at the time
           of at invocation will determine which shell is used  to
           execute  the at job commands.

So Bash should be the shell running my function.

What going on here? Is there a way to get my Bash function to run with at?

  • 1
    http://lists.gnu.org/archive/html/bug-bash/2014-09/msg00251.html – llua May 29 '15 at 21:56
  • @llua - Great find. I think I am running into the same issue, which is apparently a problem with Bash. Here's a similar report on the Red Hat bug tracker (linked to from that thread). For the record, I am running GNU bash, version 4.1.2(1)-release, which predates the shellshock fixes discussed in that thread by a few years. So perhaps it's not exactly the same issue. I should try with the latest release of Bash to see if this has been fixed. – Nick Chammas May 29 '15 at 22:22
  • Wow, this is weird. If I upgrade to the latest (I think) release, 4.3.30(1)-release and try the original snippet from my question, I now get: /bin/bash: line 1: stupid: command not found. So now it looks like bash and not sh is running, which is good, but the function is no longer accessible. – Nick Chammas May 29 '15 at 22:49
  • Even weirder on Bash 4.3.30: If I submit bash -c 'stupid' to at instead of just stupid, I get the original syntax error message with sh:. I'm lost now. ¯\_(ツ)_/¯ – Nick Chammas May 29 '15 at 22:51

3 Answers3

10

Bash functions are exported via the environment. The at command makes the environment, the umask and the current directory of the calling process available to the script by generating shell code that reproduces the environment. The script executed by your at job is something like this:

#!/bin/bash
umask 022
cd /home/nick
PATH=/usr/local/bin:/usr/bin:/bin; export PATH
HOME=/home/nick; export HOME
…
stupid

Under older versions of bash, functions were exported as a variable with the name of the function and a value starting with () and consisting of code to define the function, e.g.

stupid="() {
    date
}"; export stupid

This made many scenarios vulnerable to a security hole, the Shellshock bug (found by Stéphane Chazelas), which allowed anyone able to inject the content of an environment variable under any name to execute arbitrary code in a bash script. Versions of bash where with a Shellshock fix use a different way: they store the function definition in a variable whose name contains characters that are not found in environment variables and that shells do not parse as assignments.

BASH_FUNC_stupid%%="() {
    date
}"; export stupid

Due to the %, this is not valid sh syntax, not even in bash, so the at job fails, whether it even attempts to use the function or not. The Debian version of at, which is used in many Linux distributions, was changed in version 3.16 to export only variables that have valid names in shell scripts. Thus newer versions of at don't pass post-Shellshock bash exported functions through, whereas older ones error out.

Even with pre-Shellshock versions of bash, the exported function only works in bash scripts launched from the at job, not in the at job itself. In the at job itself, even if it's executed by bash, stupid is just another environment variable; bash only imports functions when it starts.

To export functions to an at job regardless of the bash or at version, put them in a file and source that file from your job, or include them directly in your job. To print out all defined functions in a format that can be read back, use declare -f.

{ declare -f; cat << EOM; } | at now + 1 minute
stupid
EOM
Stephen Kitt
  • 434,908
  • Thank you for the thorough answer and history behind this behavior. One thing I think this answer doesn't explain, is why sh shows up in the error messages when $SHELL is correctly set to /bin/bash and that is supposedly what determines which shell executes the job. That just may be another artifact of the older version of Bash I was using. But in any case, it sounds like the headache-free solution to do what I want is to put my functions in a file and source them from the job, as you recommend. – Nick Chammas May 30 '15 at 22:07
  • @NickChammas What version of at do you have, on what distribution? The one maintained by Debian and used by many Linux distributions (and I think shared common ancestry with *BSD) always runs the supplied code with sh. It tells me warning: commands will be executed using /bin/sh when I run it. Given the passage you quote from your man page, you're using a different or patched version, which come to think of it might mean that it works differently (though with the same symptoms here). – Gilles 'SO- stop being evil' May 30 '15 at 22:20
  • at -V gives 3.1.10. I'm running on Amazon Linux AMI release 2013.03. – Nick Chammas Jun 01 '15 at 20:08
0

at is designed to use /bin/sh instead of /bin/bash, so it prepends a code prologue that you can see with at -c {jobnumber}.  If you want to insert stuff for bash and use it, here is a simple function:

at_with_bash_alias_and_functions(){
  ( 
    echo "exec /bin/bash <<END"
    alias
    typeset -f
    cat
    echo END
  ) | sed 's/\$/\\$/g' | at "$@"
}

Show alias and functions are present:

at_with_bash_alias_and_functions now <<< "alias;typeset -f"

Limits: inlining may lose syntax due to shell substitution, it doesn't work exactly like at.  Verify your syntax with exec cat instead of exec bash.

-1

Well, you already know you get mail. That's pretty cool, because you definitely can execute a bash function when mail arrives.

This is from man bash:

  • $MAILPATH

    • A colon-separated list of filenames to be checked for mail. The message to be printed when mail arrives in a particular file may be specified by separating the filename from the message with a ?. When used in the text of the message, $_ expands to the name of the current mailfile. Example:

      MAILPATH='/var/mail/bfox?"You have mail":~/shell-mail?"$_ has mail!"'
      
    • bash supplies a default value for this variable, but the location of the user mail files that it uses is system dependent (e.g., /var/mail/$USER).

Incidentally, $_ is not the only expansion that might occur in that context - you can expand practically anything, to include command substitutions. In fact, you don't necessarily need to print anything at all. And the files don't necessarily need to contain mail either - the shell will attempt to notify when a check performed every $MAILCHECK seconds indicates a file has been modified since it last had a look.

Still though, the way shells handle $MAILxxxx is usually dependent on prompts - which is to say that regardless of $MAILCHECK's value if you don't pull a new prompt at the right time you don't get notified - and won't until you trigger the next prompt. I'm not certain about this, but it maybe set -b would affect that.

In any case, there is more than one way to do that kind of thing. Perhaps most obvious would be to use a signal - schedule an at job like:

echo kill -USR1 "$$" | at now + 1 minute
trap some_func USR1

And see what happens.

mikeserv
  • 58,310