Regex: захват только подходящей альтернативы в Ruby

Я ломаю голову от этого: Я создаю простой генератор паролей, который использует шаблон, созданный пользователем. Шаблон содержит буквы a-f, которые обозначают 6 групп символов:

Итак, если пользователь введет ababcd, он/она получит что-то вроде abce1! Шаблон Ababababababccd будет генерировать что-то вроде Robuxejiqu23#. Поскольку Ababababababccd кажется трудночитаемым, я хочу дать возможность сократить его повторениями: Abababababccd можно было бы записать как Ab(ab){5}ccd или даже Ab(ab){5}c{2}d (что было бы глупо, но для полноты картины). [ab] вступает в игру, если вы хотите иметь персонажа из группы a или группы b.

Я «построил» подходящее выражение Regex, которое фиксирует повторения в следующих комбинациях:

  1. повторение одного символа, например a{3}, превратится в aaa
  2. групповое повторение (ab){2}будет abab
  3. Повторение группы XOR [ab]{2} становится либо ab, либо aa, либо ba и т. д.
  4. комбинации 2 и 3: (a[bc]d){n}

Выражение регулярного выражения, которое я построил до сих пор, находит все вышеперечисленные случаи: /(?:(\[[abcdef]+\])|\(([\[\]abcdef]+)\)|([abcdef]))\{(\d+)\}/i

Мой рабочий код Ruby выглядит так, но хотелось бы найти более элегантное решение:

# class
REPETITION = /(?:(\[[abcdef]+\])|\(([\[\]abcdef]+)\)|([abcdef]))\{(\d+)\}/i
GROUPS = /\[([abcdefxyz]+)\]/i;

# method generate
pattern = params[:pattern]
group = {
  'A' => params[:group_a].upcase,
  'a' => params[:group_a],
  'B' => params[:group_b].upcase,
  'b' => params[:group_b],
  'C' => params[:group_c].upcase,
  'c' => params[:group_c],
  'D' => params[:group_d].upcase,
  'd' => params[:group_d],
  'E' => params[:group_e].upcase,
  'e' => params[:group_e],
  'F' => params[:group_f].upcase,
  'f' => params[:group_f]
}

# Evaluate repetitions: ...{n}
if pattern =~ REPETITION
  pattern.gsub!(REPETITION) do
    match = $1 != nil ? $1 : $2 != nil ? $2 : $3
    count=$4.to_i
    expanded = ""
    count.times do
      expanded+=match
    end
    expanded
  end
end

# Evaluate character groups [...]
if pattern =~ GROUPS
  pattern.gsub!(GROUPS) do
    $1[rand($1.length)]
  end
end

# Evaluate the final pattern (repetitions and []-groups processed)
password = ""
pattern.each_char do |c|
  password+=group[c][rand(group[c].length)]
end
@password=password;

Моя идея состоит в том, чтобы найти все вхождения и заменить повторения расширенными повторениями, чтобы Ab(ab){5}ccd стало Ababababababccd, которое я потом обрабатываю.

Я предполагаю, что .gsub обрабатывает каждое событие индивидуально, и я могу заменить каждое совпадение подходящим шаблоном и счетчиком. Если gsub попытается заменить их все сразу, это будет неправильный метод.

Для меня не имеет значения, какая из групп в (?:a|b|c){count} соответствует, но совпадение должно быть возвращено как $1, а количество — как $2.

Приведенное выше регулярное выражение приводит к совпадениям $1, $2, $3 и $4. 4 доллара — это всегда мой счет повторений. Но тогда мне нужно выяснить, какие $1 .. $3 не являются nil, и использовать их для расширения. Я мог бы использовать «дешевый» корпус, а если и чехлы, то я хочу иметь элегантное решение.

В regex101 я заставил его работать (?|, но Руби этого не понимает.

Надеюсь, понятно, чего я хочу!?

В Typescript и C# у меня все заработало, и теперь я хочу, чтобы оно работало в Ruby... :-)

«Надеюсь, понятно, чего я хочу!?» - не совсем. Вы пытаетесь создать регулярное выражение, соответствующее другим регулярным выражениям?

Stefan 02.05.2024 13:37

Да, в каком-то смысле... В качестве вдохновения я использовал шаблон генератора паролей Keepass.

El Hippo 02.05.2024 13:38

Вы уверены, что хотите создавать пароли на основе шаблонов? Кажется, это плохая идея

Thomas 02.05.2024 13:55

@Stefan Стефан, я только что отредактировал свой вопрос, надеюсь, теперь он стал более понятным. :-)

El Hippo 02.05.2024 14:24

@Thomas Да, вы не ошибаетесь, но я часто создаю пароли для пользователей, которые должны быть произносимыми, и здесь вступает в силу мой шаблон. :-)

El Hippo 02.05.2024 14:26

В «моей системе» я мог бы написать [abcdef]{16} и получить совершенно случайный пароль.

El Hippo 02.05.2024 14:27

@engineersmnky Я соответствующим образом изменил пример, спасибо :-)

El Hippo 02.05.2024 16:00

@engineersmnky Я прочитал ваше предложение, но мне, как новичку в Ruby, показалось оно нелогичным. Это элегантно, без вопросов, но я боюсь, что если я посмотрю на это позже, то воскликну: «Что за черт!?»

El Hippo 03.05.2024 13:28
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
8
130
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Ответ принят как подходящий

Вот что я придумал для решения предоставленных случаев.

REPETITION = /(\[[abcdef]+\]|\([\[\]abcdef]+\)|[abcdef])(?:\{(\d+)\})?/i

GROUPS = {
  'a' => 'bcdfghjklmnpqrstvwxyz',
  'b' => 'aeiou',
  'c' => '0123456789',
  'd' => '!$%&/()=?*+#_.,:;_'
}.then {|h| h.merge(h.map {|k,v| [k.upcase,v.upcase]}.to_h)}

def expand(str)
  str.scan(REPETITION).map do |group, count|
    sub_pattern = group.start_with?('(') ? group[/(?<=\()(.*)(?=\))/, 1] : group
    count ? sub_pattern * count.to_i : sub_pattern
  end.join.gsub(/\[.*?\]/) {|match| match[1..-2].chars.shuffle.first}
end 

def generate(str, groups=GROUPS)
  expanded = expand(str)
  puts "Expansion: #{expanded}"
  expanded.each_char.sum(""){|c| groups[c][rand(groups[c].length)]}
end 

Пример вывода:

patterns = ['(ab){2}','a{3}','[ab]{2}','(a[bc]d){16}','Ab(ab){5}ccd','Ab(ab){5}c{2}d']

patterns.each do |pattern|
  puts "-------Pattern: #{pattern}-------"
  puts "Generated: #{generate(pattern)}"
end

# -------Pattern: (ab){2}-------
# Expansion: abab
# Generated: niha
# -------Pattern: a{3}-------
# Expansion: aaa
# Generated: svh
# -------Pattern: [ab]{2}-------
# Expansion: ba
# Generated: ak
# -------Pattern: (a[bc]d){16}-------
# Expansion: abdacdabdabdabdabdabdacdacdacdacdabdabdacdabdabd
# Generated: lu+s5$cu%ye#re+mo%qu)s4?c1*h8(l5!ja*zu?g9_lu/ze!
# -------Pattern: Ab(ab){5}ccd-------
# Expansion: Ababababababccd
# Generated: Meqonicovala75!
# -------Pattern: Ab(ab){5}c{2}d-------
# Expansion: Ababababababccd
# Generated: Pezotuwegona98#

Мы сопоставляем повторения, расширяем их по количеству, а затем заменяем XOR, чтобы расширить весь шаблон.

Затем мы просто ищем каждый символ в хэше GROUPS и выбираем случайный элемент.

Примечание: это не обрабатывает другие случаи, такие как:

  • Группа внутри XOR Ab[a(bc)d]{12}
  • Повторение внутри группы, которое затем тиражируется Ab(ab{5}){4}
  • Вероятно, еще много

Вау, это впечатляет! Это также сложно проглотить, но я «расшифроваю» это и соответствующим образом изменю свой код. Спасибо большое за ваши старания!!!

El Hippo 03.05.2024 13:30

Хорошо, хотя часть GROUPS.then немного напоминает хвастовство ;) (без обид!), метод расширения действительно умен! Мне нравится функциональный подход. Еще раз спасибо!

El Hippo 03.05.2024 14:00

@ElHippo, без обид, вы просили об элегантности, что для меня приравнивается к минимализму, где это возможно, и эта реализация немного СУШИТ. Спасибо за публикацию настоящего рубинового вопроса, я определенно предпочитаю его всей магии рельсов.

engineersmnky 03.05.2024 14:52

Честно говоря, вопрос родился в контексте Rails... Я понимаю ваше понимание элегантности как минимализма, но существует тонкая грань между элегантностью и загадочностью кода. Итак, для меня элегантность означает использование языка, на котором написан код, чтобы выразить как можно больше с помощью как можно меньшего количества кода (как вы сказали), сохраняя при этом простоту и читабельность. :-)

El Hippo 03.05.2024 15:32

В итоге я использовал это решение:

REPETITION = /(\[[abcdef]+\]|\([\[\]abcdef]+\)|[abcdef])(?:\{(\d+)\})?/i

pattern = params[:pattern]
group = {
  'A' => params[:group_a].upcase,
  'a' => params[:group_a],
  'B' => params[:group_b].upcase,
  'b' => params[:group_b],
  'C' => params[:group_c].upcase,
  'c' => params[:group_c],
  'D' => params[:group_d].upcase,
  'd' => params[:group_d],
  'E' => params[:group_e].upcase,
  'e' => params[:group_e],
  'F' => params[:group_f].upcase,
  'f' => params[:group_f]
}

password = pattern.scan(REPETITION).map do |p, c|
  # Evaluate repetitions: ...{n}
  s = p.start_with?('(') ? p[1..-2] : p
  c ? [s] * c.to_i : [s]
end.flatten.map do |m|
  # Evaluate character groups [...]
  m.start_with?('[') ? m[1..-2].chars.sample : m
end.join.chars.map do |e|
  # Evaluate the final pattern (repetitions and []-groups processed)
  group[e][rand(group[e].length)]
end.join

@password = password

Он включает в себя магию @engineersmnky и делает отдельные шаги более понятными — по крайней мере, для меня.

Другие вопросы по теме