読者です 読者をやめる 読者になる 読者になる

余白の書きなぐり

aueweのブログ

vimをパイプにする

Vim シェルスクリプト

この記事は Vim Advent Calendar 2016 (その2) の3日目の記事です。

UNIXのテキスト処理

UNIXでテキストを自動整形する際、パイプ機能は欠かせない。

$ cat a.txt
1 hoge
2 piyo
3 fuga

$ cat a.txt |sed 's/piyo/foo/' |grep '2'
2 foo

シェル上で | というパイプ記号を使ってコマンドを次々繋げることで、複雑なテキスト処理をこなすわけだ。 パイプは便利だが、テキストエディタをパイプとして使う人はあまり見かけない。 テキストエディタ=対話的 という常識があるため、パイプのような自動処理とは相性が悪いと思われているのだろう。 しかし今日はあえて、シェルスクリプトワンライナーの中にvimを埋め込み、パイプとして静的に使ってみたい。

vimをパイプとして使う

vimを普通に使って冒頭の$ cat a.txt |sed 's/piyo/foo/'を行うには、ノーマルモードでjwCfooと入力すればよい。 このjwCfooという呪文をシェル上で使用するには以下のようにすれば良い。

# 最も普通の方法
$ cat a.txt |vim -es +'norm jwCfoo' +%p +q! /dev/stdin

# あるいは
$ cat a.txt |ex -s +'norm jwCfoo' +%p +q! /dev/stdin

# このやり方は Vim: Reading stdin... 問題が生じる
$ cat a.txt |vim - -es +'norm jwCfoo' +%p +q! |sed '1d'

とすればよい。一番上のワンライナーの説明をしておくと

  • vim -eexと等価で、vimを非対話(exモード)で起動するという意味。
  • -sはサイレントモードで起動し、標準出力を汚さないというオプション。
  • +'norm jwCfoo'normは、ノーマルモードのコマンドを使うという意味。
  • jwCfoovimの呪文。中にESCを入れるにはCtrl+vしてESCを押す。
  • +%p はファイルの全内容を標準出力に表示するexコマンド。
  • +q!vimを強制終了するexコマンド。馴染み深い。
  • /dev/stdinは読み込み先を標準入力 (cat a.txt) にするという意味。

三番目のワンライナー/dev/stdinの代わりにハイフン(標準入力のシンボル)を使っていて、スマートに見える。 ただしこの方法では、標準出力の冒頭にVim: Reading from stdin...というクッソうざい文字列が勝手に挿入される。 こいつを消すには、直後にシェルコマンド|sed '1d'等を噛ます必要がある。

外部コマンドにしよう

以下のようなvipeコマンドを作っておくと、シェル上でvimの呪文が使いやすくなる。

# .bashrcや.zshrcに書き込む
vipe () {
  COMMAND=$(echo "$*")
  # コロン':'でESC入力を代替する場合はコメントを外す。^[はCtrl+vしてESC押して入力
  # COMMAND=$(echo "$*" |sed -e 's/:/^[/g')
  vim - -es +":norm gg" +":norm $COMMAND" +:%p +:q! |sed '1d'
}

使い方は以下のような感じ。

$ cat a.txt
1 hoge
2 piyo
3 fuga

$ cat a.txt |vipe jwCfoo |grep '2'
2 foo

# ^[はESC文字で、Ctrl+Vした後にESCを押して入力する
$ cat a.txt |vipe Abar^[oxxxx
1 hogebar
xxxx
2 piyo
3 fuga

# コマンドに空白文字を含む場合は'か"でくくる
$ cat a.txt |vipe "A bar baz"
1 hoge bar baz
2 piyo
3 fuga

シェル上でvimのマクロ機能を使う

vimには超便利なマクロ機能が存在する。 簡単に説明すると、ノーマルモードにおける一連の操作をマクロ文字列として記録再生する機能で、 qaと押すことでレジスタaに記録し、@aと押すことで再生できる。 上述のjwCfooという呪文はvimのマクロの一種といえるだろう。

vimを対話的に使って (qaを使って) 記録したマクロを表示するには、ノーマルモードで"apと押せば良い。 こうして表示されたレジスタaの中身は、vipeコマンドの引数として食わせることが出来る。

$ cat a.txt |vipe "レジスタの中身を書き込む"

これで「vimのマクロを使いまわしたいな〜」なんて場合も安心ですね!

参考

vim -esとした時に表示されるVim: Reading from stdin...問題についてはコチラ。 vimソースコードを修正してくれ~

apache2でcgiがダウンロードされる時のチェック項目一覧

Linux

自鯖http://hogehoge/piyo.cgi にアクセスしたとき、piyo.cgi が実行されず、なぜかダウンロードが始まる時のチェック項目一覧

AddHandlerしたか ?

# httpd.conf
AddHandler cgi-script .cgi

AddTypeしたか?

# httpd.conf
AddType applications/x-httpd-cgi .cgi

OptionでExecCGIしたか?

# httpd.conf
Options Indexes FollowSymLinks ExecCGI

a2enmodを有効化したか?

$ sudo a2enmod cgi # Ubuntu14.04LTSの場合

設定終えた後restartしたか?

$ sudo service apache2 restart

cgiのパーミションは大丈夫か?

$ cd /.../cgi-dir/
$ chmod 755 hoge.cgi

cgiを設置したディレクトリのパーミションは?

$ 755 /.../cgi-dir

cgiスクリプトの表示内容は正しいか?

#!/usr/bin/perl
print "Content-typo: text/html\n\n";    # タイポしてる!!
print "Hello, World.";

他なんかあるかな。 a2enmodの有効化はググってもあんまり出ないし盲点な気がする。

さくらVPSのUbuntu LTS 14.04にSoftether-VPNをインストール

Linux

あちこちにインストール方法が書かれてるけど、少しハマったのでメモ。 まずsoftetherのウェブサイトからソースをダウンロードする

$ uname -a   # x86_64を確認。64bit用のインストーラーをダウンロードする
$ mkdir vpninstall
$ cd vpninstall
$ wget http://jp.softether-download.com/files/softether/v4.19-9605-beta-2016.03.06-tree/Linux/SoftEther_VPN_Server/64bit_-_Intel_x64_or_AMD64/softether-vpnserver-v4.19-9605-beta-2016.03.06-linux-x64-64bit.tar.gz
$ tar xvfz softether-vpnserver-v4.19-9605-beta-2016.03.06-linux-x64-64bit.tar.gz

コンパイルしてインストールする

$ cd vpnserver
$ make # 1を三回選んだ
$ cd ..
$ sudo mv vpnserver /usr/local  # make installコマンドは無いようなので、愚直にmvする
$ cd /usr/local/vpnserver
$ chmod 600 *
$ chmod 700 vpncmd vpnserver
$ chown root:root *
$ chown root:root **/*
$ /usr/local/vpnserver/vpncmd   # 正しくインストールされてるかチェック

サーバーの電源ON時にvpnserverが自動起動するよう設定

$ sudo vi /etc/init.d/vpnserver
$ sudo apt-get install sysv-rc-conf   # sys-rc-confはchkconfigのUbuntu版
$ sysv-rc-conf --add vpnserver

/etc/init.d/vpnserverの中身

#!/bin/sh
# chkconfig: 2345 99 01
# description: SoftEther VPN Server
DAEMON=/usr/local/vpnserver/vpnserver
LOCK=/var/lock/subsys/vpnserver
test -x $DAEMON || exit 0
case "$1" in
    start)
        $DAEMON start
        touch $LOCK
        ;;
    stop)
        $DAEMON stop
        rm $LOCK
        ;;
    restart)
        $DAEMON stop
        sleep 3
        $DAEMON start
        ;;
    *)
        echo "Usage: $0 {start|stop|restart}"
        exit 1
esac
exit 0

管理権限のあるアクセス元IPアドレスadmini.tmpに指定。 「管理権限のあるアクセス元のIPアドレス」であり、「クライアントのIPアドレス」ではない。 通常はVPNサーバをインストールしたサーバー自身のIPアドレスを書く。

$ sudo vi /usr/local/vpnserver/adminip.tmp
# ファイルには以下のように書く。
192.168.1.2
127.0.0.1

vpnサーバを立ちあげてパスワードを設定

$ sudo /usr/local/vpnserver/vpnserver start # この操作を忘れてハマった
$ sudo /usr/local/vpnserver/vpncmd # 何も入力せずエンターを二回押した後、パスワード設定

参考サイト SoftEther VPN Server を CentOS にインストールする 3.5 仮想 HUB のセキュリティ - SoftEther VPN プロジェクト

Windows10でbashが公式サポートされたらしい

Windows シェルスクリプト Linux

www.publickey1.jp

これはすごいニュースやで。 今までmsys2でzsh使ってきたけど、これからはそういうトリッキーなことせずに済むらしい。 WindowsにおけるLaTeXやgit、sshの環境が根本的に変わる予感。

Haskellの型推論でハマった話

久しぶりにHaskellで遊んでたらハマったのでメモ。 100の階乗 100!=1*2*...*100 の桁数を求めるプログラムを書いた。

main = print $ kaijo 100
kaijo n = length $ show $ product [1..n]
-- 158が表示される。

上記のプログラムは正しい値(158)を返すが、下記のように型を明示すると、誤った値(0)を返す。

main = print $ kaijo 100
kaijo :: Int -> Int
kaijo n = length $ show $ product [1..n]
-- 0が表示される。なんでや!!

ちなみに100ではなく10の階乗なら、どちらのプログラムも正しい結果を返す。 この不条理の原因は、Intを明示するとproduct関数の計算がIntで実行されて、 100の階乗という大きな値が桁溢れを起こすせいだと思われる。 Intを明示しなければ、自動で多倍長計算してくれる。

「100を158にする関数」なのに、Int -> Intとするとバグっちゃう。こわいこわい。

zshのプロンプトを256色にする方法

端末

zshから256色を使う詳細については、以下のサイトの情熱がものすごい。 http://misc.flogisoft.com/bash/tip_colors_and_formatting

しかし肝心のプロンプトの設定方法が載ってなかったので、ここにメモしておこう。 ~/.zshrcで以下のように記述する。

# 例1
# 注意! ^[ は特殊文字。Vim上で Ctrl+Vした後にESCを押せば入力できる。
# 赤(001番)文字+デフォルト背景
PROMPT="%{^[[38;5;001m%}RED%{^[[0m%}"
# 例2
# デフォルト文字色+緑(082番)背景
PROMPT="%{^[[30;48;5;082m%}Green Background%{^[[0m%}"
# 例3
# 赤(001番)文字+緑(082番)背景
PROMPT="%{^[[30;48;5;082m%}%{^[[38;5;001m%}Red on Green Background%{^[[0m%}"
# 例4
# 以下のように文字色を定義しておくと便利
COLOR_FG="%{^[[38;5;001m%}"      # 表を赤に
COLOR_BG="%{^[[30;48;5;082m%}"   # 背景を緑に
COLOR_END="%{^[[0m%}"            # 色を元に戻す
PROMPT="${COLOR_BG}${COLOR_FG}Red on Green Backgroun${COLOR_END}"

ちなみに zshのプロンプトカラーを設定を変更してみた - HAM MEDIA MEMO に書いてある形式

PROMPT=$'%{\e[38;5;46m%}%m%(!.#.$)%{\e[m%} '

ではうまくいかなかった。

シェルスクリプトの平文パスワードをセキュアにする方法(続き)

シェルスクリプト リモートログイン

この記事は、一年前に書いた シェルスクリプトの平文パスワードをセキュアにする方法 という記事の続きです。 なぜか今頃になって大量のはてブが付き、戦慄している次第です。 ブックマーカーのコメントには

  • chmod 700 の平文パスワードファイルを読み込むのと大差なくね。
  • 秘密鍵を使う方法は環境固定なのがイヤ。
  • GitHubシェルスクリプト共有するときに便利そう。

といった指摘が並んでいました。せっかくなので、少し補足します。

chmod 700 の平文パスワードファイルを読み込むのと大差なくね。

確かにその通りですね。 暗号化したパスワードファイルが盗まれる状況では、ssh秘密鍵も一緒に盗まれそうです。 そう考えると、平文のパスワードを外部ファイルに保存してパーミションを700に設定するのと、本質的に同程度のセキュリティしか担保されていません。

この問題を解決するのは簡単です。 ssh-keygen秘密鍵を作成する際にパスワードを設定すればよろしい*1 こうしておけば、もし秘密鍵と暗号化したパスワードファイルの両方が盗まれたとしても、秘密鍵のパスワードが破られない限り複合できません。 もう少し具体的に書くと、まず

$ ssh-keygen -f pasu_tuki_rsa
Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase): # パスワードを入力
Enter same passphrase again: # パスワードを再入力
Your identification has been saved in pasu_tuki_rsa.
Your public key has been saved in pasu_tuki_rsa.pub.
The key fingerprint is:
...

としてpasu_tuki_rsaというパスワード付きの秘密鍵を作成します。 後は前回の記事に書いたのと同じ手順で暗号化や複合が可能ですが、その時にpasu_tuki_rsaのパスワードを問われます*2

秘密鍵を使う方法は環境固定なのがイヤ。

この問題については、パスワードファイル専用のssh秘密鍵を作ることで解決します。 パスワードファイルと(パスワード付きの)ssh秘密鍵をペアで管理する訳ですが、このペアは環境に依存しないので、各環境に配布することが可能です。 便利な使い方としては、例えばcronrsyncでパスワードファイルを各環境に同期すれば、Dropbox + KeePassのコマンドライン版を手軽に実装できます*3

GitHubシェルスクリプト共有するときに便利そう。

まさにそういう目的で前回の記事を執筆しました。 他人とシェルスクリプトを共有する際、スクリプトに自分専用の平文パスワードをべた書きするのは論外です。 対策として、パスワードを外部に保存してスクリプトから読みこめば、スクリプトを共有できると考えました。 しかし外部に平文のパスワードファイルを置くのも気持ち悪いと思い至り、前回記事の着想を得ました。

まとめ

要点は以下に集約されます。

  • opensslを使うことで、パスワードファイルのセキュリティコストをssh秘密鍵に負わせることが可能。
  • ssh秘密鍵にはパスワードが設定できる。
  • opensslは大抵の環境に入っているので、ソフトのインストールが制限されている環境でも使用可能。

こういう特性をうまく使って、各人にあった運用方法を探してね~

*1:そもそもsshログイン用の秘密鍵にパスワードを設定しないのは、かなり危険だと思う。もし秘密鍵が漏れた場合、その公開鍵が登録されてる全環境にログインされるので、二次被害が半端ない

*2:パスワードを保存したファイルを開くために、また別のパスワードが必要なのはナンセンスでは?という質問については、「パスワード管理ソフト」でググるのが教育的だと思う

*3:こういう秘密鍵の使い方は、公開鍵暗号システムの本質から外れていると思う。もっと良い方法がないものか