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}

Download the source code

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}