From: Akinori MUSHA Date: 2010-09-12T19:51:21+09:00 Subject: [ruby-dev:42237] Introducing "rb_scan_keyword_args()" (was Re: Re: Enhancing Numeric#step) --pgp-sign-Multipart_Sun_Sep_12_19:51:14_2010-1 Content-Type: text/plain; charset=ISO-2022-JP 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 and are mandatory, and 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 and are mandatory, and 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 , , and 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 + ; モード時のみ)を出力せよという指定 + + 以下のように使います. + + /* + * キーワード引数 と は必須, は任意とする; + * それ以外のキーワード引数を指定すると ArgumentError が発生する. + */ + rb_scan_keyword_args(opt, "2!", "x", &x, "y", &y, "color", &color, NULL); + + /* + * キーワード引数 と は必須, は任意とする; + * それ以外に指定されたキーワード引数はハッシュとして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); + + /* + * キーワード引数 , , はすべて任意; + * それ以外のキーワード引数を指定すると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/ --pgp-sign-Multipart_Sun_Sep_12_19:51:14_2010-1 Content-Type: application/pgp-signature Content-Transfer-Encoding: 7bit -----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.10 (FreeBSD) iEYEABECAAYFAkyMsKIACgkQkgvvx5/Z4e6jVwCg2XkMZe5dG2cfJCw1G2I7pZly SO0An0p0+mk7LY+PF32k3eWgE6oBpje6 =I2n2 -----END PGP SIGNATURE----- --pgp-sign-Multipart_Sun_Sep_12_19:51:14_2010-1--