Пока что придумал только такое решение.
Описал интерпретацию правила через комментарии, чтобы много места здесь не занимало.
SECTION = or_(
rule(SECTION_NAME, FEATURE, FEATURE, FEATURE, FEATURE, FEATURE, FEATURE),
rule(SECTION_NAME, FEATURE, FEATURE, FEATURE, FEATURE, FEATURE),
rule(SECTION_NAME, FEATURE, FEATURE, FEATURE, FEATURE),
rule(SECTION_NAME, FEATURE, FEATURE, FEATURE),
rule(SECTION_NAME, FEATURE, FEATURE),
rule(SECTION_NAME, FEATURE)
)
# На SECTION используется interpretation(Node)
# SECTION_NAME - interpretation(Node.name)
# FEATURE - interpretation(Node.successors).repeatable()
В моём случае это более-менее работающее решение, поскольку у меня в одной секции почти не встречается больше шести свойств. Однако оно, конечно, не подойдёт для более неопределённых случаев.
P. S. Ближе к концу написания этого комментария вспомнил, что в cookbook'е yargy-парсера встречал упоминание генераторов правил вывода. Постараюсь разобраться с этими генераторами, и в редакции этого ответа приведу вариант с генератором.
UPD (25 апр.)
Написал функцию, с помощью которой правила, подобные указанному выше, описываются в несколько строк.
'''
Генерирует интерпретированную правую часть правил конечной рекурсии следующего вида.
left -> right + sep + left | right
right – повторяющееся в правой части правило.
right_interpretation – интерпретация для right.
sep – правило-разделитель последовательности из правил вида right.
max_recursion_depth – глубина рекурсии.
'''
def get_recursive_interpreted_right_part(right, right_interpretation=None, sep=None, max_recursion_depth=10):
if right_interpretation is not None:
right = rule(right.interpretation(right_interpretation).repeatable())
list_of_right_rules = []
for cur_len in reversed(range(1, max_recursion_depth+1)):
if sep is None:
right_rule_args = [right] * cur_len
else:
right_rule_args = [right, sep] * (cur_len - 1)
right_rule_args.append(right)
list_of_right_rules.append(rule(*right_rule_args))
return or_(*list_of_right_rules)
К примеру, вместо того, чтобы писать это:
FEATURE_BLOCK = or_(
rule(
FEATURE.interpretation(Node.successors).repeatable()
),
rule(
FEATURE.interpretation(Node.successors).repeatable(), EOL, FEATURE.interpretation(Node.successors).repeatable()
),
rule(
FEATURE.interpretation(Node.successors).repeatable(), EOL, FEATURE.interpretation(Node.successors).repeatable(), EOL, FEATURE.interpretation(Node.successors).repeatable()
)
)
Можно написать это:
FEATURE_BLOCK = get_recursive_interpreted_right_part(FEATURE, Node.successors, EOL, 3)
Это гораздо более удобный вариант, хотя всё ещё рассчитанный на известное максимальное количество повторений правой части. Для моей задачи этого более-менее достаточно.
Но я по-прежнему надеюсь, что просто пропустил какую-либо из возможностей yargy-парсера, с помощью которой можно провернуть такое из коробки. Буду рад увидеть такой вариант в ответах.
Также буду рад, если кто-то посчитает, что описанную мною функцию можно описать более лаконично без потери читаемости, и предложит более качественное её описание.