コマンドライン引数 | Programming Place Plus 新C++編

トップページ新C++編

このページの概要

このページでは、コマンドライン引数について解説します。この機能を使うと、プログラムを実行するときに、ユーザーの側からプログラムの側へと情報を渡すことができ、プログラムが処理する対象のファイルを指示したり、動作の一部を切り替える指示を与えたりできます。この機能を使って、バイナリエディタのプログラムを完成まで導きます。

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



コマンドライン引数

前のページまでで、バイナリエディタを作るために必要な機能のほとんどは説明しました。これまでのプログラムは、読み込むファイルのパスをソースコードに直接書いていたので、ユーザーが自由にファイルを指定する方法がありません。「どのファイル」の内容を見たいのかを指定する方法があれば、実用上、最低限の機能が手に入ります。

そこでこのページでは、コマンドライン引数(コマンド引数) (commandline arguments)という機能を解説します。これは、プログラムを実行するとき、実行する側から情報をプログラムへ引き渡す機能です。この機能を使えば、ユーザーがバイナリエディタのプログラムを実行するときに、内容を確認したいファイルを指定できるようになります。

コマンドライン引数に指定できる情報は 1個以上の文字列です。何個指定するべきか、またそれぞれがどのような意味なのか、どんな順番で指定すればいいのかは、プログラム側が決めることです。ユーザー側には、そのルールに合うように指定する責任があります。ただし、プログラム側は、間違った入力がなされたとしても、致命的な結果を生まないようにする責務もあります(ユーザーは必ず間違えるものです)。

引数がある main関数

C++ でコマンドライン引数を扱うには、main関数に仮引数を追加します。

int main(int argc, char** argv)
{
}

// または

int main(int argc, char* argv[])
{
}

追加する仮引数は2つです。仮引数に付ける名前は何でもいいですが、型についてはルールがあって、第1引数は int型、第2引数は char型のポインタを指すポインタにしなければなりません。第2引数が難しそうですが、詳しいことはあとであらためて取り上げます

第1引数には、「実行時に指定されたコマンドライン引数の個数 + 1」が渡されてきます。「+ 1」されるのは、指定されたコマンドライン引数のほかにもう1つ、プログラムを実行するための名前というものが付いてくるためです。

第2引数には、実行時に指定されたコマンドライン引数が入っています。

実際にプログラムを作って確認してみます。実行時に指定されたコマンドライン引数をすべて、標準出力へ書き出すだけのプログラムです。

#include <iostream>

int main(int argc, char** argv)
{
    for (int i {0}; i < argc; ++i) {
        std::cout << argv[i] << " ";
    }
    std::cout << "\n";
}

実行結果:

C:\test.exe

これまでどおり、コマンドライン引数を指定せずに実行すると、実行ファイルのパスが出力される環境が多いと思います。これは、argv[0] のところに入っている文字列です。そして、これがあるため argv は最低でも 1 になります。ただし、argv[0] に入っている内容が具体的にどういう形式の文字列なのかは環境次第ですし、空文字列("")になっている可能性もある1ので、基本的には、信用して使えるものではありません。

コマンドライン引数を指定した場合は、それぞれの内容も出力されます。たとえば、123 xyz を指定した場合の実行結果は次のようになります。

実行結果:

C:\test.exe 123 xyz

コマンドライン引数を指定して実行する

そもそも、コマンドライン引数を指定する方法を説明していないですが、これはいくつかやり方があります。Windows環境では以下のような方法があります。

Visual Studio から実行する場合

プログラムの開発中は、何度もコマンドライン引数を変更して動作確認することになるので、Visual Studio から指定できると便利です。この方法は、Visual Studio編>「コマンドライン引数を指定して実行する」 で説明しているので、そちらを参照してください。

コマンドプロンプトから実行する場合

コマンドプロンプトから実行する場合は、実行ファイルの指定に続けて、コマンドライン引数を空白文字で区切って入力します。詳細は、Windows編>「コンソールアプリを実行する」を参照してください。

【上級】バッチファイルから実行する場合も同じように記述します。

エクスプローラーから実行する場合

Windows のエクスプローラーから実行する場合は、ひと手間必要になります。詳細は、Windows編>「エクスプローラーで、コマンドライン引数を指定して実行する」を参照してください。

コマンドライン引数のエンコーディング形式

コマンドライン引数は文字列なので、エンコーディング形式(「マルチバイト文字」のページを参照)が影響する可能性があります。

C++ としては、コマンドライン引数のエンコーディング形式は、終端文字があるマルチバイト文字列だということしか規定がありません2。あとは環境次第ということになります。

Windows の場合、「UTF-8」のページで触れたとおり、CP932 (Shift_JIS) が使われているため、コマンドライン引数も CP932 で渡されます。日本語の文字も、CP932 で表現できる範囲のものは使用できます。

ポインタへのポインタ

main関数の第2引数は char** という型になっています。これまでのページで登場したポインタchar* のように、* が1つだけでしたが、今回は2つ付いています。これは、指し示している先にあるものもポインタであるという意味です。したがって、char** は、「char型のポインタを指し示すポインタ」ということになります。このような、2段階で何かを指し示すポインタを、ポインタへのポインタ (pointer to pointer) と呼びます。

複雑になりすぎるので避けたほうがいいですが、char*** とか char**** のように、さらに段階を増やしていくことも可能です。ちなみに、ポインタへのポインタの時点でも十分複雑なので、不必要に使うべきではありません。

前述したとおり、main関数の第2引数に渡されてくるのは、コマンドライン引数として入力された数個の文字列です。つまり、「文字列を要素とする配列」のかたちで渡されてきます。ソースコード上では main関数の呼び出し元がみえないので想像するしかないですが、次のコードのようなものがあると思えばいいです。

int argc = ???;  // コマンドライン引数の個数 + 1

char* commandline_args[argc + 1] = {
    実行ファイルのパスなど(環境依存),
    指定されたコマンドライン引数 1個目(あれば),
    指定されたコマンドライン引数 2個目(あれば),
    // 3個目以降があれば続く ,
    0  // 最後に 0 が置かれる
};

// main関数を呼び出す
main(argc, commandline_args);

commandline_args の要素数が argc より1つ多いのは、最後の要素に 0 が入る仕様があるためです3

関数に配列を直接渡すことはできず、ポインタに変換されるのでした(「配列とポインタ」のページを参照)。そのため、char* の配列である commandline_args は、char* を指すポインタ、すなわち char** に変換されて main関数に渡されてきます。

ここでは char** は、char[] を指すポインタという意味で使われていますが、もう1つの可能性として、単独の char型変数を指すポインタ(char*)を指すポインタであることも考えられます。char** という型からは、2つの可能性のどちらであるかを区別することはできません。そこで、配列を指していることを分かりやすく示すために、char* argv[] と書くことが許されていますが、扱いはまったく変わらず char** argv と同じです。実際には配列を指していなかったとしても、それを教えてくれるようなことはありません。

argv は constポインタではないので、指し示す先の値を書き換えられますが、上記のコードでいうところの commandline_args がどのように実装されているか見えないわけですし、基本的には書き換えないほうがいいのではないかと思います。


コマンドライン引数の仕組みはC言語の仕様をそのまま受け継いだものであるため、char** という分かりづらい表現になっていますが、C++ らしく表現すると std::vector<std::string> のイメージ(つまり文字列の配列)です。もし、少々余計な処理をしても問題がないのであれば、std::vector<std::string> に格納してから作業すると分かりやすくなり、便利で安全かもしれません。

#include <iostream>
#include <string>
#include <vector>

// コマンドライン引数を std::vector<std::string> に格納して返す
static auto argv_to_vector(int argc, char** argv)
{
    std::vector<std::string> vec {};
    for (int i {0}; i < argc; ++i) {
        vec.push_back(argv[i]);
    }
    return vec;
}

int main(int argc, char** argv)
{
    // いったん、std::vector<std::string> に入れ直す
    auto cmdline_args = argv_to_vector(argc, argv);
    
    for (const auto& a : cmdline_args) {
        std::cout << a << "\n";
    }
}

コマンドライン引数:

123 xyz

実行結果:

C:\test.exe
123
xyz

コマンドライン引数の解析

コマンドライン引数には、その個数や値の表現形式や有効範囲など、プログラム側が想定したルールがあります。これに沿わない入力がなされてしまっても、プログラムが異常な動作を取らないようにしなければなりません。

まず、個数が適切かどうかのチェックは、argc の値を確認することで行えます。たとえば、3個(argv[0] の分も含む)あるのが正しいのなら、次のようにチェックできます。

#include <cstdlib>
#include <iostream>

int main(int argc, char** argv)
{
    if (argc != 3) {
        std::cerr << "コマンドライン引数の個数が正しくありません。\n";
        std::quick_exit(EXIT_FAILURE);
    }

    // ...
}

ここでは省略していますが、正しくない入力が行われた場合、正しい入力はどんなものであるかが分かるようにメッセージを出力すると親切です。

単純なケースでは、このサンプルのようなチェックが可能ですが、コマンドライン引数の仕様がもっと複雑な場合があります。よくあるのは、動作の一部を変更するオプション機能です。バイナリエディタでいえば、16進数の A~F を a~f で表示するように変えるオプションが考えられます。たとえば、コマンドライン引数に -l を追加した場合には a~f で表示するという仕様なら、binary_editor test.bin -l のように入力します。-l が指定された場合にだけコマンドライン引数の個数が1つ増えることになるので、argc の値を単純にチェックする方法が使えなくなります。

【上級】さらにいえば、オプションに複数の種類があるなら、その指定順が入れ替わっても認識するべきでしょうし、オプションに追加のパラメータが必要なこともあるでしょう(表示を開始する位置を --begin 1024 のように指定するオプションなど)。また、-l-s のように1文字のオプションに種類がある場合、合体して -ls と指定できる仕様も存在します。こうした対応をきちんと実装することは、それなりに難易度が高いです。

使い方や、そのプログラムのバージョン情報を確認できるように、-h--help)や -v--version)といったオプションを用意することもよくあります。この場合、そのプログラムの本来の仕事をさせる意図ではないため、コマンドライン引数はオプションだけになるのが普通です(たとえば、binary_editor -h のように)。


また、数値が入力されるべきところに、数値とみなせないものが入力されるかもしれません。そもそも文字列化された状態で渡されてくるので、まず整数型や浮動小数点型になおす必要があります。これは、std::istringstream を使って行えますから、合わせてエラーチェックを入れることもできます。次のサンプルでは、argv[1] が整数とみなせないときにエラーとして扱っています。

#include <cstdlib>
#include <iostream>
#include <sstream>

int main(int argc, char** argv)
{
    std::istringstream iss(argv[1]);
    int value {};
    iss >> value;
    if (!iss) {
        std::cerr << "1つ目のコマンドライン引数は整数でなければなりません。\n";
        std::quick_exit(EXIT_FAILURE);
    }

    // ...
}

【上級】文字列を分解するというより、変換したいだけの場面なので、std::istringstream を使うのは大がかりすぎるかもしれません。よりシンプルな、std::stoi関数4なども検討するといいです(変換後の型ごとに別の関数があります)。その場合、エラーチェックは例外の捕捉によって行うことになります。

バイナリエディタプログラム

では、バイナリエディタのプログラムを完成させましょう。

コマンドライン引数には、ファイルパスを1つ指定してもらう仕様とします。また、ヘルプを表示させる -h または --help、バージョン情報を表示させる -v または --version も対応します。

#include <cstdlib>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <iterator>
#include <string>
#include <vector>

constexpr auto app_name = "binary_editor";
constexpr auto version_str = "1.0.0";

// コマンドライン引数を std::vector<std::string> に格納して返す
static std::vector<std::string> argv_to_vector(int argc, char** argv);

// ヘルプメッセージを出力
static void print_help();

// バージョン情報を出力
static void print_version();

// バイト列を標準出力へ出力する
//
// address: 対象のバイト列を指し示すポインタ
// size: 対象のバイト列のバイト数
static void print_byte_string(const void* address, std::size_t size);


int main(int argc, char** argv)
{
    // コマンドライン引数を std::vector に詰め替える
    auto cmdline_args = argv_to_vector(argc, argv);

    // コマンドライン引数の解析
    if (argc != 2) {
        std::cerr << "コマンドライン引数の個数が正しくありません。\n";
        print_help();
        std::quick_exit(EXIT_FAILURE);
    }
    if (cmdline_args.at(1) == "-h" || cmdline_args.at(1) == "--help") {
        print_help();
        std::quick_exit(EXIT_SUCCESS);
    }
    if (cmdline_args.at(1) == "-v" || cmdline_args.at(1) == "--version") {
        print_version();
        std::quick_exit(EXIT_SUCCESS);
    }

    // 対象のファイルをオープン
    std::string path {cmdline_args.at(1)};
    std::ifstream ifs(path, std::ios_base::in | std::ios_base::binary);
    if (!ifs) {
        std::cerr << path << "が開けません。\n";
        std::quick_exit(EXIT_FAILURE);
    }

    // ファイルの中身をすべて読み込む
    std::istreambuf_iterator<char> it_ifs_begin(ifs);
    std::istreambuf_iterator<char> it_ifs_end {};
    std::vector<char> bytes(it_ifs_begin, it_ifs_end);

    // バイト列を出力
    print_byte_string(bytes.data(), bytes.size());
}

// コマンドライン引数を std::vector<std::string> に格納して返す
static std::vector<std::string> argv_to_vector(int argc, char** argv)
{
    std::vector<std::string> vec {};
    for (int i {0}; i < argc; ++i) {
        vec.push_back(argv[i]);
    }
    return vec;
}

// ヘルプメッセージを出力
static void print_help()
{
    std::cout << "使い方:\n"
              << "> " << app_name << " 対象のファイルパス\n"
              << "> " << app_name << " -h(または --help)\n"
              << "> " << app_name << " -v(または --version)\n"
              << "\n"
              << "-h(--help)    使い方を表示します。\n"
              << "-v(--version) バージョン情報を表示します。" << std::endl;
}

// バージョン情報を出力
static void print_version()
{
    std::cout << app_name << " version: " << version_str << std::endl;
}

// バイト列を標準出力へ出力する
static void print_byte_string(const void* address, std::size_t size)
{
    auto p = static_cast<const unsigned char*>(address);
    auto remain_byte = size;

    while (remain_byte > 0) {
        std::size_t byte_num {remain_byte >= 16 ? 16 : remain_byte};

        for (std::size_t i {0}; i < byte_num; ++i) {
            std::cout << std::setw(2) << std::setfill('0') << std::hex << std::uppercase << static_cast<unsigned int>(*p) << " ";
            ++p;
        }
        std::cout << "\n";

        remain_byte -= byte_num;
    }
}

適当に用意したビットマップ画像 test.bmp を指定して実行してみると、次のようになりました。

コマンドライン引数:

binary_editor test.bmp

実行結果:

42 4D 36 03 00 00 00 00 00 00 36 00 00 00 28 00
00 00 10 00 00 00 10 00 00 00 01 00 18 00 00 00
00 00 00 03 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 0E C9 FF 0E C9 FF 0E C9 FF 0E
C9 FF 0E C9 FF 0E C9 FF 0E C9 FF 0E C9 FF EA D9
99 EA D9 99 EA D9 99 EA D9 99 EA D9 99 EA D9 99
EA D9 99 EA D9 99 0E C9 FF 0E C9 FF 0E C9 FF 0E
C9 FF 0E C9 FF 0E C9 FF 0E C9 FF 0E C9 FF EA D9
99 EA D9 99 EA D9 99 EA D9 99 EA D9 99 EA D9 99
EA D9 99 EA D9 99 0E C9 FF 0E C9 FF 0E C9 FF 0E
C9 FF 0E C9 FF 0E C9 FF 0E C9 FF 0E C9 FF EA D9
99 EA D9 99 EA D9 99 EA D9 99 EA D9 99 EA D9 99
EA D9 99 EA D9 99 0E C9 FF 0E C9 FF 0E C9 FF 0E
C9 FF 0E C9 FF 0E C9 FF 0E C9 FF 0E C9 FF EA D9
99 EA D9 99 EA D9 99 EA D9 99 EA D9 99 EA D9 99
EA D9 99 EA D9 99 0E C9 FF 0E C9 FF 0E C9 FF 0E
C9 FF 0E C9 FF 0E C9 FF 0E C9 FF 0E C9 FF EA D9
99 EA D9 99 EA D9 99 EA D9 99 EA D9 99 EA D9 99
EA D9 99 EA D9 99 0E C9 FF 0E C9 FF 0E C9 FF 0E
C9 FF 0E C9 FF 0E C9 FF 0E C9 FF 0E C9 FF EA D9
99 EA D9 99 EA D9 99 EA D9 99 EA D9 99 EA D9 99
EA D9 99 EA D9 99 0E C9 FF 0E C9 FF 0E C9 FF 0E
C9 FF 0E C9 FF 0E C9 FF 0E C9 FF 0E C9 FF EA D9
99 EA D9 99 EA D9 99 EA D9 99 EA D9 99 EA D9 99
EA D9 99 EA D9 99 0E C9 FF 0E C9 FF 0E C9 FF 0E
C9 FF 0E C9 FF 0E C9 FF 0E C9 FF 0E C9 FF EA D9
99 EA D9 99 EA D9 99 EA D9 99 EA D9 99 EA D9 99
EA D9 99 EA D9 99 4C B1 22 4C B1 22 4C B1 22 4C
B1 22 4C B1 22 4C B1 22 4C B1 22 4C B1 22 24 1C
ED 24 1C ED 24 1C ED 24 1C ED 24 1C ED 24 1C ED
24 1C ED 24 1C ED 4C B1 22 4C B1 22 4C B1 22 4C
B1 22 4C B1 22 4C B1 22 4C B1 22 4C B1 22 24 1C
ED 24 1C ED 24 1C ED 24 1C ED 24 1C ED 24 1C ED
24 1C ED 24 1C ED 4C B1 22 4C B1 22 4C B1 22 4C
B1 22 4C B1 22 4C B1 22 4C B1 22 4C B1 22 24 1C
ED 24 1C ED 24 1C ED 24 1C ED 24 1C ED 24 1C ED
24 1C ED 24 1C ED 4C B1 22 4C B1 22 4C B1 22 4C
B1 22 4C B1 22 4C B1 22 4C B1 22 4C B1 22 24 1C
ED 24 1C ED 24 1C ED 24 1C ED 24 1C ED 24 1C ED
24 1C ED 24 1C ED 4C B1 22 4C B1 22 4C B1 22 4C
B1 22 4C B1 22 4C B1 22 4C B1 22 4C B1 22 24 1C
ED 24 1C ED 24 1C ED 24 1C ED 24 1C ED 24 1C ED
24 1C ED 24 1C ED 4C B1 22 4C B1 22 4C B1 22 4C
B1 22 4C B1 22 4C B1 22 4C B1 22 4C B1 22 24 1C
ED 24 1C ED 24 1C ED 24 1C ED 24 1C ED 24 1C ED
24 1C ED 24 1C ED 4C B1 22 4C B1 22 4C B1 22 4C
B1 22 4C B1 22 4C B1 22 4C B1 22 4C B1 22 24 1C
ED 24 1C ED 24 1C ED 24 1C ED 24 1C ED 24 1C ED
24 1C ED 24 1C ED 4C B1 22 4C B1 22 4C B1 22 4C
B1 22 4C B1 22 4C B1 22 4C B1 22 4C B1 22 24 1C
ED 24 1C ED 24 1C ED 24 1C ED 24 1C ED 24 1C ED
24 1C ED 24 1C ED

ヘルプを呼び出すと、次のように出力されます。

コマンドライン引数:

binary_editor --help

実行結果:

使い方:
> binary_editor 対象のファイルパス
> binary_editor -h(または --help)
> binary_editor -v(または --version)

-h(--help)    使い方を表示します。
-v(--version) バージョン情報を表示します。

ソースコードは、これまでのページの解説を総まとめしたような内容になっているので、追加で説明することは特にありません。このページまでの知識で読めない部分はないはずです。

最低限の機能ながら、バイナリエディタのプログラムが完成しました。このページの練習問題や、次のページからは、追加機能を加えたり、ソースコードの改良を行ったりします。

まとめ


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク


練習問題

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (基本★)

コマンドライン引数で、0個以上の整数を入力させ、その合計値を標準出力へ出力するプログラムを作成してください。整数とみなせない入力は 0 として扱うことにします。

解答・解説

問題2 (基本★★)

コマンドライン引数で指定された個数の疑似乱数を、標準出力に出力するプログラムを作成してください。

たとえば、gen_rand 30 とした場合は 30個の乱数を、改行しながら出力します。

解答・解説

問題3 (応用★★★)

問題2で作成したプログラムに、生成する乱数の下限値を指定する –min オプションと、上限値を指定する –max オプションを実装してください。

--min の直後、--max の直後に整数を置くようにします。たとえば、gen_rand 30 --min 0 --max 10000 と指定された場合、0~10000 の範囲の乱数を 30個生成します。2つのオプションが指定される順番は入れ替わっても構いませんし、片方だけ指定することもできるものとします。

そのほかの仕様は自由に決めてください。

解答・解説

問題4 (応用★★★)

バイナリエディタのプログラムに、16進数の A~F の表示を a~f に変更するオプションを実装してください。

解答・解説


解答・解説ページの先頭



更新履歴




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る