雑に作ったPythonスクリプトを雑に使う(Windows)

TL;DR

  • Windowshogehoge.pyを適当な場所に保存した後、任意のカレントディレクトリからこれを> hogehogeで呼べるようにしたい。
  • 言い換えると、以下のような真似ができるようになるdeploy.exeが欲しい。
C:\some\dir> echo print("hoge") > hogehoge.py
C:\some\dir> deploy hogehoge.py
C:\some\dir> cd C:\another\dir
C:\another\dir> hogehoge
hoge 
  • ……ので作りました。
  • でも力技気味なのでもう少しスマートな方法があったら教えてください。
    • →ありました(コメント欄)

本編

経緯

簡単なツールを雑に作るとき、スクリプト言語は大層便利です。

具体的には、LoL*1で内戦*2をするときにサクッとチーム決めをしたいなら、Pythonの出番です:

import sys
import os.path
import random


members = {
    "a": "A-man",
    "b": "B-man",
    "c": "C-man",
    "d": "D-man",
    "e": "D-man",
}


def team_gacha(s):
    a = list(map(lambda c: members[c], s))
    random.shuffle(a)
    return a[:len(a) // 2], a[len(a) // 2:]


if __name__ == "__main__":
    if len(sys.argv) > 1:
        print(team_gacha(sys.argv[1]))
    else:
        print(f"Usage: {os.path.basename(__file__)} <member-string>")
        print()
        print("Following chars are available in <member-string>:")
        for k, v in members.items():
            print(f"\t{k}: {v}")

これをteam_gacha.pyで保存すれば……

> python team_gacha.py abcd
['A-man', 'C-man'] ['B-man', 'D-man']

簡単です!これでいつでもLOLで内戦ができます!

……と、コードまで貼り付けておいて何ですが、そんな話はどうでもいいわけです。 重要なのはこの後です。

さっきのように最高の便利スクリプトを作ったとして、これをいつまでもデスクトップに置いておくわけにはいきません。邪魔なので。 すると当然適当なディレクトリにお引越しをするわけです。 では、引越し後のteam_gacha.pyを適当なカレントディレクトリから実行するためにはどうすればよいでしょう? 答えはこうです:

> python C:\my\tools\dir\team_gacha.py abcd

あれ???面倒では???

ここで1つ名案があります。引越し先のC:\my\tools\dirにパスを通せばいいのです。 しかし残念でした。適当なカレントディレクトリから不思議な力で呼び出せるのはパスの通ったディレクトリにある.exe.batだけです*3

「じゃあteam_gacha.pyを呼ぶコマンドを.batに書いてパスの通った場所に置けばいいのでは?」と思ったあなたは疑いようもなく正しいです。 でも僕は> team_gacha.batとすら打ちたくないんです。やだーーー!!!> team_gachaだけで実行したいーーーー!!!

この七面倒くさい客*4を黙らせるにはteam_gacha.pyを呼ぶ.exeを作るしかありません!

"コマンドを叩くexe"を生成するexeを作る

以下、解決編です。目新しいことは特にやってないので、ソースコード斜め読みでも十分な気がする。

さて、先程の客*5の要求を一般化すると「適当なコマンドを叩く.exeが欲しい」ということになります。要は、本来は.batでやるようなことを.exeでやろうというのです。

実際のところそれ自体はなんら難しいことではありません。C言語stdlib.hにはsystem()というコマンドを叩くための関数があるからです。しかし、.batを書く感覚でいちいちC書きたいかっていうとそんなわけはないわけで……。つまり何が言いたいかというと、「適当なコマンドを叩く.exe」を簡単に作るツールが欲しいのです。欲を言えばそのツールも簡単に、つまり> hogehogeのように呼び出せたら嬉しいので、ツールは.exeの姿をしていることが好ましいです。

そんなことできるのか……と一瞬思いましたが、何のことはなく、実行時にソースコードを生成してビルドまでやってしまえばいいだけです。特にひねりも何もないのでソースだけ乗っけて説明は省きます。こちらがシェフの気まぐれソースのソース詰めでございます。

C言語を大学の講義以外で書いたの初めてでは……?(過言)

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define BUF_SIZE 1024
#define CNAME_SIZE 256

int main(int argc, char const *argv[])
{
  if (argc < 3) {
    printf("Usage: cm2exe <command-name> <command>\n");
    return EXIT_SUCCESS;
  }

  // 第1引数: 生成するexeの名前
  const char* cname = argv[1];
  // 第2引数: 生成するexeが実行するコマンド
  const char* command = argv[2];

  if (strlen(cname) > CNAME_SIZE) {
    printf("CM2EXE ERROR: Command name length must be less than %d.\n", CNAME_SIZE);
    return EXIT_FAILURE;
  }

  // 一時的な作業ディレクトリを生成
  if (system("mkdir temp")) {
    printf("CM2EXE ERROR: \"temp\" directory already exists.\n");
    return EXIT_FAILURE;
  }

  // 一時的なソースコードを生成
  FILE* source_code = fopen("temp/source_code.c", "w");
  if (source_code == NULL) {
    printf("CM2EXE ERROR: cannot create \"temp/source_code.c\".\n");
    return EXIT_FAILURE;
  }
  fprintf(source_code,
    "#include <stdio.h>\n"
    "#include <stdlib.h>\n"
    "#include <string.h>\n"
    "#define BUF_SIZE %d\n"
    "int main(int argc, char const *argv[]) {"
    "char c[BUF_SIZE] = {};"
    "int len = 0;"
    "strcat(c, \"%s\");"
    "len += strlen(c);"
    "if (len > BUF_SIZE) {"
    "printf(\"CM2EXE ERROR: Command length must be less than %%d\", BUF_SIZE);"
    "return 1;"
    "}"
    "for (int i = 1; i < argc; i++) {"
    "len += 1 + strlen(argv[i]);"
    "if (len > BUF_SIZE) {"
    "printf(\"CM2EXE ERROR: Command length must be less than %%d\", BUF_SIZE);"
    "return 1;"
    "}"
    "strcat(c, \" \");"
    "strcat(c, argv[i]);"
    "}"
    "system(c);"
    "return 0;"
    "}",
    BUF_SIZE,
    command);
  fclose(source_code);

  // ビルド
  char buf[CNAME_SIZE + 32];
  sprintf(buf, "gcc temp/source_code.c -o %s", cname);
  system(buf);

  // 一時ディレクトリと一時ファイルを消去
  system("rd temp /S /Q");

  return EXIT_SUCCESS;
}

ちなみにソースの中にあるソースはこんな感じ:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUF_SIZE 1024

int main(int argc, char const *argv[]) {
  char c[BUF_SIZE] = {};
  int len = 0;
  strcat(c, "%s");  // %s にはコマンドが入る
  len += strlen(c);
  
  if (len > BUF_SIZE) {
    printf("CM2EXE ERROR: Command length must be less than %d", BUF_SIZE);
    return 1;
  }

  for (int i = 1; i < argc; i++) {
    len += 1 + strlen(argv[i]);
    if (len > BUF_SIZE) {
      printf("CM2EXE ERROR: Command length must be less than %d", BUF_SIZE);
      return 1;
    }

    strcat(c, " ");
    strcat(c, argv[i]);
  }
  
  system(c);
  return 0;
}

これをビルドしたものをcm2exe.exeとして、> cm2exe hogehoge hugahugaとすれば、hugahugaを叩くだけのhogehoge.exeが出来上がるというわけです。

ただ、以下には十分注意すべきです:

  • 任意コマンドを叩けるので取り扱い注意
  • hugahuga部分は文字列リテラルに直接放り込まれるのでエスケープに注意

「"コマンドを叩くexe"を生成するexe」を自動で叩くexeを生成する

これからは新しく便利なhogehoge.pyを書いたら

> move hogehoge.py C:\my\tools\dir
> cm2exe hogehoge "python C:\my\tools\dir\hogehoge.py"
> move hogehoge.exe C:\my\tools\dir
> hogehoge
very cool output!!

するだけ!

……手数が多い。自動化します。今度はPythonで書きましょう。

from pathlib import Path
import os
import sys
import shutil
import subprocess


BIN_DIR = r"exeを置いておく場所"
LIB_DIR = r"スクリプトを置いておく場所"
SCRIPT_CMD = {
    # 拡張子: 実行のためのコマンド
    ".py": "python",
    ".exs": "elixir",    # 別にPythonに限らずとも使える
}

# ファイル名を拡張子とそれ以外に分割する
def split_basename(path):
    basename = Path(str(path)).name
    tokens = str(basename).split(".")
    if len(tokens) < 2:
        return basename, None
    else:
        *base, ext = tokens
        return ".".join(base), "." + ext


def deploy(script_path):
    # 実行コマンドを決定するために拡張子を確認
    name = Path(script_path).name
    base, ext = split_basename(script_path)
    if ext is None:
        raise Exception(f"No extension in {script_path}.")
    if ext not in SCRIPT_CMD:
        raise Exception(f"Extension \".{ext}\" is not supported.")

    try:
        # スクリプトを所定の場所にコピー
        shutil.copy(script_path, LIB_DIR)

        # スクリプトを呼び出すexeを生成
        subprocess.run([
            "cm2exe",
            f"{BIN_DIR}/{base}",
            f"{SCRIPT_CMD[ext]} {LIB_DIR}/{name}"
        ])
    except Exception as e:
        # clean up
        copied_script = Path(LIB_DIR) / name
        if copied_script.exists():
            copied_script.unlink()
        deploied_bin = Path(BIN_DIR) / f"{base}.exe"
        if deploied_bin.exists():
            deploied_bin.unlink()
        raise e


if __name__ == "__main__":
    if len(sys.argv) > 1:
        try:
            deploy(sys.argv[1])
        except Exception as e:
            print(f"Deploy Error: {e}")
    else:
        print(f"Usage: {Path(__file__).name} <script-path>")

こちらをdeploy.pyとして、> python deploy.py deploy.pyで自分自身をデプロイします。すると今後は、冒頭の

> move hogehoge.py C:\my\tools\dir
> cm2exe hogehoge "python C:\my\tools\dir\hogehoge.py"
> move hogehoge.exe C:\my\tools\dir
> hogehoge
very cool output!!

> deploy hogehoge.py
> hogehoge
very cool output!!

で完結するようになりました。めでたしめでたし。

おわりに

絶対もっといい方法あるでしょ…………

追記

コメント欄にて素晴らしい方法を教えていただきました。eblblさんありがとうございました!

*1:League of Legendsの略。オンラインのPCゲームで、メインは5vs5のチーム戦。

*2:身内同士で行う対戦のこと。

*3:あとdllとかも。

*4:筆者のことである。

*5:言わずもがな筆者である。