Macro that translates marlin params to klipper params

In the example for the M117 macro, the raw params are parsed in the macro itself to remove comments.

I did not want to parse arguments for every single marlin macro, so I wrote one that does the parsing:

# Translates marlin gcode arguments into klipper arguments.
#
# Usage:
#     _PARSE_MARLIN_PARAMS TARGET=[<macro>] ARGS='{rawparams}'
# Parameters:
# [<macro>] The name of the macro that should be called with the parsed arguments 
#
# Examples:
# ```
# [gcode_macro _T123]
# gcode:
#     { action_respond_info("_T123 called with: params={}, rawparams={}".format(params, rawparams)) }
#
# [gcode_macro T123]
# gcode:
#     _PARSE_MARLIN_PARAMS TARGET=_T123 ARGS='{rawparams}'
# ```
#
# Note:
# Klipper gcode_macros expect key value arguments like 'MY_MACRO KEY=VALUE'.
#
# The traditional gcode style 'MY_MACRO H123 B C' is not allowed for all macros.
# If 'MY_MACRO' does not have the correct format, klipper will complain about "malformed command".
# 
# The following must be satisfied (based on the current source code):
# - Starts with a character 'c' for which the python expression 'c.upper().isupper()' is True
#   (all characters 'A-Z' and 'a-z' satisfy this, numbers or special characters like '_' do not)
# - After that a number must follow
# - The format must be '<letter><float>' where <float> must start with a number and the python
#   expression 'float("<float>"")' must not fail
#
# I recommend that your macro has a name in the format '<letter><number>' like for example 'T123'.
#
# There is no boolean filter in jinja, so you can not write
# {% set FLAG = params.F|default(false)|boolean %}
#
# As a workaround, I recommend writing this instead:
# {% set FLAG = params.F|default(false)|lower == "true" %}
[gcode_macro _PARSE_MARLIN_PARAMS]
description: Translates marlin params into klipper params
gcode:
    {% set TARGET = params.TARGET|string %}
    {% set ARGS = params.ARGS|default("")|string %}

    # This parses the rawparams by first removing trailing comments with ; and '\x23' = '#', then splitting
    # the result by whitespace (to obtain a list of arguments)
    {% set parsed_params = ARGS.split(';', 1)[0].split('\x23', 1)[0].lower().split() %}

    # { action_respond_info("ARGS: {}, parsed: {}".format(ARGS, parsed_params)) }

    {% set macro_command = [ TARGET ] %}
    {% for x in parsed_params %}
        {% if x|length > 1 %}
            {% set _ = macro_command.append("{}={}".format(x[0:1], x[1:])) %}
        {% else %}
            # Flags are set to true
            {% set _ = macro_command.append("{}=true".format(x)) %}
        {% endif %}
    {% endfor %}

    # { action_respond_info("Command: {}".format(macro_command|join(' '))) }
    { macro_command|join(' ') }

This is especially useful for macros where an argument has a value like H<number>.

For example the gcode G12 accepts the following arguments

G12 [P<0|1|2>] [R<radius>] [S<count>] [T<count>] [X] [Y] [Z]

[gcode_macro _G12]
gcode:
    { action_respond_info("_G12 called with: params={}, rawparams={}".format(params, rawparams)) }
    # G12 [P<0|1|2>] [R<radius>] [S<count>] [T<count>] [X] [Y] [Z]

    {% set P = params.P|default(0)|int %}
    {% set R = params.R|default(0)|float %}
    {% set T = params.T|default(0)|int %}
    {% set S = params.S|default(0)|int %}

    {% set X = params.X|default(false)|lower == "true" %}
    {% set Y = params.Y|default(false)|lower == "true" %}
    {% set Z = params.Z|default(false)|lower == "true" %}

    { action_respond_info("_G12 P={} R={} T={} S={}  X={} Y={} Z={}".format(P, R, T, S, X, Y, Z)) }

[gcode_macro G12]
gcode:
    _PARSE_MARLIN_PARAMS TARGET=_G12 ARGS='{rawparams}'
$ G12 P1 R2.1 T3 S5 X Y
_G12 called with: params={'P': '1', 'R': '2.1', 'T': '3', 'S': '5', 'X': 'true', 'Y': 'true'}, rawparams=p=1 r=2.1 t=3 s=5 x=true y=true
_G12 P=1 R=2.1 T=3 S=5 X=True Y=True Z=False

$ G12
_G12 called with: params={}, rawparams=
_G12 P=0 R=0.0 T=0 S=0 X=False Y=False Z=False

Not having a boolean filter is a bit of a pain (especially when some might use 0 or 1 to indicate true/false). Jinja supports creating custom filters, but not inside templates: python - Embed custom filter definition into jinja2 template? - Stack Overflow

This would require a PR against the Klipper Repo, which I would be willing to do if it will be merged.