Stimulator

機械学習とか好きな技術話とかエンジニア的な話とかを書く

Python製シェルxonshを半年使った所感や環境設定のまとめ

- はじめに -

Pythonにおけるpython-prompt-toolkit(以下ptk)を使って作られたシェルである「xonsh」を同僚にオススメされて、大体半年くらい使ったので設定とかxontribとか所感を晒していく。

前半でxonshのメリット、デメリットの概要を記載し、後半に自身が利用する設定やxontribについて記載する。

この記事は、xonsh導入に至る人もしくは、環境設定について広く知りxonshを扱える人を増やす事が目的である。


追記:2018/07/18
xonsh 0.7.0が出ていますが、現在prompt-toolkit2.0の各機能対応中という感じです。
これは、ptk1.x -> 2.xにおいて結構な破壊的変更があるためです。
現状はpip install xonshする時に
pip install prompt_toolkit==1.0.15
pip install xonsh==0.6.10
としておくのが良いと思います。
この記事の内容も0.6.10のものであり、0.7.0でptk2.0を利用した場合にxonshrcが動作しない箇所があります。

追記:2018/08/03
xonsh 0.7.2になり大分ptk周りのバグが気にならなくなってきました。
jupyterが未だptk2.xで起動しない問題がありますが、そちらが気にならなければ、もう普通にpipでxonsh入れても良い頃合いだと思います。
jupyterで普段作業する方は先にpip install prompt_toolkit==1.0.15

追記:2018/09/22
ipythonもjupyter周りもptk2.xになったので、これからは心おきなく以下の1コマンドでOK!
pip install xonsh


追記:2018/12/05
アドベントカレンダー2018で書いたので以下も参考に

バージョンの違いに対応したり、xonshのprint出力color設定を設定したりする記事:
vaaaaaanquish.hatenablog.com

xonshの2018年のptk, カラー, 設定, bug fixなどの変遷:
vaaaaaanquish.hatenablog.com

2018年最後のxonshrc:
vaaaaaanquish.hatenablog.com




 

- xonshについて -

xonshはPythonのptkを用いて作成されたShellである。
bash, zshを除くシェルで代表的な物としてfishがあるが、「fishに比べ、よりPythonに寄ったシェル」という位置付けになる。

github.com

読み方についてはゾンシュ、カンク、コンク、エックスオンエスエッチ等諸説あるが、公式ページでランダムに表示される文言の中に [コンシュ] が入っている事からコンシュが正解であると思われる。
f:id:vaaaaaanquish:20180520181037p:plain

導入は以下だけ

pip install xonsh

追記 2018/07/03:
今ちょうどxonsh 0.6.8でprompt-toolkitが2.0対応中なのでpip install xonshする前にpip install prompt_toolkit==1.0.15 しておくと良いです
バージョンの違いを鑑みるとbrew等でなはくpipが良いと私は思います

 

xonshの良さ

筆者は、何周か回って「xonshの良さ」は端的に以下に集約されると考えている。

  1. python資産の活用とワンライナーシェルコマンドからの開放
  2. defaultで利用可能なリッチな補完と履歴
  3. 中身も全てPythonでOSに依存しない高速な環境構築が簡易

 
1の「python資産の活用とワンライナーシェルコマンドからの開放」は最も筆者に得を与えている。「シェル芸」と呼ばれる技術は、テクニカルで高速で最も我々の生活に馴染んでいるものであるが、時に複雑な処理を書きたい場合において人類にはやや理解し難い構文が発生してしまう。何より、各コマンドを中度以上に知る人間が読まなければ、何をどういう原理で処理しているのかワンライナーの理解でさえ時間を要する。

xonshでは以下の例のように、既存のシェルコマンドの入出力を利用しながら、Pythonで行いたい処理に流したり、逆流する事も可能である。もちろん、これらを関数としてコマンド化する事もxonshであれば簡易に行える。

# lsコマンドの結果を利用しながらpythonでの処理を行う例
for x in ![ls ~/work/]:
    print(x)
# > hoge.txt
# > piyo.md
# > ....

# pythonスクリプトの結果をシェルコマンドに流す例
import sys
echo @(sys.version_info) | sed 's/ /\\\x0A/g'
# > 3
# > 6
# > 3
# > candidate
# > 1

日々Pythonを書き慣れている人間であれば、manやhelpを読み込む必要無く複雑な処理を記述し、スクリプトやコマンドとして再利用する事ができるであろう。

 
2の「defaultで利用可能なリッチな補完と履歴」はxonshの魅力の1つである。コマンドの補完は履歴からhistoryからの補完やMan-pageを参照した補完が(ptkがinstallされていれば)defaultで扱える。ちなみにこの補完は、fishを参考に作られている。
f:id:vaaaaaanquish:20180622142703g:plain:w350

 
3の「中身も全てPythonでOSに依存しない高速な環境構築が簡易」が筆者が最もxonshを多く利用している理由である。永らくzshを利用してきた筆者だが、様々な管理ソフトウェア、パッケージをインストールして利用するため、環境構築スクリプトは幾度もの修正によって異様な形相となっている。oh-my-zsh、Antigen、prezto、zplugを経てこそ短くなったrcファイルも、管理できているとは言えないようなものだ。

xonshは、本体がpythonパッケージ管理であるpipでインストールできるだけでなく、拡張となるxontribもpipで導入できる。「サーバにDockerは導入できないがpythonは入っている」といった状態であれば、作業用Dcokerコンテナが無くともpipであらかたの環境が整う。これは、初学者にも効能があると考えていて、簡易な導入で高度な機能が使えるのCLIというのは魅力になり得る。

また、シェル自体もPythonである故、コマンドだけでなくシェル自体の動作もOverrideがPythonによって記述でき、configもrc fileもPythonで記述できる。Pythonを習熟していれば、CLIを習熟できるようなものだ(言い過ぎ)。


余談だが、bashやfishとの比較に関しては以下のツイートは結構すき。
https://twitter.com/LinSocist/status/971904816505958400

メイン開発者のscopatzは強めの比較がすきっぽい。


まあ http://xon.sh/#comparison の比較もなかなかパワフルだしそういうものか。


 

xonshのまだまだなところ

xonshがまだまだな点も同様に挙げておく。

  1. 完全なbash互換ではない
  2. 特定条件下で落ちる
  3. Pythonが生理的に無理な人にはオススメできない

1つに、完全にbash互換ではない事が挙げられる。コマンドはPythonのサブプロセスを利用して走るため権限的に触れない部分があったり、Pythonとの競合部分故OSコマンドやブレース展開は正しく扱えない場合がある。Hacker News内のXonsh, a Python-ish, Bash-compatible shell language and command promptでも、fishのリードエンジニアとxonshの作者が「bash互換と言うのはやめた方がええんちゃうか」と話しており、完全なPOSIXサポートシェルではなく「Pythonが扱える者であれば、気軽に高度なシェル操作が行える」辺りを目指しているようだ。

 
そして「たまに落ちる」。普段の作業中に落ちる事はほぼ無いと言っていいのだが、設定書き換えてロードした直後や、xonsh上でゴリゴリにメモリを使うとシェル自体が落ちてしまう。同僚はxonshを数週間使い続けるとPC全体が重くなってしまうとも言っており、Pythonのガベージコレクタや膨大になった履歴jsonとの戦いがたまに見られる。こちらに対しては後述するが、筆者は簡易に設定したzshの上でxonshを走らせ、いつ死んでも良いようにしている。

追記2018/06/26:xonsh 0.6.7でpromptがゾンビ化してCPU持っていく現象が解消されたのでだいぶ良くなったと思います

 
加えてPythonシェルが生理的に無理という人にはオススメできない。ただ、こればかりはどうしようもない。Pythonはそもそもシェル向きの言語かと聞かれれば筆者も口を噤むし、Pythonじゃなくて〇〇だったら?と聞かれれば簡単に心が揺れるだろう。コマンドシェル - ArchWikiですら、xonshはレトロなシェルとして紹介されているし、もうすまんそれならzshを使ってくれという感じである。


 

- 筆者のxonsh環境 -

以降は、筆者が利用しているxonsh環境について記載する。
2018/06/22、xonsh 0.6.1 時点であり、絶対最強という訳でもないと思う。

 

zshの上で動かす

これ大事。xonshが死んで作業できなくなったら終わり。
もちろんbashでも良い。

色々試した結果、~/.zshrcの最後にxonsh起動をコマンド書いてデフォルトシェルをzshにする形に落ち着いた。起動が若干遅くなるが、どうせシェルは常時起動しているので心を広く保つ事でカバーしている。

Mac向けの設定なのでPATHはよしなに。bashだとalias以降は動くはず。

# 人類最低限zshrc
autoload -U compinit; compinit
setopt auto_cd
setopt auto_pushd
setopt pushd_ignore_dups
setopt histignorealldups
setopt always_last_prompt
setopt complete_in_word
setopt IGNOREEOF
export LANG=ja_JP.UTF-8
zstyle ':completion:*' matcher-list 'm:{a-z}={A-Z}'
autoload -Uz colors
colors
alias l='ls -ltr --color=auto'
alias ls='ls --color=auto'
alias la='ls -la --color=auto'
PROMPT="%(?.%{${fg[red]}%}.%{${fg[red]}%})%n${reset_color}@${fg[blue]}%m${reset_color} %~ %# "


# vim
export VISUAL='/usr/local/bin/vim'

# pyenv
export PYENV_ROOT="$HOME/.pyenv"
export PATH="$PYENV_ROOT/bin:$PATH"
eval "$(pyenv init -)"

# gcloud
if [ -f '/Users/shukawai/google-cloud-sdk/path.zsh.inc' ]; then source '/Users/shukawai/google-cloud-sdk/path.zsh.inc'; fi
if [ -f '/Users/shukawai/google-cloud-sdk/completion.zsh.inc' ]; then source '/Users/shukawai/google-cloud-sdk/completion.zsh.inc'; fi

# xonsh起動
alias x='xonsh'
x

自分が作業していく中で見たxonshから利用しづらいものとしてpyenv、gcloudコマンドがある。
こればかりは仕方ないのでzshで操作するようにしている。

また、LD_LIBRARY_PATHPATHもxonshrcではなく上層のシェルで書いておかないと動かなかったりする(動く時もある)。
$PATH.append("/usr/local/bin")のようにxonshrcに記載するのが正解だと考えているが、試してできた記憶は今の所ない。


なんか「他にこれもダメだった」というのがあれば、はてブTwitterにでも書いてくれればエゴサします。


 

xonshrc

設定ファイル。~/.xonshrcに書く。
hoge.xshファイルを作って~/.xonshrc内でfrom hoge import *としてやる事でファイル分割もできる。
~/.config/xonsh/rc.xshでも多分大丈夫。
最低限な部分だけ以下に抜粋。

python環境以外に特別に入れているのはvimとpeco。vimはよしなに。
pecoはMacならbrewLinuxなら以下のスクリプトを実行するだけですのでgoとか入れる必要なしです。Windowsgithubのreleaseからバイナリ取ってきてPATH通せば動く。
Linux に最新版の peco をインストールするシェルスクリプト - Qiita

vim, pecoとpipでインストールできるものを導入した上で下記のようにしています。

# -*- coding: utf-8 -*-
# エディタ
$EDITOR = '/usr/local/bin/vim'
$VISUAL = '/usr/local/bin/vim'
# vi風の操作がシェル上で直感的でないのでFalse
$VI_MODE = False
# 補完をEnterで直接実行しない
$COMPLETIONS_CONFIRM = True
# Ctrl + D で終了しない
$IGNOREEOF = True
# tabではなく空白4つ
$INDENT = "    "
# 補完時に大小区別しない
$CASE_SENSITIVE_COMPLETIONS = False
# 連続重複コマンドを保存しない
$HISTCONTROL = "ignoredups"
# 括弧を補完
$XONSH_AUTOPAIR = True
# ディレクトリ名を入力でcd
$AUTO_CD = True
# エラー全て吐くように
$XONSH_SHOW_TRACEBACK = True
# サブプロセスタイムアウトのメッセージ抑制
$SUPPRESS_BRANCH_TIMEOUT_MESSAGE = True
# キー入力即評価(サイコー)
$UPDATE_COMPLETIONS_ON_KEYPRESS = True
# プロンプトの表記
$PROMPT = "{INTENSE_RED}{user}{INTENSE_GREEN}@{INTENSE_BLUE}{hostname}{INTENSE_YELLOW} [ {cwd} ] {GREEN}$ "
# lsコマンドの結果の見た目
$LS_COLORS="di=34:ln=35:so=32:pi=33:ex=31:bd=46;34:cd=43;34:su=41;30:sg=46;30:tw=42;30:ow=43;30"

# alias
# OS判別をplatformで
import platform
if platform.system() == 'Darwin':
    # Mac(iTerm2のimgcat)
    aliases['icat'] = 'imgcat'
else:
    aliases['f'] = 'free -h'
    aliases['wf'] = 'watch free -h'
    aliases['tm'] = 'tmux'
aliases['ls'] = "ls --color=auto"
aliases["l"] = "ls -l"
aliases["lf"] = "ls -f"
aliases["ld"] = "ls -d"
aliases["la"] = "ls -la"
aliases["ll"] = "ls -l"
aliases["v"] = "vim"
aliases["vi"] = "vim"
aliases["vx"] = "vim ~/.xonshrc"
aliases["vz"] = "vim ~/.zshrc"
aliases["vv"] = "vim ~/.vimrc"
aliases["vs"] = "vim ~/.ssh/config"


# 履歴をpecoに流す
# https://qiita.com/riktor/items/4a90b4e125cd091a9d07
# pecoのinstall : https://qiita.com/ngyuki/items/94a7e638655d9910971b
import json
from collections import OrderedDict
from operator import itemgetter
def get_history(session_history=None, return_list=False):
    hist_dir = __xonsh_env__['XONSH_DATA_DIR']
    files = [ os.path.join(hist_dir,f) for f in os.listdir(hist_dir)
              if f.startswith('xonsh-') and f.endswith('.json') ]
    file_hist = [ json.load(open(f))['data']['cmds'] for f in files ]
    cmds = [ ( c['inp'].replace('\n', ''), c['ts'][0] )
                 for cmds in file_hist for c in cmds if c]
    cmds.sort(key=itemgetter(1))
    cmds = [ c[0] for c in cmds[::-1] ]
    if session_history:
        cmds.extend(session_history)
    # dedupe
    zip_with_dummy = list(zip(cmds, [0] * len(cmds)))[::-1]
    cmds = list(OrderedDict(zip_with_dummy).keys())[::-1]
    if return_list:
        return cmds
    else:
        return '\n'.join(cmds)

# sshをpecoに流す
def get_ssh():
    items = re.sub(r'(?i)host ', '', $(cat ~/.ssh/config /etc/ssh/ssh_config | grep -i '^host'))
    return items


# キーバインド
from prompt_toolkit.keys import Keys
from prompt_toolkit.filters import (Condition, IsMultiline, HasSelection, EmacsInsertMode, ViInsertMode)
@events.on_ptk_create
def custom_keybindings(bindings, **kw):
    # ptk 2.xでは不要
    handler = bindings.registry.add_binding

    # ptk 2.xでは @bindings.add('c-v') とする
    # コマンド入力中にctrl+vでvim編集
    @handler(Keys.ControlV)
    def edit_in_editor(event):
        event.current_buffer.tempfile_suffix = '.py'
        event.current_buffer.open_in_editor(event.cli)

    # ctrl+rで履歴をpecoに流して検索
    @handler(Keys.ControlR)
    def select_history(event):
        sess_history = $(history).split('\n')
        hist = get_history(sess_history)
        selected = $(echo @(hist) | peco)
        event.current_buffer.insert_text(selected.strip())

    # ctrl+sでssh先をpeco
    @handler(Keys.ControlS)
    def select_ssh(event):
        hosts = '\n'.join([x for x in get_ssh().split('\n') if x!='*' and 'HostKey' not in x])
        selected = $(echo @(hosts) | peco)
        if selected:
            event.current_buffer.insert_text('ssh ' + selected.strip())


# 直近のxonshjobころすマン
# https://github.com/zardus/xonshrc/blob/master/xonshrc
def _kill_last(args, stdin=None):
	if __xonsh_active_job__ is None:
		print("No active job. Aborting.")
		return
	cmd = 'kill %s %d' % (''.join(args), __xonsh_all_jobs__[__xonsh_active_job__]['pgrp'])
	os.system(cmd)
aliases['kill_last'] = _kill_last

# diskutil infoを見る
# https://github.com/asmeurer/dotfiles/blob/master/.xonshrc
def _free(args, stdin=None):
    disk_info = $(diskutil info /)
    return [i for i in disk_info.splitlines() if "Free" in i][0] + '\n'
aliases['fr'] = _free

# gc
import gc
def _gc(args, stdin=None):
    gc.collect()
aliases['gc'] = _gc

# ライブラリの実行時import
# https://vaaaaaanquish.hatenablog.com/entry/2017/12/26/190153
# xonsh上で使うときがありそうなライブラリはlazyasdで補完時、実行時に読み込み
from xonsh.lazyasd import lazyobject
import importlib
lazy_module_dict = {
    'sys': 'sys',
    'random': 'random',
    'shutil': 'shutil',
    'pd': 'pandas',
    'np': 'numpy',
    'requests': 'requests',
    'os': 'os',
    'plt': 'matplotlib.pyplot',
    'Path': 'pathlib.Path',
        }
for k,v in lazy_module_dict.items():
    t = "@lazyobject\ndef {}():\n    return importlib.import_module('{}')".format(k, v)
    exec(t)

設定可能な環境変数値は日本語含めて以下にまとめてあるが、ver6.0の時に書いたものなので、公式の http://xon.sh/envvars.html をウォッチしておくと良い。
vaaaaaanquish.hatenablog.com

またGithubでxonshrcで検索する他、Xonsh Advent Calendar 2017 - Qiita でも設定周りの記事を書いている人がいるので参考になると思います。xonshrc書く上でのオススメは以下辺り。
qiita.com
qiita.com

また手前味噌ですが、xonshの各種eventについても書いてます。
vaaaaaanquish.hatenablog.com

またまた手前味噌ですが、自分もxonshの記事結構書いています。PROMPTに画像出したりdatetime出したり、対話的な選択コマンド作ったり、matplotlibで遊んだりしています。
xonsh カテゴリーの記事一覧 - Stimulator



 

xontrib

xonshを使う上で楽しい拡張について記載する。
基本的には以下にまとめています。
vaaaaaanquish.hatenablog.com


自分が使っている物だけ抜粋。

# xonshrc - xontrib

# Docker周りの補完 pip install xonsh-docker-tabcomplete
xontrib load docker_tabcomplete

# tracebackを省略し見やすくする pip install xontrib-readable-traceback
xontrib load readable-traceback
$READABLE_TRACE_STRIP_PATH_ENV=True
$READABLE_TRACE_REVERSE=True

基本的にdockerがが動作する環境では上記を利用している。
zコマンド、fzfコマンドのcontribも存在するが、環境によって日本語文字化けが発生する事があり、xonshrcにある通りpecoを利用している。
peco使い始めて以降zコマンドもあまり使ってないので省略。


最後のreadable-tracebackは手前味噌ですが、筆者が作っています。
GitHub - vaaaaanquish/xontrib-readable-traceback: xonsh readable traceback


 

config.json

より外側の設定ファイル。~/.config/xonsh/config.jsonに書く。
一応以下の記事にまとめてあるが、config.jsonはサポートされなくなり、xonshrcになったので不要。
Xonshのconfigを書く - Stimulator

WARNING! old style configuration
(/Users/xxx/.config/xonsh/config.json)
is no longer supported. 
Please migrate to xonshrc.

 

基本的な操作と編集

xonsh独特なやつだと補完候補を確定させたい時はctrl+e、ctrl+↑↓で複数行のコマンドまたいで履歴移動、複数行にまたがるコマンドを途中で実行したければesc, enterくらい覚えておけば良さそう。ctrl+cでコマンドキャンセル、ctrl+a,eで行頭尾に移動とか、ctrl+←→で移動とか基本的なやつは身体で感じて覚えるかカスタマイズする。

複数行の記述は基本vimでやっている。xonshrcでconsole入力中にctrl+vでvim呼び出せるようにしており、vimで複数行コマンドを編集、quitすると入力されている状態となる。
f:id:vaaaaaanquish:20180622202308g:plain:w350

また、以下のようにvimrcを設定し、SyntaxHighlightや補完をxonsh周りのファイルでも効くようにしておけば便利。

" vimrc
autocmd BufRead,BufNewFile *.xonshrc setfiletype python
autocmd BufRead,BufNewFile *.xsh setfiletype python

検索はctrl+rだが、peco等に流せるなら設定しておくと便利。


 

おわりに

xonshのメリット、デメリットの概要と、自身が利用する設定やxontribについて記載した。

Pythonは(無理な所ももちろんあるが)生理的に嫌いという程ではないので、できるだけ多くの人にxonsh使って欲しいし、みんなxontrib作って公開して欲しい。

ブログも書いて欲しい。

いやほんとマジで。