Q:

DuckyScript precompiler for Arduino Leonardo

I've made a python program to convert DuckyScript code to Arduino code for Leonardo boards. Below you can find both the project files and a sample script.

compiler.py (the program base):
#!/usr/bin/env python3

"""
Usage: {} <input> <output>

<input> should be a DuckieScript file and <output> will be an Arduino sketch
"""

import os
import sys
import fileinput
import textwrap
from data import keycodes
from data import code

delay = 0
cmdtype = 0
commands = ""
includes = ""
defines = ""
loop = ""

def info(type, msg, **kwargs):
    lineno = fileinput.lineno()
    path = os.path.basename(input_file)
    exit = kwargs.get('exit', None)
    types = ['error', 'warning', 'info', 'screw up risk']
    message = '{}:{}: {}: {}'.format(path, lineno, types[type], msg)
    print(message)
    if exit:
        print('{}: compilation aborted.'.format(path))
        sys.exit(exit)

def getkey(keys):
    global commands
    keys = keys.split()
    normalkey = list()
    modifiers = 0
    for key in keys:
        if not key.isupper():
            key = key.upper()
            if len(key) is not 1:
                info(1, 'you should type all the special keys in uppercase')

        try:
            key = [keycodes.get(code) for code in keycodes if key in code][0]
        except IndexError:
            info(0, 'unrecognized key: {}'.format(key), exit=127)

        if key[1]:
            modifiers |= key[0]
        else:
            normalkey.append(key[0])

    if len(normalkey) > 6:
        info(0, 'you can\'t press more than 6 keys at the same time', exit=126)

    arguments = [0] * 7
    arguments[6] = modifiers
    for number, argument in enumerate(normalkey):
        arguments[number] = argument
    arguments = [format(byte, '#04x') for byte in arguments]
    commands += 'sendKey({},{},{},{},{},{},{});'.format(*arguments)
    commands += '\n'

    return True

if len(sys.argv) is not 3:
    print(__doc__.format(sys.argv[0]))
    sys.exit(1)
elif not os.path.isfile(sys.argv[1]):
    print()
    print('Error: You must specify a valid input file')
    print(__doc__.format(sys.argv[0]))
    sys.exit(2)
else:
    output_file = sys.argv[2]
    input_file = sys.argv[1]

for line in fileinput.input([input_file]):
    base = list(map(str.strip, line.split(maxsplit=1)))
    if len(base) is 2:
        command, options = base
    elif len(base) is 1:
        command = base[0]
        options = None
    else:
        continue

    if not command.isupper() and command not in ('#', '//', ';', '@', '%'):
        command = command.upper()
        info(1, 'commands should be typed in uppercase')

    if command in ('REM', 'COMMENT', '#', '//', ';'):
        commands += '// {}'.format(options)
        commands += '\n'
        needs_delay = False
        cmdtype = -1

    elif command in ('INCLUDE'):
        info(3, 'don\'t use {} unless you really need it'.format(command))
        if cmdtype > 0:
            info(0, 'you shouldn\'t mix {} in the code'.format(command), exit=5)
        includes += '#include {}'.format(options)
        includes += '\n'
        needs_delay = False
        cmdtype = 0

    elif command in ('DEFINE'):
        info(3, 'don\'t use {} unless you really need it'.format(command))
        if cmdtype > 1:
            info(0, 'you shouldn\'t mix {} in the code'.format(command), exit=5)
        includes += '#define {}'.format(options)
        includes += '\n'
        needs_delay = False
        cmdtype = 1

    elif command in ('LOOP', '@'):
        info(3, "don't use {} unless you really need it".format(command))
        if cmdtype > 2:
            info(0, 'you should put {} in the top of the program'.format(command), exit=5)
        loop += options
        loop += '\n'
        needs_delay = False
        cmdtype = 2

    elif command in ('ARDUINO', 'CODE', '%'):
        info(3, "don't use {} unless you really need it".format(command))
        commands += options
        commands += '\n'
        needs_delay = False
        cmdtype = 3

    elif command in ('DEFAULT_DELAY', 'DEFAULTDELAY'):
        if not options.isdigit():
            info(0, '{} only accepts integers'.format(command), exit=11)
        delay = int(options)
        needs_delay = False
        cmdtype = -1

    elif command in ('SLEEP', 'DELAY', 'WAIT'):
        if not options.isdigit():
            info(0, '{} only accepts integers'.format(command), exit=11)
        commands += 'delay({});'.format(options)
        commands += '\n'
        needs_delay = False
        cmdtype = 3

    elif command in ('REPEAT'):
        if not options.isdigit():
            info(0, '{} only accepts integers'.format(command), exit=11)
        last_command = commands.splitlines()[-1]
        commands += 'int i=0;'
        commands += '\n'
        commands += 'for(i; i<={}; i++) {{'.format(options)
        commands += '\n'
        commands += '  {}'.format(last_command)
        commands += '\n'
        commands += '  delay({});'.format(delay)
        commands += '\n'
        commands += '}'
        commands += '\n'
        needs_delay = False
        cmdtype = 3

    elif command in ('STRING', 'TEXT', 'PRINT'):
        commands += 'Keyboard.print("{}");'.format(options.replace('"', '\\"'))
        commands += '\n'
        needs_delay = True
        cmdtype = 3

    else:
        cmdtype = 3
        if not getkey(line):
            info(0, 'unrecognized command: {}'.format(command), exit=10)

    if delay > 0 and needs_delay:
        commands += 'delay({});'.format(delay)
        commands += '\n'

commands = "\n".join(["  " + i for i in commands.splitlines()])
loop = "\n".join(["  " + i for i in loop.splitlines()])

code = code.format(includes=includes,
                   defines=defines,
                   setup=commands,
                   loop=loop)

with open(output_file, 'w') as output:
    output.write(code)

data.py (the data storage):
#!/usr/bin/env python3

"""
This module contains all the compiler data, including a keycode lookup table
and the arduino program base.
"""

import textwrap

# Here is the base code of the output. Note that the double curl braces escapes
# the format strings. This will avoid later to get confused the format function
code = textwrap.dedent("""
    {includes}

    {defines}

    void sendKey(byte k0, byte k1, byte k2, byte k3,
                 byte k4, byte k5, byte modifiers)
    {{
      KeyReport report = {{0}};

      report.keys[0] = k0;
      report.keys[1] = k1;
      report.keys[2] = k2;
      report.keys[3] = k3;
      report.keys[4] = k4;
      report.keys[5] = k5;
      report.modifiers = modifiers;
      report.reserved = 1;
      Keyboard.sendReport(&report);

      report = {{0}};
      Keyboard.sendReport(&report);
    }}

    void setup() {{
    {setup}
    }}

    void loop() {{
    {loop}
    }}
""")

# Dictionary with the keys and their hexadecimal values.
# The structure follows these rules:
#
#    ('KEY_ALIAS_1', 'KEY_ALIAS_2', 'KEY_ALIAS_N'): [0xKeyCode, IsModifier]
#
# If the key is a modifier key (CTRL, ALT, SHIFT or GUI), IsModifier will be 1,
# else it will be 0.
#
# The keycodes were extracted from the arduino defines and slightly modified to
# respect the DuckieScript coding standards.
#
# You can get the base of this dictionary piping the arduino defines to these
# commands (only if you're in a *nix system):
#
#    sed 's/KEY_//' |
#    awk '{ print "(\x27"$2"\x27, \x27KEY_"$2"\x27): ["$3", 0]," }'
keycodes = {
    ('A', 'KEY_A'): [0x04, 0],
    ('B', 'KEY_B'): [0x05, 0],
    ('C', 'KEY_C'): [0x06, 0],
    ('D', 'KEY_D'): [0x07, 0],
    ('E', 'KEY_E'): [0x08, 0],
    ('F', 'KEY_F'): [0x09, 0],
    ('G', 'KEY_G'): [0x0A, 0],
    ('H', 'KEY_H'): [0x0B, 0],
    ('I', 'KEY_I'): [0x0C, 0],
    ('J', 'KEY_J'): [0x0D, 0],
    ('K', 'KEY_K'): [0x0E, 0],
    ('L', 'KEY_L'): [0x0F, 0],
    ('M', 'KEY_M'): [0x10, 0],
    ('N', 'KEY_N'): [0x11, 0],
    ('O', 'KEY_O'): [0x12, 0],
    ('P', 'KEY_P'): [0x13, 0],
    ('Q', 'KEY_Q'): [0x14, 0],
    ('R', 'KEY_R'): [0x15, 0],
    ('S', 'KEY_S'): [0x16, 0],
    ('T', 'KEY_T'): [0x17, 0],
    ('U', 'KEY_U'): [0x18, 0],
    ('V', 'KEY_V'): [0x19, 0],
    ('W', 'KEY_W'): [0x1A, 0],
    ('X', 'KEY_X'): [0x1B, 0],
    ('Y', 'KEY_Y'): [0x1C, 0],
    ('Z', 'KEY_Z'): [0x1D, 0],
    ('1', 'KEY_1'): [0x1E, 0],
    ('2', 'KEY_2'): [0x1F, 0],
    ('3', 'KEY_3'): [0x20, 0],
    ('4', 'KEY_4'): [0x21, 0],
    ('5', 'KEY_5'): [0x22, 0],
    ('6', 'KEY_6'): [0x23, 0],
    ('7', 'KEY_7'): [0x24, 0],
    ('8', 'KEY_8'): [0x25, 0],
    ('9', 'KEY_9'): [0x26, 0],
    ('0', 'KEY_0'): [0x27, 0],
    ('ENTER', 'NEWLINE', 'KEY_ENTER'): [0x28, 0],
    ('ESC', 'ESCAPE', 'KEY_ESC'): [0x29, 0],
    ('BACKSPACE', 'KEY_BACKSPACE'): [0x2A, 0],
    ('TAB', 'KEY_TAB'): [0x2B, 0],
    ('SPACE', 'KEY_SPACE'): [0x2C, 0],
    ('MINUS', 'KEY_MINUS'): [0x2D, 0],
    ('EQUAL', 'KEY_EQUAL'): [0x2E, 0],
    ('LEFTBRACE', 'KEY_LEFTBRACE'): [0x2F, 0],
    ('RIGHTBRACE', 'KEY_RIGHTBRACE'): [0x30, 0],
    ('BACKSLASH', 'KEY_BACKSLASH'): [0x31, 0],
    ('NONUSHASH', 'KEY_NONUSHASH'): [0x32, 0],
    ('SEMICOLON', 'KEY_SEMICOLON'): [0x33, 0],
    ('APOSTROPHE', 'KEY_APOSTROPHE'): [0x34, 0],
    ('GRAVE', 'KEY_GRAVE'): [0x35, 0],
    ('COMMA', 'KEY_COMMA'): [0x36, 0],
    ('DOT', 'KEY_DOT'): [0x37, 0],
    ('SLASH', 'KEY_SLASH'): [0x38, 0],
    ('CAPSLOCK', 'KEY_CAPSLOCK'): [0x39, 0],
    ('F1', 'KEY_F1'): [0x3A, 0],
    ('F2', 'KEY_F2'): [0x3B, 0],
    ('F3', 'KEY_F3'): [0x3C, 0],
    ('F4', 'KEY_F4'): [0x3D, 0],
    ('F5', 'KEY_F5'): [0x3E, 0],
    ('F6', 'KEY_F6'): [0x3F, 0],
    ('F7', 'KEY_F7'): [0x40, 0],
    ('F8', 'KEY_F8'): [0x41, 0],
    ('F9', 'KEY_F9'): [0x42, 0],
    ('F10', 'KEY_F10'): [0x43, 0],
    ('F11', 'KEY_F11'): [0x44, 0],
    ('F12', 'KEY_F12'): [0x45, 0],
    ('SYSRQ', 'KEY_SYSRQ'): [0x46, 0],
    ('SCROLLLOCK', 'KEY_SCROLLLOCK'): [0x47, 0],
    ('PAUSE', 'KEY_PAUSE'): [0x48, 0],
    ('INSERT', 'KEY_INSERT'): [0x49, 0],
    ('HOME', 'KEY_HOME'): [0x4A, 0],
    ('PAGEUP', 'KEY_PAGEUP'): [0x4B, 0],
    ('DELETE', 'KEY_DELETE'): [0x4C, 0],
    ('END', 'KEY_END'): [0x4D, 0],
    ('PAGEDOWN', 'KEY_PAGEDOWN'): [0x4E, 0],
    ('RIGHT', 'RIGHTARROW', 'KEY_RIGHT'): [0x4F, 0],
    ('LEFT', 'LEFTARROW', 'KEY_LEFT'): [0x50, 0],
    ('DOWN', 'DOWNARROW', 'KEY_DOWN'): [0x51, 0],
    ('UP', 'UPARROW', 'KEY_UP'): [0x52, 0],
    ('NUMLOCK', 'KEY_NUMLOCK'): [0x53, 0],
    ('KPSLASH', 'KEY_KPSLASH'): [0x54, 0],
    ('KPASTERISK', 'KEY_KPASTERISK'): [0x55, 0],
    ('KPMINUS', 'KEY_KPMINUS'): [0x56, 0],
    ('KPPLUS', 'KEY_KPPLUS'): [0x57, 0],
    ('KPENTER', 'KEY_KPENTER'): [0x58, 0],
    ('KP1', 'KEY_KP1'): [0x59, 0],
    ('KP2', 'KEY_KP2'): [0x5A, 0],
    ('KP3', 'KEY_KP3'): [0x5B, 0],
    ('KP4', 'KEY_KP4'): [0x5C, 0],
    ('KP5', 'KEY_KP5'): [0x5D, 0],
    ('KP6', 'KEY_KP6'): [0x5E, 0],
    ('KP7', 'KEY_KP7'): [0x5F, 0],
    ('KP8', 'KEY_KP8'): [0x60, 0],
    ('KP9', 'KEY_KP9'): [0x61, 0],
    ('KP0', 'KEY_KP0'): [0x62, 0],
    ('KPDOT', 'KEY_KPDOT'): [0x63, 0],
    ('102ND', 'KEY_102ND'): [0x64, 0],
    ('COMPOSE', 'KEY_COMPOSE'): [0x65, 0],
    ('POWER', 'KEY_POWER'): [0x66, 0],
    ('KPEQUAL', 'KEY_KPEQUAL'): [0x67, 0],
    ('F13', 'KEY_F13'): [0x68, 0],
    ('F14', 'KEY_F14'): [0x69, 0],
    ('F15', 'KEY_F15'): [0x6A, 0],
    ('F16', 'KEY_F16'): [0x6B, 0],
    ('F17', 'KEY_F17'): [0x6C, 0],
    ('F18', 'KEY_F18'): [0x6D, 0],
    ('F19', 'KEY_F19'): [0x6E, 0],
    ('F20', 'KEY_F20'): [0x6F, 0],
    ('F21', 'KEY_F21'): [0x70, 0],
    ('F22', 'KEY_F22'): [0x71, 0],
    ('F23', 'KEY_F23'): [0x72, 0],
    ('F24', 'KEY_F24'): [0x73, 0],
    ('OPEN', 'KEY_OPEN'): [0x74, 0],
    ('HELP', 'KEY_HELP'): [0x75, 0],
    ('PROPS', 'MENU', 'APP', 'CONTEXTMENU', 'KEY_PROPS'): [0x76, 0],
    ('FRONT', 'KEY_FRONT'): [0x77, 0],
    ('STOP', 'KEY_STOP'): [0x78, 0],
    ('AGAIN', 'KEY_AGAIN'): [0x79, 0],
    ('UNDO', 'KEY_UNDO'): [0x7A, 0],
    ('CUT', 'KEY_CUT'): [0x7B, 0],
    ('COPY', 'KEY_COPY'): [0x7C, 0],
    ('PASTE', 'KEY_PASTE'): [0x7D, 0],
    ('FIND', 'KEY_FIND'): [0x7E, 0],
    ('MUTE', 'KEY_MUTE'): [0x7F, 0],
    ('VOLUMEUP', 'KEY_VOLUMEUP'): [0x80, 0],
    ('VOLUMEDOWN', 'KEY_VOLUMEDOWN'): [0x81, 0],
    ('RETURN', 'KEY_RETURN'): [0x9E, 0],
    # ('LEFTCTRL', 'KEY_LEFTCTRL'): [0xE0, 0],
    # ('LEFTSHIFT', 'KEY_LEFTSHIFT'): [0xE1, 0],
    # ('LEFTALT', KEY_LEFTALT'): [0xE2, 0],
    # ('LEFTGUI', 'KEY_LEFTGUI'): [0xE3, 0],
    # ('RIGHTCTRL', 'KEY_RIGHTCTRL'): [0xE4, 0],
    # ('RIGHTSHIFT', 'KEY_RIGHTSHIFT'): [0xE5, 0],
    # ('RIGHTALT', 'KEY_RIGHTALT'): [0xE6, 0],
    # ('RIGHTGUI', 'KEY_RIGHTGUI'): [0xE7, 0],
    ('KEY_MODIFIER_LEFT_CTRL',
     'KEY_LEFTCONTROL',
     'KEY_LEFTCTRL',
     'KEY_CONTROL'
     'KEY_CTRL',
     'LEFTCONTROL',
     'LEFTCTRL',
     'CONTROL',
     'CTRL',): [0x01, 1],
    ('KEY_MODIFIER_LEFT_SHIFT',
     'KEY_LEFTSHIFT',
     'KEY_SHIFT',
     'LEFTSHIFT',
     'SHIFT'): [0x02, 1],
    ('KEY_MODIFIER_LEFT_ALT',
     'KEY_LEFTOPTION',
     'KEY_LEFTALT',
     'KEY_OPTION',
     'KEY_ALT',
     'LEFTOPTION',
     'LEFTALT',
     'OPTION',
     'ALT'): [0x04, 1],
    ('KEY_MODIFIER_LEFT_GUI',
     'KEY_COMMAND',
     'KEY_WINDOWS',
     'KEY_SUPER',
     'KEY_META',
     'KEY_GUI',
     'KEY_LEFTCOMMAND',
     'KEY_LEFTWINDOWS',
     'KEY_LEFTSUPER',
     'KEY_LEFTMETA',
     'KEY_LEFTGUI',
     'LEFTCOMMAND',
     'LEFTWINDOWS',
     'LEFTSUPER',
     'LEFTMETA',
     'LEFTGUI'
     'COMMAND',
     'WINDOWS',
     'SUPER',
     'META',
     'GUI'): [0x08, 1],
    ('KEY_MODIFIER_RIGHT_CTRL',
     'KEY_RIGHTCONTROL',
     'KEY_RIGHTCTRL',
     'RIGHTCONTROL',
     'RIGHTCTRL'): [0x10, 1],
    ('KEY_MODIFIER_RIGHT_SHIFT',
     'KEY_RIGHTSHIFT',
     'RIGHTSHIFT'): [0x20, 1],
    ('KEY_MODIFIER_RIGHT_ALT',
     'KEY_RIGHTOPTION',
     'KEY_RIGHTALT',
     'RIGHTOPTION',
     'RIGHTALT'): [0x40, 1],
    ('KEY_MODIFIER_RIGHT_GUI',
     'KEY_RIGHTCOMMAND',
     'KEY_RIGHTWINDOWS',
     'KEY_RIGHTSUPER',
     'KEY_RIGHTMETA',
     'KEY_RIGHTGUI',
     'RIGHTCOMMAND',
     'RIGHTWINDOWS',
     'RIGHTSUPER',
     'RIGHTMETA',
     'RIGHTGUI'): [0x80, 1],
    ('PLAYPAUSE', 'KEY_PLAYPAUSE'): [0xE8, 0],
    ('STOPCD', 'KEY_STOPCD'): [0xE9, 0],
    ('PREVIOUSSONG', 'KEY_PREVIOUSSONG'): [0xEA, 0],
    ('NEXTSONG', 'KEY_NEXTSONG'): [0xEB, 0],
    ('EJECTCD', 'KEY_EJECTCD'): [0xEC, 0],
    ('WWW', 'KEY_WWW'): [0xF0, 0],
    ('BACK', 'KEY_BACK'): [0xF1, 0],
    ('FORWARD', 'KEY_FORWARD'): [0xF2, 0],
    ('SCROLLUP', 'KEY_SCROLLUP'): [0xF5, 0],
    ('SCROLLDOWN', 'KEY_SCROLLDOWN'): [0xF6, 0],
    ('EDIT', 'KEY_EDIT'): [0xF7, 0]
}

script (the test script):
REM Script made specially for CodeReview users
REM Only works in Linux machines

DEFAULT_DELAY 500

INCLUDE <SoftwareSerial.h>

DEFINE TEXT "Hello"

LOOP // I only will put a comment on the loop

DELAY 3000

CONTROL ALT T

STRING echo "This is a demo"
ENTER

DELAY 3000

ALT F4

ARDUINO Serial.begin(9600);
ARDUINO Serial.println(TEXT);

REM Program end

I'm so new in Python, so please correct me. I'm sure that I'm missing some tricks. Also I want to make the program the most readable and structured possible.

To all those never heard of DuckyScript:

Ducky Script is the language of the USB Rubber Ducky. Writing scripts
  for can be done from any common ascii text editor such as Notepad, vi,
  emacs, nano, gedit, kedit, TextEdit, etc.

Quote from https://github.com/hak5darren/USB-Rubber-Ducky/wiki/Duckyscript

A:

Globals...
You freely modify 
delay = 0
cmdtype = 0
commands = ""
includes = ""
defines = ""
loop = ""

from anywhere, this can get real hard to keep track of, pass things as arguments and return instead.
Use multiline strings
    commands += 'int i=0;'
    commands += '\n'
    commands += 'for(i; i<={}; i++) {{'.format(options)
    commands += '\n'
    commands += '  {}'.format(last_command)
    commands += '\n'
    commands += '  delay({});'.format(delay)
    commands += '\n'
    commands += '}'
    commands += '\n'

Becomes:
commands = """\
int i = 0

for(i; ...
...

""".format(options, last_command, delay)

The benefit in readability is clear.

