[ruby-dev:42237] Introducing "rb_scan_keyword_args()" (was Re: Re: Enhancing Numeric#step)
From:
"Akinori MUSHA" <knu@...>
Date:
2010-09-12 10:51:21 UTC
List:
ruby-dev #42237
At Thu, 9 Sep 2010 09:59:45 +0900,
matz wrote:
> でキーワード辞書を取れたり、
>
> rb_scan_keywords(kw, "by", &step, "to", &limit, NULL);
>
> で、キーワード辞書を分解したりするようなAPIはどうだろうかと
> 考えています。
少し考えてから、 rb_scan_keyword_args() というものを設計・実装
してみました。仕様は以下の差分中に(日本語も)あるので見てみて
ください。
改良点として、 fmt の頭に ':' を置くと、キーワード名として
const char * でなく ID を取るという機能を付けると、何度も呼び
出されることを考慮して効率のために rb_intern() の結果を取って
おくようなところでも使えるのかなと思っています。
まあ、そこはAPI仕様としては枝葉として、幹としての機能はこれで
いかがでしょうか。
diff --git a/README.EXT b/README.EXT
index c2b2d9d..2410595 100644
--- a/README.EXT
+++ b/README.EXT
@@ -1150,6 +1150,75 @@ means the corresponding captured argument(s) should be just dropped.
The number of given arguments, excluding an option hash or iterator
block, is returned.
+ rb_scan_keyword_args(VALUE hash, const char *fmt, ...)
+
+Retrieve keyword arguments from hash (Qnil is allowed and treated as
+empty hash) according to the format string and an arbitrary number of
+pairs of a keyword and a VALUE reference that follows, terminated by a
+single occurrence of NULL. Another VALUE reference may follow to
+capture unlisted keyword arguments as a hash.
+
+The format can be described in ABNF as follows:
+
+--
+scan-kw-arg-spec := [num-of-mandatory-args] how-to-deal-with-unlisted-spec
+
+how-to-deal-with-unlisted-spec := sym-for-capture / sym-for-raise / sym-for-warn
+
+num-of-mandatory-args := DIGIT ; The number of mandatory
+ ; keywords
+sym-for-capture := "*" ; Indicates that all the
+ ; keywords that are not on the
+ ; list should be captured as a
+ ; ruby hash.
+sym-for-raise := "!" ; Indicates that an
+ ; ArgumentError should be
+ ; raised if any keyword that
+ ; are not on the list is given.
+sym-for-warning := "" ; Indicates that a soft warning
+ ; should be emitted raised if
+ ; any keyword that are not on
+ ; the list is given.
+
+Typical usage of this function is as follows:
+
+ /*
+ * The keywords <x> and <y> are mandatory, and <color> is optional;
+ * If any other keyword arguments are given, an ArgumentError is raised.
+ */
+ rb_scan_keyword_args(opt, "2!", "x", &x, "y", &y, "color", &color, NULL);
+
+ /*
+ * The keywords <x> and <y> are mandatory, and <color> is optional;
+ * If other keyword arguments are given, they are packed into a hash
+ * and assigned to the variable rest.
+ */
+ rb_scan_keyword_args(opt, "2*", "x", &x, "y", &y, "color", &color, NULL, &rest);
+
+ /* Unknown parameters can just be dropped by passing a NULL. */
+ rb_scan_keyword_args(opt, "2*", "x", &x, "y", &y, "color", &color, NULL, NULL);
+
+ /*
+ * The keywords <x>, <y>, and <color> are all optional;
+ * If any other keyword arguments are given, a soft warning (only
+ * reported in verbose mode) is emitted.
+ */
+ rb_scan_keyword_args(opt, "0", "x", &x, "y", &y, "color", &color, NULL);
+
+For each optional keyword arguments that are not given, the
+corresponding VALUE reference is set to Qundef unlike rb_scan_args()
+which sets Qnil. This is because in rb_scan_args() distinction
+between nil and unspecified can be made by checking argc but in
+rb_scan_keyword_args() there is no concept of argc where hash is
+unordered.
+
+If you want to settle more than nine mandatory keyword arguments,
+split up the retrieval into several steps, i.e. retrieve the first
+nine arguments with "9*", apply "9*" to the captured rest to retrieve
+the next nine, and so on.
+
+--
+
** Invoking Ruby method
VALUE rb_funcall(VALUE recv, ID mid, int narg, ...)
diff --git a/README.EXT.ja b/README.EXT.ja
index 54ab449..4592c2a 100644
--- a/README.EXT.ja
+++ b/README.EXT.ja
@@ -1236,6 +1236,66 @@ sym-for-block-arg := "&" ;
返り値は与えられた引数の数です.オプションハッシュおよびイ
テレータブロックは数えません.
+rb_scan_keyword_args(VALUE hash, const char *fmt, ...)
+
+ ハッシュ値hash(Qnilも可; 空のハッシュと見なされる)からキー
+ ワード引数を指定されたフォーマットおよび以下に続くキーワー
+ ドとVALUEへの参照の組に従って取り出します.キーワードと
+ VALUEの組は任意個指定でき,単一のNULLで終端されます.最後
+ に,VALUEへの参照を1つ置くことができ,そこに列挙されていな
+ いキーワード引数のハッシュが取得されます.
+
+ このフォーマットは,ABNFで記述すると以下の通りです.
+
+--
+scan-kw-arg-spec := [num-of-mandatory-args] how-to-deal-with-unlisted-spec
+
+how-to-deal-with-unlisted-spec := sym-for-capture / sym-for-raise / sym-for-warn
+
+num-of-mandatory-args := DIGIT ; 必須キーワード引数の数
+sym-for-capture := "*" ; 列挙されていないキーワード引数は
+ ; ハッシュとして取得せよという指定
+sym-for-raise := "!" ; 列挙されていないキーワード引数が
+ ; 指定された場合にはArgumentErrorを
+ ; 発生せよという指定
+sym-for-warning := "" ; 列挙されていないキーワード引数が
+ ; 指定された場合にはwarning(verbose
+ ; モード時のみ)を出力せよという指定
+
+ 以下のように使います.
+
+ /*
+ * キーワード引数 <x> と <y> は必須,<color> は任意とする;
+ * それ以外のキーワード引数を指定すると ArgumentError が発生する.
+ */
+ rb_scan_keyword_args(opt, "2!", "x", &x, "y", &y, "color", &color, NULL);
+
+ /*
+ * キーワード引数 <x> と <y> は必須,<color> は任意とする;
+ * それ以外に指定されたキーワード引数はハッシュとしてrestに格納する.
+ */
+ rb_scan_keyword_args(opt, "2*", "x", &x, "y", &y, "color", &color, NULL, &rest);
+
+ /* &restの代わりにNULLを指定すれば,単に読み捨てることもできる. */
+ rb_scan_keyword_args(opt, "2*", "x", &x, "y", &y, "color", &color, NULL, NULL);
+
+ /*
+ * キーワード引数 <x>, <y>, <color> はすべて任意;
+ * それ以外のキーワード引数を指定するとwarningが出力される.
+ * (verboseモードのみ)
+ */
+ rb_scan_keyword_args(opt, "0", "x", &x, "y", &y, "color", &color, NULL);
+
+ 指定されなかった任意キーワード引数については,対応する変数
+ にはQundefがセットされる(Qnilがセットされるrb_scan_args()と
+ の違いに注意).これは,rb_scan_args()ではnilと無指定の区別
+ をargcから知ることができるが,ハッシュを扱う
+ rb_scan_keyword_args()ではそれができないためである.
+
+ なお、必須引数を10個以上設定した場合は,工程を複数回に分け
+ る.つまり,まず "9*" で最初の9個を取り出し,得られた「残り」
+ に対して "9*" を適用して次の9個を取り出し,といった具合.
+
** Rubyメソッド呼び出し
VALUE rb_funcall(VALUE recv, ID mid, int narg, ...)
diff --git a/dir.c b/dir.c
index f9867e4..fe4279b 100644
--- a/dir.c
+++ b/dir.c
@@ -382,26 +382,15 @@ dir_initialize(int argc, VALUE *argv, VALUE dir)
{
struct dir_data *dp;
rb_encoding *fsenc;
- VALUE dirname, opt;
- static VALUE sym_enc;
+ VALUE dirname, opt, enc;
- if (!sym_enc) {
- sym_enc = ID2SYM(rb_intern("encoding"));
- }
fsenc = rb_filesystem_encoding();
argc = rb_scan_args(argc, argv, "1:", &dirname, &opt);
+ rb_scan_keyword_args(opt, "0", "encoding", &enc, NULL);
- if (!NIL_P(opt)) {
- VALUE v, enc=Qnil;
-
- v = rb_hash_aref(opt, sym_enc);
- if (!NIL_P(v)) enc = v;
-
- if (!NIL_P(enc)) {
- fsenc = rb_to_encoding(enc);
- }
- }
+ if (enc != Qundef && enc != Qnil)
+ fsenc = rb_to_encoding(enc);
GlobPathValue(dirname, FALSE);
diff --git a/hash.c b/hash.c
index 73f9012..fb7509d 100644
--- a/hash.c
+++ b/hash.c
@@ -1949,6 +1949,153 @@ rb_hash_compare_by_id_p(VALUE hash)
return Qfalse;
}
+static const char *
+keyword_inspect(VALUE key)
+{
+ if (TYPE(key) == T_SYMBOL)
+ return rb_id2name(SYM2ID(key));
+
+ return RSTRING_PTR(rb_inspect(key));
+}
+
+void
+rb_scan_keyword_args(VALUE hash, const char *fmt, ...)
+{
+ const char *p = fmt;
+ const char *key;
+ va_list vargs;
+ int rest = 0;
+ int n_mand = 0, n_missing;
+ int i;
+ VALUE mhash;
+
+ if (ISDIGIT(*p)) {
+ n_mand = *p - '0';
+ p++;
+ }
+ switch (*p) {
+ case '*':
+ case '!':
+ rest = *p;
+ p++;
+ }
+ if (*p != '\0')
+ rb_fatal("bad format: %s", fmt);
+
+ if (NIL_P(hash)) {
+ va_start(vargs, fmt);
+
+ if (n_mand > 0) {
+ key = va_arg(vargs, const char *);
+ n_missing = n_mand;
+ missing:
+ if (n_missing == 1) {
+ va_end(vargs);
+ rb_raise(rb_eArgError, "missing keyword argument: <%s>", key);
+ }
+ else {
+ VALUE msg = rb_str_new2("missing keyword arguments:");
+ int i;
+
+ for (i = 0; i < n_missing; i++) {
+ if (i > 0) rb_str_buf_cat2(msg, ",");
+ rb_str_buf_cat2(msg, " <");
+ rb_str_buf_cat2(msg, key);
+ rb_str_buf_cat2(msg, ">");
+
+ (void)va_arg(vargs, VALUE *);
+ key = va_arg(vargs, const char *);
+ }
+
+ rb_raise(rb_eArgError, "%s", RSTRING_PTR(msg));
+ }
+ }
+ else {
+ int i;
+
+ for (i = 0; ; i++) {
+ VALUE *var;
+
+ key = va_arg(vargs, const char *);
+ if (!key) break;
+
+ var = va_arg(vargs, VALUE *);
+ if (var) *var = Qundef;
+ }
+ }
+ va_end(vargs);
+ return;
+ }
+
+ if (TYPE(hash) != T_HASH)
+ rb_fatal("invalid object given");
+
+ mhash = rb_hash_dup(hash);
+ va_start(vargs, fmt);
+
+ for (i = 0; ; i++) {
+ VALUE val, *var;
+
+ key = va_arg(vargs, const char *);
+
+ if (!key) {
+ if (i < n_mand) {
+ va_end(vargs);
+ rb_fatal("not enough keys given (%d for %d)", i, n_mand);
+ }
+ break;
+ }
+
+ val = rb_hash_delete_key(mhash, ID2SYM(rb_intern(key)));
+
+ if (val == Qundef && i < n_mand) {
+ n_missing = n_mand - i;
+ goto missing;
+ }
+
+ var = va_arg(vargs, VALUE *);
+ if (var) *var = val;
+ }
+
+ if (RHASH_EMPTY_P(mhash))
+ mhash = Qnil;
+
+ if (rest == '*') {
+ VALUE *var = va_arg(vargs, VALUE *);
+ if (var) *var = mhash;
+ }
+ else if (!NIL_P(mhash)) {
+ VALUE keys = rb_hash_keys(mhash);
+ VALUE msg;
+
+ va_end(vargs);
+
+ if (RARRAY_LEN(keys) == 1) {
+ msg = rb_sprintf("unknown keyword given: <%s>", keyword_inspect(RARRAY_PTR(keys)[0]));
+ }
+ else {
+ int i;
+
+ msg = rb_str_new2("unknown keywords given:");
+
+ for (i = 0; i < RARRAY_LEN(keys); i++) {
+ if (i > 0) rb_str_buf_cat2(msg, ",");
+ rb_str_buf_cat2(msg, " <");
+ rb_str_buf_cat2(msg, keyword_inspect(RARRAY_PTR(keys)[i]));
+ rb_str_buf_cat2(msg, ">");
+ }
+ }
+
+ if (rest == '!')
+ rb_raise(rb_eArgError, "%s", RSTRING_PTR(msg));
+ else
+ rb_warning("%s", RSTRING_PTR(msg));
+ }
+
+ va_end(vargs);
+}
+
+
static int path_tainted = -1;
static char **origenviron;
diff --git a/include/ruby/ruby.h b/include/ruby/ruby.h
index 028f4e9..5199879 100644
--- a/include/ruby/ruby.h
+++ b/include/ruby/ruby.h
@@ -1124,6 +1124,7 @@ VALUE rb_funcall(VALUE, ID, int, ...);
VALUE rb_funcall2(VALUE, ID, int, const VALUE*);
VALUE rb_funcall3(VALUE, ID, int, const VALUE*);
int rb_scan_args(int, const VALUE*, const char*, ...);
+void rb_scan_keyword_args(VALUE hash, const char *fmt, ...);
VALUE rb_call_super(int, const VALUE*);
VALUE rb_gv_set(const char*, VALUE);
--
Akinori MUSHA / http://akinori.org/