- はじめに -
この記事はXonsh Advent Calendar 2018 - Qiita最終日の記事です。
私のxonshrc、業務用のコマンドとか含めると大体1500行くらいあるんでgithubで公開するんじゃなくて一部ずつ切り取ってここで紹介しようかなと思いました。
なんか作る時の参考になれば幸いです。
- 基本的な設定 -
以下に過去の大まかな設定が書いてあります。
xonshのPROMPTにdatetimeを表示する - Stimulator
環境変数
基本的な設定は大体以下のようになっています。
$EDITOR = '/usr/local/bin/vim' $VISUAL = '/usr/local/bin/vim' $VI_MODE = False $COMPLETIONS_CONFIRM = True $IGNOREEOF = True $INDENT = " " $CASE_SENSITIVE_COMPLETIONS = False $HISTCONTROL = "ignoredups" $XONSH_AUTOPAIR = False $AUTO_CD = True $XONSH_SHOW_TRACEBACK = True $SUPPRESS_BRANCH_TIMEOUT_MESSAGE = True $UPDATE_COMPLETIONS_ON_KEYPRESS = True def get_git_user(): return '{BLUE}' + $(git config user.name).strip() + ' {INTENSE_GREEN}{hostname}{WHITE} ( {YELLOW}"{cwd}" {WHITE}) {INTENSE_RED}$ ' $PROMPT = get_git_user $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" $XONSH_HISTORY_SIZE = (3000, 'commands') XONSH_HISTORY_MATCH_ANYWHERE = True $PTK_STYLE_OVERRIDES={ "completion-menu": "bg:ansiblack ansiwhite", "completion-menu.completion": "bg:ansiblack", "completion-menu.completion.current": "bg:ansiwhite ansiblack", "scrollbar.background": "bg:ansibrightblack", "scrollbar.arrow": "bg:ansiblack ansiwhite bold" , "scrollbar.button": "bg:ansiblack", "auto-suggestion": "ansibrightblack", "aborting": "ansibrightblack", }
前年との違いで言えば以下辺りが大きい気がします。
- get_git_userによってPROMPTにgit user nameを出すようにした
- PTK_STYLE_OVERRIDESによってstyleを上書きした
githubの所持ユーザ名が増えて、関係ないリポジトリにvaaaaanquishでpushしてしまうみたいな事が2回起こったので確認するようにしました。
後述しますが、gitユーザ変える関数とかも作っています。
またPTK_STYLE_OVERRIDESは以下のissueでも発言した通り、目がチカチカするのを抑えるために必須です。
Change auto_completer's color style · Issue #2840 · xonsh/xonsh · GitHub
キー入力ごとに更新されるCOMPLETIONSですが、iTerm2のcolorと合わせて調整しています。
aliases
エイリアスは別にgithubでxonshrcで検索して好きなだけ改造すればと思いますが一応省略して外観を。
if platform.system() == 'Darwin': aliases["lt"] = "colorls --tree" aliases["l"] = "colorls -ltr --sf" aliases["la"] = "colorls -la" else: aliases['ls'] = "ls --color=auto" aliases["l"] = "ls -lh" aliases["la"] = "ls -lha" aliases['free'] = "free -h" aliases['f'] = 'free -h' aliases['wf'] = 'watch free -h' aliases['ee'] = "exit" aliases["v"] = "vim" aliases["vi"] = "vim" aliases["vx"] = "vim ~/.xonshrc" aliases["vv"] = "vim ~/.vimrc" aliases["vs"] = "vim ~/.ssh/config" ...(gitとか色々etc)
MacとLinuxで分けれるように書いてあります。
あと "ee" でexitして、zshrcでもbashrcでもに "x" でxonsh起動するようにしています。xonshrcもすぐ開けるようにしておけば、xonsh本体の開発にコントリビュートする時に便利です。
xontrib
xontribは以下に落ち着いています。zコマンド病みつき。
xontrib load z xontrib load readable-traceback $READABLE_TRACE_STRIP_PATH_ENV=True $READABLE_TRACE_REVERSE=True xontrib load coreutils
最近はxonshもptkも開発が早く、まだptkのスピードに追いついてないxontribが見受けられます。
まあ書き直せばいいだけですが、自分は今の所こんな感じです。
(readable-tracebackもpython3.7で動かない情報があるので時間見つけて直します…)
他のxontribはこのへんに
Xontribs — xonsh 0.8.8 documentation
xonshにおけるxontribの紹介 - Stimulator
- ライブラリ周り -
ライブラリの管理はpipでやってます。
その他import周りを工夫しておくことでより便利に扱う事ができます。
import, xontrib_load時の自動install
ライブラリの管理について以下記事で書いていますが、import、xontrib load時にライブラリがない場合、自動でpip installするようにしてあります。
Python moduleがない場合に自動でpip installする - Stimulator
以下のようにすることで、新しい環境でひとまずxonshだけ動かしたいという時に、xonshrcを送るだけで対応できます。
import importlib import builtins from xonsh.xontribs import xontrib_context, update_context import xonsh.xontribs def _import(name, module, ver=None): try: globals()[name] = importlib.import_module(module) except ImportError: try: import subprocess cmd = "sudo pip install -I {}".format(name) subprocess.call(cmd, shell=True) globals()[name] = importlib.import_module(module) except: print("can't import: {}".format(module)) _import('pexpect','pexpect') def _update_context(name, ctx=None): if ctx is None: ctx = builtins.__xonsh__.ctx if not hasattr(update_context, "bad_imports"): _update_context.bad_imports = [] modctx = xontrib_context(name) if modctx is None: import subprocess cmd = "sudo xonsh -c 'xpip install -I xontrib-{}'".format(name) subprocess.call(cmd, shell=True) remodctx = xontrib_context(name) if remodctx is None: _update_context.bad_imports.append(name) return ctx return ctx.update(remodctx) return ctx.update(modctx) xonsh.xontribs.update_context = _update_context
xonshアップグレードでPATHが見えなくなったりした時にも発動するので割と重宝しています。
実際削除
importの遅延ロード
以下の記事で書いていますが、xonshrcでは起動が遅くなるのでimportしないけど、xonsh実行時に逐一import書く必要がないようにlazy_moduleを利用しています。
Pythonモジュールの遅延import - Stimulator
import importlib from xonsh.lazyasd import lazyobject lazy_module_dict = { 'pd': 'pandas', 'np': 'numpy', 'requests': 'requests', 'sci': 'scipy', '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)
色々追加したり消したりしましたが、今は利用頻度高いけどimportにちょっと時間がかかる系統が残り上記で落ち着いています。
- 認証周り -
基本的にパスワード入力をpexpectで処理しています。大体以下にも書いています。
xonshにおけるpexpectを利用した対話コマンド自動化 - Stimulator
ssh等は2要素認証等も含めて自動でやるようにしています。
1passwordを利用したpassword, one-time-passwordの取得
私は、ワンタイムパスワードも利用したい事を理由に全てのパスワード管理に1password、それらをshell上で利用するため1password-cliを使っています。
1Password command-line tool: Full documentation
import pexpect # masterパスワードをstrで取得する(自分で書いて) masterp=get_master_pasword() # 1password-cliから1password.appへの認証 def _pass_auth(): # 1pass auth p = pexpect.spawn("op signin my.1password.com --output=raw") while 1: i=p.expect([ r'.*(Enter the password for).*', pexpect.EOF, pexpect.TIMEOUT], timeout=3) if i==0: p.sendline(masterp) p.sendline('') elif i in [1,2]: break return str(p.before.strip())[2:-1] # サービスxのパスワードを取得 def _get_p(x): # get_pass $op_key = _pass_auth() p = $[echo $op_key | op get item @(x) | jq '.details.fields[] | select(.designation=="password").value'] return p aliases['getp'] = _get_p # サービスxのワンタイムパスワードを取得 def _get_op(x): # get one time password print('auth 1password...') $op_key = _pass_auth() print('get one time pass...') p = $(echo $op_key | op get totp -v @(x)) return p.strip() aliases['getop'] = _get_op
masterパスワードをstrで取得する部分はファイル読み込みとかで自身で書いて欲しいです。
これでコマンドで、パスワードやワンタイムパスワードを取得できます。
$ getp yahoo
hoge1234
$ getop yahoo
987123
これをpbcopyコマンドとかに送ればクリップボードから直接ペーストできます。
最近の悩みは_pass_authが若干遅い事です。(多分認証キャッシュの機能で解消できるけどやってない)
sshの自動化
上記のパスワード取得の仕組みを利用しながらssh周りも自動化しています。
サーバの接続の際にも踏み台などでパスワード、ワンタイムパスワードの入力が必須な作業環境で便利です。
import pexpect # masterパスワードをstrで取得する(自分で書いて) masterp=get_master_pasword() # 画面サイズ import curses curses.setupterm() term_lines = int(curses.tigetnum("lines")) term_cols = int(curses.tigetnum("cols")) # ssh認証 def _ssh_pex(p): while 1: i = p.expect([ r".*(Enter passphrase for key).*", r".*(Are you sure you want to continue).*", r".*(Verification code).*", pexpect.EOF, pexpect.TIMEOUT], timeout=3) if i==0: print('enter passphrase.') p.sendline(masterp) p.sendline('') elif i==1: print('continue: yes.') p.sendline('yes') p.sendline('') elif i==2: print('[auth]') otp = _get_op('yahoo') p.sendline(otp) p.sendline('') elif i in [3,4]: break p.interact()
こんな感じで書いておけば上手くいくと思います。
ワンタイムパスワードの期限がきれても_get_opリトライしてくれるので便利です。
これを利用してsshやscp周りを拡張しています。
import pexpect def _ssha(x): if '/' in x[0]: x[0]=x[0].replace('/','') p = pexpect.spawn("ssh " + x[0]) #, encoding='utf-8') p.setwinsize(term_lines,term_cols) # p.logfile = sys.stdout _ssh_pex(p) aliases["ssha"] = _ssha def _scpa(x): if '/' in x[0]: x[0]=x[0].replace('/','') p = pexpect.spawn("scp -r " + x[0] + ' ' + x[1]) _ssh_pex(p) aliases["scpa"] = _scpa
なんかたまに入るゴミのために変な処理が入ってますが、大体これでなんとかなると思います。
これとconfig組み合わせてワンタイムパス必須なサーバへの接続も自動化しています
local $ ssha gpu001 [auth] auth 1password... get one time pass...[123456] bastion server... fowarding port [8888] gpu001.server.hoge.co.jp $
この辺柔軟に書けるのは良いところだと思ってます。
ssh-hostの管理
会社でも趣味でもめちゃくちゃ沢山のhostにsshしたりコマンド飛ばしたりします。
そのため~/.ssh/configがぐちゃぐちゃになってしまう問題があったので、xonshコマンドで管理しています。
~/.ssh/configを雑にparseして表示、もしくはstrで返すxonsh関数です。
ptkのstyleを利用して、colorで出したり出さなかったりしています。
from prompt_toolkit import print_formatted_text from prompt_toolkit.styles import Style inquirer_style = Style.from_dict({ 'qa': '#5F819D', 'qu': '#FF9D00', 'dp': '#000' }) def _get_host(color=False): all_text = '' text = '' for x in $(cat ~/.ssh/config).split('\n'): if 'LocalForward' in x: text += ', ' + x.strip().split(' ')[1] if 'HostName' in x: text += ', ' + x.strip().split(' ')[1] elif 'Host ' in x: if text!='': all_text += text + '\n' text = x.split(' ')[1] all_text += text + '\n' if not color: all_d = [] for x in all_text.split('\n'): for i,y in enumerate(x.split(', ')): if i==0: all_d.append(('class:qu', y)) if i==1: all_d.append(('', ', ')) all_d.append(('class:qa', y)) if len(x.split(', '))==2: all_d.append(('','\n')) if i==2: all_d.append(('', ', ')) all_d.append(('class:qp', y)) all_d.append(('','\n')) print_formatted_text(FormattedText(all_d), style=inquirer_style) return return all_text aliases['host']=_get_host
こんな感じでhost一覧出したりgrepしたりpecoしたりしています。
これを利用して~/.ssh/configも読み書きできるようにしておくと、柔軟にお仕事できます。
一部切り抜いて紹介すると以下。
# _get_host()してparse def _ssh_host_to_dic(): host = {} for x in _get_host(True).split('\n'): rows = x.split(', ') if len(rows)>=2: host[rows[0]]={'s':rows[1], 'p':None} if len(rows)==3: host[rows[0]]['p']=rows[2] return host # config生成 def _w_host(host): # 中身はよしなに with open('~/.ssh/config', 'w') as f: f.write('HostKeyAlgorithms +ssh-dss\n') f.write('AddKeysToAgent yes\n') f.write('host *\n') f.write(' ForwardAgent yes\n') for k,v in host.items(): f.write('\nHost {}\n'.format(k)) f.write(' HostName {}\n'.format(v['s'])) if k != 'hoge': f.write(' IdentityFile ~/.ssh/id_rsa\n') f.write(' ProxyCommand ssh hoge.co.jp ncat %h %p\n') # springboard f.write(' IdentitiesOnly yes\n') if v['p'] is not None: f.write(' LocalForward {} localhost:{}\n'.format(v['p'],v['p'])) #LocalForward import argparse def _update_host(args): host = _ssh_host_to_dic() parser = argparse.ArgumentParser() parser.add_argument('name') parser.add_argument('-s') parser.add_argument('-p') args = parser.parse_args(args) if args.name not in host.keys(): host[args.name]={'s':None, 'p':None} if args.s is not None: host[args.name]['s'] = args.s if args.p is not None: host[args.name]['p'] = args.p _w_host(host) aliases['uh'] = _update_host
ちょっと切り抜きなので雑ですが、こんなん作っとけば以下でconfig更新したりしてサーバが増減しても安心です。
$ cat ~/.ssh/config HostKeyAlgorithms +ssh-dss AddKeysToAgent yes $ uh hogehoge -s hoge.server -p 9920 $ $ cat ~/.ssh/config HostKeyAlgorithms +ssh-dss AddKeysToAgent yes host * ForwardAgent yes Host hoge HostName hoge.server IdentityFile ~/.ssh/id_rsa IdentitiesOnly yes LocalForward 9920 localhost:9920
あとは、自身が欲しいままにconfig_generatorを作っておけば安心。
Tipsですが、ちょっと複雑なコマンド作るときはargparseしておくと --help でオレオレdiscription出せるのでいい感じに使えます。
gitアカウントの管理
gheとかgitのcliは充実してますが、自分は以下だけ。
いくつかあるgitアカウント間違えないように変更しやすくしています。
# git chenge def g_change(account): account = account[0] if account=='vanquish': account='vaaaaanquish' git config --global user.name f"{account}" if account=='hoge': git config --global user.email hoge@company.jp print(f'change:{account}') elif account=='vaaaaanquish': git config --global user.email 6syun9@gmail.com print(f'change:{account}') else: print(f'check account name:{account}') aliases['gac']=g_change
gitacコマンドで複数のアカウントを切り替えて、上記「 環境変数」の項目で書いたようなスクリプトでshellのPROMPTに表示しています。
hoge $ hoge $ gac vanquish shukawai $
多分もっと優勝できると思ってます。
- 移動、操作 -
移動はもっぱらzとpecoを使っています。
GitHub - peco/peco: Simplistic interactive filtering tool
GitHub - rupa/z: z - jump around
xontrib-zを導入しても良いですし、以下のように書いても良い感じに使えると思います。
def z(): lines = open($DIR_HIST_PATH[0]).read().rstrip("\n").split("\n") return("\n".join([p for p, c in Counter(lines).most_common()]))
履歴の取得
以下の記事で紹介されているものを利用しています。
Xonshを快適にするptk(Python Prompt Toolkit) - Qiita
以下の記事と書きましたが、書いたのは元同僚で私にxonshを薦めてきた諸悪の根源です。
import os import json from collections import OrderedDict 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 = [] for f in files: try: file_hist.append(json.load(open(f))['data']['cmds']) except: pass 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] cmds = reversed(cmds) if return_list: return cmds else: return '\n'.join(cmds)
後述するkeybindとpecoの連携によって、過去のコマンド実行をあいまい検索できるようにしています。
dirの保存
移動したディレクトリは、on_chdirで発火する関数を作成しファイルでざっくり管理しています。
# file save dir $DIR_HIST_PATH = "~/.dirhist" @events.on_chdir def add_to_file(olddir, newdir, **kw): with open($DIR_HIST_PATH[0], 'a') as dh: print(newdir, file=dh)
これも後述のkeybindとpecoの連携部分で最近行ったdirをpecoで利用したいがためです。
普段はこれをもうちょっと拡張して、特定の作業ディレクトリに行きやすくなったりするようにしています。
keybindings
キーバインドです。
何でもかんでもpecoに流しています。
from prompt_toolkit.keys import Keys from prompt_toolkit.filters import (Condition, IsMultiline, HasSelection, ViInsertMode) @events.on_ptk_create def custom_keybindings(bindings, **kw): # ctrl+vで入力中の単一、複数行コマンドをvimで開く @bindings.add('c-v') def edit_in_editor(event): event.current_buffer.tempfile_suffix = '.py' event.current_buffer.open_in_editor(event.cli) # ctrl+rで過去の実行コマンドをpeco @bindings.add('c-r') 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_config内のhost先をpeco @bindings.add('c-s') def select_ssh(event): hosts = _get_host(True) selected = $(echo @(hosts) | peco) if selected: event.current_buffer.insert_text('ssha ' + selected.strip().split(', ')[0]) # ctrl+fで今いるディレクトリのfileをpeco @bindings.add('c-f') def select_file(event): r = lambda x: './'+x if os.path.isdir(x) else x files = '\n'.join([r(x.split(' ')[-1]) for x in $(ls -l).split('\n')]) selected = $(echo @(files) | peco) event.current_buffer.insert_text(selected.strip()) # ctrl+dで過去移動したディレクトリをpeco @bindings.add('c-d') def _z(event): selected = $(echo @(z()) | peco) cd_cmd = "cd " + selected.strip() event.current_buffer.insert_text(cd_cmd) # ctrl+tで翻訳コマンドを入力 @bindings.add('c-t') def _engs(event): event.current_buffer.insert_text('t ') ....
利用してるkeybinding、実際はもっとあるんですが、業務に関わる部分が多かったものは削っています。
tコマンドは後述しますがGoogle翻訳としてよく使っているので書いときました。
- other -
大体上記のような環境でやっていってますが、他に結構使ってる便利なお手製コマンドを書いておきます。
google-translation
仕事で英語圏メンバーとMTGを毎日してますが、英語力低いので翻訳が手元に欲しかったりします。
GitHub - soimort/translate-shell: Command-line translator using Google Translate, Bing Translator, Yandex.Translate, etc.
keybindingで ctrl+t でt を入力、あとは日本語か英数字か正規表現で判定してGoogle翻訳かけるというコマンドを作っています。
VivaldiなるブラウザのWebパネルにもGoogle翻訳をセットしていますが、Shellだとそのままpbcopyでクリップボードに流したり、英語か日本語かの判定文自分で雑にかけるので何だかんだ結構こちらを使っています。
import re jap = re.compile('[あ-んア-ン一-鿐]') def _eng(x): if len(x)==0: return x = ' '.join(x) if jap.search(x) is None: y = $(trans en:ja @(x)) else: y = $(trans ja:en @(x)) return y aliases['t'] = _eng
vaaaaanquish $ t 翻訳 翻訳 (Hon'yaku) translation 翻訳 の定義 [ 日本語 -> English ] 名詞 translation 翻訳, 訳書, 翻訳物, トランスレーション deciphering 解読, 翻訳 翻訳 translation
最初はDMM英会話の「英語でなんていうknow」なるサービスがすごくよくてスクレイピングするコマンドを作って多用していましたが、慣用句以外なら単語さえわかればconversationに盛り込めるくらいにはなってきたので今はこれだけに落ち着いています。
ドキュメントやコード内の謎の英語とか調べるのにも重宝しています。
画像の表示
ディレクトリ内の画像から雑にsampleして、iterm2経由で表示するやつです。
画像処理なんかをやってる時に、「あ〜、このtrainディレクトリって何入ってんだっけ」となるので使ってます。
import matplotlib.pyplot as plt import xontrib.mplhooks import numpy as np import cv2 import random from mimetypes import guess_extension def _imgls(path): fig, ax = plt.subplots(3, 3, sharex='col', sharey='row') fig.set_size_inches(10, 10) d=[] for x in os.listdir(path): if x.split('.')[-1] in ('jpe', 'jpeg', 'png'): d.append(path+x) if len(d)>9: d = random.sample(d,9) for i, imgid in enumerate(d): col = i % 3 row = i // 3 img = cv2.imread(imgid) img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) ax[row, col].imshow(img) xontrib.mplhooks.display_figure_with_iterm2(fig) aliases['imgls'] = _imgls
以下あたりを参考に作っていて、基づいて少し変更すればLinuxやwinでも動きます。
Pythonでyahoo画像検索した結果をimgcatに流して表示してURLをクリップボードにコピーするやつ - Stimulator
Xonshでmatplotlibグラフをコンソールにインライン描画してメモリ状況を観察する - Stimulator
これが結構便利なので割と使います。
おわりに
1つ1つ記事にも出来ましたが、Xonsh Advent Calendar 2018 - Qiitaがまさかの大盛況で助かりました。
一旦業務部分を切り取ってとりあえず書いたので、今後もうちょっとこの記事は更新されると思います。
業務的なところでは、社内サーバの起動から、GPUクラスタのGPUを監視してリソース奪い取るコマンド、勤怠や日報の自動化、社内チャットへの投稿、社内のMTGルームの取得…など全部適当に書いたコマンドで何とかしています。(なかなか切り分けできてないので見せられないですが)
サッと書いたスクリプトがxonshrcに入り常用してしまうという1年だったため、結構rc自体が汚く肥大化しています。
今現在も、xonshrcのリファクタリングとか、コマンドのhistory backendをAWSに乗せるとか、起動の高速化とかをやっていきたいと思ってるので2019年も更にがんばっていきたいと思います。
それではhappy xonsh year。