Search code examples
pythonraspberry-piat-job

Execute bash-command with "at" (<<<) via python: syntax error, last token seen


I'm using a radio sender on my RPi to control some light-devices at home. I'm trying to implement a time control and had successfully used the program "at" in the past.

#!/usr/bin/python

import subprocess as sp
##### some code #####
sp.call(['at', varTime, '<<<', '\"sudo', './codesend', '111111\"'])

When I execute the program, i receive the

errmsg: syntax error. Last token seen: <

Garbled time

This codesnipped works fine with every command by itself (as long every parameter is from type string).

It's neccessary to call "at" in this way: at 18:25 <<< "sudo ./codesend 111111" to hold the command in the queue (viewable in "atq"), because sudo ./codesend 111111 | at 18:25 just executes the command directly and writes down the execution in "/var/mail/user".

My question ist, how can I avoid the syntax error. I'm using a lot of other packages in this program, so I have to stay with Python

I hope someone has a solution for this problem or can help to find my mistake. Many thanks in advance


Solution

  • Preface: Shared Code

    Consider the following context to be part of both branches of this answer.

    import subprocess as sp
    try:
        from shlex import quote # Python 3
    except ImportError:
        from pipes import quote # Python 2
    
    # given the command you want to schedule, as an array...
    cmd = ['sudo', './codesend', '111111']
    
    # ...generate a safely shell-escaped string.
    cmd_str = ' '.join(quote(x) for x in cmd))
    

    Solution A: Feed Stdin In Python

    <<< is shell syntax. It has no meaning to at, and it's completely normal and expected for at to reject it if given as a literal argument.

    You don't need to invoke a shell, though -- you can do the same thing directly from native Python:

    p = sp.Popen(['at', vartime], stdin=sp.PIPE)
    p.communicate(cmd_str)
    

    Solution B: Explicitly Invoke A Shell

    Moreover, <<< isn't /bin/sh syntax -- it's an extension honored in bash, ksh, and others; so you can't reliably get it just by adding the shell=True flag (which uses /bin/sh and so guarantees only POSIX-baseline features). If you want it, you need to explicitly invoke a shell with the feature, like so:

    bash_script = '''
    at "$1" <<<"$2"
    '''
    sp.call(['bash', '-c', bash_script,
             '_',                      # this is $0 for that script
             vartime,                  # this is its $1
             cmd_str,                  # this is its $2
             ])
    

    In either case, note that we're using shlex.quote() or pipes.quote() (as appropriate for our Python release) when generating a shell command from an argument list; this is critical to avoid creating shell injection vulnerabilities in our software.