Emacsでカーソル周辺のキーワードをpythonに渡して処理させる elisp スクリプト

Emacsでカーソル周辺のキーワードをpythonに渡して処理させる elisp スクリプト

eslip はこんなの。

(defun call-external-script-test ()
  (interactive)
  (let* (cur beg end str cmd)
    (save-excursion
      (setq cur (point))
      (setq beg (+ cur (skip-chars-backward "_A-Za-z0-9_. \t" (bolp))))
      (setq end (+ beg (skip-chars-forward  "_A-Za-z0-9_. \t" (eolp))))
      (setq str (buffer-substring-no-properties beg end))
      (setq cmd (concat "~/python/test.py \"" str "\""))
      ;;(message "beg=\"%d\"\nend=\"%d\"\nstring=\"%s\"\n" beg end str)
      (message (shell-command-to-string cmd))
      )))

eslip から呼び出す python スクリプトはたとえばこんなの

#!/usr/bin/python3

import sys

print("Hello Python! argv={}".format(sys.argv[1:]))

Emacs でたとえばこんな C ファイルを開く

if ((AAA.for_x == 1) ||
    (BBB  .  for_y == 1))
{
   printf("true\n");
}

AAA.for_x という文字列のどこかにカーソルを置いて M-x call-external-script-test <Enter> とすると、ミニバッファに Hello Python! argv=['AAA.for_x'] と表示される。

カーソル位置はどこでも良い。BBB . for_y みたいに空白があっても一つの文字列と見なすようにしてみた。結果→ Hello Python! argv=[’BBB . for_y ’]

Windows 上で、hta アプリの設定を JSON ファイルに保存する Javascript クラス。localstorage 風味。

MyStorage.hta

<!DOCTYPE html>
<html>

<head>
   <meta charset="utf-8" />
   <meta http-equiv="X-UA-Compatible" content="IE=edge">
   <title>Class Test</title>
   <meta name="viewport" content="width=device-width, initial-scale=1">
   <!-- <link rel="stylesheet" type="text/css" media="screen" href="ClassTest.css" />
   <script src="ClassTest.ja"></script> -->
</head>

<body>
    <script>
       "use strict"
       // class Storage {
       //  constructor() {
       //      this.hello = "Hello"
       //  }
       //  alert() {
       //      window.alert(this.hello)
       //  }
       // }

       // ------------------------------------------------------------
       // namespace My
       var My = My || {};

       // ------------------------------------------------------------
       // class Storage
       // this.rootKey:    ルートキー。このキーで JSON ファイルに保存される。文字列。
       // this.rootData:   ルートキーに対応する value として保存されるデータ。連想配列。
       My.Storage = function (rootKey) {
           this.rootKey = rootKey;
           this.rootData = {};
       }
       My.Storage.prototype = {
           // key に対応する value を返す。
           // 対応する value が内場合は unefined を返す。
           getItem: function (key) {
               if (key in this.rootData) {
                   return this.rootData[key];
               }
               return undefined;
           },
           // key, value のペアを保存する。
           // save() するまでは JSON ファイルに書き出されない。
           setItem: function (key, value) {
               this.rootData[key] = value;
           },
           // JSON ファイル名を返す。
           // この hta ファイルの名前から拡張子を .json にしたファイル名を作って返す。
           getJsonFileName: function () {
               let filename = location.pathname;
               // なぜか先頭に "/" が入っているので削除する
               filename = filename.replace(/^\//, "");
               // alert(filename);
               let jsonfile = filename.replace(/^(.+)\.hta$/gi, "$1.json");
               // alert(jsonfile);
               return jsonfile;
           },
           // JSON ファイルから this.rootKey に紐付く連想配列を読み込む。
           load: function () {
               const ForReading = 1;
               const TristateTrue = -1; // unicode
               let allData = {};
               let fso = new ActiveXObject("Scripting.FileSystemObject");
               let jsonfile = this.getJsonFileName();
               if (fso.FileExists(jsonfile)) {
                   const ForReading = 1;
                   let ts = fso.OpenTextFile(jsonfile, ForReading, true, TristateTrue);
                   if (!ts.AtEndOfStream) {
                       allData = JSON.parse(ts.ReadAll());
                   }
                   ts.Close();
               }
               this.rootData = allData[this.rootKey] || {};
           },
           // JSON ファイルに this.rootKey に紐付く連想配列を保存する。
           save: function () {
               const ForReading = 1;
               const TristateTrue = -1; // unicode
               let allData = {};
               allData[this.rootKey] = this.rootData;
               let fso = new ActiveXObject("Scripting.FileSystemObject");
               let jsonfile = this.getJsonFileName();
               if (fso.FileExists(jsonfile)) {
                   let ts = fso.OpenTextFile(jsonfile, ForReading, true, TristateTrue);
                   if (!ts.AtEndOfStream) {
                       let readed = JSON.parse(ts.ReadAll());
                       readed[this.rootKey] = this.rootData;
                       allData = readed;
                   }
                   ts.Close();
               }
               let ts = fso.CreateTextFile(jsonfile, true, TristateTrue);
               ts.Write(JSON.stringify(allData));
               ts.Close();
           },
       }
       // ------------------------------------------------------------

       window.onload = function () {
           window.resizeTo(800, 640);
           // alert("onload");
           let sto = new My.Storage("Class1");
           sto.load();
           alert(sto.getItem("Hello"));
           alert(sto.getItem("ハロー"));
           sto.setItem("Hello", "World");
           sto.setItem("ハロー", "ワールド")
           sto.save();
       }
   </script>
</body>

</html>

秀丸のキーアサインをEmacs風にする

秀丸のキーアサインを Emacs 風にする

VS CodeEmacs Friendly Keymap という Extension を入れて Emacs 風の キーマップにしたので、秀丸Emacs Friendly Keymap に合わせてキーマップを変更した。あと自分用に少し追加。

ダウンロード

ぼくの秀丸マクロ

  • LittleEmacs.KEY ファイル
  • kill-line.mac (C-k)
  • open-line.mac (C-o)
  • recenter-top-bottom.mac (C-l)
  • delete-other-window.mac (C-x 1)
  • comment-region.mac (C-c C-c)
  • keyboard-quit.mac (C-g)
  • tab-to-tabstop.mac (M-i)
  • just-one-space.mac (M-space)
  • delete-horizontal-space.mac (M-)
  • delete-blank-lines.mac (C-x C-o)
  • other-window (C-x o)
  • C-u.mac (C-u 7 2 = で = を72個挿入したりする)
  • C-x_o.mac (C-x o と C-x C-o を切り分ける)
  • C-x-experimental.mac (C-x.mac の実験用)

おまけ

  • hr.mac 水平線ひく
  • datetime.mac 日付挿入
  • hr-and-datetime.mac.mac 水平線引いて日付挿入
  • keycode.mac キーコードを表示 (C-x の実験用)
  • todo.mac TODO管理用のツール
  • today.mac 今日の日付と曜日を挿入 "04/23(木)" とか
  • backtab.mac Shift+TAB でカーソルより後ろの空白を一つ前のタブ位置まで消す

インストール

  1. .mac ファイルを秀丸のマクロフォルダに入れる。メニューからツール>マクロ用のフォルダ で開く。たぶんデフォルトではここ↓ C:\Users\name\AppData\Roaming\Hidemaruo\Hidemaru\Macro
  2. LittleEmacs.KEY を秀丸の設計ファイル置き場に置く。たぶんここ↓ C:\Users\name\AppData\Roaming\Hidemaruo\Hidemaru\Settings
  3. LittleEmacs.KEY ファイルを秀丸のメニュー>その他>キー割り当て>読込み で読み込む。

キーストローク

基本的に Emacs Friendly Keymap Extension と同じ。 いくつかのキーはうまく設定できなかった(後述)。

追加したキーストローク

いくつか、Emacs Fridndly Keymap にはない秀丸の機能をキーに割り当てたり、マクロを書いたりした。

キーストローク 機能 メモ
C-k カーソルより後ろをカット kill-region.mac で一応対応。
マクロ内の変数 #kill_whole_line が 1 なら行頭で C-k すると改行も切り取る。
0 なら Emacs のデフォルトの動作になる。
C-g キャンセル keyboard-quit.mac で一応対応。秀丸で Esc 押すのと同じ。
M-i タブ tab-to-tabstop.mac で対応。
C-o 空行を挿入。カーソル位置はそのまま open-line.mac で対応。
C-x i カーソル位置へ読込み insert-file.mac で対応。
C-c C-c 選択範囲をコメント comment-region.mac でざっくり対応。
Emacs では C-u C-c C-c でアンコメントだが C-c C-c でトグルします。
対応言語は C/C++/Java/C#/Swift/Go/html/css/javascript/
TypeScript/PHP/perl/python/ruby/shell/
秀丸マクロ(mac)/bat を予定。
言語はファイルの拡張子で判断する。判断できないときは '#' をコメント用の行頭文字と見なす。
C/C++ のコメント形式はデフォルトで // コメント。
/ / コメントにする場合はマクロ内の $comment_text_pre/post を変更する。
他の言語も同様。言語ごとに設定できるようにしてある。(すべての言語でテストはしてないけど)
M-space カーソルの周囲の空白を1つにする just-one-space.mac で実現。
M-\ カーソルの周囲の空白を全部削除 delete-horizontal-space.mac で実現
C-x C-o 空行を一つにする delete-blank-lines.mac で実現。
C-x C-x 前のカーソル位置 Emacs では exchange-point-and-mark.
C-@ 直前の操作の繰り返し Emacs では C-x z zzz...
C-x y やり直しのやり直し 元 Ctrl-y。 C-x u が Undo なのでその隣。
C-_ やり直しのやり直し C-/ がやり直しなので隣のキーに設定。
C-\ やり直しのやり直し C-/ がやり直しなのでBackslashに逆の意味を。
M-l 小文字変換 範囲選択後実施する
M-u 大文字変換 これも範囲選択必要。
M-y 貼り付け+履歴戻し Emacs の M-y (yank-pop) とはやや動きが違うけれど少し似てる。そのうちマクロで yank-pop 同等にしたい。
M-s u TAB -> 空白変換 よくやるのでキー割り当て。
M-s t 空白 -> TAB 変換 よくやるのでキー割り当て。
M-s s すべての候補を選択 直近の検索文字列をすべて選択。VS Code の Ctrl-d に似た機能。
M-s c すべての候補を色つけ 直近の検索文字列に色つけしてハイライト
M-s l すべての候補を一覧表示 直近の検索文字列にヒットする一覧
M-s y 引用符付き貼り付け メールの返信で使いたい。

制限付きのキーストローク

キーストローク 機能 メモ
C-x C-l 英小文字へ変換 「大文字 ←→ 小文字変換」にした。これは一文字ずつしか変換できない。いまいちだったので、M-l, M-u に別途、小文字変換、大文字変換を設定した。
C-x 1 画面分割を解除して1画面にする 秀丸に分割解除の単独コマンドはないので左右分割を割り当てた。左右分割中にもう一度左右分割すると1つに戻るので。
C-x 2 左右二分割 左右分割するが、最大2分割まで。もう一度左右分割すると1つに戻る。
C-x 3 上下二分割 上下分割するが最大2分割まで。もう一度丈夫分割すると1つに戻る。
C-x z 全画面表示 Emacs では操作の repeat 機能。Emacs Friendly Keymap では Zenモード(動かないけど)。とりあえず秀丸では全画面表示を割り当てる。
C-l 最後の編集箇所へ移動 Emacs ではカーソル位置を画面センターにする機能。秀丸にこの機能は無い様子。かわりにもともとアサインされていたままコマンドにしておく。
C-Enter 改行 Emacs Friendly Keymap ではなにか置換ダイアログで1つ置換に使うらしいが改行にしとく。
C-M-n 対応する括弧に移動 Emacs Friendly Keymap ではeditor.action.addSelectionToNextFindMatch(デフォルト Ctrl-d)だが、 Emacs では forward-list とかなので Emacs の動きに近い処理にする
C-' (C-^) 単語補間 Intellisense は秀丸にはないので単語補間のリスト表示にした。
C-M-Space Toggle Sidebar Visivility これは秀丸のファイルマネージャ枠表示の切替にしてみた。でもトグル動作してくれないなあ。

設定していないキーストローク

キーストローク 機能 メモ
C-x C-u 英大文字へ変換 C-x u (Undo) とかぶるので設定不可。Emacs でも間違いやすいので disabled になっている。
C-x C-k タブを全部閉じる Emacs Friendly Keymap 独自のキーアサイン。C-x k (ファイルを閉じる)と区別が付かないので設定不可。
M-x コマンド入力窓 この機能は秀丸には無い。
C-; 行ごとにコメント 取り合えす C-c C-c で代替
M-; 選択範囲をコメント これも C-c C-c で代替

制限

  • 秀丸は C-x s と C-x C-s とかの区別が付かない。 区別つく。iskeydown(0x11) でCtrlキーが押されているかどうか判定できた。
  • C-x s とか C-x r t とか多段キーストロークで一つのコマンドを実行する機能は、秀丸ではユーザーメニューを C-x などのキーに割り当てて実現するが、ユーザーメニューは最大 8 枚しかないのでせいぜい 2 ストロークまでかな。 3 ストロークの割り当てはできるけど数が限られる。でも、マクロの中で inputchar() でキー入力を受け取ることが可能なためユーザーメニューを使わずに多段キーストロークに対応可能だった。そのとき、 iskeydown(0x10) でのShiftキー判定、iskeydown(0x11) のCtrlキー判定、iskeydown(0x12) のAltキー判定を組み合わせれば何でもできちゃいそう。たとえば C-x キーにC-x.macマクロを割り当て、C-x.mac の中でinputchar()で次の入力を受け取って、 r が押されてたら Prefi Command だから再び inputchar() で次の入力を待ち合わせる・・・とかやれば、いくらでも多段キーストロークのコマンドがつくれそう。(但し inputchar() で読むと rC-r は違うキーコードになるので、なんか汎用的に作るのはそれなりにたいへんそうな気がする)
  • 当然ながら、用意されている機能は Emacs の機能とは違う。秀丸の機能をそのまま割り当てても Emacs と同じようには機能しない場合がある。
  • 但し、いくつかは同じ動作になるようマクロを書いた。
  • LittleEmacs.KEY には、ここに記載のないコマンドも登録されてたりするかも。普段使っていたキーマップに追加してしまったので。

Windows のファイルをドラッグ&ドロップして Cygwin 上のコマンド、スクリプトを実行するための Python スクリプト

モチベーション

cygwin 上で動くスクリプトエクスプローラーからドラッグ&ドロップして実行させたかったので作成。

ただし、ドロップするファイルの絶対パス中に日本語があるとうまくいかない。 (英文字だけなら動く)

Python ver

>>> import sys
>>> sys.version
'3.7.2 (tags/v3.7.2:9a3ffc0492, Dec 23 2018, 23:09:28) [MSC v.1916 64 bit (AMD64)]'

コード

#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# このスクリプトは Windows のファイルをドラッグ&ドロップするだけで
# cygwin 向けに書かれたスクリプトを実行できるようにするために書かれた。
#
# 具体的には、このスクリプトは、渡された引数をすべて検査し、Windows の
# 絶対パスらしき箇所を cygwin 形式の絶対パスに変換し、cygwin 上のスク
# リプトに引き渡す。
#
# cygwin 上のスクリプトは、このスクリプトの第一引数に指定する。このス
# クリプトの第二引数以降は、第一引数に指定したスクリプトの引数となる。
#
# コマンドラインからの実行例
# python execute-on-cygwin.py echo "Hello World!"
#
# ファイルを引数にとるスクリプトの実行例
# python execute-on-cygwin.py script-name.py C:\path\to\file1 C:\path\to\file2
#
# ショートカットを作り、ファイル名の指定だけを省いてショートカットのリ
# ンク欄に書いておくと、そのショートカットにファイルをドロップすること
# で cygwin 上のスクリプトを実行できる。
#
# 但し、フォルダ名、ファイル名に日本語を含むとうまく動かない。

import os
import sys
import subprocess
import re
import traceback


def conv_path_win2unix(path):
    if re.search(r'^\w:\.*', path):
        path = path[0].lower() + path[1:]
        path = re.sub(r'^(\w):', r'/cygdrive/\1', path)
        path = re.sub(r'\\', r'/', path)

    return path


if __name__ == '__main__':
    try:
        # 先頭はarg[0]はこのスクリプトのファイル名なので省く
        argv = sys.argv[1:]

        # commandline はあとで bash -c に渡すので全体を "" で囲んでおく
        commandline = '"'
        for arg in argv:
            # print(f'arg={arg}')
            tmp = conv_path_win2unix(arg)
            #tmp = arg

            # 空白を含む場合はダブルクオーテーションでクォートする。
            # commandline 全体がダブルクォートで囲まれるのでエスケーブ
            # しておく。
            if re.search('\s', tmp):
                tmp = r'\"' + tmp + r'\"'

            commandline += tmp
            commandline += ' '

        commandline = commandline[:-1]  # 最後の空白は消しておく
        commandline += '"'
        # print(f'commandline={commandline}')

        os.chdir(r'C:/cygwin64/bin')
        subprocess.run(f'bash --login -i -c {commandline}')

    except:
        ei = sys.exc_info()
        print('Exception detected.')
        # スタックトレースの取得
        traceback.print_tb(ei[2])
        # throw された例外とその説明を表示
        print(f'\n  {ei[0]} {ei[1]}')

    # ファイルをドロップしたときにコンソール画面がすぐに消えないように
    # しておく。コマンドラインから実行されたときは必要ないけど、どう判
    # 定すればいい?
    input('\nHit any key to finish. ')

アイディア

  • なんとか日本語にも対応させたい。日本語を含む場合、cygwin 上の bash が No such file or directory と言ってエラーになる。echo の引数に日本語を渡してもダメ。 "echo ハローワールド" とかダメ。"echo Hello World" はOK

タスクごとにフォルダを掘る作業を自動化する Python スクリプト

モチベーション

一度に複数の仕事が降ってくるので、タスクごとにフォルダを掘って関連する資料を一か所にまとめるようにしている。

それからそのタスクの作業ログを専用のテキストファイルに書いている。

テキストファイルは週一でバックアップしていて、すぐにバックアップできるよう一つのフォルダに全部のタスクのを入れている。

タスクごとのフォルダからはショートカットでそのテキストにリンクを張っている。

仕事を振られるたびに専用のフォルダを掘って、専用のテキストファイルを用意して、ショートカットを作る。

この一連の作業をいつか自動化したいと思っていたので、やってみた。 とりあえず PythonCUI スクリプトで実現してみた。

#!/usr/bin/python3
# -*- coding: utf-8 -*-
#
# TASKDIR フォルダに指定したタスク専用のフォルダを掘り、
# TASKLOGDIR フォルダにそのタスク専用のログファイルを作成する。
# その後、ログファイルのショートカットを作成したフォルダに作る。
#

import os
import sys
import re
from datetime import datetime
import traceback
import subprocess
from pathlib import Path
import glob
import win32com.client

# \\ を / に置換した HOME
# HOME = re.sub(r'\\', '/', os.environ['HOME'])
HOME = "C:/cygwin64/home/somebody"

# ★フォルダ名をここで指定する
TASKDIR = f"{HOME}/タスク"

# ★共通ログ置き場をここで指定する
TASKLOGDIR = f"{HOME}/タスクログ"


def get_task_dir_and_logfile_path(title):
    # TASKDIR フォルダがなければ何もしない
    if not os.path.exists(TASKDIR):
        return

    # YYYYMM_xxx で始まるフォルダをリストアップ
    today = datetime.today()
    dir_header = f"{TASKDIR}/{today.year:04d}{today.month:02d}_"
    taskdirs = sorted([d for d in glob.iglob(dir_header + "*") if os.path.isdir(d)])
    # YYYYMM_xxx の数が一番大きいものを取り出す
    length = len(taskdirs)
    if length != 0:
        latest_dir = taskdirs[length-1]
        name = os.path.basename(latest_dir)
        # YYYYMM_xxx_yyy の xxx (番号)を取り出す
        number = int(name[7:10], base=10)
        # 次の番号は + 10 する
        number += 10
        numstr = f"{number:03d}"
    else:
        # 初期値は 010
        numstr = "010"

    dir_path = dir_header + f"{numstr}_{title}"

    logfile_path = f"{TASKLOGDIR}/{os.path.basename(dir_path)}.txt"

    return dir_path, logfile_path


def win32_create_shortcut(target_path, shortcut_path):
    shell = win32com.client.Dispatch("WScript.shell")
    shortcut = shell.CreateShortcut(shortcut_path)
    shortcut.TargetPath = target_path
    shortcut.WindowStyle = 1
    shortcut.Save()


def win32_start(path):
    shell = win32com.client.Dispatch("WScript.shell")
    shell.Run(path)


def create_task(title):
    # タスクフォルダとログファイルのパスを取得
    dir_path, logfile_path = get_task_dir_and_logfile_path(title)
    # タスクフォルダがなければ新規作成
    os.makedirs(dir_path, exist_ok=True)
    # ログファイルがなければ新規作成
    if not os.path.exists(logfile_path):
        with open(logfile_path, "w") as f:
            # ちょっとした記述を追加しておく
            f.write("\n\n\n\n\n")
            f.write("-" * 72 + "\n")
            today = datetime.today()
            f.write(
                f"{today.year:04d}{today.month:02d}{today.day:02d}\n\n\n")

    # ショートカットの作成
    shortcut_path = f"{dir_path}/{os.path.basename(logfile_path)}.lnk"
    win32_create_shortcut(logfile_path, shortcut_path)

    return dir_path, logfile_path, shortcut_path


if __name__ == "__main__":
    title = input("Input new task title (q:quit): ")
    if title != "q":
        dir, logfile, shortcut = create_task(title)
        print("Task Created:")
        print("task dir:", dir)
        print("task log:", logfile)
        print("shortcut:", shortcut)
        win32_start(dir)
        win32_start(logfile)

    # input('\nHit any key to finish. ')

アイディア

  • タスクフォルダ、タスクログファイル、ショートカットはクラスとしてまとめてみよう
  • そもそもタスクログをタスクフォルダとは別のところに置くのはやめたほうがいいだろうか。
  • バックアップもスクリプト化して、タスクフォルダから、タスクログを拾ってくるようにすればいいかも。
  • バックアップは zip 化して一つにまとめとこう。
  • いま input() でタスク名を入れるようにしているが、GUI化したい。
  • ...

VS Code のキーバインディングを Emacs 風にしてみた

↓この Extension を利用(キーバインディングもここに書かれている)

https://github.com/SebastianZaha/vscode-emacs-friendly

日本語キーボードだとちょっとキーアサインが異なる模様

ドキュメントの記述 日本語キーボードの場合 説明
C-; C-: 行のコメント・アンコメントのトグル
M-; M-: 選択範囲のコメント・アンコメントのトグル
C-' C-^ Intelisense の表示
  • 取りあえず記号を含むやつはうまくいかないらしい。英語キーボードの配列をもとに書かれているのでしょう。

Python メモ

win32com を使ってショートカットを作る

↓参考にしたサイト
[python] デスクトップにショートカットを作る | Reincarnation+

#!/usr/bin/python3
# -*- coding: utf-8 -*-

import win32com.client

# ショートカットを作る
def win32_create_shortcut(target_path, shortcut_path):
    shell = win32com.client.Dispatch("WScript.shell")
    shortcut = shell.CreateShortcut(shortcut_path)
    shortcut.TargetPath = target_path
    shortcut.WindowStyle = 1
    shortcut.Save()

ついでに start コマンドみたいな python 関数も作ってみた

# cmd.exe で実行する start みたいな、ファイルを開くやつ
def win32_start(path):
    shell = win32com.client.Dispatch("WScript.shell")
    shell.Run(path)



でも、win32com は標準でついてないのでインストール必要。

win32com 本家

GitHub - mhammond/pywin32: Python for Windows (pywin32) Extensions

バイナリからインストールが簡単らしい
https://github.com/mhammond/pywin32/releases

VS CodePython 関連

Python の選択

> Python: Select Interpreter // Python の実行ファイルを選ぶ

Linter の選択

> Python: Select Linter // linter を選ぶ

VSCode のターミナルからの win32com インストールしてみた

C:/Users/<your-name>/AppData/Local/Programs/Python/Python37/python.exe -m pip install -U pywin32 --user

(Python3.7が入っている前提)