Monthly Archives: 4月 2016

2016-04-26 ,

日本語grep(lvgrep)

日本語のgrepってみんなどうやってるんだろう。メジャーなディストリビューションとか、日本語でgrepが出来ないとは思えないけど。あ、今はもうUTF-8統一なのか。

Emacs25でdiredのAとQがdired-do-find-系(dired-do-find-regexpとdired-do-find-regexp-and-replace)に変わってしまったのでさあ大変。 これまでのdired-do-searchとdired-do-query-replace-regexpは外部コマンド(find,grep)に依存していないのでWindows上でも安定して使えたのだけど、dired-do-find-系はそれらに依存しているのでトラブルに。 dired-do-searchとdired-do-query-replace-regexpはまだ使えるのでキーを割り当て直せば良いんだろうけど、dired-do-find-系はディレクトリを指定できるメリットがあるのでこれを機に使えるようにした。

まずはfind。そのままだと環境変数PATHの優先順位の関係でWindowsのfindコマンドが使われてしまう。 私の場合、シェル(shell-file-name)はcmdproxy.exeではなくてCygwinのshにしてあるので、find-programに /bin/find と絶対パスを指定して解決。

次はgrep。日本語のgrepは前世紀からいろんな人が苦労していた気がするけど、いまさらまたこの設定をいじるなんて。 日本語(正確には日本語でよく使われる文字符号化方式でエンコードされたテキスト)のgrepの方法としては次のようなものが思い当たる。

  • lgrepを使う
  • nkfを噛ます
  • iconvを噛ます
  • 日本語に対応する改造を施したgrepを使う

私はlgrepを使ってきた。lgrepはlvの検索機能部分で、Cygwinのパッケージになっていて簡単にインストールできる。lv&lgrepの文字変換機能は素晴らしいけれど、最大の欠点はGNUのgrepとオプションに互換性がないことだ。特に-Hでファイル名が表示できないのが痛い。代わりになるオプションもない。

nkfはパッケージになってないからビルドするのが面倒くさいし(lvも昔はパッケージになっていなかったので自分でビルドしてた)、日本語対応grepをビルドするのも同じ。iconvは自動判別がそのままでは難しい。

結局やりたいことは、文字コードを自動的判別して変換して、それに対してgrepをすることだから、次のようなスクリプトを作成した。

#!/bin/sh
export LANG=ja_JP.CP932
lv -Os ${@:$#} | grep --label=${@:$#} ${@:1:$#-1}

LANGが無いとCygwinのgrepはUTF-8前提になってしまうのでLANGを指定した。私はCP932前提のコマンドライン用プログラムを多数抱えているのでWindowsでUTF-8に移行する気にはまだなれない。

lvには-Osだけを指定して、入力は自動判別にし出力はShift_JIS(≒CP932)とする。

入力ファイルの指定はスクリプトの最後の引数とする。それをlvとgrepの–label=オプションへ渡す。その他の引数はgrepにそのまま渡すことにした。

これをlvgrepとして保存して、Emacsではgrep-program、grep-command、find-grep-optionsあたりでこれを指定するようにした。

とりあえずうまく行っているみたい?

dired-do-searchと比べて外部のgrepを使っているせいで空白の検索が素直に指定できない等不便なことはあるけど仕方なし。Emacsの正規表現と一貫性を持たせることを考えると、あまり外部コマンドに依存するのは考え物だと思うんだけどなぁ。

(2019-01-15追記)

grepのオプションにできるだけ対応した。複数ファイル処理可能。ただしディレクトリを指定する機能は対応していない。

#!/bin/bash
export LANG=ja_JP.CP932

# option alias map
declare -A OPT_ALIASES;
OPT_ALIASES["--regexp"]="-e";
OPT_ALIASES["--file"]="-f";
OPT_ALIASES["--ignore-case"]="-i";
OPT_ALIASES["--invert-match"]="-v";
OPT_ALIASES["--word-regexp"]="-w";
OPT_ALIASES["--line-regexp"]="-x";
OPT_ALIASES["--count"]="-c";
OPT_ALIASES["--colour"]="--color";
OPT_ALIASES["--files-without-match"]="-L";
OPT_ALIASES["--files-with-matches"]="-l";
OPT_ALIASES["--max-count"]="-m";
OPT_ALIASES["--only-matching"]="-o";
OPT_ALIASES["--quit"]="-q";
OPT_ALIASES["--silent"]="-q";
OPT_ALIASES["--no-messages"]="-s";
OPT_ALIASES["--byte-offset"]="-b";
OPT_ALIASES["--with-filename"]="-H";
OPT_ALIASES["--no-filename"]="-h";
OPT_ALIASES["--line-number"]="-n";
OPT_ALIASES["--initial-tab"]="-T";
OPT_ALIASES["--unix-byte-offsets"]="-u";
OPT_ALIASES["--null"]="-Z";
OPT_ALIASES["--after-context"]="-A";
OPT_ALIASES["--before-context"]="-B";
OPT_ALIASES["--context"]="-C";
OPT_ALIASES["--text"]="-a";
OPT_ALIASES["--devices"]="-D";
OPT_ALIASES["--directories"]="-d";
OPT_ALIASES["--recursive"]="-r";
OPT_ALIASES["--dereference-recursive"]="-R";
OPT_ALIASES["--binary"]="-U";
OPT_ALIASES["--null-data"]="-z";

# parse command line
declare -a files=()
declare -a opt_arr=()
declare -A opt_hash
declare unresolved_arg=""

function push_opt () {
    if [[ -v OPT_ALIASES[$1] ]]; then
        opt_hash[${OPT_ALIASES[$1]}]=$2;
    else
        opt_hash[$1]=$2;
    fi
}

for arg in "$@"; do
    # \ -> \\
    qarg="${arg//\\/\\\\}"
    # " -> \"
    qarg="${qarg//\"/\\\"}"

    if [[ -n $unresolved_arg ]]; then # -prev curr
        options+=($unresolved_arg);
        options+=("$qarg");
        push_opt $unresolved_arg "$qarg";
        unresolved_arg="";
    elif [[ ${arg} =~ ^(--[^=]+)=(.*)$ ]]; then # --???=???
        options+=("$qarg");
        push_opt ${BASH_REMATCH[1]} "${BASH_REMATCH[2]}";
    elif [[ $arg =~ ^-- ]]; then # --???
        options+=("$qarg");
        push_opt $arg "";
    elif [[ $arg =~ ^-[efmABCDd]$ ]]; then # -curr next
        unresolved_arg=$arg;
    elif [[ $arg =~ ^-[^-] ]]; then # -???
        options+=("$qarg");
        i=1;
        while [[ $i -lt ${#arg} ]]; do
            push_opt -${arg:(i++):1} "";
        done;
    elif [[ ! -v opt_hash["-e"] ]]; then # PATTERN
        options+=("$qarg");
        push_opt "-e" "$qarg";
    else # FILE
        files+=("$qarg");
    fi
done

# echo files="${files[@]}" >> ~/tmp/lvgrep.log
# echo options="${options[@]}" >> ~/tmp/lvgrep.log
# for x in "${!opt_hash[@]}"; do printf "%s = %s\n" "$x" "${opt_hash[$x]}" >> ~/tmp/lvgrep.log; done

# -h or -H
if [[ -v opt_hash["-h"] || -v opt_hash["-H"] ]]; then
    :
elif [ ${#files[@]} -lt 2 ]; then
    options+=("--no-filename");
else
    options+=("--with-filename");
fi

# execute each file
found=false
for file in "${files[@]}"; do
    cmd="lv -a -Os \"${file}\" | grep --label=\"${file}\" ${options[@]@Q}"
    # echo "$cmd" >> ~/tmp/lvgrep.log
    if bash -c "$cmd"; then
        found=true
    fi
done

# return exit status code
if [ "$found" = true ]; then
    exit 0;
else
    exit 1;
fi;

(2016-12-21追記)

複数のファイルを指定したときに正しく動作するようにした。

#!/bin/bash
export LANG=ja_JP.CP932

# parse command line
files=()
pattern=""
options=()
for arg in "$@"; do
    # \ -> \\
    qarg="${arg//\\/\\\\}"
    # " -> \"
    qarg="${qarg//\"/\\\"}"
    if [[ ${arg} = -* ]]; then
        options+=(${qarg});
    elif [[ -z "${pattern// }" ]]; then
        pattern=${qarg};
    else
        files+=(${qarg});
    fi
done
#echo pattern=${pattern}
#echo files="${files[@]}"
#echo options="${options[@]}"

# -h or -H
if echo " ${options[@]} " | grep -e " \\(--no-filename\\|--with-filename\\|-[^ -]*[hH][^ -]*\\) "; then
    :
elif [ ${#files[@]} -lt 2 ]; then
    options+=("--no-filename");
else
    options+=("--with-filename");
fi

# execute each file
found=false
for file in "${files[@]}"; do
    c="lv -Os \"${file}\" | grep --label=\"${file}\" ${options[@]} \"${pattern}\""
    #echo "$c"
    if bash -c "$c"; then
        found=true
    fi
done

# return exit status code
if [ "$found" = true ]; then
    exit 0;
else
    exit 1;
fi;
2016-04-23

Emacs25(MinGW64)+cygwin-mount.elでtramp経由でリモートファイルを開けない

diredでリモートのディレクトリリストは表示されてもファイルを開こうとすると次のようなエラーが。

Tramp: Encoding remote file ‘/scpx:user@example.jp:/home/user/tmp/test1.txt’ with ‘openssl enc -base64 <%s’...done
Tramp: Decoding local file ‘c:/app/cygwin/tmp/tramp.6920y_V.txt’ with ‘base64-decode-region’...done
Tramp: Inserting ‘/scpx:user@example.jp:/home/user/tmp/test1.txt’...failed
Removing old name: No such file or directory, /app/cygwin/app/cygwin/tmp/tramp.6920y_V.txt

調べていくとカレントディレクトリ(default-directory)によって結果が変わった。 scratchバッファからリモートファイルをfind-fileすると問題なく開いた。 tramp上のパスをdiredで開いて、そこから開こうとするとエラーになった。

  1. find-file
  2. insert-file-contents
  3. tramp-handle-insert-file-contents
  4. tramp-sh-handle-file-local-copy
  5. with-temp-file
  6. write-region

と追っていくと、次のようなコードでエラーが再現。

(let ((default-directory "/scpx:user@example.jp:/home/user/tmp"))
  (write-region nil nil "c:/app/cygwin/tmp/a.txt"))

default-directoryがローカルなら何事もなく成功する。

write-regionはCで書かれているので見てみると、中でexpand-file-nameを使ってファイル名を展開し、そのファイルに対して出力している。

絶対パスにしているならカレントディレクトリによって動作が変わるはずはない。

エラーメッセージとして/app/cygwin/app/cygwin/tmp/tramp.6920y_V.txtのような全くおかしなパスが表示されているので、念のためexpand-file-nameを調べたところ、おかしな挙動に気がついた。

  • (expand-file-name "c:/a.txt" "c:/")c:/a.txt
  • (expand-file-name "c:/a.txt" "/ssh:/")/a.txt

あれれ?

expand-file-nameもCで書かれていたので中身を見てみると、default-directoryを元にfind-file-name-handlerして実際にexpand-file-nameを処理する関数を求めていた。

あー、ローカルとリモートでファイルシステムが違うんだ。default-directoryがtrampパスの時tramp-sh-handle-expand-file-nameが使われるんだ。 で、tramp-sh-handle-expand-file-nameはWindowsのことをこれっぽっちも考慮していないからc:/aが/aになってしまうのね。

C:/app/cygwin/tmp/a.txt が tramp-sh-handle-expand-file-name によって /app/cygwin/tmp/a.txt になり、これがcygwin-mount.elによってcygwinルートディレクトリを基準にしたパスとして展開され、/app/cygwin/app/cygwin/tmp/a.txtになるわけだ。ヤレヤレ。

というか、そもそもexpand-file-nameにローカルの絶対パスを渡したときにtrampのexpand-file-nameが使われるってどうなのよ。 ここは展開対象パスがローカルの絶対パスなのだから、default-directoryに関わらずローカルのハンドラによってexpandするべきなんじゃないのか? というわけで、expand-file-nameにadvice-addしたのだけど、何故かうまく行かなかった。Cから呼んでいるからだろうか。

仕方ないのでtramp-sh-handle-expand-file-name側にadvice-addしてc:/がc:/のまま/にならないようにしたところ、リモートのファイルがtramp経由で開けるようになった。

(advice-add
 'tramp-sh-handle-expand-file-name :around
 (lambda (orig-func name &optional dir)
   (if (save-match-data (string-match "^[a-z]+:[/\\]" name))
       (expand-file-name name (file-name-directory name))
     (apply orig-func (list name dir)))))

ちなみに、リモート・ローカル間でコピーできない現象は次のページの tramp-do-copy-or-rename-file-out-of-band へのadviceで治った。

NTEmacs @ ウィキ - tramp を tramp-method “scp” で使うための設定 - @ウィキモバイル

最後に、trampのコードを追うためにはファイル名ハンドラのことを知っていないといけないのでしっかり押さえておくこと。

2016-04-22 ,

Windows10でウィンドウのアクティブタイトルバーに色をつける

Vistaからだったろうか、Win7からだったろうか。アクティブタイトルバーに色が付かなくなったのは。主にキーボードで操作している人間にとって、今どこにキーフォーカスが当たっているのかは非常に重要な情報だ。特に最近のディスプレイは大きい。23インチ、マルチディスプレイ環境で沢山のウィンドウがあると、一目で全てのウィンドウに目を通すのは難しい。ましてやわずかな明暗の差しか無いアクティブウィンドウを識別するのは容易ではない。

そんな状況が続きつつも最近は諦めていたのだが、ひょんな事から何か状況が変わっていないかと「windows10 active titlebar」で検索したら「タイトルバーに色づけ」なんて記事が大量に出てくるではないか!

どうもWindows10のThreshold2から(Insider Previewではその前から)出来るようになっていたらしい。

最近の私の情報感度の低さというか、情弱っぷりには我ながらびっくりするくらいだ。

などと記事を辿っていくと、Aero Liteなるテーマがあったようだ。

何はともあれ、あるべき設定があるべき場所にあるのは良いことだ。