Techioz Blog

Regex: Ruby で一致する代替のみをキャプチャする

概要

私はこれで頭をかち割っています。 ユーザーが作成したパターンを使用する単純なパスワードジェネレーターを構築しています。パターンには、6 つの文字グループを表す文字 a ~ f が含まれています。

したがって、ユーザーが ababcd と入力すると、abce1 のような結果が得られます。 パターン Ababababababccd は Robuxejiqu23# のようなものを生成します。 「Ababababababccd」はほとんど読みにくいので、これを繰り返して省略できるようにしたいと思います。 Ababababababccd は、Ab(ab){5}ccd または Ab(ab){5}c{2}d と書くこともできます (これは愚かですが、完全を期すために)。 グループ a またはグループ b のいずれかのキャラクターを使用したい場合は、[ab] が機能します。

次の組み合わせで繰り返しをキャプチャする適切な正規表現を「構築」しました。

これまでに作成した正規表現では、上記のすべてのケースが検出されます。 /(?:([[abcdef]+])|(([[]abcdef]+))|([abcdef])){()}/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 を後で処理する Abababababab Ccd にすることです。

私の仮定は、.gsub は各出現を個別に処理し、各一致を適切なパターンとカウントに置き換えることができるということです。 gsub がそれらすべてを一度に置き換えようとすると、それは間違った方法になります。

私にとって、(?:a|b|c){count} 内のどのグループが一致するかは関係ありませんが、一致は として返され、カウントは として返される必要があります。

上記の正規表現は、 、 、および に一致します。は常に私の繰り返し回数です。しかし、その後、どれが nil ではないかを見つけて、それを使用して展開する必要があります。 「安価な」ケースやケースを使用することもできますが、エレガントな解決策が必要です。

regex101 では (?| で動作するようになりましたが、Ruby はそれを理解できません。

私が何を望んでいるのかが明確だといいのですが!?

Typescript と C# では動作するようになりましたが、Ruby でも動作させたいと考えています… :-)

解決策

提供されたケースを処理するために私が思いついたものは次のとおりです。

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 ハッシュから各文字を検索し、ランダムな要素を選択します。

注: これは、次のような他のケースには対応しません。