はじめに
この記事は、Paper Plane xUI(PPx)というファイラーを使い、シェルのコマンド置換機能をコマンドプロンプトでも使えるように実装してみた、という記事です。
コマンド置換とは?
コマンドの標準出力をコマンドに含める機能です、記法には`コマンド`
1と$(コマンド)
があり、$(コマンド)
は入れ子にできます。
$ pwd
/home/wordi/hoge
$ echo pwd
pwd
$ echo `pwd`
/home/wordi/hoge
PPxでコマンド置換をする仕組み
PPxからコマンド実行をする際にはコマンドプロンプトを使用します、しかしコマンドプロンプトは標準ではコマンド置換に対応していません。
なので、PPxから呼び出せるスクリプト(JScript)からコマンド文字列をパースし、一つ一つコマンドを実行していきます、要は力技です。
シェルとの違い
コマンドプロンプトで実行しているコマンドに、コマンド置換処理を独自実装しているため、シェルとは動作が違うところが幾つかあります。
- シェルとの違い
- シングルクォーテーション(')は特殊文字ではない
- 単なる文字、そのため文字列は基本エスケープされる2
- コマンド置換は常に処理される
-
「foo "\$(bar 1 2)"」
と$をエスケープしても処理される(実装を省いたため) - 半面、
「foo \$(bar 1 2)」
は実行できる
-
- シングルクォーテーション(')は特殊文字ではない
ツール構成
スクリプト
//!*script
// 汎用ライブラリ
(function() {
var o = {};
var _insts = new Object();
o.getWshShell = function() { return _insts.sh = _insts.sh || new ActiveXObject("WScript.Shell"); }
// 同期実行
o.execSync = function(cmd) {
var oRet = o.getWshShell().Exec(cmd);
while (oRet.Status == 0) PPx.Sleep(100);
return oRet;
}
return o;
})();
//! script
//
// コマンド実行結果を返す(コマンド置換対応)
//
function _read(file) {
var fs = new ActiveXObject("Scripting.FileSystemObject");
var cwd = fs.GetParentFolderName(PPx.ScriptFullName);
var fullpath = cwd + '\\' + file;
if (!fs.FileExists(fullpath)) { PPx.Echo('* file not found:' + file); PPx.Quit(-1); }
var f = fs.OpenTextFile(fullpath, 1);
try { return f.ReadAll(); } finally { f.Close(); }
}
var util = eval(_read('./lib/util.js'));
///
// 環境変数の記号を置換する
function replaceEnvSymbol(cmd, sep, newsep) {
while (true) {
var m = cmd.match(new RegExp(sep + "[^ \\\/]+?" + sep));
if (!m) break;
var left = cmd.substring(0, m.index);
var target = cmd.substring(m.index + sep.length, m.lastIndex - sep.length);
var right = cmd.substring(m.lastIndex);
cmd = left + newsep + target + newsep + right;
}
return cmd;
}
function escapeEnv(cmd) { return replaceEnvSymbol(cmd, '%', '_@ENV@_'); }
function restoreEnv(cmd) { return replaceEnvSymbol(cmd, '_@ENV@_', '%'); }
// 文字列を置換する
var g_escape_i = 1;
var g_escaped = {};
function escapeString(cmd) {
while (true) {
var m = null;
if (!m) m = cmd.match(/'.*?[^\\]'/);
if (!m) m = cmd.match(/".*?[^\\]"/);
if (!m) break;
var left = cmd.substring(0, m.index);
var l = cmd.substring(m.index, m.index + 1);
var target = cmd.substring(m.index + 1, m.lastIndex - 1);
var r = cmd.substring(m.lastIndex - 1, m.lastIndex);
var right = cmd.substring(m.lastIndex);
// 文字列内のコマンド置換を事前に実行しておく
target = internalCommand(target).cmd;
var key = "__@ESCAPED{" + g_escape_i++ + "}@__";
g_escaped[key] = l + target + r;
cmd = left + key + right;
}
return cmd;
}
function restoreString(cmd) {
var m;
while ((m = cmd.match(/__@ESCAPED{\d+}@__/))) {
var key = cmd.substring(m.index, m.lastIndex);
cmd = cmd.replace(key, g_escaped[key]);
}
return cmd;
}
// 内部のコマンドを実行し、結果へと置き換える
function internalCommand(_cmd) {
// 文字列内のコマンド置換と混ざらないように退避
var cmd = escapeString(_cmd);
var m, l, r;
if ((m = cmd.match(/`.+?`/))) {
l = '`';
r = '`';
} else if ((m = cmd.match(/\$\((?!.*\$\().*?\)/))) {
l = '$(';
r = ')';
}
if (!m) {
return {
'cmd': _cmd,
'exec': false
};
}
// 内部のコマンドを発見
var left = cmd.substring(0, m.index);
var target = cmd.substring(m.index + l.length, m.lastIndex - r.length);
var right = cmd.substring(m.lastIndex);
left = restoreString(left);
target = restoreString(target);
right = restoreString(right);
var ret = internalCommand(target);
if (ret.exec) {
// コマンドのネスト
cmd = left + l + ret.cmd + r + right;
} else {
var ret = util.execSync('cmd /c ' + target);
// NOTE : コマンド置換された後は、複数行の結果は一行にまとめられ、末尾の改行も削除される
var stdout = ret.StdOut.ReadAll().replace(/\r?\n/g, ' ').replace(/ $/, '');
var stderr = ret.StdErr.ReadAll().replace(/\r?\n/g, ' ').replace(/ $/, '');
if (ret.ExitCode != 0) {
PPx.Echo('エラー発生!\n'
+ 'command:' + target + "\n"
+ 'StdOut:' + stdout + "\n"
+ 'StdErr:' + stderr);
PPx.Quit(-1);
}
cmd = left + stdout + right;
}
return {
'cmd': cmd,
'exec': true
};
}
var cmd = PPx.Arguments.item(0);
while (true) {
// エイリアス/マクロ文字を展開、環境変数はそのまま
while (true) {
var exec = false;
cmd = escapeEnv(cmd); // 環境変数をPPx.Extractの対象にならないように退避(%PATH% -> _@_PATH_@_)
if (cmd.match(/%/)) { // エイリアス/マクロ文字を展開
cmd = PPx.Extract(cmd);
exec = true;
}
cmd = restoreEnv(cmd); // 退避してた環境変数を戻す(_@_PATH_@_ -> %PATH%)
if (!exec) break;
}
var ret = internalCommand(cmd);
cmd = ret.cmd;
if (!ret.exec) break;
}
PPx.Result = cmd;
スクリプト解説
internalCommand関数はコマンド置換対象のコマンドを取り出して実行します。
その際、$(コマンド)
は入れ子になる場合があるため、正規表現を使って一番内側を取り出します。
/\$\((?!.*\$\().*?\)/
正規表現内の(?!.*\$\().*?
が、$(
を含まない0以上の最短一致で、あとは\$\(
と\)
で囲めば、一番内側のコマンド置換対象文字列がヒットします。
escapeString関数は、文字列にもコマンド置換を埋め込んでいる場合がありますが、それをinternalCommand関数の対象とならないように一端退避させる処理です。
次にメイン処理についてです。
var cmd = PPx.Arguments.item(0);
while (true) {
// エイリアス/マクロ文字を展開、環境変数はそのまま
while (true) {
var exec = false;
cmd = escapeEnv(cmd); // 環境変数をPPx.Extractの対象にならないように退避(%PATH% -> _@_PATH_@_)
if (cmd.match(/%/)) { // エイリアス/マクロ文字を展開
cmd = PPx.Extract(cmd);
exec = true;
}
cmd = restoreEnv(cmd); // 退避してた環境変数を戻す(_@_PATH_@_ -> %PATH%)
if (!exec) break;
}
var ret = internalCommand(cmd);
cmd = ret.cmd;
if (!ret.exec) break;
}
PPx.Result = cmd;
最初にエイリアス/マクロ文字のPPx.Extractで展開しますが、その際に環境変数も展開されると予期せぬ展開のされ方をします、
そのため、"%"を一端別の文字列へと置換して、PPx.Extractの展開対象から外しています。
その次にコマンド置換を行い、最終的に処理が不要となれば最終結果を戻り値として返します。
PPx.CFG
サンプルではCtrl+Shift+Sを今回のコマンド実行に割り当てます。
※自身の設定が消えないように、適用前にバックアップを取るか、追加取り込みをしてください
※スクリプトのパスは自身の環境へと合わせて下さい
K_edit = { ; 一行編集/PPe兼用
^\S ,
*set CMD=%"コマンド?"%{command%}
%OB cmd /c %*script(%0\script\ppxshell.js, %'CMD')
}
デモ
試しに、「(1 + 2) * (3 + 4)」をexprコマンドを使って計算してみます。
※MSYS2環境のコマンドを使っています
環境変数の他に、エイリアスやマクロ文字も使えます。
PPxディレクトリ内にある自作スクリプト群の一つをpecoで選んで、vimで開いてみたり
設定用意して
A_exec = { ; エイリアス
vim = E:\tool\editor\vim80-kaoriya-win64\vim.exe
topath = sed -E "s/\\\\/\//g" | tr -d ':'
frompath = sed -E "s/^\/([a-zA-Z])\//\1:\//" | sed -E "s/\//\\\\/g"
}
MSYS2上のlsを使うため、パスを「/e/tool/ppx」<-->「e:\tool\ppx」間で相互変換出来るエイリアスを用意しています。
いっその事これもエイリアスにしてみたり
A_exec = { ; エイリアス
vim = E:\tool\editor\vim80-kaoriya-win64\vim.exe
topath = sed -E "s/\\\\/\//g" | tr -d ':'
frompath = sed -E "s/^\/([a-zA-Z])\//\1:\//" | sed -E "s/\//\\\\/g"
openppxscript = %'vim' $(ls -d $(echo /%0script\* | %'topath') | peco | %'frompath')
}