web-dev-qa-db-ja.com

文字列をargv / argcに解析します

テキストがコマンドラインでアプリケーションに渡されたかのように、Cでテキストを解析してargvとargcの値を取得する方法はありますか?

これはWindowsで動作する必要はなく、Linuxで動作します。引数の引用についても気にしません。

30
codebox

Glibソリューションがあなたのケースに過剰である場合は、自分でコーディングすることを検討してください。

次に、次のことができます。

  • 文字列をスキャンし、そこにある引数の数を数えます(そしてあなたはあなたのargcを取得します)
  • char *の配列を割り当てます(argv用)
  • 文字列を再スキャンし、割り当てられた配列にポインタを割り当て、スペースを「\ 0」に置き換えます(引数を含む文字列を変更できない場合は、複製する必要があります)。
  • 割り当てたものを解放することを忘れないでください!

以下の図は、(うまくいけば)明確にする必要があります。

             aa bbb ccc "dd d" ee         <- original string

             aa0bbb0ccc00dd d00ee0        <- transformed string
             |  |   |    |     |
   argv[0] __/  /   /    /     /
   argv[1] ____/   /    /     /
   argv[2] _______/    /     /
   argv[3] ___________/     /
   argv[4] ________________/ 

可能なAPIは次のとおりです。

    char **parseargs(char *arguments, int *argc);
    void   freeparsedargs(char **argv);

Freeparsedargs()を安全に実装するには、追加の考慮事項が必要になります。

文字列が非常に長く、2回スキャンしたくない場合は、argv配列にさらに要素を割り当てる(必要に応じて再割り当てする)などの代替案を検討できます。

編集:提案されたソリューション(引用された引数を処理しません)。

    #include <stdio.h>

    static int setargs(char *args, char **argv)
    {
       int count = 0;

       while (isspace(*args)) ++args;
       while (*args) {
         if (argv) argv[count] = args;
         while (*args && !isspace(*args)) ++args;
         if (argv && *args) *args++ = '\0';
         while (isspace(*args)) ++args;
         count++;
       }
       return count;
    }

    char **parsedargs(char *args, int *argc)
    {
       char **argv = NULL;
       int    argn = 0;

       if (args && *args
        && (args = strdup(args))
        && (argn = setargs(args,NULL))
        && (argv = malloc((argn+1) * sizeof(char *)))) {
          *argv++ = args;
          argn = setargs(args,argv);
       }

       if (args && !argv) free(args);

       *argc = argn;
       return argv;
    }

    void freeparsedargs(char **argv)
    {
      if (argv) {
        free(argv[-1]);
        free(argv-1);
      } 
    }

    int main(int argc, char *argv[])
    {
      int i;
      char **av;
      int ac;
      char *as = NULL;

      if (argc > 1) as = argv[1];

      av = parsedargs(as,&ac);
      printf("== %d\n",ac);
      for (i = 0; i < ac; i++)
        printf("[%s]\n",av[i]);

      freeparsedargs(av);
      exit(0);
    }
11
Remo.D

標準のPOSIX機能を使用して最も簡単な答えを誰も提供していないことに驚きます。

http://www.opengroup.org/onlinepubs/9699919799/functions/wordexp.html

29
R..

これが私の貢献です。その素晴らしくて短いですが、注意すべき点は次のとおりです。

  • Strtokを使用すると、元の「commandLine」文字列が変更され、スペースが\ 0文字列終了区切り文字に置き換えられます
  • argv []は「commandLine」を指すようになるため、argv []で終了するまで変更しないでください。

コード:

enum { kMaxArgs = 64 };
int argc = 0;
char *argv[kMaxArgs];

char *p2 = strtok(commandLine, " ");
while (p2 && argc < kMaxArgs-1)
  {
    argv[argc++] = p2;
    p2 = strtok(0, " ");
  }
argv[argc] = 0;

Argcとargvを使用するか、「foo(int argc、char ** argv)」のように宣言された他の関数に渡すことができます。

15
sstteevvee

常に素晴らしい glib にはg_Shell_parse_args()があり、あなたが求めているように聞こえます。

引用すらしたくない場合は、やり過ぎかもしれません。空白をトークン文字として使用して、トークン化するだけです。それを行うための簡単なルーチンを書くことは、本当に長くはかからないはずです。

あなたがメモリに極端にこだわっていない場合は、再割り当てなしでワンパスでそれを行うのは簡単です。 1文字おきに最悪の場合はスペースであると想定するだけで、n文字の文字列には最大で(n + 1) / 2引数、および(もちろん)最大nバイトの引数テキスト(ターミネーターを除く)。

9
unwind

これは、WindowsとUnixの両方のソリューションです(Linux、OSX、Windowsでテスト済み)。 Valgrind および Dr。Memory でテストされています。

POSIXシステムでは wordexp を使用し、Windowsでは CommandLineToArgvW を使用します。

Windowsソリューションの場合、ほとんどのコードはchar **およびwchar_t **利用可能なCommandLineToArgvAがないため、美しいWin32 APIを使用します(ANSIバージョン)。

#ifdef _WIN32
#include <windows.h>
#else
#include <wordexp.h>
#endif

char **split_commandline(const char *cmdline, int *argc)
{
    int i;
    char **argv = NULL;
    assert(argc);

    if (!cmdline)
    {
        return NULL;
    }

    // Posix.
    #ifndef _WIN32
    {
        wordexp_t p;

        // Note! This expands Shell variables.
        if (wordexp(cmdline, &p, 0))
        {
            return NULL;
        }

        *argc = p.we_wordc;

        if (!(argv = calloc(*argc, sizeof(char *))))
        {
            goto fail;
        }

        for (i = 0; i < p.we_wordc; i++)
        {
            if (!(argv[i] = strdup(p.we_wordv[i])))
            {
                goto fail;
            }
        }

        wordfree(&p);

        return argv;
    fail:
        wordfree(&p);
    }
    #else // WIN32
    {
        wchar_t **wargs = NULL;
        size_t needed = 0;
        wchar_t *cmdlinew = NULL;
        size_t len = strlen(cmdline) + 1;

        if (!(cmdlinew = calloc(len, sizeof(wchar_t))))
            goto fail;

        if (!MultiByteToWideChar(CP_ACP, 0, cmdline, -1, cmdlinew, len))
            goto fail;

        if (!(wargs = CommandLineToArgvW(cmdlinew, argc)))
            goto fail;

        if (!(argv = calloc(*argc, sizeof(char *))))
            goto fail;

        // Convert from wchar_t * to ANSI char *
        for (i = 0; i < *argc; i++)
        {
            // Get the size needed for the target buffer.
            // CP_ACP = Ansi Codepage.
            needed = WideCharToMultiByte(CP_ACP, 0, wargs[i], -1,
                                        NULL, 0, NULL, NULL);

            if (!(argv[i] = malloc(needed)))
                goto fail;

            // Do the conversion.
            needed = WideCharToMultiByte(CP_ACP, 0, wargs[i], -1,
                                        argv[i], needed, NULL, NULL);
        }

        if (wargs) LocalFree(wargs);
        if (cmdlinew) free(cmdlinew);
        return argv;

    fail:
        if (wargs) LocalFree(wargs);
        if (cmdlinew) free(cmdlinew);
    }
    #endif // WIN32

    if (argv)
    {
        for (i = 0; i < *argc; i++)
        {
            if (argv[i])
            {
                free(argv[i]);
            }
        }

        free(argv);
    }

    return NULL;
}
7
Joakim

私はプレーンCの埋め込みプロジェクトに対してこれを実行しました。シリアルポート入力を解析し、パラメーター付きの限られたコマンドセットを実行する小さなCLIがあります。

これはおそらく最も近いものではありませんが、私が得ることができるほど小さくて効率的です:

int makeargs(char *args, int *argc, char ***aa) {
    char *buf = strdup(args);
    int c = 1;
    char *delim;
    char **argv = calloc(c, sizeof (char *));

    argv[0] = buf;

    while (delim = strchr(argv[c - 1], ' ')) {
        argv = realloc(argv, (c + 1) * sizeof (char *));
        argv[c] = delim + 1;
        *delim = 0x00;
        c++;
    }

    *argc = c;
    *aa = argv;

    return c;
}

テストする:

int main(void) {
    char **myargs;
    int argc;

    int numargs = makeargs("Hello world, this is a test", &argc, &myargs);
    while (numargs) {
        printf("%s\r\n", myargs[argc - numargs--]);
    };

    return (EXIT_SUCCESS);
}
3
Andi

Matt Peitrekの [〜#〜] libtinyc [〜#〜] には、argcargv.cppと呼ばれるモジュールがあり、文字列を受け取り、引用符で囲まれた引数を考慮して引数配列に解析します。これはWindows固有ですが、非常にシンプルなので、必要なプラットフォームに簡単に移動できます。

2
Michael Burr

私はこれを自分で行うための関数を作成しましたが、あまり良いとは思いませんが、それは私の目的のために機能します-将来これを必要とする人のために改善を提案してください:

void parseCommandLine(char* cmdLineTxt, char*** argv, int* argc){
    int count = 1;

    char *cmdLineCopy = strdupa(cmdLineTxt);
    char* match = strtok(cmdLineCopy, " ");
 // First, count the number of arguments
    while(match != NULL){
        count++;
        match = strtok(NULL, " ");
    }

    *argv = malloc(sizeof(char*) * (count+1));
    (*argv)[count] = 0;
    **argv = strdup("test"); // The program name would normally go in here

    if (count > 1){
        int i=1;
        cmdLineCopy = strdupa(cmdLineTxt);
        match = strtok(cmdLineCopy, " ");
        do{
            (*argv)[i++] = strdup(match);
            match = strtok(NULL, " ");
        } while(match != NULL);
     }

    *argc = count;
}
2
codebox

さらに別の実装を検討してください。 実行

#include <cctype>  // <ctype.h>  for isspace()

/** 
 * Parse out the next non-space Word from a string.
 * @note No nullptr protection
 * @param str  [IN]   Pointer to pointer to the string. Nested pointer to string will be changed.
 * @param Word [OUT]  Pointer to pointer of next Word. To be filled.
 * @return  pointer to string - current cursor. Check it for '\0' to stop calling this function   
 */
static char* splitArgv(char **str, char **Word)
{
    constexpr char QUOTE = '\'';
    bool inquotes = false;

    // optimization
    if( **str == 0 )
        return NULL;

    // Skip leading spaces.
    while (**str && isspace(**str)) 
        (*str)++;

    if( **str == '\0')
        return NULL;

    // Phrase in quotes is one arg
    if( **str == QUOTE ){
        (*str)++;
        inquotes = true;
    }

    // Set phrase begining
    *Word = *str;

    // Skip all chars if in quotes
    if( inquotes ){
        while( **str && **str!=QUOTE )
            (*str)++;
        //if( **str!= QUOTE )
    }else{
        // Skip non-space characters.
        while( **str && !isspace(**str) )
            (*str)++;
    }
    // Null terminate the phrase and set `str` pointer to next symbol
    if(**str)
        *(*str)++ = '\0';

    return *str;
}


/// To support standart convetion last `argv[argc]` will be set to `NULL`
///\param[IN]  str : Input string. Will be changed - splitted to substrings
///\param[IN]  argc_MAX : Maximum a rgc, in other words size of input array \p argv
///\param[OUT] argc : Number of arguments to be filled
///\param[OUT] argv : Array of c-string pointers to be filled. All of these strings are substrings of \p str
///\return Pointer to the rest of string. Check if for '\0' and know if there is still something to parse. \
///        If result !='\0' then \p argc_MAX is too small to parse all. 
char* parseStrToArgcArgvInsitu( char *str, const int argc_MAX, int *argc, char* argv[] )
{
    *argc = 0;
    while( *argc<argc_MAX-1  &&  splitArgv(&str, &argv[*argc]) ){
        ++(*argc);
        if( *str == '\0' )
            break;
    }
    argv[*argc] = nullptr;
    return str;
};

使用法コード

#include <iostream>
using namespace std;

void parseAndPrintOneString(char *input)
{
    constexpr size_t argc_MAX = 5;
    char* v[argc_MAX] = {0};
    int c=0;

    char* rest = parseStrToArgcArgvInsitu(input,argc_MAX,&c,v);
    if( *rest!='\0' )  // or more clear `strlen(rest)==0` but not efficient
        cout<<"There is still something to parse. argc_MAX is too small."<<endl;

    cout << "argc : "<< c << endl;
    for( int i=0; i<c; i++ )
        cout<<"argv["<<i<<"] : "<<v[i] <<endl;
    /*//or condition is `v[i]`
    for( int i=0; v[i]; i++ )
        cout<<"argv["<<i<<"] : "<<v[i] <<endl;*/
}



int main(int argc, char* argv[])
{
    char inputs[][500] ={
              "Just another TEST\r\n"
            , "  Hello my world 'in quotes' \t !"
            , "./hi 'Less is more'"
            , "Very long line with \"double quotes\" should be parsed several times if argv[] buffer is small"
            , "   \t\f \r\n"
    };

    for( int i=0; i<5; ++i ){
        cout<<"Parsing line \""<<inputs[i]<<"\":"<<endl;
        parseAndPrintOneString(inputs[i]);
        cout<<endl;
    }
}

出力:

Parsing line "Just another TEST\r\n":
argc : 3
argv[0] : Just
argv[1] : another
argv[2] : TEST

Parsing line "  Hello my world 'in quotes'   !":
There is still something to parse. argc_MAX is too small.
argc : 4
argv[0] : Hello
argv[1] : my
argv[2] : world
argv[3] : in quotes

Parsing line "./hi 'Less is more'":
argc : 2
argv[0] : ./hi
argv[1] : Less is more

Parsing line "Very long line with "double quotes" should be parsed several times if argv[] buffer is small":
There is still something to parse. argc_MAX is too small.
argc : 4
argv[0] : Very
argv[1] : long
argv[2] : line
argv[3] : with

Parsing line "       

":
argc : 0
1
kyb

私が書いたものは引用符も考慮します(ただしネストされていません)

貢献してください。

/*
Tokenize string considering also quotes.
By Zibri <zibri AT zibri DOT org>
https://github.com/Zibri/tokenize
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>

int main(int argc, char *argv[])
{
  char *str1, *token;
  int j;
  char *qstart = NULL;
  bool quoted = false;

  if (argc != 2) {
    fprintf(stderr, "Usage: %s string\n", argv[0]);
    exit(EXIT_FAILURE);
  }

  for (j = 1, str1 = argv[1];; j++, str1 = NULL) {
    token = strtok(str1, " ");
    if (token == NULL)
      break;
    if ((token[0] == 0x27) || (token[0] == 0x22)) {
      qstart = token + 1;
      quoted = true;
    }
    if ((token[strlen(token) - 1] == 0x27) || (token[strlen(token) - 1] == 0x22)) {
      quoted = false;
      token[strlen(token) - 1] = 0;
      printf("%d: %s\n", j, qstart);
    } else {
      if (quoted) {
        token[strlen(token)] = 0x20;
        j--;
      } else
        printf("%d: %s\n", j, token);
    }
  }

  if (quoted) {
    fprintf(stderr, "String quoting error\n");
    return EXIT_FAILURE;
  } else
    return EXIT_SUCCESS;
}

出力例:

$ ./tokenize "1 2 3 '4 5 6' 7 8 \"test abc\" 10 11"
1: 1
2: 2
3: 3
4: 4 5 6
5: 7
6: 8
7: test abc
8: 10
9: 11
1
Zibri

動的メモリ割り当てを使用したくない場合の解決策(例:埋め込み)

コマンド文字列をargcおよびargv形式にトークン化するための基礎としてtokenise_to_argc_argv()を使用する組み込みプロジェクト用にstrtok_r()を記述しました。ここでのほとんどの回答とは異なり、私は通常、メモリを静的に割り当てます。したがって、私の実装では、上限が_argv_length_であると想定しています。ほとんどの一般的な組み込みアプリケーションでは、これで十分です。以下にサンプルコードも含めたので、すぐに使用できます。

_int tokenise_to_argc_argv(
        char     *buffer,     ///< In/Out : Modifiable String Buffer To Tokenise
        int      *argc,       ///< Out    : Argument Count
        char     *argv[],     ///< Out    : Argument String Vector Array
        const int argv_length ///< In     : Maximum Count For `*argv[]`
      )
{ /* Tokenise string buffer into argc and argv format (req: string.h) */
  int i = 0;
  for (i = 0 ; i < argv_length ; i++)
  { /* Fill argv via strtok_r() */
    if ( NULL == (argv[i] = strtok_r( NULL , " ", &buffer)) ) break;
  }
  *argc = i;
  return i; // Argument Count
}
_

注意:

  • 提供される文字バッファーは、変更可能でなければなりません(strtok_r()が_\0_をバッファーに挿入して文字列トークンを区切るため)。
  • この関数のstrtok_rは現在、_" "_スペース文字を唯一の区切り文字として使用しています。これは、通常のコマンドラインインターフェイスでの動作main(int argc, char *argv[])をエミュレートします。
  • この関数はmallocやcallocを使用せず、代わりにargv配列を個別に割り当て、argvの長さを明示的に指定する必要があります。これは、組み込みデバイスでこれを使用するつもりなので、手動で割り当てたいからです。
  • strtok_r()はスレッドセーフであるため使用されます(strtok()は内部静的ポインターを使用するため)。また、標準Cライブラリ_string.h_の一部であるため、非常に移植可能です。

以下は、デモコードとその出力です。さらに、これは、tokenise_to_argc_argv()がほとんどの文字列のケースを処理できるため、テスト済みであることを示しています。また、この関数はmallocやcallocに依存しないため、(_stdint.h_型を使用した後の)組み込み用途に適しています。


デモコード

_/*******************************************************************************
  Tokenise String Buffer To Argc and Argv Style Format
  Brian Khuu 2017
*******************************************************************************/
#include <stdio.h>  // printf()
#include <ctype.h>  // isprint()
#include <string.h> // strtok_r()

/**-----------------------------------------------------------------------------
  @brief Tokenise a string buffer into argc and argv format

  Tokenise string buffer to argc and argv form via strtok_r()
  Warning: Using strtok_r will modify the string buffer

  Returns: Number of tokens extracted

------------------------------------------------------------------------------*/
int tokenise_to_argc_argv(
        char     *buffer,     ///< In/Out : Modifiable String Buffer To Tokenise
        int      *argc,       ///< Out    : Argument Count
        char     *argv[],     ///< Out    : Argument String Vector Array
        const int argv_length ///< In     : Maximum Count For `*argv[]`
      )
{ /* Tokenise string buffer into argc and argv format (req: string.h) */
  int i = 0;
  for (i = 0 ; i < argv_length ; i++)
  { /* Fill argv via strtok_r() */
    if ( NULL == (argv[i] = strtok_r( NULL, " ", &buffer)) ) break;
  }
  *argc = i;
  return i; // Argument Count
}

/*******************************************************************************
  Demonstration of tokenise_to_argc_argv()
*******************************************************************************/

static void print_buffer(char *buffer, int size);
static void print_argc_argv(int argc, char *argv[]);
static void demonstrate_tokenise_to_argc_argv(char buffer[], int buffer_size);

int main(void)
{ /* This shows various string examples */
  printf("# `tokenise_to_argc_argv()` Examples\n");
  { printf("## Case0: Normal\n");
    char  buffer[] = "tokenising example";
    demonstrate_tokenise_to_argc_argv(buffer, sizeof(buffer));
  }
  { printf("## Case1: Empty String\n");
    char  buffer[] = "";
    demonstrate_tokenise_to_argc_argv(buffer, sizeof(buffer));
  }
  { printf("## Case2: Extra Space\n");
    char  buffer[] = "extra  space here";
    demonstrate_tokenise_to_argc_argv(buffer, sizeof(buffer));
  }
  { printf("## Case3: One Word String\n");
    char  buffer[] = "one-Word";
    demonstrate_tokenise_to_argc_argv(buffer, sizeof(buffer));
  }
}

static void demonstrate_tokenise_to_argc_argv(char buffer[], int buffer_size)
{ /* This demonstrates usage of tokenise_to_argc_argv */
  int   argc     = 0;
  char *argv[10] = {0};

  printf("* **Initial State**\n");
  print_buffer(buffer, buffer_size);

  /* Tokenise Command Buffer */
  tokenise_to_argc_argv(buffer, &argc, argv, sizeof(argv));

  printf("* **After Tokenizing**\n");
  print_buffer(buffer, buffer_size);
  print_argc_argv(argc,argv);
  printf("\n\n");
}

static void print_buffer(char *buffer, int size)
{
  printf(" - Buffer Content `");
  for (int i = 0 ; i < size; i++) printf("%c",isprint(buffer[i])?buffer[i]:'0');
  printf("` | HEX: ");
  for (int i = 0 ; i < size; i++) printf("%02X ", buffer[i]);
  printf("\n");
}

static void print_argc_argv(int argc, char *argv[])
{ /* This displays the content of argc and argv */
  printf("* **Argv content** (argc = %d): %s\n", argc, argc ? "":"Argv Is Empty");
  for (int i = 0 ; i < argc ; i++) printf(" - `argv[%d]` = `%s`\n", i, argv[i]);
}
_

出力

tokenise_to_argc_argv()の例

Case0:通常

  • 初期状態
    • バッファの内容_tokenising example0_ | HEX:74 6F 6B 65 6E 69 73 69 6E 67 20 65 78 61 6D 70 6C 65 00
  • トークン化後
    • バッファの内容_tokenising0example0_ | HEX:74 6F 6B 65 6E 69 73 69 6E 67 00 65 78 61 6D 70 6C 65 00
  • Argv content(argc = 2):
    • _argv[0]_ = tokenising
    • _argv[1]_ = example

Case1:空の文字列

  • 初期状態
    • バッファの内容_0_ | 16進数:00
  • トークン化後
    • バッファの内容_0_ | 16進数:00
  • Argv content(argc = 0):Argv Is Empty

ケース2:追加スペース

  • 初期状態
    • バッファの内容_extra space here0_ | HEX:65 78 74 72 61 20 20 73 70 61 63 65 20 68 65 72 65 00
  • トークン化後
    • バッファの内容_extra0 space0here0_ | HEX:65 78 74 72 61 00 20 73 70 61 63 65 00 68 65 72 65 00
  • Argv content(argc = 3):
    • _argv[0]_ = extra
    • _argv[1]_ = space
    • _argv[2]_ = here

ケース3:1つの文字列

  • 初期状態
    • バッファの内容_one-Word0_ | HEX:6F 6E 65 2D 77 6F 72 64 00
  • トークン化後
    • バッファの内容_one-Word0_ | HEX:6F 6E 65 2D 77 6F 72 64 00
  • Argv content(argc = 1):
    • _argv[0]_ = _one-Word_

ツールチェーンにstring.hまたはstrtok_r()がありません。

何らかの理由でツールチェーンにstrtok_r()がない場合。このstrtok_r()の簡易バージョンを使用できます。これは、GNU Cのstrtok_r()の実装の修正バージョンですが、スペース文字のみをサポートするように簡略化されています。

これを使用するには、単にtokenise_to_argc_argv()の上に配置してから、strtok_r( NULL, " ", &buffer)strtok_space(&buffer)に置き換えます。

_/**-----------------------------------------------------------------------------
  @brief Simplied space deliminated only version of strtok_r()

  - save_ptr : In/Out pointer to a string. This pointer is incremented by this
                function to find and mark the token boundry via a `\0` marker.
                It is also used by this function to find mutiple other tokens
                via repeated calls.

  Returns:
    - NULL  : No token found
    - pointer to start of a discovered token

------------------------------------------------------------------------------*/
char * strtok_space(char **save_ptr)
{ /* strtok_space is slightly modified from GNU C Library `strtok_r()`  implementation. 
      Thus this function is also licenced as GNU Lesser General Public License*/
  char *start = *save_ptr;
  char *end = 0;

  if (*start == '\0') {
    *save_ptr = start;
    return NULL;
  }

  /* Scan leading delimiters.  */
  while(*start == ' ') start++;
  if (*start == '\0') {
    *save_ptr = start;
    return NULL;
  }

  /* Find the end of the token.  */
  end = start;
  while((*end != '\0') && (*end != ' ')) end++;
  if (*end == '\0') {
    *save_ptr = end;
    return start;
  }

  /* Terminate the token and make *SAVE_PTR point past it.  */
  *end = '\0';
  *save_ptr = end + 1;
  return start;
}
_
1
Brian

残念ながらC++ですが、この種類のライブラリを検索する可能性がある他の人にはお勧めします。

ParamContainer-使いやすいコマンドラインパラメータパーサー

本当に小さくて本当に簡単です。

p.addParam("long-name", 'n', ParamContainer::regular, 
           "parameter description", "default_value");  

programname --long-name = value

cout << p["long-name"];
>> value

私の経験から:

  • とても便利でシンプル
  • 生産上安定
  • 十分にテスト済み(私が)
0
bua