ジョギング
- お休み
- お昼休みに近所のマッサージ屋さんで軽く腰から足をほぐしてもらう
日課
- お休み
- 完全に痛みが引いたら再開する
明日は
- 福岡マラソンの準備をする (ゼッケンを取りに行ったり...)
Facebook でもたくさんのおめでとうごコメントを頂いて, 本当に有難うございました. コメントを返しながら, 誕生日で懐かしい人達との再開を与えてくれたり, それらの人たちに対して感謝をする日なんだろうなあと.
夕飯は奥さんが気を利かせて新型 MacBook Air をサプライズで買ってきてくれると思っていたが, それは流石に無かったけど, 尾ノ上の特上寿司をテイクアウトしてくれて細やかな誕生祭を催してくれた. ありがとう . 尾ノ上のお寿司, 本当に美味しかった.
ということで, 今年一年, 家族のみんなが元気に過ごせますように.
放送大学教養学部の「アルゴリズムとプログラミング」という授業で使われる「アルゴリズムとプログラミング」という教材書籍を自分なりにまとめたものです.
以下は配列と添字の関係を示したもの.
添字 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
---|---|---|---|---|---|---|---|---|---|---|
値 | 30 | 20 | 10 | 25 | 15 |
以下は C 言語での配列の宣言例.
int a[10];
この例では 10 個の整数型の様子を持つ a という名前の配列を宣言している.
以下, 配列を使わずに 5 つの整数の平均値を求めるコード.
/* code: ex6-1.c */ #include <stdio.h> int main () { int a, b, c, d, e; int sum, avg; a = 30; b = 20; c = 10; d = 25; e = 15; sum = a + b + c + d + e; avg = sum / 5; printf ("%d\n", avg); return 0; }
上記のコードについて, 配列を使うと以下のようになる.
/* code: ex6-2.c */ #include <stdio.h> int main () { int a[10]; int i, sum, avg; a[0] = 30; a[1] = 20; a[2] = 10; a[3] = 25; a[4] = 15; sum = 0; for (i = 0; i < 5; i++) { sum += a[i]; } avg = sum / 5; printf ("%d\n", avg); return 0; }
このコードでは, for ループにより変数 i が増加する. この変数は配列の添字となっており, 配列の各要素が加算されて 5 つの数の合計が求められる.
root@0be431eebb77:/work# gcc ex6-2.c -o ex6-2 -g3 root@0be431eebb77:/work# ./ex6-2 20
以下は ex6-2.c と同じ動作をするコードである.
/* code: ex6-3.c */ #include <stdio.h> int main () { int a[10] = { 30, 20, 10, 25, 15 }; int i, sum, avg; sum = 0; for (i = 0; i < 5; i++) { sum += a[i]; } avg = sum / 5; printf ("%d\n", avg); return 0; }
これをコンパイルして実行してみる.
root@0be431eebb77:/work# gcc ex6-3.c -o ex6-3 -g3 root@0be431eebb77:/work# ./ex6-3 20
このコードでは配列の初期化を配列の宣言文内で行っている. 配列の要素は波括弧 {}
内に列挙する. 要素数が宣言した配列の数より少ない場合は, 列挙した要素以外は初期化されない. 列挙した要素が多い場合は, 多くのコンパイラでは警告が出る. 例えば, 以下のように宣言した数よりも要素の数が多い場合.
/* code: ex6-3a.c */ #include <stdio.h> int main () { int a[3] = { 30, 20, 10, 25, 15 }; int i, sum, avg; sum = 0; for (i = 0; i < 5; i++) { sum += a[i]; } avg = sum / 5; printf ("%d\n", avg); return 0; }
以下のようにコンパイラ時エラーが発生することを確認した.
# gcc ex6-3a.c -o ex6-3a -g3 ex6-3a.c: In function 'main': ex6-3a.c:6:28: warning: excess elements in array initializer int a[3] = { 30, 20, 10, 25, 15 }; ^~ ex6-3a.c:6:28: note: (near initialization for 'a') ex6-3a.c:6:32: warning: excess elements in array initializer int a[3] = { 30, 20, 10, 25, 15 }; ^~ ex6-3a.c:6:32: note: (near initialization for 'a')
以下のコードは, 要素数 10 の配列の中に 0 〜 99 まで範囲の乱数を入れていくコードである.
/* code: ex6-4.c */ #include <stdio.h> #include <stdlib.h> #define ARRAY_SIZE 10 int main () { int a[ARRAY_SIZE]; int i, sum, avg; for (i = 0; i < ARRAY_SIZE; i++) { a[i] = rand () % 100; } for (i = 0; i < ARRAY_SIZE; i++) { printf ("%03d ", a[i]); } printf ("\n"); return 0; }
これをコンパイルして実行する. 以下のように 0 パディングされた 100 以下のランダムな整数が 10 個出力される.
root@0be431eebb77:/work# gcc ex6-4.c -o ex6-4 -g3 root@0be431eebb77:/work# ./ex6-4 083 086 077 015 093 035 086 092 049 021
上記の例では #define
を利用して, 配列の要素数 ARRAY_SIZE
を定義している. 配列の要素数を変更したい場合には, ARRAY_SIZE
を変更するだけで良い. rand 関数は, 0 以上, RAND_MAX で定義された値以下の疑似乱数整数を返す. %
記号はモジュロ演算で, 剰余の計算をする.
多くのプログラミング言語では, 多次元配列を利用することが出来る. 以下は 2 次元配列を図示したものである.
C0 | C1 | C2 | C3 | |
---|---|---|---|---|
R0 | a[0][0] | a[0][1] | a[0][2] | a[0][3] |
R1 | a[1][0] | a[1][1] | a[1][2] | a[1][3] |
R2 | a[2][0] | a[2][1] | a[2][2] | a[2][3] |
以下のコードは, 上記の 2 次元配列表が示すような整数型の 2 次元の配列を作成し, 値を代入した後に二重の for ループによって配列内の値を出力する.
/* code: ex6-5.c */ #include <stdio.h> int main () { int i, j; int a[3][4] = { {0, 10, 20, 30}, {40, 50, 60, 70}, {80, 90, 100, 110} }; for (i = 0; i < 3; i++) { for (j = 0; j < 4; j++) { printf ("array[%d][%d] = %3d\n", i, j, a[i][j]); } } return 0; }
これをコンパイラして実行してみる.
root@0be431eebb77:/work# gcc ex6-5.c -o ex6-5 -g3 root@0be431eebb77:/work# ./ex6-5 array[0][0] = 0 array[0][1] = 10 array[0][2] = 20 array[0][3] = 30 array[1][0] = 40 array[1][1] = 50 array[1][2] = 60 array[1][3] = 70 array[2][0] = 80 array[2][1] = 90 array[2][2] = 100 array[2][3] = 110
以下は, 3 次元配列のコード例である.
/* code: ex6-6.c */ #include <stdio.h> int main () { int i, j, k; int a[2][3][4] = { {{0, 1, 2, 3}, {4, 5, 6, 7}, {8, 9, 10, 11}}, {{0, 10, 20, 30}, {40, 50, 60, 70}, {80, 90, 100, 110}} }; for (i = 0; i < 2; i++) { for (j = 0; j < 3; j++) { for (k = 0; k < 4; k++) { printf ("array[%d][%d][%d] = %3d\n", i, j, k, a[i][j][k]); } } } return 0; }
これをコンパイラして実行してみる.
root@0be431eebb77:/work# gcc ex6-6.c -o ex6-6 -g3 root@0be431eebb77:/work# ./ex6-6 array[0][0][0] = 0 array[0][0][1] = 1 array[0][0][2] = 2 array[0][0][3] = 3 array[0][1][0] = 4 array[0][1][1] = 5 array[0][1][2] = 6 array[0][1][3] = 7 array[0][2][0] = 8 array[0][2][1] = 9 array[0][2][2] = 10 array[0][2][3] = 11 array[1][0][0] = 0 array[1][0][1] = 10 array[1][0][2] = 20 array[1][0][3] = 30 array[1][1][0] = 40 array[1][1][1] = 50 array[1][1][2] = 60 array[1][1][3] = 70 array[1][2][0] = 80 array[1][2][1] = 90 array[1][2][2] = 100 array[1][2][3] = 110
C 言語の場合, 要素数を宣言する []
を増やすことによって, 配列の次元を増やすことが出来る. 多次元配列は 1 次元配列に変換することが可能である. 例えば, 3 次元配列 a の各次元が X, Y, Z であり, 0 以上の値となる添字を i, j, k とすると, 1 次元配列 b は以下の式で変換することが出来る.
b[X × Y × i + Y × j + k] = a[i][j][k]
一般的に多次元の配列は要素数が大きくなりやすい為, 配列に使用出来るメモリの上限に注意する必要がある. データを格納する為の配列のメモリは, スタック領域と呼ばれる場所に確保される. スタック領域で利用出来るメモリサイズは OS やコンパイラの制限を受ける.
以下のコードは OUJ
という文字列を出力するコードである.
/* code: ex6-7.c */ #include <stdio.h> int main () { char s[4]; s[0] = 'O'; s[1] = 'U'; s[2] = 'J'; s[3] = '\0'; printf ("%s\n", s); return 0; }
これをコンパイルして実行する.
root@0be431eebb77:/work# gcc ex6-7.c -o ex6-7 -g3 root@0be431eebb77:/work# ./ex6-7 OUJ
このコードは文字型の要素数が 4 の配列 s を宣言し, その配列の先頭から順番に O
, U
, J
の文字を配列に代入している. 最後には \0
という文字列の終端を表す為の特別な文字 (ヌル文字) を配列に代入している. 長さ n の文字列を格納する為には, ヌル文字も必要になる為, n + 1 個の要素数を持つ配列を用意する必要がある.
C 言語では, 文字列のような配列に対しては代入演算子を利用することが出来ない為, ある配列から別の配列への要素を代入する場合, 以下のコードのように, ループ文等を用いて配列の要素を 1 つずつコピーしていく必要がある.
/* code: ex6-8.c */ #include <stdio.h> void string_copy (char *target, char *source) // void string_copy (char target[], char source[]) と記述出来る { int i; i = 0; while (source[i] != '\0') { target[i] = source[i]; i++; } target[i] = '\0'; } int main () { char s[20] = "University"; char t[20]; string_copy (t, s); printf ("%s\n", t); return 0; }
これをコンパイルして実行する.
root@0be431eebb77:/work# gcc ex6-8.c -o ex6-8 -g3 root@0be431eebb77:/work# ./ex6-8 University
このコードの string_copy 関数は, for ループを用いて, 配列 source の各要素からヌル文字以外を配列 target の各要素にコピーしていく. 最後にヌル文字を追加して文字列をコピーする. この関数を呼び出す時には, コピー先に十分なメモリが確保されていなければならない.
以下の表のように, C 言語のライブラリには文字列を扱う関数が多数含まれている. これらの関数は string.h に含まれる為, 利用する場合には #include <string.h>
を宣言する必要がある.
関数 | 英語での意味 | 関数の説明 |
---|---|---|
strcpy | copy a string | 文字列をコピー |
strcat | concatenate two strings | 2 つの文字列を連結 |
strcmp | compare two strings | 2 つの文字列を比較 |
strncmp | compare part of two strings | 2 つの文字列を文字数を指定して比較 |
strchr | locate character in strings | 文字列の先頭から文字を探索 |
strstr | locate a substrings | 文字列から文字列を探索 |
strlen | calculate the length of string | 文字列の長さを求める |
以下のコードは, 文字列をコピーする関数 strcpy 関数を利用した例で ex6-8.c と同様の結果となる.
/* code: ex6-9.c */ #include <stdio.h> #include <string.h> int main () { char s[20] = "University"; char t[20]; strcpy (t, s); printf ("%s\n", t); return 0; }
これをコンパイルして実行する.
root@0be431eebb77:/work# gcc ex6-9.c -o ex6-9 -g3 root@0be431eebb77:/work# ./ex6-9 University
関数を使うことで, ex6-8.c よりもだいぶんコードの記述量が減っている. 特に理由が無い限りは関数を使うべきなのかな.
以下のコードは strcmp 関数を利用したコードである.
/* code: ex6-10.c */ #include <stdio.h> #include <string.h> int main () { char s0[] = "aaaaa"; char s1[] = "bbbbb"; char s2[] = "aaaaaaa"; int i; printf ("strcmp(str1, str2)\n"); i = strcmp (s0, s0); printf ("[%s] [%s] (%d)\n", s0, s0, i); i = strcmp (s0, s1); printf ("[%s] [%s] (%d)\n", s0, s1, i); i = strcmp (s1, s0); printf ("[%s] [%s] (%d)\n", s1, s0, i); i = strcmp (s0, s2); printf ("[%s] [%s] (%d)\n", s0, s2, i); return 0; }
これをコンパイルして実行する.
root@0be431eebb77:/work# gcc ex6-10.c -o ex6-10 -g3 root@0be431eebb77:/work# ./ex6-10 strcmp(str1, str2) [aaaaa] [aaaaa] (0) [aaaaa] [bbbbb] (-1) [bbbbb] [aaaaa] (1) [aaaaa] [aaaaaaa] (-97)
strcmp 関数は 2 つの文字列 s1 と s2 を比較する. strcmp 関数は以下の書式で定義されている. const
は定数である為, 書き換え不可能な文字列であり, 関数は文字列を関数内では変更しないことを意味している.
int strcmp(const char *s1, const char *s2)
この関数は s1 が s2 に比べて小さい場合, 等しい場合, 大きい場合にそれぞれ, 以下のような整数値を返す.
コード 6.2 では, 整数型の配列が宣言されている. これを浮動小数点型の配列に変更したプログラムを作成しなさい.
/* code: q6-1.c */ #include <stdio.h> int main () { float a[10]; int i; float sum, avg; a[0] = 30; a[1] = 20; a[2] = 10; a[3] = 25; a[4] = 15; sum = 0.0; for (i = 0; i < 5; i++) { sum += a[i]; } avg = sum / 5.00; printf ("%f\n", avg); return 0; }
このコードをコンパイルして実行してみる.
root@0be431eebb77:/work# gcc q6-1.c -o q6-1 -g3 root@0be431eebb77:/work# ./q6-1 20.000000 root@0be431eebb77:/work#
ポイントは変数の宣言に float
を利用している点. また, 平均値を出力する printf 関数において, %d\n
ではなく %f\n
としている点.
コード 6.5 を参考にして, 九九表 (掛け算表) を表示するプログラムを作成しなさい. コードでは, 九九表の値を一度, 2 次元配列に代入してから, 配列内の値を出力しなさい.
/* code: q6-2.c */ #include <stdio.h> #define TABLE 9 int main () { int i, j; int a[TABLE][TABLE]; for (i = 0; i < TABLE; i++) { for (j = 0; j < TABLE; j++) { a[i][j] = (i + 1) * (j + 1); } } for (i = 0; i < TABLE; i++) { for (j = 0; j < TABLE; j++) { printf ("%02d ", a[i][j]); } printf ("\n"); } return 0; }
このコードをコンパイルして実行してみる.
root@0be431eebb77:/work# gcc q6-2.c -o q6-2 -g3 root@0be431eebb77:/work# ./q6-2 01 02 03 04 05 06 07 08 09 02 04 06 08 10 12 14 16 18 03 06 09 12 15 18 21 24 27 04 08 12 16 20 24 28 32 36 05 10 15 20 25 30 35 40 45 06 12 18 24 30 36 42 48 54 07 14 21 28 35 42 49 56 63 08 16 24 32 40 48 56 64 72 09 18 27 36 45 54 63 72 81
コード 6.6 を変更にして, 3 次元配列に 1 以上 100 以下の乱数を代入するプログラムを作成しなさい. コードでは, 乱数の値を一度, 3 次元配列に代入してから, 配列内の値を出力しなさい.
/* code: q6-3.c */ #include <stdio.h> #include <stdlib.h> int main () { int i, j, k; int a[2][3][4]; for (i = 0; i < 2; i++) { for (j = 0; j < 3; j++) { for (k = 0; k < 4; k++) { a[i][j][k] = (rand () % 100) + 1; } } } for (i = 0; i < 2; i++) { for (j = 0; j < 3; j++) { for (k = 0; k < 4; k++) { printf ("%03d ", a[i][j][k]); } printf ("\n"); } printf ("\n"); } return 0; }
これをコンパイルして実行してみる.
root@0be431eebb77:/work# gcc q6-3.c -o q6-3 -g3 root@0be431eebb77:/work# ./q6-3 084 087 078 016 094 036 087 093 050 022 063 028 091 060 064 027 041 027 073 037 012 069 068 030
コード 6.10 を変更にして, 文字列の最初の 3 文字のみを比較するようにしなさい. 文字列の比較には, srtncmp 関数を用いること.
/* code: q6-4.c */ #include <stdio.h> #include <string.h> int main () { char s0[] = "aaaaa"; char s1[] = "bbbbb"; char s2[] = "aaaaaaa"; int i; printf ("strncmp(str1, str2)\n"); i = strncmp (s0, s0, 3); printf ("[%s] [%s] (%d)\n", s0, s0, i); i = strncmp (s0, s1, 3); printf ("[%s] [%s] (%d)\n", s0, s1, i); i = strncmp (s1, s0, 3); printf ("[%s] [%s] (%d)\n", s1, s0, i); i = strncmp (s0, s2, 3); printf ("[%s] [%s] (%d)\n", s0, s2, i); return 0; }
これをコンパイルして実行してみる.
root@0be431eebb77:/work# gcc q6-4.c -o q6-4 -g3 root@0be431eebb77:/work# ./q6-4 strncmp(str1, str2) [aaaaa] [aaaaa] (0) [aaaaa] [bbbbb] (-1) [bbbbb] [aaaaa] (1) [aaaaa] [aaaaaaa] (0)
strncmp を man してみる.
$ man strncmp ... #include <string.h> int strcmp(const char *s1, const char *s2); int strncmp(const char *s1, const char *s2, size_t n); ...
strncmp は第三引数に比較する文字数を指定する.
strlen 関数を用いて文字列 "abcdefg" の長さを表示するプログラムを作成しなさい.
/* code: q6-5.c */ #include <stdio.h> #include <string.h> int main () { char s[] = "abcdefg"; int i; i = strlen (s); printf ("[%s] (%d)\n", s, i); return 0; }
これをコンパイルして実行してみる.
root@0be431eebb77:/work# gcc q6-5.c -o q6-5 -g3 root@0be431eebb77:/work# ./q6-5 [abcdefg] (7)
入力した文字列の長さを返すように改変してみる.
/* code: q6-5a.c */ #include <stdio.h> #include <string.h> int main () { char s[10]; printf ("Enter Words: "); scanf ("%s", &s); int i; i = strlen (s); printf ("[%s] (%d)\n", s, i); return 0; }
実行してみる.
root@0be431eebb77:/work# ./q6-5a Enter Words: foobar [foobar] (6)
配列はデータの集まりを処理する為に重要である. 配列と一緒に使うと便利なものとして構造体 (structure) がある. 構造体を用いると複数のデータ型を 1 つにまとめて扱うことが出来る. (1) 構造体の配列, (2) 構造体のポインタ配列を使ったプログラムを作成しなさい.
chapter 06 (Algorithms and Programming 2016) より引用.
/* code: q6-6.c (v1.16.00) */ #include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX 10 struct student { int id; char grade; char name[128]; }; typedef struct student STUDENT_TYPE; /* ------------------------------------------- */ int main () { STUDENT_TYPE db1[MAX]; STUDENT_TYPE *db2[MAX]; int i; printf ("database1\n"); for (i = 0; i < MAX; i++) { db1[i].id = 100 + i; db1[i].grade = 'a' + rand () % 5; strcpy (db1[i].name, "John Doe"); printf ("%d %c %s\n", db1[i].id, db1[i].grade, db1[i].name); } printf ("\n"); printf ("database2\n"); for (i = 0; i < MAX; i++) { db2[i] = malloc (sizeof (STUDENT_TYPE)); db2[i]->id = 200 + i; db2[i]->grade = 'a' + rand () % 5; strcpy (db2[i]->name, "John Doe"); printf ("%d %c %s\t\t", db2[i]->id, db2[i]->grade, db2[i]->name); printf ("%d %c %s\n", (*db2[i]).id, (*db2[i]).grade, (*db2[i]).name); } for (i = 0; i < MAX; i++) { free (db2[i]); } return 0; }
これをコンパイルして実行してみる.
root@0be431eebb77:/work# gcc q6-6.c -o q6-6 -g3 root@0be431eebb77:/work# ./q6-6 database1 100 d John Doe 101 b John Doe 102 c John Doe 103 a John Doe 104 d John Doe 105 a John Doe 106 b John Doe 107 c John Doe 108 e John Doe 109 b John Doe database2 200 c John Doe 200 c John Doe 201 c John Doe 201 c John Doe 202 a John Doe 202 a John Doe 203 e John Doe 203 e John Doe 204 d John Doe 204 d John Doe 205 b John Doe 205 b John Doe 206 a John Doe 206 a John Doe 207 b John Doe 207 b John Doe 208 c John Doe 208 c John Doe 209 b John Doe 209 b John Doe
ふむ. 現時点で, このコードを理解するのは難しい. ごめんなさい. 以下, 印刷教材より引用.
db2[i]->id
と (*db2[i]).id
は同義であるman 関数名
でいけるギョームにて GraphQL について, Hello World レベルから調査する. AWS だと AppSync を利用すると, サクッと GraphQL の環境を用意出来ることが解った. AppSync はデータソースとして DynamoDB や Amazon Elasticsearch Service, Lambda を利用出来る. スキーマを定義して, スキーマとデータソースはリゾルバマッピングを書くことで関連付けることが出来ることがだいたい理解出来た.
www.slideshare.net
しかし, この仕組を導入するメリットがあるのかどうかについては, GraphQL 自体がどのようなものなのかの理解をより深める必要があるなと思った.
AWS SDK for Ruby を使ってコマンドラインツールを作る時にテストまで含めた雛形みたいなのがあったら楽だよなーと思いつつ, ソフトバンクが日本シリーズを制したり, 楽しそうな JAWS FESTA の様子が SNS のタイムラインに流れてくるのを羨ましく横目に見つつ, もくもくサンプル的な何かを作ってみました.
これ.
EC2 インスタンス ID 一覧, S3 バケット名一覧を返すだけのあくまでもサンプル的なものとなります.
$ bundle exec sample-cli --help Commands: sample-cli buckets # list up bucket name. sample-cli help [COMMAND] # Describe available commands or one specific command sample-cli input WORD # input words print. sample-cli instances # list up instance ids. sample-cli version # version print.
環境変数に AWS_PROFILE と AWS_REGION を定義して利用することを想定しています.
$ export AWS_PROFILE=xxxxxxxxxxxxxxxxxxx $ export AWS_REGION=ap-northeast-1 $ bundle exec sample-cli instances i-xxxxxxxxxxxxxxxx1 i-xxxxxxxxxxxxxxxx2 i-xxxxxxxxxxxxxxxx3 i-xxxxxxxxxxxxxxxx4 i-xxxxxxxxxxxxxxxx5 $ bundle exec sample-cli buckets bucket-a bucket-b bucket-c bucket-d bucket-e ...
で, コマンドラインツールを作る上でいろいろと検討した内容を以下の通り書いていきたいと思います. 誤りや認識不足がありおかしなことが書かれているかと思いますので, コメント等で指摘いただければ幸いでございます.
コマンドラインツールを作る際に引数の処理等を良しなに面倒を見てくれる erikhuda/thor がとても便利だと思います. 例えば, 上述の例で実行されている instances
や buckets
のサブコマンドをメソッドとして記述することで, 簡単にサブコマンドを追加することが出来ます.
以下, 実装例です.
module SampleCli class CLI < Thor desc 'version', 'version print.' def version puts SampleCli::VERSION end desc 'input WORD', 'input words print.' def input(word = nil) puts 'Please input `word`' if word.nil? puts word end desc 'instances', 'list up instance ids.' def instances ec2 = SampleCli::Ec2.new puts ec2.instances end desc 'buckets', 'list up bucket name.' def buckets s3 = SampleCli::S3.new puts s3.buckets end end end
Thor
クラスを継承して, 上記のように実装しておくと, 以下のような感じでサブコマンド化してくれます.
$ bundle exec sample-cli --help Commands: sample-cli buckets # list up bucket name. sample-cli help [COMMAND] # Describe available commands or one specific command sample-cli input WORD # input words print. sample-cli instances # list up instance ids. sample-cli version # version print.
これだけでも十分にテンションが上がります.
各サブコマンドで呼ばれる処理を実装していくことで, それなりにコマンドラインツールが数分で出来上がってしまいます.
好みの問題になってしまうかもしれませんが, 今回は以下のようにファイルを分割してみました. よく利用するツールのフォルダ構成を程よく参考させて頂きました.
$ tree -L 3 . . ├── Gemfile ├── Gemfile.lock ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin │ ├── console │ └── setup ├── exe │ └── sample-cli ├── lib │ ├── sample_cli │ │ ├── cli.rb │ │ ├── client.rb │ │ ├── ec2.rb │ │ ├── s3.rb │ │ ├── stub │ │ ├── stub.rb │ │ └── version.rb │ └── sample_cli.rb ├── sample-cli.gemspec ├── spec │ ├── default_spec.rb │ ├── ec2_spec.rb │ ├── s3_spec.rb │ └── spec_helper.rb └── vendor └── bundle └── ruby
instances
で利用する処理を実装buckets
で利用する処理を実装instances
や buckets
の処理内容をテストする際に利用するスタブを定義 (テストについは後述)という感じです.
今後, S3 オブジェクトの一覧を取得するサブコマンドを追加する場合には, lib/sample_cli/cli.rb に objects
という名前のメソッドを追加して, lib/sample_cli/s3.rb に実際の処理を追加していく感じを想定しています.
# lib/sample_cli/cli.rb に以下を追加 ... desc 'objects', 'list up S3 objects' option :bucket, type: :string, aliases: '-b', desc: 'S3 バケットを指定する.' def objects s3 = SampleCli::S3.new puts s3.objects(option[:bucket]) end ... # lib/sample_cli/s3.rb に以下を追加 def objects(bucket) puts list_objects(bucket) end private def list_objects(bucket) objects = [] options = { bucket: bucket } loop do res = s3.list_objects_v2(options) objects << res.contents.map(&:key) options[:continuation_token] = res.next_continuation_token break unless options[:continuation_token] end objects end
尚, stub まわりの処理については, awspec を参考にさせて頂きました. 有難うございました.
Thor のテストが参考になることを最近知りました ありがとうございます. コマンド出力を以下の capture というメソッドで取得して, その出力をパースして評価しているようです.
# https://github.com/erikhuda/thor/blob/master/spec/helper.rb#L51-L62 ... def capture(stream) begin stream = stream.to_s eval "$#{stream} = StringIO.new" yield result = eval("$#{stream}").string ensure eval("$#{stream} = #{stream.upcase}") end result end ...
このメソッドは, eval で文字列をコマンドとして実行されるようになっていて, 一見, 何をやっているか良く解りませんでしたが, よく見ると以下のような処理になっています.
$stdout = StringIO.new # 標準出力の出力先を StringIO クラスに変更 yield # ブロックで渡された処理を実行する output = $stdout.string # 標準入力を変数 output に代入 $stdout = STDOUT # 標準出力の出力先を STDOUT に戻す (デフォルトが STDOUT なので)
これをそのまま spec_helper.rb に押し込んでおいて, テストには以下のように実装しました.
describe 'sample_cli check subcommand objects' do it 'have object keys by cli' do output = capture(:stdout) { SampleCli::CLI.start(%w{objects --bucket=foo}) } expect(output).to match('foo\nbar\nbaz/key\n') end end
このテストの場合, 先述のオブジェクト一覧を取得する objects
サブコマンドの正常系テストを想定しています.
sample-cli objects --bucket=foo
これを実行すると以下のようにテストがパスします.
$ bundle exec rake spec:s3 ... sample_cli check subcommand buckets have bucket names have bucket names by cli sample_cli check subcommand objects have object keys have object keys by cli Finished in 0.11143 seconds (files took 3.24 seconds to load) 4 examples, 0 failures
これは, Docker を利用して仮想的な AWS 環境, Ruby の実行環境を用意し, 実際にコマンドを AWS 環境に対して実行する方法を検討しました. これについては, 別の記事でまとめたいと思います.
AWS 環境に対するテストを行う場合, 実際の AWS リソースを叩くというのは出来るだけ避けたいところです. もちろん, テスト用の環境を AWS 上に用意して実際にリソースを叩くことでより精度の高いテスト結果を得られる可能性があると思いますが, うっかり変更してはいけない環境を操作してしまう事故の可能性も 0 ではありませんし, 利用料が発生することも有り得ます. これらを解決する方法として, 以下の 2 点のアプローチが取れると考えています.
1 については, 前節の「コマンドラインの実行をどのようにテストするか (2)」でも触れていますが, localstack や moto 等のローカル環境に AWS 環境を実行するツールを Docker 上で起動して, その環境に対して実際のコマンドを発行して結果を解析してテストを行います.
本節 (今回) は 2 のスタブを利用したテストについて検討しています. スタブを利用する場合, AWS SDK for Ruby ではクライアントを初期化する際に以下のように指定することで, スタブ化されたデータを返すようになります.
require 'aws-sdk' s3 = Aws::S3::Client.new(stub_responses: true) ...
詳細は以下のドキュメントに記載されています.
このドキュメントによると, スタブ化した場合, 特にデータを用意しない場合には, 以下のようなデータを返却するとのことです.
例えば, S3 バケットの一覧を取得する list_buckets というメソッドをスタブ化した場合には以下のような結果が返ってきます.
# スタブデータを用意しない場合 irb(main):001:0> require 'aws-sdk-s3' => true irb(main):002:0> s3 = Aws::S3::Client.new(stub_responses: true) => #<Aws::S3::Client> irb(main):003:0> s3.list_buckets => #<struct Aws::S3::Types::ListBucketsOutput buckets=[], owner=#<struct Aws::S3::Types::Owner display_name="DisplayName", id="ID">> irb(main):004:0> s3.list_buckets.buckets => [] # スタブデータを用意した場合 (foo と bar というバケット名を返されることを想定する) irb(main):005:0> bucket_data = s3.stub_data(:list_buckets, :buckets => [{name:'foo'}, {name:'bar'}]) => #<struct Aws::S3::Types::ListBucketsOutput buckets=[#<struct Aws::S3::Types::Bucket name="foo", creation_date=nil>, #<struct Aws::S3::Types::Bucket name="bar", creation_date=nil>], owner=#<struct Aws::S3::Types::Owner display_name="DisplayName", id="ID">> irb(main):006:0> s3.stub_responses(:list_buckets, bucket_data) => [{:data=>#<struct Aws::S3::Types::ListBucketsOutput buckets=[#<struct Aws::S3::Types::Bucket name="foo", creation_date=nil>, #<struct Aws::S3::Types::Bucket name="bar", creation_date=nil>], owner=#<struct Aws::S3::Types::Owner display_name="DisplayName", id="ID">>}] irb(main):007:0> s3.list_buckets.buckets => [#<struct Aws::S3::Types::Bucket name="foo", creation_date=nil>, #<struct Aws::S3::Types::Bucket name="bar", creation_date=nil>] irb(main):008:0> s3.list_buckets.buckets.map(&:name) => ["foo", "bar"]
今回は awspec の実装をそのまま参考にさせて頂いて, 以下のようにスタブが利用されるように実装を行いました.
stub_responses: true
を追加# spec/spec_helper.rb require 'rspec' require 'sample_cli' Aws.config.update(stub_responses: true) ...
list_buckets
や list_object_v2
メソッドのスタブデータ)# lib/stub/s3.rb Aws.config[:s3] = { stub_responses: { list_buckets: { buckets: [ { name: 'foo' }, { name: 'bar' } ] }, list_objects_v2: { contents: [ { key: 'foo' }, { key: 'bar' }, { key: 'baz/key' } ] } } }
# lib/stub.rb module SampleCli class Stub def self.load(type) require File.dirname(__FILE__) + '/stub/' + type end end end
require 'spec_helper' SampleCli::Stub.load 's3' describe 'sample_cli check subcommand buckets' do it 'have bucket names' do expect(SampleCli::S3.new.buckets).to eq(%w(foo bar)) end it 'have bucket names by cli' do output = capture(:stdout) { SampleCli::CLI.start(%w{buckets}) } expect(output).to match('foo\nbar\n') end end describe 'sample_cli check subcommand objects' do it 'have object keys' do expect(SampleCli::S3.new.objects('foo')).to match(%w(foo bar baz/key)) end it 'have object keys by cli' do output = capture(:stdout) { SampleCli::CLI.start(%w{objects --bucket=foo}) } expect(output).to match('foo\nbar\nbaz/key\n') end end
上記の通り, テストはサブコマンドを実行した場合 (have bucket names by cli
や have object keys by cli
) と, サブコマンドから呼ばれるメソッド (have bucket names
や have object keys
) に対してテストを行っています. これを以下のように実行してテストします.
$ bundle exec rake spec:s3 ... sample_cli check subcommand buckets have bucket names have bucket names by cli sample_cli check subcommand objects have object keys have object keys by cli Finished in 0.11505 seconds (files took 3.01 seconds to load) 4 examples, 0 failures
いい感じです.
せっかくなので, rubocop でコーディングスタイルに準拠しているかもチェックしています. 例えば, 以下のようなコードは警察の取締対象となります.
it "have object keys by cli" do output = capture(:stdout) { SampleCli::CLI.start(%w{objects --bucket=foo}) } expect(output).to match("foo\nbar\nbaz/key\n") end
容疑は Prefer single-quoted strings when you don't need string interpolation or special symbols.
です. 実際の取締の様子です.
$ bundle exec rake spec:rubocop Running RuboCop... Inspecting 18 files .....C............ Offenses: spec/s3_spec.rb:20:6: C: Prefer single-quoted strings when you don't need string interpolation or special symbols. it "have object keys by cli" do ^^^^^^^^^^^^^^^^^^^^^^^^^ 18 files inspected, 1 offense detected RuboCop failed!
釈放される為にはダブルクウォートをシングルクォートに書き換えることで釈放されます.
it 'have object keys by cli' do output = capture(:stdout) { SampleCli::CLI.start(%w{objects --bucket=foo}) } expect(output).to match('foo\nbar\nbaz/key\n') end
釈放の様子です.
$ bundle exec rake spec:rubocop Running RuboCop... Inspecting 18 files .................. 18 files inspected, no offenses detected
いい感じですね.
こちらもせっかくなので Travis CI でテストを走らせます. .travis.yml は以下の通りです.
sudo: false language: ruby rvm: - 2.5.1 before_install: gem install bundler -v 1.16.2
テスト結果は以下の通りです.
AWS SDK for Ruby を利用した CLI ツールのサンプル的なものを実装検討してみました. 素人のたわごとになりますので, いろいろとツッコミどころがあるとは思いますが, これをテンプレートとしてより良いオレオレツールが作れるようになると嬉しいなあ. あと, 先人が書かれたコードを読むというのは本当に勉強になりました.
社内で Real World HTTP 輪読会に参加したけど, 今まで軽々しく HTTP を語って申し訳ございませんという感じになった.
— Yohei Kawahara(かっぱ) (@inokara) 2018年11月2日
本当に申し訳ございません...