2.3. C++でコマンドを作成する/Write a command in C++
C++で書かれたソースコードをコンパイルして、いつでも使えるコマンド として、PATHで指定されたディレクトリに格納するための手順を紹介します。
Here we will show how to compile a C++ program and store it into a directory that is specified by the PATH environment variable, so that you can use it as a command at any time.
2.3.1. ソースコード/The source code
題材に使うのは、次のプログラムです。これは、ソースコードの文字コードを チェックするプログラムです。演習の中でソースを提出する前に、このプログラム でチェックしてください。長いのでタイプしてもらう必要はありません。
The following program is the subject. This is a character code checker program. Use this program to check your source when you submit source code during the seminar. You don’t need to type in the code.
1#include <iostream>
2#include <fstream>
3#include <cassert>
4#include <cstdlib>
5
6/* check the encoding of one file */
7bool checkOneFile(const char *filename);
8
9/* check the encoding of files */
10int main(int argc, const char *argv[])
11{
12 /* loop through the arguments */
13 for (int i = 1; i < argc; i++) {
14 bool res = checkOneFile(argv[i]);
15 if (! res) {
16 /* failure (following linux convention) */
17 return EXIT_FAILURE;
18 }
19 }
20 /* success (following linux convention) */
21 return EXIT_SUCCESS;
22}
23
24/* states for the state machine to check source code bytes */
25enum State {
26 S_NORMAL, /* during a line, after a regular character */
27 S_BOL, /* beginning of line */
28 S_CR, /* after a CR */
29 S_UTF_EXPECT3, /* expecting 3 more UTF8 trailers to follow */
30 S_UTF_EXPECT2, /* expecting 2 more */
31 S_UTF_EXPECT1, /* expecting 1 more */
32};
33
34void report(int incident_count, const char *msg, const char *filename, int line_count) {
35 /* report the incident just once per file. */
36 if (incident_count == 1) {
37 std::cout << filename << "(" << (line_count+1) << ") [ERROR] :" << msg << std::endl;
38 }
39}
40
41bool isUtf8Trailer(char c) {
42 // check if the byte has the bits 10xx xxxx
43 return (c & 0xc0) == 0x80;
44 // 0xc0 is "C0" in hexadecimal. C is 12. (0123456789ABCDEF are the digits)
45 // 12 is 2^3 + 2^2. in binary it is 1100
46 // 0xc0 in binary is 11000000
47 // 0x80 in binary is 10000000
48}
49
50bool isUtf8Header1(char c) {
51 // 110x xxxx
52 return (c & 0xe0) == 0xc0;
53}
54
55bool isUtf8Header2(char c) {
56 // 1110 xxxx
57 return (c & 0xf0) == 0xe0;
58}
59
60bool isUtf8Header3(char c) {
61 // 1111 0xxx
62 return (c & 0xf8) == 0xf0;
63}
64
65bool checkOneFile(const char *filename)
66{
67 std::cout << "Checking " << filename << std::endl;
68 std::ifstream ifs(filename, std::ios::binary);
69 if (! ifs.is_open()) {
70 /* standard C function to print an error message */
71 perror(filename);
72 return false;
73 }
74 /* number of End Of Line(EOL) characters we have seen */
75 int line_count = 0;
76 /* number of tab characters (banned by our coding convention). */
77 int tab_count = 0;
78 /* number of CR-LF sequences (banned) */
79 int crlf_count = 0;
80 /* number of CR occurences (banned) */
81 int cr_count = 0;
82 /* number of other C1 segment control characters (0x01 through 0x1f) */
83 /* They do not appear in normal text files */
84 int c1_count = 0;
85 /* number of bad UTF-8 sequences. Happens when character code setting of the editor is incorrect */
86 int bad_utf_count = 0;
87
88 /* state variable for the state machine */
89 State ss = S_BOL;
90
91 /*
92 * when an ifstream class object is referenced in an if condition,
93 * ifstream will be casted into a bool type by a custom-made
94 * cast function. The bool value 'true' will mean that the
95 * ifstream object is in good state. False will mean that no more
96 * data can be obtained from the stream.
97 */
98 while (ifs) {
99 char c; // note that this is a 'signed' type and the range is -128 to +127
100 // read one byte from the stream.
101 ifs.get(c);
102
103 // reenter point, when state transition happens without
104 // consuming the input character
105 reenter_point:
106
107 switch (ss) {
108 case S_BOL:
109 ss = S_NORMAL;
110 /* fall through */
111 case S_NORMAL:
112 {
113 switch(c) {
114 case '\n':
115 line_count++;
116 ss = S_BOL;
117 break;
118 case '\r':
119 ss = S_CR;
120 break;
121 case '\t':
122 tab_count++;
123 report(tab_count, "Tab character", filename, line_count);
124 break;
125 default:
126 if (isUtf8Header3(c)) {
127 ss = S_UTF_EXPECT3;
128 } else if (isUtf8Header2(c)) {
129 ss = S_UTF_EXPECT2;
130 } else if (isUtf8Header1(c)) {
131 ss = S_UTF_EXPECT1;
132 } else if (isUtf8Trailer(c)) {
133 bad_utf_count++;
134 report(bad_utf_count, "Bad multibyte sequence", filename, line_count);
135 } else if (((unsigned char)c) < 0x20) {
136 c1_count++;
137 report(c1_count, "Unexpected control character", filename, line_count);
138 }
139 break;
140 }
141 }
142 break;
143 case S_CR:
144 {
145 if (c == '\n') {
146 // CRLF = Windows EOL code
147 crlf_count++;
148 report(crlf_count, "Windows newline sequence (CR,LF)", filename, line_count);
149 line_count++;
150 ss = S_BOL;
151 } else {
152 // CR = Very Old MacOS EOL code
153 cr_count++;
154 report(cr_count, "Old-time MacOS newline sequence (CR)", filename, line_count);
155 line_count++;
156 ss = S_BOL;
157 /* We do not consume the current cc. */
158 goto reenter_point;
159 }
160 }
161 break;
162 case S_UTF_EXPECT3:
163 {
164 if (isUtf8Trailer(c)) {
165 // correct UTF-8 sequence.
166 ss = S_UTF_EXPECT2;
167 } else {
168 bad_utf_count++;
169 report(bad_utf_count, "Bad multibyte sequence", filename, line_count);
170 ss = S_NORMAL;
171 }
172 }
173 break;
174 case S_UTF_EXPECT2:
175 {
176 if (isUtf8Trailer(c)) {
177 // correct UTF-8 sequence.
178 ss = S_UTF_EXPECT1;
179 } else {
180 bad_utf_count++;
181 report(bad_utf_count, "Bad multibyte sequence", filename, line_count);
182 ss = S_NORMAL;
183 }
184 }
185 break;
186 case S_UTF_EXPECT1:
187 {
188 if (isUtf8Trailer(c)) {
189 // correct UTF-8 sequence.
190 ss = S_NORMAL;
191 } else {
192 bad_utf_count++;
193 report(bad_utf_count, "Bad multibyte sequence", filename, line_count);
194 ss = S_NORMAL;
195 }
196 }
197 break;
198 default:
199 assert(0);
200 }
201 }
202 if (ss != S_BOL) {
203 report(1, "Missing EOL at end of file", filename, line_count);
204 }
205 /*
206 * the ifs object should be in eof (end of file) state now.
207 * If it isn't, that means some other error happened.
208 */
209 if (! ifs.eof()) {
210 perror(filename);
211 return false;
212 }
213 ifs.close();
214 return true;
215}
2.3.2. 作業用のディレクトリを用意する/Prepare a directory for the work
プログラムをコンパイルするために、ホームディレクトリの下に ディレクトリを作ります。
Make a directory inside your home directory to compile the program.
Note
ホームディレクトリに、過去の授業や演習などで作成したファイルが 残っている場合には、それらを格納するためのディレクトリをまず作って、 過去のファイルはそこにしまっておきましょう。 不要なファイルでしたら消してしまいましょう。
If your home directory is scattered with files from former classes, seminars, or any other activities, make a directory to stow those files away. If you think they are unnecessary, you can choose to delete them.
ここではディレクトリ名を tools/source_checker とします。
Name the directory tools/source_checker:
$ cd
$ mkdir -p tools/source_checker
$ cd tools/source_checker
引数なしで cd を使うと、ホームディレクトリに移動します。 また、 mkdir に -p オプションをつけると、ディレクトリの階層を一度に 作ることができます。
Using cd with no arguments will take you to your home directory. By giving the -p option to the mkdir command, you can make multiple levels of directories at once.
Note
本文書に従って操作するときはは、(マウスでコピー&ペーストするのではなく)、 実際にタイプしてみて下さい。タイプすると覚えます。マウスでなぞっても、 マウスのなぞり方しか記憶に残らないようです。 タイプして覚えると、タイプを省力化するためのいろいろな仕掛けを活用できるようになります。
When you are following the instructions in this document, we urge you to actually type the commands, rather than copy-and-pasting them with a mouse. You will memorize by typing. Dragging the mouse only lets you memorize how to drag the mouse. Once you memorize through typing, you will be able to leverage the various tools to reduce the amount of typing.
2.3.3. ソースコードをそこに配置する/Place the source code
ソースコードのダウンロードリンクが、このページのソースコードのリストの 末尾にあります。作成したディレクトリにダウンロードしたソースを格納して 下さい。
もしもブラウザでデフォルトのダウンロードディレクトリに格納した場合には、 mv で source_checker ディレクトリに移動させてみてください。 ブラウザのダウンロード先ディレクトリはOSやブラウザによって異なるので、自分で 調べてください。
Get the source file from the download link at the end of the above source listing and store it in the directory you just created.
If you stored the file to the browser’s default download directory, try moving that file to the ‘source_checker’ directory with the mv command. The default download destination directory differs depending on browsers and OSes, so check for yourself:
$ pwd # make sure you are in the right directory
/home/.../your-username/tools/source_checker
$ mv ~/Downloads/source_checker.cpp .
pwd は、print working directory の略で、シェルのカレント ディレクトリを確認するために使えます。念の為、どこにいるのか確認したいときに 使います。
mv の後のチルダ “~” はシェルによってホームディレクトリのパス名に展開 されます。”~ユーザ名” とすると、任意のユーザのホームディレクトリを得る ことができます。
mv の最後の “.” はカレントディレクトリのパス名です。ファイルの移動先として カレントディレクトリを指定しています。
pwd is short for “print working directory”, and it does just that. You can use it to confirm where your shell is looking at.
The tilde “~” after mv will be expanded to your home directory path name by the shell. The form “~username” can also be used and will be expanded to the home directory specified by the user name.
The “.” at the end of the mv command line is the path name for the current directory. The current directory is specified as the destination of the move operation.
2.3.4. コンパイルする/Compile the code
C++のソースコードをコンパイルするにはC++用のコンパイラを使います。
コンパイラの種類がいろいろありますが、ここではLinux上で広く使える g++ を 使います。
To compile C++ source code, you need to use a C++ compiler.
There are many compilers available, but here we will use g++, which is widely available on Linux:
$ pwd
/home/.../your-user-name/tools/source_checker
$ g++ -o source_checker source_checker.cpp
存在しているパス名をタイプするときは、TABキーによる入力補完が 効くことが多いので試してみてください。これから作り出すファイルに ついては無理です。
When you type an existing pathname, keep in mind that type assisting (input completion) is available through the tab key. However, this will not work for pathnames you are going to create.
2.3.5. 実行する/Run the program
プログラムを実行するには、プログラムのファイル名をシェルに入力します。 シェルはコマンド名を受け取ると、環境変数PATHに列挙されたディレクトリに コマンドを探しに行きます。”ls” コマンドなどはそのように実行されます。 一方、パスセパレータ “/” を含んだパス名をコマンド名として入力すると、 環境変数PATHとは関係なく、そのパス名を探しに行きます。
今、コンパイルして、出来上がったばかりの source_checker プログラムの ファイル名だけを入力すると、シェルはPATHの中を探しに行き、おそらく エラーになります。
手元のディレクトリにあるファイルを実行するには、プログラムのファイル名 だけを入力したのではだめで、パスセパレータ ‘/’を1つ以上含んだパス名を 指定する必要があります。”カレントディレクトリにある source_checker” でしたら、 ./source_checker がパス名になります。
To run a program, you enter the file name of the program to the shell. The shell will search for that file in the directories listed in the environment variable PATH. The “ls” command and others are executed in this way. On the other hand when you enter a path name of a file which includes at least one path separator “/”, the shell will look directly at that pathname without using PATH.
If you enter the file name “source_checker” into the shell, the shell will search that name in PATH, likely resulting in an error.
To run a program file in the current directory, it is not enough to enter the file name. You must enter a path name with at least one path separator. “the source_checker file in the current directory” can be expressed by the path “./source_checker”:
$ ls # make sure we are in the correct directory
source_checker source_checker.cpp
% ./source_checker source_checker.cpp
Checking source_checker.cpp
2.3.6. PATH経由で使えるようにする/make it usable via PATH
環境変数PATHで指定されたディレクトリにプログラムを置いておけば、 カレントディレクトリがどこであろうと、すぐにコマンドとして使う ことができます。
ここでは、ホームディレクトリの下に自作コマンド用のディレクトリを 設けて、環境変数PATHにそのディレクトリを指定してみます。
If you store your program in a directory that is included in the environment variable PATH, you will be able to invoke it as a command no matter where the current directory of your shell is.
Here we will create a directory inside your home directory for storing your own commands, and point to that directory from PATH.
まず、ホームディレクトリの下に .local/bin というディレクトリを 作ります。このディレクトリ名を使うツールも存在するので、人によっては 最初から出来ているかも知れません。
First, we make a directory .local/bin under your home directory. This directory name is used by some tools, so some of you may already have this directory:
$ ls # make sure we are in the correct directory
source_checker source_checker.cpp
$ mkdir -p ~/.local/bin
$ cp source_checker ~/.local/bin
次に、環境変数PATHにこのディレクトリを登録します。 環境変数の設定の仕方についてのチュートリアル も合わせて読んでください。
Next, we add an entry for this new directory in PATH. Also read the tutorial on environment variables.
$ PATH=$PATH:~/.local/bin $ export PATH
これで準備ができました。いまや先頭の “./” をつけずにコマンドとして起動できます。
We are done. You no longer need to add the “./” in front of the file name:
$ source_checker *.cpp
Checking source_checker.cpp
2.3.7. プログラムの中身/The content of the program
プログラムの中身について簡単に説明します。
このプログラムは文字コードのチェックツールであり、 シミュレーション計算と直接の関係はありませんが、 シミュレーションプログラムの実装にも役立つテクニックをいくつか使っています。
このチュートリアルに取り組んだ時点で難しいと感じる場合は、少しC++言語に 慣れてから読んでみてください。
We will briefly explain the content of the program.
This program is a character code checker tool, and is not related with simulation calculation. However it uses some techniques that can be applied to simulation programs.
If you find the content difficult at the time you work on this tutorial, you can revisit the content later when you get more used to the C++ language.
2.3.7.1. main関数/The main function
8/* check the encoding of files */
9int main(int argc, const char *argv[])
10{
11 /* loop through the arguments */
12 for (int i = 1; i < argc; i++) {
13 bool res = checkOneFile(argv[i]);
14 if (! res) {
15 /* failure (following linux convention) */
16 return EXIT_FAILURE;
17 }
18 }
19 /* success (following linux convention) */
20 return EXIT_SUCCESS;
main関数は、argvで渡されたパス名をひとつずつ checkOneFile 関数に渡します。 checkOneFileは、パス名の誤りなどによって処理に失敗した場合にはfalseを返します。 処理に失敗した時点でループは終了します。
main関数の戻り値は、プロセスの終了ステータス値として、呼び出し元の親プロセス
(典型的にはシェル)に伝達されます。
Linuxも従う POSIX規格 では終了ステータスの値として 0または
EXIT_SUCCESS
は正常終了を意味し、 EXIT_ERROR
は、異常終了を意味します。
The main function will loop through the path names given as argv, and calls the checkOneFile function for each path name. checkOneFile will return false on a failure. It can fail if the given pathname is not readable. Upon failure, the loop will be terminated.
The return value of the main function will become the exit status code of the process and will be sent to the parent process, which typically is a shell.
Acoording to the POSIX Standard, which Linux complies, the exit status
value 0, or EXIT_SUCCESS
denotes a success, and EXIT_ERROR
denotes an error.
2.3.7.2. The checkOneFile function
64bool checkOneFile(const char *filename)
65{
66 std::cout << "Checking " << filename << std::endl;
67 std::ifstream ifs(filename, std::ios::binary);
68 if (! ifs.is_open()) {
69 /* standard C function to print an error message */
70 perror(filename);
71 return false;
72 }
73 /* number of End Of Line(EOL) characters we have seen */
74 int line_count = 0;
75 /* number of tab characters (banned by our coding convention). */
76 int tab_count = 0;
77 /* number of CR-LF sequences (banned) */
78 int crlf_count = 0;
79 /* number of CR occurences (banned) */
80 int cr_count = 0;
81 /* number of other C1 segment control characters (0x01 through 0x1f) */
82 /* They do not appear in normal text files */
83 int c1_count = 0;
84 /* number of bad UTF-8 sequences. Happens when character code setting of the editor is incorrect */
85 int bad_utf_count = 0;
86
87 /* state variable for the state machine */
88 State ss = S_BOL;
89
90 /*
91 * when an ifstream class object is referenced in an if condition,
92 * ifstream will be casted into a bool type by a custom-made
93 * cast function. The bool value 'true' will mean that the
94 * ifstream object is in good state. False will mean that no more
95 * data can be obtained from the stream.
96 */
97 while (ifs) {
98 char c; // note that this is a 'signed' type and the range is -128 to +127
99 // read one byte from the stream.
100 ifs.get(c);
checkOneFile関数では、ファイルから1バイトずつ読み込むために、std::ifstream クラスの変数 ifs を作ります。コンストラクタ引数にはファイル名と、 バイナリモード を指定するフラグを渡しています。 バイナリモードを指定しないと、ifstreamは空白、タブ、改行などのいわゆる whitespace character を全て区切り文字として読み飛ばしてしまいます。 このプログラムではタブ文字を検出したり、改行を数えたりしたいので、 バイナリモードを指定します。
ifstreamに指定したパス名に問題があると、ファイルのオープンに失敗します。 成功したか失敗したかを、is_open() メソッドで確認しています。 perror関数は、C/C++標準ライブラリの中で生じたエラーの種類を説明するメッセージを 印字する関数です。perror関数の引数には、ファイル名など、そのときの操作の 対象だったモノの名前を渡します。この名前は、メッセージの一部に使われます。
メッセージとしては、以下のようなものが得られます:
no such file or directory: spel_misteik.cpp
ファイルのオープンに成功したら、while文の中で1バイトずつファイルを読んでいきます。 whileの条件には変数 ifs をそのまま渡しています。 while ( 式 ) の式の部分には論理値 (bool型の値) が求められます。 ifsはbool型ではなく std::ifstream クラスです。std::ifstreamクラスには、bool 型への変換関数が設けられており、その時点でストリームからデータをさらに 読み出すことができれば true, 読み出すことができなければ false を返すように なっています。そのため、この書き方で、「ファイルからデータが読み取れる間は 繰り返す」と書いたことになります。
関数の残りの部分では、ファイルに登場する文字コードのチェックをしています。
以下の事項をチェックしています。
タブ文字は登場しないか。
改行文字として、 Unix形式の改行 以外のものが使われていないか。
改行以外の制御コード(Backspace, Beep, Form feed等)が登場していないか。
マルチバイト文字は UTF-8 の規則に則っているか。Windowsの漢字コードを 使うと、このチェックに抵触します。 * ファイルの最後の改行の後に、文字がないか。つまり、最終行の末尾の改行を 忘れていないか。
97 while (ifs) {
98 char c; // note that this is a 'signed' type and the range is -128 to +127
99 // read one byte from the stream.
100 ifs.get(c);
101
102 // reenter point, when state transition happens without
103 // consuming the input character
104 reenter_point:
105
106 switch (ss) {
107 case S_BOL:
108 ss = S_NORMAL;
109 /* fall through */
110 case S_NORMAL:
111 {
112 switch(c) {
113 case '\n':
114 line_count++;
115 ss = S_BOL;
116 break;
117 case '\r':
118 ss = S_CR;
119 break;
120 case '\t':
121 tab_count++;
122 report(tab_count, "Tab character", filename, line_count);
123 break;
124 default:
125 if (isUtf8Header3(c)) {
126 ss = S_UTF_EXPECT3;
127 } else if (isUtf8Header2(c)) {
128 ss = S_UTF_EXPECT2;
129 } else if (isUtf8Header1(c)) {
130 ss = S_UTF_EXPECT1;
131 } else if (isUtf8Trailer(c)) {
132 bad_utf_count++;
133 report(bad_utf_count, "Bad multibyte sequence", filename, line_count);
134 } else if (((unsigned char)c) < 0x20) {
135 c1_count++;
136 report(c1_count, "Unexpected control character", filename, line_count);
137 }
138 break;
139 }
140 }
141 break;
142 case S_CR:
143 {
144 if (c == '\n') {
145 // CRLF = Windows EOL code
146 crlf_count++;
147 report(crlf_count, "Windows newline sequence (CR,LF)", filename, line_count);
148 line_count++;
149 ss = S_BOL;
150 } else {
151 // CR = Very Old MacOS EOL code
152 cr_count++;
153 report(cr_count, "Old-time MacOS newline sequence (CR)", filename, line_count);
154 line_count++;
155 ss = S_BOL;
156 /* We do not consume the current cc. */
157 goto reenter_point;
158 }
159 }
160 break;
161 case S_UTF_EXPECT3:
162 {
163 if (isUtf8Trailer(c)) {
164 // correct UTF-8 sequence.
165 ss = S_UTF_EXPECT2;
166 } else {
167 bad_utf_count++;
168 report(bad_utf_count, "Bad multibyte sequence", filename, line_count);
169 ss = S_NORMAL;
170 }
171 }
172 break;
173 case S_UTF_EXPECT2:
174 {
175 if (isUtf8Trailer(c)) {
176 // correct UTF-8 sequence.
177 ss = S_UTF_EXPECT1;
178 } else {
179 bad_utf_count++;
180 report(bad_utf_count, "Bad multibyte sequence", filename, line_count);
181 ss = S_NORMAL;
182 }
183 }
184 break;
185 case S_UTF_EXPECT1:
186 {
187 if (isUtf8Trailer(c)) {
188 // correct UTF-8 sequence.
189 ss = S_NORMAL;
190 } else {
191 bad_utf_count++;
192 report(bad_utf_count, "Bad multibyte sequence", filename, line_count);
193 ss = S_NORMAL;
194 }
195 }
196 break;
197 default:
198 assert(0);
199 }
200 }
201 if (ss != S_BOL) {
202 report(1, "Missing EOL at end of file", filename, line_count);
203 }
204 /*
205 * the ifs object should be in eof (end of file) state now.
206 * If it isn't, that means some other error happened.
207 */
208 if (! ifs.eof()) {
209 perror(filename);
210 return false;
211 }
212 ifs.close();
213 return true;
214}