余白の書きなぐり

aueweのブログ

sedでファイルの最終4行を消す処理について

結論から言うと、sedを使ったスマートな方法は無いので、headコマンドを使いましょう。

$ head -n -4 file

# どうしてもsedを使いたければ
$ cat file |sed -e '$d' |sed -e '$d' |sed -e '$d' |sed -e '$d'

デマが蔓延している

以下のような$-3記法(そんなsedはない)をデッチ上げて紹介している記事が多い。

$ sed -e '$-3,$d' file

デマの発生源?

SBクリエイティブ:【正誤情報】入門UNIXシェルプログラミング 改訂第2版

●第7章:p.175、下から3-1行目

<修正前> また、ファイルの最後の 4 行を消すには、 sed -e '$-3,$d' file と書きます。$-3 は最後から数えて 3 行目、つまり下から 4 行目のことです。

<修正後> ただし、次のように最終行の $ から何行目という指定はできません。 sed -e '$-3,$d' file sed では $-3 のような相対的な行の表現ができずにエラーになります。 最後の行から数行をまとめて消したい場合には、最後の行が何行目なのかを先に調べるか、最後の行だけを消す処理を何回か繰り返すことで対応します。

vimのメモ取り環境

僕は全てのメモを「Gmailで自分宛てに送信」して記録している。 インストールログや便利なツールのメモ、数学のパズル、役所の手続きから買い物リストまで、何もかもをGmailに送信しているのだ。 リマインダもToDoリストも全てメール管理。 閲覧は主にスマホとPCのブラウザで、検索はGmailの検索窓を使っている。

話は変わるが、人生とgitの最大の違いは因果律にあると思う。 人生にタイムマシンは存在せず、git reset --hardgit rebaseなどの便利コマンドもなく、ひたすらコミットを積み重ねることしかできない。 メモは人生のコミットログであり、その媒体に相応しいのは再編集不可能なメールだと、僕は思う。 EvernoteやSlackを使いこなす意識の高い連中には分かるまい~

だいぶ脱線したが、僕のWindowsのメモ取り環境は以下のような感じ。

  1. Winキー + R で「ファイル名を指定して実行」を開く
  2. "gvim"と入力して、gvim.exeを起動
  3. 無名バッファにメモ内容を書く
  4. コマンドラインモードで:Gmailと入力してメール送信

コンセプトは「とにかく素早いメモ書き」「原始的」の2点。 後者について補足すると、vimプラグインは一切使わず、vim以外のフリーソフトも一切使わず、Windows謹製の機能だけで手軽なメモ取り環境を作りたい。 ちなみにLinuxMacだと、msmtp等を使えば原始的なメモ環境の構築など造作も無いので、今回は(厄介な)Windowsに焦点を絞った。

Windowsvimを即座に起動する

パスの通ったディレクトリにgvim.exeへのショートカットリンクgvim.lnkを設置すれば、上で書いた

  1. Winキー + R で「ファイル名を指定して実行」を開く
  2. "gvim"と入力して、gvim.exeを起動

が実現できる。起動時間の短縮には、やはりマウスよりキーボードの方が向いていると思う。
参考:http://www.kamomer.com/entry/windows10-run-filename

gvim.exeの起動は簡単だが、問題はメモ内容をメール送信する部分だ。これを外部ツールに頼らずに実現したい。

vimで書いたメモをSMTPGmailに送信

まず以下のsendGmail.vbsというファイルを、:set fenc=cp932で保存する。 このファイルは標準入力を受け取ってメール送信するVBSスクリプト

Dim objStdIn
Dim intExitCode
Set objStdIn = Wscript.StdIn

Set cdo = Wscript.CreateObject("CDO.Message")
With cdo
  .From             = "hoge@gmail.com"
  .To               = "hoge@gmail.com"
  .Subject          = "自分用メモ"
  .ReplyTo          = "hoge@gmail.com"
  .Textbody         = objStdIn.ReadAll
  .BodyPart.Charset = "Shift-JIS"
end With

schemas_url ="http://schemas.microsoft.com/cdo/configuration/"
With cdo.Configuration.Fields
  .Item (schemas_url & "sendusing")             = 2
  .Item (schemas_url & "smtpserver")            = "smtp.gmail.com"
  .Item (schemas_url & "smtpserverport")        = 465
  .Item (schemas_url & "smtpconnectiontimeout") = 30
  .Item (schemas_url & "smtpusessl")            = true
  .Item (schemas_url & "smtpauthenticate")      = true
  .Item (schemas_url & "sendusername")          = "hoge@gmail.com"
  .Item (schemas_url & "sendpassword")          = "パスワード"
  .Update
end With
cdo.Send

再度注意するが、sendGmail.vbsは:set fenc=cp932で保存するように!。 パスワードは平文直書きで、セキュリティコストは自腹。ワイルドだろう?

次に~/.vimrcに以下を追加

command! Gmail set fenc=cp932 | w !cscript C:/Path/To/sendGmail.vbs

" Markdownで書く場合はfiletype切替コマンド(sm)を自作すると便利
nnoremap sm :<C-u>set filetype=MARKDOWN<CR>

以上で設定は終わりだ。

使い方は冒頭で説明したが、vimでメモを書いた後コマンドラインモードで

:Gmail

とすれば、メモがGmailに送信される。めでたしめでたし。

蛇足

メールをUTF-8で送りたかったので、sendGmail.vbs

.BodyPart.Charset = "Shift-JIS"

のあたりをいじくり回したのだが、うまくいかなかった。 普通メールの文字コードといえばiso-2022-jpなのだが、windowsでコードを変換するには外部のソフトウェアが必要になる(たぶん)。 少なくとも僕の環境では、vim単体で無名バッファ(UTF-8)をiso-2022-jpに変換することができなかった。 またVBSスクリプトでメールを送信する際も、UTF-8は扱いづらいようだった。

" 変換に失敗する
:set fenc=iso-2022-jp
:w
" うまくいく
:set fenc=cp932
:w

文字コードをcp932(Shift-JIS)にしておけば、vimもVBSスクリプトもハマることなく動作した。 やはりWindowsではShift-JISが最強なのだと思いました。

参考

vimをパイプにする

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

ノーマルモード以外をパイプとして使う際の情報を追記しました (2016/12/04)

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ソースコードを修正してくれ~

追記

ブコメで良い指摘をいただきました。

vimをパイプにする - 余白の書きなぐり

Vimをパイプで使うのめっちゃ便利!ただ、なんでnormalコマンド限定の書き方にしたんだろ?

2016/12/04 01:23

確かに記事中ではパイプの対象をvimのノーマルモードに限定していました。 もしvimのビジュアルモードが使いたければ、以下のようにvを入れれば良いです。

$ cat a.txt
1 hoge
2 piyo
3 fuga

$ cat a.txt |vipe lvjhd
1 piyo
3 fuga

矩形選択が使いたければ、vの代わりにCtrl+vを二回連打すれば良いですね。

もしvimコマンドラインモードを使ってテキストの整形がしたければ...まてよ、vimの親はviでその親はedで、ストリーム版のedはsed... なので、vimの叔父にあたるsedを使うと良いでしょう。

コマンドラインモードを使ったファイルの作成や書き込みは、vipeでむりやり実装するより、シェルのリダイレクション機能を使う方が良いと思います。 「どうしてもvipeでファイルを上書きしたいんやああ」という方がいらっしゃれば、

vim - -es +":norm gg" +":norm $COMMAND" +:%p +:q! |sed '1d'

:q!の部分を":wq! hoge.txt"に変更するなど、うまく調整してください~

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

自鯖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をインストール

あちこちにインストール方法が書かれてるけど、少しハマったのでメモ。 まず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が公式サポートされたらしい

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とするとバグっちゃう。こわいこわい。