rcmdnk's blog

Dictionary of Prefixes and Suffixes: Useful English Affixes (English Word Power)

Zshにはsuffix aliasやauto cdと呼ばれる機能があります。

suffix aliasは指定の拡張子をコマンドラインにいきなり書くと、 指定のコマンドで開いてくれると言う機能、 auto cdはディレクトリをいきなり書くとそのディレクトリに移動してくれる機能です。

この機能をBashで実現してみました。

Sponsored Links

Zshのsuffix alias/auto cd

Zshでは

alias -s txt='vim'

としておくとtest.txtがあるディレクトリで

$ test.txt

txt拡張子がついた名前を直接コマンドラインで打つと

$ vim test.txt

としたのと同じように扱ってくれます。

複数ファイルも

alias -s {md,markdown,txt}='vim'

等として指定可能。

またこの様にして指定したファイルに対しては 実行ファイルでなくてもtabによる補完も効く様になります。

また

setopt auto_cd

としておくと、tmpというディレクトリがある場所で

$ tmp

とするだけでtmpディレクトリへ移動できます。

こちらはディレクトリなのでZshでディレクトリも最初から補完される様になっていれば補完が効きます。

Bashでの実装

Bash 4の場合

Bash 4にはZshのauto_cdに当たる機能がshoptにあって、

shopt -s autocd

としておけば

$ tmp
cd tmp

と言って移動してくれます。 補完に関しては、Bashでは他にコマンドの候補がない時にはディレクトリが補完されるので、 それと同様の補完は効きます。

suffix aliasの機能を実現するためには、Bash4から実装されてる command_not_found_handleという関数を使います。

command_not_found_handleが設定されていると、 打ち込んだコマンドが間違ってたりして

$ test.md
bash: test.md: command not found

みたいに出る様な状況の時、代わりにこの関数を実行する様になります。

1
2
3
4
5
6
7
8
9
10
11
12
_suffix_vim=(md markdown txt text tex cc c C cxx h hh java py rb sh)
command_not_found_handle () {
  if [ -f "$1" ];then
    if echo " ${_suffix_vim[*]} "|grep -q "${1##*.}";then
      echo "$cmd is a file, open $cmd with vi..."
      vi "$1"
      return $?
    fi
  fi
  echo "bash: $1: command not found"
  return 127
}

こんな感じのcommand_not_found_handle.bashrcなんかに書いておくと、

$ test.md

とするだけで

$ vi test.md

と同じ事になります。

また、もし該当ファイルが存在しない場合は 通常の出力通りcommand not foundと言って127(command not foundで通常返す値)を返す様にしています。

注意として、環境によっては command_not_found_handleが既に実装されている環境もあります 1

その機能の恩恵を受けていなければ上書きしてしまっても構いませんが、 もし使ってる場合はcommand_not_found_handleが定義されてない時だけ こんな感じで定義するとか

1
2
3
4
5
if ! type -a command_not_found_handle >& /dev/null;then
  command_not_found_handle () {
  ...
  }
fi

元のをキープしたいなら

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
alias_function() {
  eval "${1}() $(declare -f ${2} | sed 1d)"
}

if ! type -a orig_command_not_found_handle >& /dev/null;then
  if type -a command_not_found_handle >& /dev/null;then
    alias_function orig_command_not_found_handle command_not_found_handle
  else
    orig_command_not_found_handle () {
      echo "bash: $1: command not found"
      return 127
    }
  fi
fi

command_not_found_handle() {
  cmd=$1
  shift
  args=( "$@" )

  if [ -f "$cmd" ];then
    if echo " ${_suffix_vim[*]} "|grep -q "${cmd##*.}";then
      echo "$cmd is a file, open $cmd with vi..."
      vi "$cmd"
      return $?
    fi
  fi

  orig_command_not_found_handle "$cmd" "${args[@]}"
}

こんな感じにしてあげればよいかと思います2

Bash 3の場合

もしBash 3を使ってる場合はcommand_not_found_handleを 設定してもコマンドが見つからなくても実行してくれません。

またautocdshoptの中にありません。

この場合、どうしても使いたい場合はPROMPT_COMMANDにフックを入れて 無理やり実装することが出来ます。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
_suffix_vim=(md markdown txt text tex cc c C cxx h hh java py rb sh)
function command_not_found_hook () {
  ret=$?
  if [ $ret -eq 126 ] || [ $ret -eq 127 ];then
    cmd=$1
    shift
    args=( "$@" )
    if [ -e "$cmd" ];then
      if [ -d "$cmd" ];then
        echo "$cmd is a directory, cd $cmd"
        cd "$cmd"
      elif echo " ${_suffix_vim[@]} "|grep -q "${cmd##*.}";then
        echo "$cmd is a file, open $cmd with vi..."
        vi "$cmd"
      fi
    fi
  fi
}
PROMPT_COMMAND="command_not_found_hook${PROMPT_COMMAND:+;${PROMPT_COMMAND}}"

こんな感じでまずPROMPT_COMMAND内で一番最初に実行される 関数をcommand_not_found_hookとして実装します(混ざるとあれなのでhandle関数とは名前を替えておきます)。

PROMPT_COMMANDでは一番最初のコマンドでは$?に 実際に使ったコマンドの返り値が入ってるので これを使ってis a directorycommand not found126, 127の場合に色々出来る様にします。 (PROMPT_COMMANDの中で先に他のコマンドが実行されると$?にその 結果が入ってしまうので一番最初に行う必要があります。)

その後、直前のコマンドをhistoryで取ってきてそれがファイルである場合には 拡張子をチェックして指定の物で開くようにしています。

ただし、この場合はviで開いて終了後、 コマンド自体の終了ステータスは126なり127ままになります。 (PROMPT_COMMANDの中では実際のコマンドのステータスは変更できない)

ちょっとキモチワルイものが残りますがこれで一応 suffix alias的な事はできます。

Bash 3, 4 両方用

Bash 3, 4両方共通の.bashrcを使う場合には こんな感じでBASH_VERSINFOの値を使って切り替えを行えばOK。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
_suffix_vim=(md markdown txt text tex cc c C cxx h hh java py rb sh)
if [ "$BASH_VERSINFO" -ge 4 ];then
  alias_function() {
    eval "${1}() $(declare -f ${2} | sed 1d)"
  }
  if ! type -a orig_command_not_found_handle >& /dev/null;then
    if type -a command_not_found_handle >& /dev/null;then
      alias_function orig_command_not_found_handle command_not_found_handle
    else
      orig_command_not_found_handle () {
        echo "bash: $1: command not found"
        return 127
      }
    fi
  fi
  command_not_found_handle() {
    cmd=$1
    shift
    args=( "$@" )

    if [ -f "$cmd" ];then
      if echo " ${_suffix_vim[*]} "|grep -q "${cmd##*.}";then
        echo "$cmd is a file, open $cmd with vi..."
        vi "$cmd"
        return $?
      fi
    fi
    orig_command_not_found_handle "$cmd" "${args[@]}"
  }
  shopt -s autocd # cd to the directory, if it is given as a command.
else
  function command_not_found_hook () {
    ret=$?
    if [ $ret -eq 126 ] || [ $ret -eq 127 ];then
      cmd=$1
      shift
      args=( "$@" )

      if [ -e "$cmd" ];then
        if [ -d "$cmd" ];then
          echo "$cmd is a directory, cd $cmd"
          cd "$cmd"
        elif echo " ${_suffix_vim[@]} "|grep -q "${cmd##*.}";then
          echo "$cmd is a file, open $cmd with vi..."
          vi "$cmd"
        fi
      fi
    fi
  }
  PROMPT_COMMAND="command_not_found_hook${PROMPT_COMMAND:+;${PROMPT_COMMAND}}"
fi

ここでは全て適当な編集しそうな拡張子をviで開く様にしてるだけですが、 必要なら他にも色々追加できます。

補完

上の実装でZshのsuffix alias的な事をBashで大体出来る様になりますが、 1つ上手く行かないのが補完です。

無理やり補完関数を書けば出来ないことも無いかも知れませんが 最初のコマンド補完を変更する方法がよくわからなかったので取り敢えずそのままに。

シェルの確認

それからBash3が入ってる環境で後からBash4を/usr/local/bin/bash等に 入れてる場合、きちんとログインシェルが変更されてるか $SHELL等を見て確認してみてください。

Macでログインシェルを確認/変更するいくつかの方法

コマンドラインからbash --versionとしても その時使うコマンドとしてのbashとログインシェルが違う事があるので、 bash --versionとしてversion 4.X.XXと出ても 今いるシェルが3.Xの場合があります。

linux - command_not_found_handler not working in mac - Stack Overflow

この中ではMacでHomebrewを使ってBashを入れて、bash --versionとかで確認しても ちゃんと4.3.30なのにcommand_not_found_handleが使えない、 と言っていますが、 上に書いたようにログインシェルを変更してないんだと思います。 (実際自分でも同じような状態になったことがあったので念のため。。。)

まとめ

使えるようになったら便利かな、と思って設定して暫く使ってみましたが、 やはり補完が効かないのが圧倒的に使えないので普通にvi ...<Tab>と打ってしまいます。 (そしてそっちのが速い。)

ただ、GNU screen上でファイル名をコピーしてパッと開いたりするのはたまに便利だったりはします。 (その場合もまだ慣れてないのでvi と先に打ってしまうことも多いですが。)

Sponsored Links
Sponsored Links

« Macでログインシェルを確認/変更するいくつかの方法 Unixで最後にリブートした時を確認する »