mirror of
https://github.com/mruby/mruby
synced 2026-06-08 16:11:16 +00:00
mruby-regexp: add named captures (?<name>...)
support named capture groups in patterns:
- compiler parses (?<name>...) syntax and builds name table
- MatchData#[:name] and MatchData#["name"] access by name
- MatchData#named_captures returns {name => value} hash
- Regexp#named_captures returns {name => group_number} hash
- named captures stored in mrb_regexp_pattern for GC safety
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,13 @@ typedef struct {
|
||||
mrb_bool utf8_any; /* match any non-ASCII byte if true */
|
||||
} re_charclass;
|
||||
|
||||
/* Named capture entry */
|
||||
typedef struct {
|
||||
const char *name;
|
||||
uint16_t name_len;
|
||||
uint16_t group;
|
||||
} re_named_capture;
|
||||
|
||||
/* Compiled regexp pattern */
|
||||
typedef struct mrb_regexp_pattern {
|
||||
re_inst *code; /* bytecode array */
|
||||
@@ -55,6 +62,8 @@ typedef struct mrb_regexp_pattern {
|
||||
uint16_t num_classes;
|
||||
uint16_t num_captures; /* number of capture groups (including group 0) */
|
||||
uint32_t flags;
|
||||
re_named_capture *named_captures;
|
||||
uint16_t num_named;
|
||||
mrb_bool has_backref; /* true if pattern uses \1-\9 */
|
||||
mrb_bool has_nongreedy; /* true if pattern uses *?, +?, ?? */
|
||||
} mrb_regexp_pattern;
|
||||
|
||||
@@ -3,14 +3,24 @@ class Regexp
|
||||
new(pattern, *args)
|
||||
end
|
||||
|
||||
# Return named captures hash: {"name" => group_number, ...}
|
||||
def named_captures
|
||||
@named_captures || {}
|
||||
end
|
||||
|
||||
def options
|
||||
@flags.to_i
|
||||
end
|
||||
|
||||
# $1-$9 convenience methods via $~
|
||||
def self.last_match(n = nil)
|
||||
md = $~
|
||||
return md if n.nil?
|
||||
md ? md[n] : nil
|
||||
end
|
||||
|
||||
# named capture info is set via C create_matchdata
|
||||
end
|
||||
|
||||
class MatchData
|
||||
# named_captures is implemented in C via md->regexp
|
||||
end
|
||||
|
||||
@@ -25,6 +25,8 @@ typedef struct {
|
||||
uint16_t class_capa;
|
||||
uint16_t num_captures;
|
||||
uint32_t flags;
|
||||
re_named_capture *named_captures;
|
||||
uint16_t num_named;
|
||||
mrb_bool has_backref;
|
||||
mrb_bool has_nongreedy;
|
||||
} re_compiler;
|
||||
@@ -289,9 +291,22 @@ compile_atom(re_compiler *c)
|
||||
next_char(c);
|
||||
mrb_bool capturing = TRUE;
|
||||
|
||||
if (peek(c) == '?' && c->p + 1 < c->src_end && c->p[1] == ':') {
|
||||
next_char(c); next_char(c); /* skip ?: */
|
||||
capturing = FALSE;
|
||||
const char *cap_name = NULL;
|
||||
uint16_t cap_name_len = 0;
|
||||
|
||||
if (peek(c) == '?' && c->p + 1 < c->src_end) {
|
||||
if (c->p[1] == ':') {
|
||||
next_char(c); next_char(c); /* skip ?: */
|
||||
capturing = FALSE;
|
||||
}
|
||||
else if (c->p[1] == '<' && c->p + 2 < c->src_end && c->p[2] != '=' && c->p[2] != '!') {
|
||||
next_char(c); next_char(c); /* skip ?< */
|
||||
cap_name = c->p;
|
||||
while (peek(c) != '>' && peek(c) >= 0) next_char(c);
|
||||
if (peek(c) != '>') compile_error(c, "unterminated named capture");
|
||||
cap_name_len = (uint16_t)(c->p - cap_name);
|
||||
next_char(c); /* skip > */
|
||||
}
|
||||
}
|
||||
|
||||
uint16_t group = 0;
|
||||
@@ -301,6 +316,15 @@ compile_atom(re_compiler *c)
|
||||
}
|
||||
group = c->num_captures++;
|
||||
emit(c, RE_SAVE, 0, group * 2);
|
||||
if (cap_name) {
|
||||
/* register named capture */
|
||||
c->named_captures = (re_named_capture*)mrb_realloc(c->mrb, c->named_captures,
|
||||
sizeof(re_named_capture) * (c->num_named + 1));
|
||||
c->named_captures[c->num_named].name = cap_name;
|
||||
c->named_captures[c->num_named].name_len = cap_name_len;
|
||||
c->named_captures[c->num_named].group = group;
|
||||
c->num_named++;
|
||||
}
|
||||
}
|
||||
|
||||
compile_alt(c);
|
||||
@@ -601,6 +625,8 @@ re_compile(mrb_state *mrb, const char *pattern, mrb_int len, uint32_t flags)
|
||||
pat->num_classes = c.num_classes;
|
||||
pat->num_captures = c.num_captures;
|
||||
pat->flags = flags;
|
||||
pat->named_captures = c.named_captures;
|
||||
pat->num_named = c.num_named;
|
||||
pat->has_backref = c.has_backref;
|
||||
pat->has_nongreedy = c.has_nongreedy;
|
||||
|
||||
@@ -613,6 +639,7 @@ re_free(mrb_state *mrb, mrb_regexp_pattern *pat)
|
||||
if (pat) {
|
||||
mrb_free(mrb, pat->code);
|
||||
mrb_free(mrb, pat->classes);
|
||||
mrb_free(mrb, pat->named_captures);
|
||||
mrb_free(mrb, pat);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <mruby/string.h>
|
||||
#include <mruby/array.h>
|
||||
#include <mruby/variable.h>
|
||||
#include <mruby/hash.h>
|
||||
#include <mruby/error.h>
|
||||
#include "re_internal.h"
|
||||
|
||||
@@ -25,6 +26,7 @@ static const struct mrb_data_type regexp_type = { "Regexp", regexp_free };
|
||||
/* MatchData */
|
||||
typedef struct {
|
||||
mrb_value source; /* source string */
|
||||
mrb_value regexp; /* Regexp object (for named captures) */
|
||||
int *captures; /* capture positions [start0,end0,start1,end1,...] */
|
||||
int num_captures; /* number of capture groups (including 0) */
|
||||
} mrb_match_data;
|
||||
@@ -89,22 +91,32 @@ regexp_init(mrb_state *mrb, mrb_value self)
|
||||
mrb_iv_set(mrb, self, mrb_intern_lit(mrb, "@source"), pattern);
|
||||
mrb_iv_set(mrb, self, mrb_intern_lit(mrb, "@flags"), mrb_int_value(mrb, (mrb_int)flags));
|
||||
|
||||
/* store named captures as hash */
|
||||
if (pat->num_named > 0) {
|
||||
mrb_value nc = mrb_hash_new_capa(mrb, pat->num_named);
|
||||
for (uint16_t i = 0; i < pat->num_named; i++) {
|
||||
mrb_value name = mrb_str_new(mrb, pat->named_captures[i].name, pat->named_captures[i].name_len);
|
||||
mrb_hash_set(mrb, nc, name, mrb_fixnum_value(pat->named_captures[i].group));
|
||||
}
|
||||
mrb_iv_set(mrb, self, mrb_intern_lit(mrb, "@named_captures"), nc);
|
||||
}
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
/* Create MatchData from captures */
|
||||
static mrb_value
|
||||
create_matchdata(mrb_state *mrb, mrb_value str, int *captures, int ncap)
|
||||
create_matchdata(mrb_state *mrb, mrb_value regexp, mrb_value str, int *captures, int ncap)
|
||||
{
|
||||
struct RClass *md_class = mrb_class_get(mrb, "MatchData");
|
||||
mrb_match_data *md = (mrb_match_data*)mrb_malloc(mrb, sizeof(mrb_match_data));
|
||||
md->source = str;
|
||||
md->regexp = regexp;
|
||||
md->num_captures = ncap / 2;
|
||||
md->captures = (int*)mrb_malloc(mrb, sizeof(int) * ncap);
|
||||
memcpy(md->captures, captures, sizeof(int) * ncap);
|
||||
|
||||
mrb_value obj = mrb_obj_value(mrb_data_object_alloc(mrb, md_class, md, &matchdata_type));
|
||||
/* store in $~ */
|
||||
mrb_gv_set(mrb, mrb_intern_lit(mrb, "$~"), obj);
|
||||
return obj;
|
||||
}
|
||||
@@ -133,7 +145,7 @@ regexp_match(mrb_state *mrb, mrb_value self)
|
||||
return mrb_nil_value();
|
||||
}
|
||||
|
||||
return create_matchdata(mrb, str, captures, pat->num_captures * 2);
|
||||
return create_matchdata(mrb, self, str, captures, pat->num_captures * 2);
|
||||
}
|
||||
|
||||
/*
|
||||
@@ -179,7 +191,7 @@ regexp_match_op(mrb_state *mrb, mrb_value self)
|
||||
mrb_gv_set(mrb, mrb_intern_lit(mrb, "$~"), mrb_nil_value());
|
||||
return mrb_nil_value();
|
||||
}
|
||||
create_matchdata(mrb, str, captures, pat->num_captures * 2);
|
||||
create_matchdata(mrb, self, str, captures, pat->num_captures * 2);
|
||||
return mrb_int_value(mrb, captures[0]);
|
||||
}
|
||||
|
||||
@@ -266,12 +278,45 @@ regexp_escape(mrb_state *mrb, mrb_value self)
|
||||
static mrb_value
|
||||
matchdata_aref(mrb_state *mrb, mrb_value self)
|
||||
{
|
||||
mrb_int idx;
|
||||
mrb_get_args(mrb, "i", &idx);
|
||||
mrb_value arg;
|
||||
mrb_get_args(mrb, "o", &arg);
|
||||
|
||||
mrb_match_data *md = DATA_GET_PTR(mrb, self, &matchdata_type, mrb_match_data);
|
||||
if (!md) return mrb_nil_value();
|
||||
|
||||
mrb_int idx;
|
||||
if (mrb_string_p(arg) || mrb_symbol_p(arg)) {
|
||||
/* named capture access */
|
||||
const char *name;
|
||||
mrb_int name_len;
|
||||
if (mrb_symbol_p(arg)) {
|
||||
name = mrb_sym_name_len(mrb, mrb_symbol(arg), &name_len);
|
||||
}
|
||||
else {
|
||||
name = RSTRING_PTR(arg);
|
||||
name_len = RSTRING_LEN(arg);
|
||||
}
|
||||
/* look up name in regexp's named captures */
|
||||
mrb_regexp_pattern *pat = NULL;
|
||||
if (!mrb_nil_p(md->regexp)) {
|
||||
pat = DATA_GET_PTR(mrb, md->regexp, ®exp_type, mrb_regexp_pattern);
|
||||
}
|
||||
if (pat) {
|
||||
for (uint16_t i = 0; i < pat->num_named; i++) {
|
||||
if (pat->named_captures[i].name_len == (uint16_t)name_len &&
|
||||
memcmp(pat->named_captures[i].name, name, name_len) == 0) {
|
||||
idx = pat->named_captures[i].group;
|
||||
goto found;
|
||||
}
|
||||
}
|
||||
}
|
||||
return mrb_nil_value();
|
||||
}
|
||||
else {
|
||||
idx = mrb_as_int(mrb, arg);
|
||||
}
|
||||
|
||||
found:
|
||||
if (idx < 0 || idx >= md->num_captures) return mrb_nil_value();
|
||||
int start = md->captures[idx * 2];
|
||||
int end = md->captures[idx * 2 + 1];
|
||||
@@ -386,6 +431,36 @@ matchdata_length(mrb_state *mrb, mrb_value self)
|
||||
return mrb_fixnum_value(md->num_captures);
|
||||
}
|
||||
|
||||
/*
|
||||
* MatchData#named_captures
|
||||
*/
|
||||
static mrb_value
|
||||
matchdata_named_captures(mrb_state *mrb, mrb_value self)
|
||||
{
|
||||
mrb_match_data *md = DATA_GET_PTR(mrb, self, &matchdata_type, mrb_match_data);
|
||||
if (!md) return mrb_hash_new(mrb);
|
||||
|
||||
mrb_regexp_pattern *pat = NULL;
|
||||
if (!mrb_nil_p(md->regexp)) {
|
||||
pat = DATA_GET_PTR(mrb, md->regexp, ®exp_type, mrb_regexp_pattern);
|
||||
}
|
||||
if (!pat || pat->num_named == 0) return mrb_hash_new(mrb);
|
||||
|
||||
mrb_value result = mrb_hash_new_capa(mrb, pat->num_named);
|
||||
for (uint16_t i = 0; i < pat->num_named; i++) {
|
||||
mrb_value name = mrb_str_new(mrb, pat->named_captures[i].name, pat->named_captures[i].name_len);
|
||||
int group = pat->named_captures[i].group;
|
||||
mrb_value val = mrb_nil_value();
|
||||
if (group >= 0 && group < md->num_captures) {
|
||||
int s = md->captures[group * 2];
|
||||
int e = md->captures[group * 2 + 1];
|
||||
if (s >= 0) val = mrb_str_substr(mrb, md->source, s, e - s);
|
||||
}
|
||||
mrb_hash_set(mrb, result, name, val);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/* --- Gem init --- */
|
||||
|
||||
void
|
||||
@@ -427,6 +502,7 @@ mrb_mruby_regexp_gem_init(mrb_state *mrb)
|
||||
mrb_define_method(mrb, md, "end", matchdata_end, MRB_ARGS_REQ(1));
|
||||
mrb_define_method(mrb, md, "pre_match", matchdata_pre, MRB_ARGS_NONE());
|
||||
mrb_define_method(mrb, md, "post_match", matchdata_post, MRB_ARGS_NONE());
|
||||
mrb_define_method(mrb, md, "named_captures", matchdata_named_captures, MRB_ARGS_NONE());
|
||||
}
|
||||
|
||||
void
|
||||
|
||||
@@ -234,3 +234,18 @@ end
|
||||
assert("Regexp - backreference no match") do
|
||||
assert_nil /(\w+) \1/.match("hello world")
|
||||
end
|
||||
|
||||
assert("Regexp - named captures") do
|
||||
md = /(?<year>\d+)-(?<month>\d+)-(?<day>\d+)/.match("2026-03-21")
|
||||
assert_equal "2026", md[:year]
|
||||
assert_equal "03", md[:month]
|
||||
assert_equal "21", md[:day]
|
||||
assert_equal "2026", md["year"]
|
||||
end
|
||||
|
||||
assert("MatchData#named_captures") do
|
||||
md = /(?<a>\w+)@(?<b>\w+)/.match("user@host")
|
||||
nc = md.named_captures
|
||||
assert_equal "user", nc["a"]
|
||||
assert_equal "host", nc["b"]
|
||||
end
|
||||
|
||||
Reference in New Issue
Block a user