C++ にも python の commands.getoutput を作ろう

C++ で外部プロセスを呼び出そうとしたらすごく大変だったから、python の commands.getoutput(“ls -l”) みたいに一行で書けて標準出力が返ってくれば幸せになれるのに・・・!という経緯です。
CreateProcess
今回は WIndows 用に調べてたんで MSDN。
CreateProcess 関数
泣きたくなるような引数の数に加え、型が外国語すぎて読めません;
それでも一応頑張って読んでみると、STARTUPINFO ってのを外から与えると、構造体の中を破壊してきて標準出力とかがこの中に書き込まれるみたいですよ。
STARTUPINFO
このへんで投げ出したけど、真面目に実装するとけっこう長くなるかんじ。ハンドルって概念が分からないんですよ。なにそれ。
popen/pclose
まだこっちのが分かるよねってのが C 標準関数の popen/pclose。
Manage of POPEN
Windows でも _popen/_pclose で実装されてるみたいで、とりあえずこっちで書いてみることに。
python commands
目指すインターフェースは以下なかんじ。とりあえず getoutput だけ。
16.16 commands — コマンド実行ユーティリティ
commands.hpp
インターフェースをまねるとこんなかんじですよね。
インスタンス生成とかせずに、シンプルに関数呼び出しで扱える形に。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | /* * File: commands.hpp * Author: kei * * Created on 2009/08/18 */ #include <string> #ifndef _COMMANDS_HPP #define _COMMANDS_HPP namespace commands { const std::string getoutput(const std::string& command); } #endif /* _COMMANDS_HPP */ |
commands.cpp
実装。
C 言語特有のエラーコードを返す関数を、全部例外投げるようにラップしてみたけど、finally がないからクローズ処理がまとめられなかったり、throws がないから再 throw になっちゃったり。
あと Java の IOException みたいな例外定義がデフォルトでなくて、そもそも catch しなくてもコンパイル通るとか。
それぞれの例外クラスを定義して、catch で分けるのが正しいんですが、とりあえず std::runtime_error 投げたんで、よい子はマネしないでね。
popen がファイルディスクリプタを返してくるんですが、この子からファイルストリームを生成するうまい方法がなくて(昔のコンパイラはコンストラクタに喰わせて fstream 作れたみたいだけど)、素直に C 言語な実装に。
あと必ず pclose で閉じること!って書いてあったから、boost::shared_ptr なんか使って解放しちゃ危なそうってことで boost も出番なし。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 | /* * File: commands.cpp * Author: kei * * Created on 2009/08/18 */ #include <cstdio> #include <sstream> #include <stdexcept> #include "commands.hpp" /** * 指定したコマンドのプロセスへのファイルディスクリプタを返します。 * 使用後は close_process(const FILE* process) でクローズしてください。 * プロセスのオープンに失敗した場合は例外を投げます。 * @param command * @return プロセスへのファイルディスクリプタ * @throw std::runtime_error */ FILE* open_process(const std::string& command) { FILE* process = popen(command.c_str(), "r"); if (process == NULL) { throw std::runtime_error("open " + command + " failed."); } return process; } /** * 実行プロセスの出力を返します。 * @param process * @return プロセス実行出力 */ const std::string read_process(FILE* process) { std::stringstream output; char buffer[128]; // allocate memory while (fgets(buffer, sizeof (buffer), process)) { output << buffer; } return output.str(); } /** * 指定したプロセスをクローズします。 * プロセスのクローズに失敗した場合は例外を投げます。 * @throw std::runtime_error */ void close_process(FILE* process) { const int result = pclose(process); if (result == -1) { throw std::runtime_error("close_process faild."); } } /** * 指定したコマンドをシェル経由で実行し、出力を返します。 * プロセスのオープン/実行/クローズのいずれかに失敗した場合は例外を投げます。 * @param command コマンド * @return コマンド出力 * @throw std::runtime_error */ const std::string commands::getoutput(const std::string& command) { FILE* process = NULL; std::string output; try { process = open_process(command); output = read_process(process); } catch (std::runtime_error error) { // finally 処理の代わりに、例外時にもクローズ処理を書いてあります。 // open/read の例外時のため、close の例外確認は行いません。 if (process != NULL) { close_process(process); } throw error; } // 正常時の close 処理です。例外確認も行います。 if (process != NULL) { try { close_process(process); } catch (std::runtime_error error) { throw error; } } return output; } |
main.cpp
date コマンドを打ってみた例。
まあこんなとこでしょ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | /* * File: main.cpp * Author: kei * * Created on 2009/08/18 */ #include <cstdlib> #include <iostream> #include <stdexcept> #include "commands.hpp" /** * メインエントリです。 * 以下は date コマンドを実行するサンプルです。 * @param argc * @param argv */ int main(int argc, char** argv) { try { std::cout << commands::getoutput("date") << std::endl; } catch(std::runtime_error error) { std::cerr << error.what() << std::endl; } return (EXIT_SUCCESS); } |
/usr/lib/python2.6/commands.py
python の実装を真面目に読んでみる。
os.popen() を使ってるみたい。
"""Execute shell commands via os.popen() and return status, output.確かに。
def getstatusoutput(cmd): """Return (status, output) of executing cmd in a shell.""" import os pipe = os.popen('{ ' + cmd + '; } 2>&1', 'r') text = pipe.read() sts = pipe.close()
/usr/lib/python2.6/os.py
subprocess の Popen からパイプを生成してるもよう。
import subprocess PIPE = subprocess.PIPE p = subprocess.Popen(cmd, shell=True, bufsize=bufsize, stdin=PIPE, stdout=PIPE, close_fds=True) return p.stdin, p.stdout
/usr/lib/python2.6/subprocess.py
あー、Windows 版はやっぱり CreateProcess を使って実装してるのね。
On UNIX, with shell=False (default): In this case, the Popen class
uses os.execvp() to execute the child program. args should normally
be a sequence. A string will be treated as a sequence with the string
as the only item (the program to execute).On UNIX, with shell=True: If args is a string, it specifies the
command string to execute through the shell. If args is a sequence,
the first item specifies the command string, and any additional items
will be treated as additional shell arguments.On Windows: the Popen class uses CreateProcess() to execute the child
program, which operates on strings. If args is a sequence, it will be
converted to a string using the list2cmdline method. Please note that
not all MS Windows applications interpret the command line the same
way: The list2cmdline is designed for applications using the same
rules as the MS C runtime.
startupinfo の文字が。やっぱり頑張らないとかー。
# Start the process try: hp, ht, pid, tid = CreateProcess(executable, args, # no special security None, None, int(not close_fds), creationflags, env, cwd, startupinfo)
最後に
boost::process みたいなのないですか。いやぐぐるとひっかかるんですけど、これ boost パッケージに含まれてます?
Pstreams ってのもあったけど使ってる人いるのかしら?
特になければ、今回作ったものをもう少しちゃんと実装して使い回していく方向かなあ。ぐぐっても出てこないあたり需要ないのか知らないけど、Windows で ssh ライブラリがないからこれで叩こうと思ったわけで。
PuTTY のソースも見たし、python paramiko も試したけど、大人の事情がもにょもにょだったので。
C++ 素人につきリファクタ職人の降臨が望まれます。

