/*
 *	app.c
 *
 *	pipefail.exe - パイプライン途中のエラーも検出するパイプライン実行ツール
 *	Copyright (C) 2016-2017 Naoyuki Sawa
 *
 *	* Sat Jan 23 21:37:38 JST 2016 Naoyuki Sawa
 *	- 1st リリース。
 *	- cmd.exeでパイプライン実行を行うと、パイプライン途中のコマンドがエラー終了した場合も、全体としては成功(0)を返してしまいます。
 *	  バッチスクリプトやMakefileの中で、パイプライン実行を行うと、全てのコマンドが正常に実行できたかを判断出来ない問題が有ります。
 *	- 上記の問題を解決するために、当ツールを作成しました。
 *	  cmd.exeでパイプライン実行を行う代りに、当ツールを使ってパイプライン実行を行って下さい。
 *	  パイプラインの中のコマンドが一つでもエラー(非0)を返した場合、当ツールの結果もエラー(非0)となります。
 *	- 当ツールの名前は、bashの'pipefail'オプションから取りました。
 *	- 当ツールが返すエラーコードは、当ツールが最初にエラーを検出したコマンドの終了コードであり、パイプライン中の位置とは関係無い事に注意して下さい。
 *	  (bashのpipefailオプションでは、パイプラインの中でエラーを返したコマンドの内、一番右のコマンドの終了コードです。当ツールの動作とは異なります。)
 *	- 当ツールの主目的は上述の通りですが、それに加えて、マイナスのエラーコードを補正するサービス機能も追加しました。
 *	  詳細は、//{{サービス～//}}サービス の部分のコメントを参照して下さい。
 *	* Wed Jan 27 22:56:05 JST 2016 Naoyuki Sawa
 *	- '-p'オプションが指定されていても、'cmd.exe /C'を含むコマンドは、特例として、各コマンド引数を"～"で囲む処理を行わないようにしました。
 *	  'cmd.exe /C'は引用符の解釈が特殊で、各コマンド引数を"～"で囲むと、ほぼエラーになるからです。
 *	- 例えば「cmd.exe /C "DIR" "."」とすると、「cmd.exe /C DIR" ".」と解釈されてエラーになります。
 *	  詳細は、「cmd.exe /?」を参照して下さい。
 *	- 上記の問題を防ぐため、'cmd.exe /C'を含むコマンドは、特例として、各コマンド引数を"～"で囲む処理を行わない事にしました。
 *	  これで全てが解決する訳ではなく、'cmd.exe /C'を含むコマンドでも引数の一部を"～"で囲む必要がある場合、エラーになります。
 *	  そのようなケースは、個別に明示的に"～"で囲って対応して下さい。(ただしエスケープの指定がかなり大変だと思います。)
 *	  'cmd.exe /C'を含むコマンドで、本当に"～"で囲む必要が有るケースは少ないので、ほとんどのケースは今回の変更で解決出来ると思います。
 *	* Sun Feb 21 21:02:01 JST 2016 Naoyuki Sawa
 *	- libclipのライブラリ化の伴い、以下の変更を行いました。
 *	  ・extclip.hとextclip.cを削除しました。extclip.hとextclip.cはlibclipに含まれるようになったからです。
 *	  ・"CLiP Files"を削除しました。CLiPライブラリの各モジュールはlibclipに含まれるようになったからです。
 *	  当ツールの動作には変更有りません。
 *	* Tue May 09 22:14:45 JST 2017 Naoyuki Sawa
 *	- ライブラリのバグ修正に伴う、ツールのリビルドを行いました。
 *	  詳細は、clipmisc.cの'Sun May 07 09:13:01 JST 2017 Naoyuki Sawa'のコメントを参照して下さい。
 *	- 前回更新後～本日までの間に、他のツール群に対して行ったのと同じビルド設定の変更を、当ツールにも対しても適用しました。
 */
#include "app.h"
#define VERSION "20170509" //バージョン定義
/****************************************************************************
 *	
 ****************************************************************************/
__declspec(noreturn) static void usage();
/****************************************************************************
 *	main
 ****************************************************************************/
#define Cmd_Max 255 //調整可
int nCmd;			//コマンド数
char*  TBL_Cmd[ Cmd_Max+1];	//コマンド名
char** TBL_Argv[Cmd_Max+1];	//コマンド引数		[0]にコマンド名も含む
HANDLE TBL_Proc[Cmd_Max+1];	//プロセスハンドル	(INVALID_HANDLE_VALUE=終了済み)
/*--------------------------------------------------------------------------*/
int main(int argc, char* argv[]) {
	int   exitCode = 0;	//終了コード
	char* glbuf = NULL;	//┬getline()用
	int   glcap = 0;	//┘
	int   iCmd;
	//--- コマンドラインオプションの処理 ---
	{
		int auto_dq = 0;	//'-q'オプションが指定されたら1にする。
		int no_exec = 0;	//'-n'オプションが指定されたら1にする。
		int opt;
		opterr = 0;	//オプションエラーは当プログラム内で処理するので、getopt()内でのエラー表示は不要です。
		while((opt = getopt(argc, argv, "f:qnh")) != -1) {
			switch(opt) {
			case 'f':
				{
					//コマンドファイルを開く。
					FILE* fp = fopen(optarg, "r");
					if(!fp) { die("%sが開けません。", optarg); }
					//コマンドファイルの各行を読み込んで…
					while(getline(&glbuf, &glcap, fp) != EOF) {
						//この行をコマンドラインと見なして分割する。
						char** strv = parse_cmdline(glbuf);
						int    strc = strv_length(strv);
						//空行でなければ…
						if(strc) {
							//コマンド数が多過ぎたら…
							if(nCmd >= Cmd_Max) {
								//エラー終了する。
								error(EXIT_FAILURE, 0, "コマンド数が多過ぎます。");
							}
							//コマンドを格納する。
							TBL_Cmd[ nCmd] = strv[0];	//コマンド名
							TBL_Argv[nCmd] = strv;		//コマンド引数		[0]にコマンド名も含む
							//コマンド数を増やす。
							nCmd++;
						}
					}
					//コマンドファイルを閉じる。
					fclose(fp);
				}
				break;
			case 'q':
				{
					auto_dq = 1;
				}
				break;
			case 'n':
				{
					no_exec = 1;
				}
				break;
			case 'h':
			default:
				usage();
			}
		}
		//オプション以降のコマンドライン引数が有れば…
		if(optind < argc) {
			//オプション以降の各コマンドライン引数について…
			do {
				//コマンドライン引数を取得する。
				char* str = argv[optind];
				//'|'ならば…
				// - 実際にコマンドラインで指定する時は'^|'のようにエスケープせよ。
				if(!strcmp(str, "|")) {
					//現在のコマンドが空ならば…
					if(!TBL_Cmd[nCmd]) {
						//エラー終了する。
						error(EXIT_FAILURE, 0, "空のコマンドが有ります。");
					}
					//コマンド数を増やす。
					nCmd++;
				//'|'以外ならば…
				} else {
					//コマンド数が多過ぎたら…
					if(nCmd >= Cmd_Max) {
						//エラー終了する。
						error(EXIT_FAILURE, 0, "コマンド数が多過ぎます。");
					}
					//現在のコマンドが空ならば…
					if(!TBL_Cmd[nCmd]) {
						//新しいコマンドを開始する。
						TBL_Cmd[ nCmd] = str;			//コマンド名
						TBL_Argv[nCmd] = strv_new(str, NULL);	//コマンド引数		[0]にコマンド名も含む
					//現在のコマンドが空でなければ…
					} else {
						//現在のコマンドに引数を追加する。
						strv_push(&TBL_Argv[nCmd], str);	//コマンド引数		[0]にコマンド名も含む
					}
				}
			} while(++optind < argc);
			//最後のコマンドが空ならば…
			if(!TBL_Cmd[nCmd]) {
				//エラー終了する。
				error(EXIT_FAILURE, 0, "空のコマンドが有ります。");
			}
			//コマンド数を増やす。
			nCmd++;
		}
		//コマンドが指定されていなければ、使い方を表示して終了する。
		if(!nCmd) { usage(); }
		//'-p'オプションが指定されていたら…
		if(auto_dq) {
			//各コマンドについて…
			for(iCmd = 0; iCmd < nCmd; iCmd++) {
				int iArg;
//{{2016/01/27追加:'cmd.exe /C'を含むコマンドは、特例として、各コマンド引数を"～"で囲む処理を行わない。
				// * Wed Jan 27 22:56:05 JST 2016 Naoyuki Sawa
				// - '-p'オプションが指定されていても、'cmd.exe /C'を含むコマンドは、特例として、各コマンド引数を"～"で囲む処理を行わないようにしました。
				//   'cmd.exe /C'は引用符の解釈が特殊で、各コマンド引数を"～"で囲むと、ほぼエラーになるからです。
				// - 例えば「cmd.exe /C "DIR" "."」とすると、「cmd.exe /C DIR" ".」と解釈されてエラーになります。
				//   詳細は、「cmd.exe /?」を参照して下さい。
				// - 上記の問題を防ぐため、'cmd.exe /C'を含むコマンドは、特例として、各コマンド引数を"～"で囲む処理を行わない事にしました。
				//   これで全てが解決する訳ではなく、'cmd.exe /C'を含むコマンドでも引数の一部を"～"で囲む必要がある場合、エラーになります。
				//   そのようなケースは、個別に明示的に"～"で囲って対応して下さい。(ただしエスケープの指定がかなり大変だと思います。)
				//   'cmd.exe /C'を含むコマンドで、本当に"～"で囲む必要が有るケースは少ないので、ほとんどのケースは今回の変更で解決出来ると思います。
				//各引数について…
				for(iArg = 0; TBL_Argv[iCmd][iArg]; iArg++) {		//コマンド引数		[0]にコマンド名も含む
					char fname[_MAX_FNAME], ext[_MAX_EXT];
					//この引数がパス名であると仮定して展開してみる。
					splitpath(TBL_Argv[iCmd][iArg], NULL, NULL, fname, ext);
					//ファイル名が'cmd'で、かつ、拡張子が'.exe'か,又は,省略されていたら、'cmd.exe'であると見なす。
					// - 'cmd.exe'の検出は、TBL_Argv[iCmd][0]だけでなく、それ以降の引数に対しても行う事に注意せよ。
					//   'env.exe FOO=BAR cmd.exe /C ～'のようにして、'cmd.exe'を実行する事も有るからです。
					if(!strcasecmp(fname, "cmd") && (!strcasecmp(ext, ".exe") || !ext[0])) {
						//'cmd.exe'以降の引数に'/C'が有れば、'cmd.exe /C'を含むコマンドであると見なす。
						//'cmd.exe /C'を含むコマンドは、特例として、各コマンド引数を"～"で囲む処理を行わない。
						for(iArg++; TBL_Argv[iCmd][iArg]; iArg++) {
							if(!strcasecmp(TBL_Argv[iCmd][iArg], "/C")) { goto L_SKIP; }
						}
						//'cmd.exe'以降の引数に'/C'が無ければ、普通どおり各コマンド引数を"～"で囲む処理を行う。
						break;	//必要。これを忘れると、外側のループが継続してしまい、TBL_Argv[iCmd][iArg]の終端を突き抜けてしまう。
					}
				}
//}}2016/01/27追加:'cmd.exe /C'を含むコマンドは、特例として、各コマンド引数を"～"で囲む処理を行わない。
				//各引数について…
				for(iArg = 0; TBL_Argv[iCmd][iArg]; iArg++) {		//コマンド引数		[0]にコマンド名も含む
					//コマンド引数を"～"で囲む。
					TBL_Argv[iCmd][iArg] = strdup_printf("\"%s\"", TBL_Argv[iCmd][iArg]);	//★重要★ _spawnvp()のcmdname引数は"～"で囲むとエラーになる。'-q'オプションが指定された場合、TBL_Argv[iCmd][0]に格納するコマンド名は"～"で囲って良いが、TBL_Cmd[iCmd]に格納するコマンド名は"～"で囲ってはいけない。従って、TBL_Cmd[iCmd]とTBL_Argv[iCmd][0]を常に共通化する事は出来ない。
				}
//{{2016/01/27追加:'cmd.exe /C'を含むコマンドは、特例として、各コマンド引数を"～"で囲む処理を行わない。
L_SKIP:				;
//}}2016/01/27追加:'cmd.exe /C'を含むコマンドは、特例として、各コマンド引数を"～"で囲む処理を行わない。
			}
		}
		//'-n'オプションが指定されていたら…
		if(no_exec) {
			//各コマンドについて…
			for(iCmd = 0; iCmd < nCmd; iCmd++) {
				int iArg, i;
				//コマンド名を表示する。
				fprintf(stderr, "%s:", TBL_Cmd[iCmd]);
				//コマンド引数を表示する。
				for(iArg = 0; TBL_Argv[iCmd][iArg]; iArg++) {		//コマンド引数		[0]にコマンド名も含む
					fprintf(stderr, " %s", TBL_Argv[iCmd][iArg]);
				}
				fputc('\n', stderr);
				//コマンド名の下線は表示しない事にした。
				for(i = strlen(TBL_Cmd[iCmd]) + 1/*':'*/; i; i--) { fputc(' ', stderr); }
				//コマンド引数の下線を表示する。
				for(iArg = 0; TBL_Argv[iCmd][iArg]; iArg++) {		//コマンド引数		[0]にコマンド名も含む
					fputc(' ', stderr);
					for(i = strlen(TBL_Argv[iCmd][iArg]); i; i--) { fputc('-', stderr); }
				}
				fputc('\n', stderr);
			}
			//'-n'オプションが指定された場合は、上記のコマンド内容を表示する処理だけを行い、コマンドを実行せずに終了する。
			//makeの'-n'オプションと同様です。
			// - '-n'オプションが指定された場合の終了コードは、エラーを返す事にしました。
			//   バッチスクリプトやMakefileの中で、うっかり(又は意図的に)'-n'オプションを指定した場合、そこでビルド処理を停止するのが適切だと思うからです。
			exit(EXIT_FAILURE);
		}
	}
	//--- コマンドパイプラインを作成する処理 ---
	{
		int fd[2];
		//各コマンドについて…
		for(iCmd = 0; iCmd < nCmd; iCmd++) {
			int save_r, save_w;
			//最初のコマンドでなければ…
			if(iCmd) {
				//標準入力のハンドルを退避する。
				if((save_r = _dup(_fileno(stdin))) == -1) { DIE(); }
				//前のコマンドのパイプの読み取り側と、このコマンドの標準入力を結合する。
				if(_dup2(fd[0/*r*/], _fileno(stdin))) { DIE(); }
				//パイプの読み取り側はもう不要なので、閉じる。
				if(_close(fd[0/*r*/])) { DIE(); }
			}
			//最後のコマンドでなければ…
			if(iCmd != (nCmd - 1)) {
				//パイプを作成する。
				if(_pipe(fd, 0, _O_TEXT|_O_NOINHERIT)) { DIE(); }
				//標準出力のハンドルを退避する。
				if((save_w = _dup(_fileno(stdout))) == -1) { DIE(); }
				//このコマンドの標準出力と、次のコマンドのパイプの書き込み側を結合する。
				if(_dup2(fd[1/*w*/], _fileno(stdout))) { DIE(); }
				//パイプの書き込み側はもう不要なので、閉じる。
				if(_close(fd[1/*w*/])) { DIE(); }
			}
			//コマンドを実行する。
			TBL_Proc[iCmd] = (HANDLE)_spawnvp(P_NOWAIT, TBL_Cmd[iCmd], TBL_Argv[iCmd]);		//★重要★ _spawnvp()のcmdname引数は"～"で囲むとエラーになる。'-q'オプションが指定された場合、TBL_Argv[iCmd][0]に格納するコマンド名は"～"で囲って良いが、TBL_Cmd[iCmd]に格納するコマンド名は"～"で囲ってはいけない。従って、TBL_Cmd[iCmd]とTBL_Argv[iCmd][0]を常に共通化する事は出来ない。
			//コマンドを実行出来なかったら…
			if(TBL_Proc[iCmd] == INVALID_HANDLE_VALUE) {
				//エラーメッセージを表示する。
				error(0/*まだ終了しない*/, errno, "%sを実行できません。", TBL_Cmd[iCmd]);
				//終了コードを格納する。
				exitCode = EXIT_FAILURE;
				//コマンド数を変更する。
				// - これ以降のTBL_Proc[]には'INVALID_HANDLE_VALUE'が格納されていないので、残っているコマンドを強制終了する処理が参照しないようにするためです。
				nCmd = iCmd;
				//既に実行してしまったコマンドを強制終了する処理へ移行する。
				goto L_END;
			}
			//最初のコマンドでなければ…
			if(iCmd) {
				//標準入力のハンドルを復帰する。
				if(_dup2(save_r, _fileno(stdin))) { DIE(); }
				//退避したハンドルはもう不要なので、閉じる。
				if(_close(save_r)) { DIE(); }
			}
			//最後のコマンドでなければ…
			if(iCmd != (nCmd - 1)) {
				//標準出力のハンドルを復帰する。
				if(_dup2(save_w, _fileno(stdout))) { DIE(); }
				//退避したハンドルはもう不要なので、閉じる。
				if(_close(save_w)) { DIE(); }
			}
		}
	}
	//--- 全てのコマンドが終了するまで待つ処理 ---
	{
		int nStillActive = nCmd;	//終了していないコマンドの数
		int tmpExitCode;
		//全てのコマンドが終了するまで待つ。
		for(;;) {
			//各コマンドについて…
			for(iCmd = 0; iCmd < nCmd; iCmd++) {
				//このコマンドがまだ終了していなければ…
				if(TBL_Proc[iCmd] != INVALID_HANDLE_VALUE) {
					//このコマンドの終了状態を取得する。
					if(!GetExitCodeProcess(TBL_Proc[iCmd], &tmpExitCode)) { DIE(); }
					//このコマンドが終了していたら…
					if(tmpExitCode != STILL_ACTIVE) {
						//このコマンドのプロセスハンドルはもう不要なので、閉じる。
						if(!CloseHandle(TBL_Proc[iCmd])) { DIE(); }
						TBL_Proc[iCmd] = INVALID_HANDLE_VALUE;
						//終了していないコマンドの数を減らす。
						nStillActive--;
						//終了コードがエラーならば、終了コードを格納して、抜ける。
						if(tmpExitCode) { exitCode = tmpExitCode; break; }
					}
				}
			}
			//全てのコマンドが終了したか,又は,エラーが発生したら、抜ける。
			// - 両方が同時に起こる事も有り得る事に注意せよ。たとえば最後のコマンドがエラー終了した場合にそうなる。
			if(!nStillActive || exitCode) { break; }
			//残りのタイムスライスを譲る。これは必須ではない。
			Sleep(0);
		}
	}
L_END://コマンドを実行出来なかった時のジャンプ位置
	//--- 途中でエラーが発生した場合に、残っているコマンドを強制終了する処理 ---
	{
		int tmpExitCode;
		//各コマンドについて…
		for(iCmd = 0; iCmd < nCmd; iCmd++) {
			//このコマンドがまだ終了していなければ…
			if(TBL_Proc[iCmd] != INVALID_HANDLE_VALUE) {
				//このコマンドのプロセスを強制終了する。
				// - TerminateProcess()が失敗を返してもDIE()してはいけない。理由は以下の通りです。
				//   時間差で、このプロセスが能動的に終了済みである可能性が有る。
				//   終了済みのプロセスに対するTerminateProcess()は、失敗を返す。
				//   意図通りの動作なので、TerminateProcess()が失敗を返してもDIE()してはいけない。
				if(!TerminateProcess(TBL_Proc[iCmd], 9/*SIGKILL*/)) { /*DIE();*/ }	//終了コードは何でも良いのだが判り易いように9(SIGKILL)とした。必須ではない。
				//このコマンドが確実に終了するまで待つ。
				for(;;) {
					//このコマンドの終了状態を取得する。
					if(!GetExitCodeProcess(TBL_Proc[iCmd], &tmpExitCode)) { DIE(); }
					//このコマンドが終了していたら、抜ける。
					if(tmpExitCode != STILL_ACTIVE) { break; }
					//残りのタイムスライスを譲る。これは必須ではない。
					Sleep(0);
				}
				//このコマンドのプロセスハンドルはもう不要なので、閉じる。
				if(!CloseHandle(TBL_Proc[iCmd])) { DIE(); }
				TBL_Proc[iCmd] = INVALID_HANDLE_VALUE;
			}
		}
	}
//{{サービス
	//コマンドの終了コードがマイナスならば、絶対値を取って返す事にしました。
	//ERRORLEVELがマイナスの時に、'IF ERRORLEVEL 1 ～'で判定すると、エラーを見落とす問題が発生し易いからです。
	//実際の所、コマンドの終了コードにマイナスを指定する事自体が間違いです。
	//本来は、'exit(-1)'のような終了コードは、使うべきではありません。
	//しかし、昔作った自作のツールや、UnxUtilsの一部のコマンド(tr.exe等)でも、マイナスのエラーコードを返す事が有ります。
	//それらのツールを全て修正する事は困難なので、サービスとして、当ツールが終了コードを補正する事にしました。
	//とはいえ、当ツールを使わずに直接それらのツールを実行した場合はこの補正は効きませんので、注意が必要です。
	exitCode = abs(exitCode);
//}}サービス
	//終了コードを返す。
	return exitCode;
}
/****************************************************************************
 *	
 ****************************************************************************/
__declspec(noreturn) static void usage() {
	fprintf(stderr, "pipefail.exe - パイプライン途中のエラーも検出するパイプライン実行ツール (%s)\n", VERSION);
	fprintf(stderr, "Copyright (C) 2016-2017 Naoyuki Sawa\n");
	fprintf(stderr, "\n");
	fprintf(stderr, "USAGE:\n");
	fprintf(stderr, "  pipefail.exe [OPTION ...] [COMMAND [ARG ...] [^| COMMAND [ARG ...]]]\n");
	fprintf(stderr, "\n");
	fprintf(stderr, "OPTIONS:\n");
	fprintf(stderr, "  -fコマンドファイル名 実行するコマンドを、コマンドファイルから読み込みます。\n");
	fprintf(stderr, "  -q                   全てのコマンド引数を、個別に\"～\"で囲みます。\n");
	fprintf(stderr, "                       (※ただし、'cmd.exe /C'を含むコマンドには適用しません。)\n");
	fprintf(stderr, "  -n                   実行するコマンドの表示だけを行い、実際の実行を行いません。\n");
	fprintf(stderr, "  -h                   使い方を表示します。\n");
	fprintf(stderr, "\n");
	fprintf(stderr, "EXAMPLE:\n");
	fprintf(stderr, "  ①pipefail.exe cat.exe sample.txt ^| dpesc5c.exe ^| wc.exe -c\n");
	fprintf(stderr, "    'cat.exe sample.txt'⇒'dpesc5c.exe'⇒'wc.exe -c'をパイプで繋いで実行します。\n");
	fprintf(stderr, "    どれか一つでもエラー終了した場合は、pipefail.exeもエラー終了します。\n");
	fprintf(stderr, "  ②pipefail.exe -f cmdlist1.txt\n");
	fprintf(stderr, "    □cmdlist1.txtの内容\n");
	fprintf(stderr, "    │cat.exe sample.txt\n");
	fprintf(stderr, "    │dpesc5c.exe\n");
	fprintf(stderr, "    │wc.exe -c\n");
	fprintf(stderr, "    ①と同じ処理を行います。コマンドファイルの中には、空行が有っても構いません。\n");
	fprintf(stderr, "  ③pipefail.exe -f cmdlist2.txt wc.exe -c\n");
	fprintf(stderr, "    □cmdlist.txt2の内容\n");
	fprintf(stderr, "    │cat.exe sample.txt\n");
	fprintf(stderr, "    │dpesc5c.exe\n");
	fprintf(stderr, "    ①と同じ処理を行います。コマンドファイルの内容とコマンドラインの内容を、マージして実行する例です。\n");
	fprintf(stderr, "\n");
	fprintf(stderr, "注意:\n");
	fprintf(stderr, "  ・引用符などの、エスケープが必要な文字を使う場合、二重にエスケープする必要が有る事に注意して下さい。\n");
	fprintf(stderr, "    当ツールと、実行されるコマンドが、それぞれエスケープを解除するからです。\n");
	fprintf(stderr, "    '-q'オプションを指定すれば、大抵の場合は解決しますが、確実では有りません。\n");
	fprintf(stderr, "    '-n'オプションを指定して、当ツールが実行するコマンド文字列を確認して下さい。\n");
	fprintf(stderr, "  ・上記のEXAMPLE:の①で、'^|'と書いている理由は、'|'がcmd.exeによって解釈されないようにするためです。\n");
	fprintf(stderr, "    '^'を付け忘れると'|'がcmd.exeによって解釈され、cmd.exeによってパイプ実行される事に注意して下さい。\n");
	exit(EXIT_FAILURE);
}
