Quantcast
Channel: My Future Sight for Past
Viewing all 265 articles
Browse latest View live

Solution for "Fatal server error: PAM authentication failed, cannot start X server."

$
0
0

Fedora Core3で以下のコマンドによりX Windowを起動してGUIをスタートさせようとした。

xinit

すると,以下のメッセージが表示され失敗してしまった。

Fatal server error:
PAM authentication failed, cannot start X server.

どうやらアクセス権限に問題があるようだ。解決策を探していたら,以下のサイトが見つかった。

参考:[SOLVED] CentOS 6.4: Cannot run startx as non-root user - CentOS

ここで説明されているように,/etc/pam.d/xserverファイルを編集して,以下の内容にしてやり直すと解決した。

#%PAM-1.0
auth sufficient pam_rootok.so
#auth required pam_console.so
auth sufficient pam_permit.so
account sufficient pam_permit.so
session optional pam_keyinit.so force revoke

Adobe AcrobatのClearScan実行時の「Pager Capture 認識サービスのエラーにより、ページを処理できません。(6)」の解決方法

$
0
0

有料版のAdobe Acrobat(StandardやPro)にはOCR(Optical Character Recognition:光学文字認識)機能がある。その中でClearScanというOCRがある。ClearScanではPDFの画像の文字を解析して,その文字とよく似たフォントで画像を置換してくれる。ClearScanでフォントを埋め込むことで,以下2点の利点がある。

  • PDFのファイルサイズを1/3程度に縮小できる。
  • 拡大しても文字がギザギザにならずとても読みやすい。

ClearScanに相当する機能は他社は提供しておらず,Adobe Acrobat独自の機能となっている。縦書き文字に対する認識はいまいちだが,上記2点の利点があるので,横書きPDFに対してはClearScanでOCRをかけることにしている。

ところが,[ClearScan]やその他のOCR方法である,[検索可能な画像]と[検索可能な画像(非圧縮)]を選択してもエラーが発生してOCRを実行できないページが存在する。エラーが発生するページが1ページでもあれば,そのPDF全体はOCRを適用できなくてとても困る。この解決方法が分かったのでそれを記す。

Adobe AcrobatはScanSnap ix500に付属してきたAdobe Acrobat X Standardで動作を確認した。

エラーの発生したページとエラーメッセージ

エラーは例えば以下のようなページに対してスキャンが実行されると発生する。

エラー発生ページ例(書籍:滝澤, ななみ (2013) : 2013-2014年版 みんなが欲しかった! FPの教科書 2級・AFP

エラーメッセージの内容はそれぞれ以下の通り。

次の理由により、テキスト認識(OCR)を実行できませんでした。
Pager Capture認識サービスのエラーにより、ページを処理できません。(6)

[ClearScan]実行時のエラーメッセージ

次の理由により、テキスト認識(OCR)を実行できませんでした。
原因不明のエラーが発生しました

[検索可能な画像]と[検索可能な画像(非圧縮)]実行時のエラーメッセージ

上記のサンプルページでは,おそらくページ下部の罫線がOCRの妨げになっていると思われる。その他にも,ローマ数字や丸付き数字,しわなどスキャン時にぐちゃぐちゃになった部分が存在すると上記のエラーが発生するようだ。

他にもネットで同様のエラーが報告されている。

参考:AcrobatのOCRエラー(その後) - Mugicha2004の日記

解決方法

このエラーの解決方法を考えたところ,以下の順位で3通りあるだろうと考えた。

  1. エラーページ内の問題になりそうな箇所をトリミングで削除
  2. エラーページだけ分離して,別のソフトでOCR後マージ
  3. Adobe AcrobatのOCRをやめ,他社製品のOCRを利用

以下でそれぞれの内容を説明する。

エラーページ内の問題になりそうな箇所をトリミングで削除

この方法が一番簡単でお勧めだ。エラーの発生したページにおいて,OCRの妨げになるような箇所をトリミングで削除してしまう。例えば,以下の画像のようにスキャンがきれいにできず,左上の赤丸で囲んだごちゃごちゃしてそうなところをトリミングで削除する。

トリミング前
トリミング後
トリミングの実行前後(書籍:墺, タカユキ (2003) : 総合英語Forest

トリミングは以下の手順で行える。

[表示]→[ツール]→[ページ]→[トリミング]

トリミングの実行手順

ページをダブルクリックするとダイアログが表示され,ページ範囲指定して一括でトリミングすることもできる。不要な余白の削除にも便利な機能だ。

なお,トリミングではあくまで表示範囲を再設定しただけで,内部的にはデータを保持している。データ自体も削除するには以下の手順で非表示情報を削除する。なお,この操作を行うとメタデータ類が全て削除されるので注意する。

[表示]→[保護]→[非表示情報を検索して削除]

トリミングしたデータの削除手順

その他,試してはいないが,Adobe Acrobat Proの隅消し機能や,他の画像編集ソフトでOCRの妨げになる部分を隠すことでも対応できるかもしれない。

エラーページだけ分離して,別のソフトでOCR後マージ

この方法は僕が最初に試した方法だ。やや手間だがこの方法も悪くない。この方法は以下の手順でエラーに対処する。

  1. エラーの発生したページを切り取って別のPDFに分離。
  2. 本体のPDFにClearScanを実行。
  3. 分離したPDFはAdobe Acrobat以外の別のOCRソフトでOCRを実行。
  4. 最後に本体と分離したページをマージ。

なお,自分の備忘録に近いメモを残しておく。

書籍をスキャンしたPDFはサイズが数十から数百MBと大きく,表示速度やHDの圧迫が気になるので,「 Smallpdf.com」というサイトでPDFを圧縮している。

このサイトで圧縮するとだいたい1/3までファイルサイズを縮小できる。ClearScan単体でも1/3程度圧縮できるので,ClearScanを行ってからSmallpdfで圧縮すると,だいたい元のPDFの1/10程度にまでファイルサイズを圧縮でき重宝している。なお,Smallpdf→ClearScanの順番で行うと,ファイルサイズは同じくらいだが,ClearScanでのフォント埋め込みが汚くなるのでClearScan→Smallpdfの順番でやったほうがいい。

この手順でPDFをマージしてしまうと,なぜかSmallpdfで圧縮できなくなることがあった。なので,マージはSmallpdfで圧縮してから行ったほうがいい。

Adobe AcrobatのOCRをやめ,他社製品のOCRを利用

これは本当に最期の手段と思う。エラーの発生ページが多すぎて,いちいち個別のページの対処ができない場合だけこの方法を採用するだろう。

OCRソフトについては以下の記事がとても参考になる。

比較2016'現行の日本語OCRソフト3機種の性能とおすすめ:縦書き文章の論文、英語日本語混在論文における性能や価格:Panasonic:読取革命 ver.15、エプソン・メディアドライブ:e.Typist v.15.0 Adobe Acrobat pro 11.0 ソースネクスト 本格読取4 フリー(無料)版との違い: 家電批評モノマニア

現状日本語OCRの選択肢としては以下の3択と考えてよい。

  • e.Typist
  • 読み取り革命
  • Adobe Acrobat

この中では,ファイルの圧縮率が高いのでe.Typistがよいと思う。もともと最初に読み取り革命を最初に購入したのだが,OCR後にOCR前のPDFの3倍程度にまでファイルサイズが肥大化してしまったので,e.Typistを買い直した。

Adobe Acrobatが苦手な縦書きの文庫本などもe.Typistで処理することになるだろう。

まとめ

Adobe AcrobatによるOCR実行時のエラーについてまとめた。あまりこういうトラブルに対してまとまった情報は見当たらなかったので参考になればよいと思う。

Solution for Amarok "ItunesStats file ('iPod_control/iTunes/iTUnesStats'): entry length smaller than expected (0"

$
0
0

2008年頃に発売されたiPod shuffle(第2世代)を高校生のときに購入して今も保有している。ほぼ英語のリスニング学習専用機としていて,ここ数年使っていなかった。久しぶりに英語を勉強しようと思って取り出した。昔と違ってメインPCがWindowsからUbuntuに変わったので,UbuntuでiPodの楽曲を管理しようと思い,Amarokというソフトを試してみた。

iPodを接続してAmarokを起動すると,こんなメッセージが表示されてしまった。

ItunesStats file ('/media/senooken/IENEKO/iPod_control/iTunes/iTunesStats'): entry length smaller than expected (0

Above problem prevents Amarok from using your iPod. You can try to re-create critical iPod folders and files (including iTunes database) using the initialize iPod button below.

Initializing iPod destroys iPod track and photo database, however it should not delete any tracks. The tracks will become orphaned.
iPod接続時のAmarokのエラー

[Ititialize iPod]ボタンを押下しても解決しない。以下のサイトでこのエラーについてやりとりされていたが,解決はしていなかった。

参考:[ubuntu] 2G ipod shuffle in gtkpod

原因がよくわからないのだが,メッセージに表示されているiTuesStatsファイルが邪魔なのかもしれない。このファイルをテキストエディタで開くとバイナリファイルとなっていた。試しに,このファイルを削除するとうまく動作するようになった。iTunesとの同期に失敗したごみファイルだったのかもしれない。

ひとまずこれで解決してよかった。

GMOとくとくBBでWiMAXを契約してはならない理由

$
0
0

GMOとくとくBBで昨年の8月にWiMAXを契約した。1年経過して失敗したのでそのことについて記す。

WiMAXを契約する動機

入学や就職,異動などで,引っ越しにより住居が変わることがある。そのたびに,インターネットの回線をどうするかというのが悩みの種だった。インターネットの回線では有線か無線か大きく2通りがある。それぞれ以下のような利点と欠点があると思う。

回線ごとの利点と欠点
回線利点欠点
有線
  • 通信速度が高速
  • 回線が安定
  • 線があり部屋のレイアウトが制限
  • 工事が必要
  • 異動などで予期しない転居の場合に費用がかさむ
  • 外出先で利用不可能
無線
  • 線がない
  • 外出先で利用可能
  • 工事不要
  • 急な異動でも再契約不要
  • 回線が不安定
  • 通信速度が有線より低速

インターネット通信の性能(速度・安定性)としては,有線が最も優れている。これは揺るぎない事実だ。そのため,2年前に始めて東京で一人暮らしを 始めた時は有線で契約した。通信速度には満足していた。しかし,1年も立たない内に会社の命令で異動となり,東京から大阪に転勤になった。最初の1-2年 は毎月の通信費には回線の工事費が含まれており,解約手数料が高くなり,はっきりいって損となった。

大阪で1年過ごして転職により東京に住むことになった。大阪にいる間から,転職を考えて再契約が不要な無線通信を契約しようと考えた。そこで,いろいろ調べて8月に「GMOとくとくBB」でWiMAXを契約した。

GMOとくとくBBがダメな理由

GMOとくとくBBを選んだ理由は,業界最大の特典が得られるからだ。GMOとくとくBBで契約すれば,約3万円のキャッシュバックを受けられる。これは他社と比べて金額がひとつ抜けている(約1万円)。これに目がくらんでGMOとくとくBBで契約した。しかし,これは失敗だった。

なぜなら,GMOとくとくBBで契約した場合の,キャッシュバックを受ける条件が思っていた以上に難しかったからだ。その条件は以下となっている。

  1. 新規契約であること。
  2. 契約を変更しないこと。
  3. 契約11か月目契約時にもらったメールアドレスに届く案内にしたがって,1か月以内に申し込む。

参考:WiMAX(ワイマックス)なら GMOとくとくBB | クチコミで人気 ! お得で安いプロバイダー

この3個目の条件が特に難しい。まず,今時プロバイダーが提供するメールアドレスなんて使いにくすぎて,使っていられない。180日経過すれば,受信メールは自動で削除される。しかも,2-3日に一回はスパムメールがもれなく届く。

ぶっちゃけ,契約して11か月後なんてどうなっているかわからないし,覚えていられない。カレンダーに登録しただけだと見逃す(実際見逃した)。キャッシュバックの案内自体も件名が「GMOとくとくBBからのお知らせ キャッシュバック特...」というようなもの。

このようにキャッシュバックの特典を受けるのがとても難しい。ネット上でも特典を受けられない声はすぐに見つかる。

この状況から,なぜGMOとくとくBBが業界最大のキャッシュバックを提供し続けられるかがわかる。それは,キャッシュバックを受けられない人が多いからだ。どんなに費用のかかる特典をつけようが,それを受ける人がいなければ,会社としては負担にならない。だから,このような特典を受ける条件を難しくすることで,実質的に特典を受けられる人を減らして,お得感だけを打ち出すことができている。

当然ながら,このキャッシュバックを受けられなければ,業界で最高値になってしまう可能性がある。万が一でも,キャッシュバックを受けられない可能性があるくらいなら,素直にキャッシュバックの金額が低くなっても他社を選んだほうがましだろう。

契約を読むので契約する側にも問題があるのはわかる。しかし,一定期間継続したユーザーにだけ特典を与えたければ,最初から受付をしておいて,その期日まで継続していれば振り込むなど,もっとユーザーにとってよい方法があるずだ。それにも関わらず,わざとこのようなややこしい手順をとらせることで客を騙す真似をするのは悪質だ。

他にもこのGMOとくとくBBの契約には問題がある。まず,解約するには契約して25か月めか49か月目でなければ,問答無用で解約料9500円が発生する。さらに,利用期間に応じて追加で解約料がとられる(2年未満:9500円,2年以上:4500円)。つまり,更新月以外で解約する場合,1.4-1.9万円の解約料が発生する(もっとも,このような解約手数料は他社でも同様のようだが)。

参考:GMOとくとくBB 「WiMAX 2+ 接続」 サービス特約 | プロバイダーなら GMOとくとくBB

まとめとそれでも利用する人への助言

このように情報弱者から金を絞りとるようなWeb企業は気に入らない。GMOは国内最大のインターネット企業だ。GMOとくとくBBはそのGMOのグループ会社だ。個人がこのような大企業に対抗できることなど限られる。それは以下だ。

  • ブログで問題を指摘
  • サービスを利用しない

今回自分が被害を受けて,怒りを感じた。だからこの2点を実行することにした。次の更新月(2017-09)で解約しようと思う。自分の中でGMOに対する印象が悪くなった。これからは注意する。

以上からWiMAXの契約にGMOとくとくBBを選ぶべきでない理由をまとめる。

  • キャッシュバック特典の受け取りが困難
  • 悪徳企業の粛清

なお,このような問題があっても,そのお得感からGMOとくとくBBを利用するのなら以下2点の助言をきいてもらいたい。

  1. キャッシュバック受け取り月をカレンダーに登録して,必ずリマインダーを設定する。
  2. 安心サポート(300円/月)とWi-Fi(公衆無線LAN)接続オプション(362円/月)は申し込まない。

GMOとくとくBBでWiMAXを契約するなら,必ずキャッシュバックを受け取れるようにする。万が一にでもキャッシュバックを受け取れないのならば,初めから他社を選んだほうがマシだ。 そのためには,カレンダーのリマインダー機能を使う。例えば,Googleカレンダーであれば,リマインダー機能を使うと,その日を過ぎても自分でチェッ クするまで予定が後まで伸びてくる。流石に1ヶ月間カレンダーを見ないということはないので気づくはずだ。また,期日が来たら自分宛にメールが来るように 設定していてもよいだろう。僕もこの機能を使っていればよかった。

また,契約を申し込むと,最初の2-3ヶ月間は安心サポートとWi-Fi(公衆無線LAN)接続オプションが無料でついてくる。しかし,これらも解約を忘れると自分で解除するまで勝手に課金されてしまう(実際解除を忘れて5ヶ月ほど課金されていた)。どうせいらないオプションなので最初から申し込まないほうがいい。オプションの解約や毎月の請求金額はBBNaviのページで確認できる。

おそらく今後もこのようなサービスに出くわすだろう。これに懲りて,ユーザーにとって不利なサービスを提供する会社は避けることにしよう。それが世のため人のためだろう。

How to execute external command in Vim on Windows network directory

$
0
0

Windowsのネットワークディレクトリ上で,Vimから外部コマンドを実行する方法を記す。

Introduction

仕事ではWindowsのPCを使っている。複数人で作業をしている場合,ネットワークディレクトリ上のソースコードなどのファイルを直接編集することがある。

ネットワークディレクトリ上のファイルをGVimで開いた状態で,:lgrepなどのVimの外部grepのように,VimからWindowsの外部コマンドを実行したいことがよくある。しかし,そのまま実行すると勝手にC:\Windowsでコマンドが実行されてしまう。

例えば,ネットワークディレクトリの\\VBOXSVR\Windows10の任意のファイルをVimで開いて,:!dirコマンドを実行すると以下のメッセージが表示されてしまい,\\VBOXSVR\Windows10とは異なるC:\Windowsの中身が一覧されてしまう。

cmd /c dir
'\\VBOXSVR\Windows10'
上記の現在のディレクトリで CMD.EXE を開始しました。
UNC パスはサポートされません。Windows ディレクトリを既定で使用します。

Vimでgrepする場合であれば,Vimの内部grep(:vimgrep)コマンドを使えば実現はできる。しかし,:vimgrepだと動作が遅く,他のWindowsコマンドが実行できないという根本的な問題が解決していない。

そこで,Windowsのネットワークディレクトリ上でコマンドを実行する方法を調べた。

Method

以下のページの情報に従えば解決した。

参考:コマンドプロンプトで「CMD では UNC パスは現在のディレクトリとしてサポートされません

レジストリーを設定して,ネットワークディレクトリ上でコマンドを実行するときのチェックを無効化すればよい。

具体的には,レジストリーエディター(regeditコマンド)を実行して,\HKEY_CURRENT_USER\Software\Microsoft\Command ProcessorDisableUNCCheckというDWORD値を作成し値に1をセットする。

なお,このレジストリーの登録は以下のコマンドでも行える。

reg add "HKCU\Software\Microsoft\Command Processor" /v DisableUNCCheck /t REG_DWORD /d 1

登録できたかどうかは,以下のコマンドで確認できる。

reg query "HKCU\Software\Microsoft\Command Processor" /s
    CompletionChar    REG_DWORD    0x9
DefaultColor REG_DWORD 0x0
EnableExtensions REG_DWORD 0x1
PathCompletionChar REG_DWORD 0x9
DisableUNCCheck REG_DWORD 0x1

削除するときは以下のコマンドで行える。

reg delete "HKCU\Software\Microsoft\Command Processor" /v DisableUNCCheck /f

これで、ネットワークパス上のコマンドで、UNCパスが利用できるようになる。PCの再ログインや再起動は不要で,値を設定・削除すれば即座に反映される。なお,HKEY_LOCAL_MACHINEで設定すれば、PC全体の設定となる。

この設定により,ネットワークディレクトリ上のファイルをVimで編集するときの外部コマンドのパスが正しく認識されるようになった。

The location of the X Window System user configuration file

$
0
0

X Window Systemの設定ファイルであるxorg.confのユーザー設定の格納場所を調べた。結論をいうと,ユーザー設定とシステム設定ファイルの場所は以下となる。

ユーザー設定
/etc/X11/xorg.conf.d
システム設定
/usr/share/X11/xorg.conf.d

Introduction

LinuxやUNIXでは,X Window Systemと呼ばれるシステムを使い,画面を描画したり,マウス,キーボード,タッチパッド,ライトといったデバイスの挙動を制御する。つまり,これらの挙動を変更したければ,X Window Systemの設定ファイルを変更することになる。

現在はX.Org FounationがX Window Systemの標準仕様を策定し,その実装であるXorgも提供している。Xorgの設定はxorg.confファイルやxorg.conf.dディレクトリで設定する。これらのxorg.confファイルを設定することで,例えば以下のような設定が可能となる。

  • タッチパッドの感度,2本指クリックや3本指クリックの動作,認識範囲
  • マウス速度
  • ポインティング・スティック(トラックポイントなど)の感度
  • キー割り当て
  • 画面明るさ

デバイスの設定をOSネイティブで行うにはXorgを使うしかなく,快適なPCの利用にあたってこの設定が非常に重要となる場合がある。しかし,これらの設定ファイルの場所がどこにあるのかはっきりとした情報があまりないと感じた。そこで,システムの標準設定の場所と,ユーザー用設定ファイルの場所を調べた。

Xorgの設定ファイルの配置場所

Xorgの設定ファイルはxorg.confと呼ばれている。xorg.confのマニュアルは公式サイトか,Linuxであれば以下のコマンドで確認できる。

man xorg.conf

このマニュアルを参考にして,Xorgの設定ファイルの場所をまとめる。

Xorgの設定は以下の2種類のファイルを使う。

  • xorg.confファイル
  • xorg.conf.d配下の.confファイル

マニュアルを見る限り,xorg.conf.d配下のファイル名は拡張子が.confであること以外に指定はない。ただし,標準のシステム設定では,以下のように数字-デバイス名.confのような命名規則となっているので,これに従うのがよいだろう。

ls /usr/share/X11/xorg.conf.d/
10-amdgpu.conf  11-evdev-quirks.conf      50-vmmouse.conf
10-evdev.conf 11-evdev-trackpoint.conf 50-wacom.conf
10-quirks.conf 50-synaptics.conf 51-synaptics-quirks.conf

Xorgの設定ファイルは大きく以下の分類で検索される。

  • 初期設定ファイル
    • 一般ユーザー
    • ルートユーザー
  • 追加設定ディレクトリ
    • 一般ユーザー
    • ルートユーザー
  • システム設定

一般ユーザーとルートユーザーの違いは,ルートユーザーでXorgを起動した場合,起動オプション-config-configdir,環境変数$XORGCONFIGで指定されるファイルやディレクトリを追加で探すことだ。

システム設定は,システム利用に予約されたディレクトリのことだ。このディレクトリの設定は,ベンダーやサードパーティのパッケージから分離されている。システム設定をユーザーが変更する必要はない。

Xorgの設定ファイルの場所一覧を以下の表に示す。

Xorgの設定ファイルの場所一覧
分類一般ユーザールートユーザー

初期設定ファイルの<cmdline>起動時の-configオプションで指定。

追加設定ディレクトリの<cmdline>起動時の-configdirオプションで指定。

<hostname>gethostname関数で得られるホスト名。

$XORGCONFIG環境変数。既定は空。
初期設定ファイル

/etc/X11/<cmdline>
/usr/etc/X11/<cmdline>

/etc/X11/$XORGCONFIG
/usr/etc/X11/$XORGCONFIG
/etc/X11/xorg.conf
/etc/xorg.conf
/usr/etc/X11/xorg.conf.<hostname>
/usr/etc/X11/xorg.conf
/usr/lib/X11/xorg.conf.<hostname>
/usr/lib/X11/xorg.conf
<cmdline>
/etc/X11/<cmdline>
/usr/etc/X11/<cmdline>
$XORGCONFIG
/etc/X11/$XORGCONFIG
/usr/etc/X11/$XORGCONFIG
/etc/X11/xorg.conf
/etc/xorg.conf
/usr/etc/X11/xorg.conf.<hostname>
/usr/etc/X11/xorg.conf
/usr/lib/X11/xorg.conf.<hostname>
/usr/lib/X11/xorg.conf
追加設定ディレクトリ

/etc/X11/<cmdline>
/usr/etc/X11/<cmdline>
/etc/X11/xorg.conf.d
/usr/etc/X11/xorg.conf.d
<cmdline>
/etc/X11/<cmdline>
/usr/etc/X11/<cmdline>
/etc/X11/xorg.conf.d
/usr/etc/X11/xorg.conf.d
システム設定
/usr/share/X11/xorg.conf.d

Xorgのユーザー設定ファイルはどこに配置すべきか?

ここまでで,Xorgの設定ファイルの格納場所が全てわかった。それでは,ユーザーが変更した設定ファイルはどこに配置するのがベストだろうか?

答えは,/etc/X11/xorg.conf.dだ。

まず,初期設定ファイルはシステムで用意されている可能性があるので,使わないほうがいいだろう。そして,Xorgの起動時のオプションは期待できないので,<cmdline>オプションを使っている場所も避けるべきだろう。これらを考慮すると,配置場所後方が以下の2個に絞られる。

/etc/X11/xorg.conf.d
/usr/etc/X11/xorg.conf.d

この2択から/etc/X11/xorg.conf.dを選ぶべき理由は以下の3点だ。

  1. /usr/share/X11/xorg.conf.d/50-synaptics.confで指示されている。
  2. FHSでは/usr/etc/X11は定義されていないが,/etc/X11は定義されている。
  3. 標準のUbuntu 16.04でxorg.confは使われておらず,xorg.conf.dを使ったほうが柔軟な設定が可能。

1点目。Ubuntu 16.04で以下のコマンドを実行すると,設定ファイルが/etc/X11/xorg.conf.dを使うように指示していることがわかる。

head /usr/share/X11/xorg.conf.d/50-synaptics.conf
# Example xorg.conf.d snippet that assigns the touchpad driver
# to all touchpads. See xorg.conf.d(5) for more information on
# InputClass.
# DO NOT EDIT THIS FILE, your distribution will likely overwrite
# it when updating. Copy (and rename) this file into
# /etc/X11/xorg.conf.d first.
# Additional options may be added in the form of
# Option "OptionName""value"
#
Section "InputClass"

2点目。FHS (Filesystem Hierarchy Standard)というLinuxにおけるディレクトリ構造の標準仕様が存在する。FHSにおいて,/etc/X11/ディレクトリは以下のとおりオプションだが定義されている。

3.7.5. /etc/X11 : Configuration for the X Window System (optional)

3.7.5.1. Purpose

/etc/X11 is the location for all X11 host-specific configuration. This directory is necessary to allow local control if /usr is mounted read only.
Filesystem Hierarchy Standard

しかし,/usr/etc/X11/は定義されていない。特に理由がなければ,FHSに従うべきだろう。

3点目。xorg.confは単体で利用可能なファイルとなっている。しかし,このファイルは標準のUbuntu 16.04では存在しない。また,Xorgの設定はデバイスごとに分類され,設定できる項目は多い。xorg.conf.d配下の.confファイルでデバイスごとや用途ごとにファイルを分けた方が管理しやすい。

まとめ

これらのことから,Xorgのユーザー設定とシステム設定ファイルの場所は以下となる。

ユーザー設定
/etc/X11/xorg.conf.d
システム設定
/usr/share/X11/xorg.conf.d

Xorgの設定はネット上であまりなく,ユーザー設定の配置場所についても特に理由や根拠もなく/etc/X11/xorg.conf.dを使うように書かれていることが多い。今回の記事でユーザー設定ファイルの場所を明確な根拠もって示すことができたので自信をもってXorgの設定に望めるだろう。Xorg自体の設定は膨大にあるので,また調べてみたい。

Bloggerへのコメント欄サービスDisqusの導入

$
0
0

Bloggerの標準のコメント欄だと,コメントした人は返信があっても通知を受け取ることができず,その都度自分でコメントがついたかどうか確認する必要がある。これは閲覧者に不便を強いることになるのでよくないと思っていた。

それで,以前から気になっていたDisqusというコメントサービスを設置してみることにした。Disqusを使えば,コメントに対して返信があればメールで通知を受け取ることができる。また,各種SNSアカウントでもログインできるのでコメントが投稿しやすい。コメント欄サービスとしては,おそらく一番使い勝手がよさそうなサービスだ。

この記事ではBloggerへのDisqusのインストール手順を記す。

インストール手順の参考ページ

まず,以下のページに既に存在しているブログにDisqusをインストールするための手順が書かれているので,これを参考にする。

参考:Adding Disqus to your site | DISQUS

基本的にウィジェットが提供されているので,それを設置するだけでよさそうだ。

Disqusコメント欄の設置

以下の手順でDisqusのWebサイトへの登録ページヘ移動する。

Disqus | Install instructions for Blogger→[Blogger widget installation]を選択→右上の[Get Disqus for your site]

Disqusへのサイト登録ページへ移動

あるいは,以下の手順でトップページからアクセスしてもよい。

トップページ→[GET STARTED]→[I want to install Disqus on my site
トップページからのアクセス手順

Disqusで管理するWebサイトを登録するために,Webサイト名とジャンルや使用言語を設定する。なお,このWebサイト名はDisqusの管理ページのURLに使われる。

Disqusで管理するWebサイトの情報の登録

Webサイトの登録が終わると,管理ページに移動する。今回は,Website NameをMy Future Sight for Pastにしたので,以下のURLとなった。

https://my-future-sight-for-past.disqus.com/admin/install/

表示されたページから,設置するプラットフォーム(今回はBlogger)を選択する。すると,Disqusのインストール手順のページ(
https://my-future-sight-for-past.disqus.com/admin/install/platforms/blogger/)に移動する。

Disqusコメントの設置手順ページ

ここで,[1 Add my-fugure-sight-for-past to my Blogger site]を選択すると,新しいタブが開き,BloggerにDisqusのウィジェットを設置するかの確認画面が表示される。

Bloggerへのウィジェットの登録ページ

[ウィジェットを追加]を押下すると,ブログにDisqusが追加され,記事のコメント欄がDisqusになる。

設置されたDisqusコメント欄

既存コメントのインポート

Disqusのコメントを設置しただけでは,既存のBlogger側で管理しているコメントは見えなくなってしまう。既存のコメントをDisqus上で表示するには,BloggerのコメントをDisqusにインポートする。

先ほどの管理ページ(例:https://my-future-sight-for-past.disqus.com/admin/discussions/import/platform/blogger/)に戻り以下の項目を実行する。

[2 Import your existing Blogger comments into Disqus at Discussions > Import.]

[Discussions > Import]を押下すると,BloggerのコメントがDisqusにインポートされ,ついでにDisqusとBloggerとで記事のコメントが同期が始まる模様。

これで,ブログへのDisqusの最低限のインストールが完了した。

最新コメント一覧の表示

Disqusでは標準で最新コメント一覧のウィジェットは用意されていない。自分でAPIにアクセスするしかないようだ。

参考: Widgets | DISQUS

Bloggerで,ダッシュボード→[レイアウト]→[ガジェットを追加]→[HTML/JavaScript]を選択する。

レイアウト画面
ガジェットを追加
HTML/JavaScriptの設定
最新コメント一覧の追加

[HTML/JavaScriptの設定]に以下のコードを記入すれば,最新コメント一覧を表示できるウィジェットを配置できる。

<div id="recentcomments">
<script type="text/javascript" src="http://my-future-sight-for-past.disqus.com/recent_comments_widget.js?num_items=5&hide_avatars=0&avatar_size=12&excerpt_length=100"></script>
</div>

なお,my-future-sight-for-pastには,Disqusでサイトを登録するときに使用したIDを使う。その他のパラメーターは以下の役割となる。

Disqusの最新コメント一覧でのパラメーターの説明
パラメーター規定値説明
num_items5表示するコメント数
hide_mods01にすれば管理者のコメントを非表示
excerpt_length1001コメントの表示文字数
hide_avatars01にすればアバターアイコンを非表示
avatar_size32アバターアイコンのサイズ[px]

参考:How To Add Disqus Recent Comments Widget - Subin's Blog

これで,例えば以下の図のように最新コメントを表示させることができる。

最新コメント一覧の表示例

Disqusのコメント欄設置場所のカスタマイズ

既定のままだと,例えばモバイルページで表示されなかったり,プレビュー中でも表示されたりと都合が悪いのでカスタマイズする。以下のページを参考にした。

参照:BloggerへのDISQUS導入メモ | @ovreneli (tech)

BloggerのテンプレートのHTMLを直接編集する。ダッシュボードから以下の順番でテンプレート編集画面に移動する。

[テンプレート]→[HTMLの編集]

Bloggerのテンプレート編集画面
モバイルページでの有効化

まず,モバイルページでのDisqusのコメント表示を有効にする。Disqus forで検索して,b:widget要素を見つける。見つかったら,mobile='yes'を追加する。

モバイルページでのDisqusコメントの有効化
<b:widget id='HTML4' locked='false'mobile='yes' title='Disqus for my-future-sight-for-past' type='HTML' visible='true'>

プレビュー中での無効化

続いて,Bloggerで記事を投稿画面のプレビュー表示ではDisqusを無効化する。上記HTMLコードのすぐ下あたりのコードを編集する。以下のコードを挿入して,プレビューページでは無効にする。

location.pathname !== &#39;/b/post-preview&#39; &amp;&amp;
プレビュー中のDisqusコメントの無効化
<b:if cond='data:blog.pageType == &quot;item&quot;'>
<style type='text/css'>
#comments {display:none;}
</style>
<script type='text/javascript'>
location.pathname !== &#39;/b/post-preview&#39; &amp;&amp;
(function() {
var bloggerjs = document.createElement(&#39;script&#39;);
bloggerjs.type = &#39;text/javascript&#39;;
bloggerjs.async = true;
bloggerjs.src = &#39;//&#39; + disqus_shortname + &#39;.disqus.com/blogger_item.js&#39;;
(document.getElementsByTagName(&#39;head&#39;)[0] || document.getElementsByTagName(&#39;body&#39;)[0]).appendChild(bloggerjs);
})();
</script>
</b:if>
<style type='text/css'>
.post-comment-link { visibility: hidden; }
</style>
<script type='text/javascript'>
location.pathname !== &#39;/b/post-preview&#39; &amp;&amp;
(function() {
var bloggerjs = document.createElement(&#39;script&#39;);
bloggerjs.type = &#39;text/javascript&#39;;
bloggerjs.async = true;
bloggerjs.src = &#39;//&#39; + disqus_shortname + &#39;.disqus.com/blogger_index.js&#39;;
(document.getElementsByTagName(&#39;head&#39;)[0] || document.getElementsByTagName(&#39;body&#39;)[0]).appendChild(bloggerjs);
})();
</script>
固定ページでの有効化

最後に,Bloggerの固定ページでもDisqusを表示させる。この方法は以下のDisqusの公式ページでも紹介されていたのでこの方法に従う。

参考:Add Disqus to Static Pages in Blogger | DISQUS

b:if要素の開始タグ<b:if cond='data:blog.pageType == &quot;item&quot;'>と終了タグ</b:if>だけを削除するかコメントアウトする。

固定ページでのDisqusコメントの有効化
<b:widget id='HTML4' locked='false' mobile='yes' title='Disqus for my-future-sight-for-past' type='HTML' visible='true'>
<b:includable id='main'>
<script type='text/javascript'>
var disqus_shortname = &#39;my-future-sight-for-past&#39;;
var disqus_blogger_current_url = &quot;<data:blog.canonicalUrl/>&quot;;
if (!disqus_blogger_current_url.length) {
disqus_blogger_current_url = &quot;<data:blog.url/>&quot;;
}
var disqus_blogger_homepage_url = &quot;<data:blog.homepageUrl/>&quot;;
var disqus_blogger_canonical_homepage_url = &quot;<data:blog.canonicalHomepageUrl/>&quot;;
</script>
<!-- <b:if cond='data:blog.pageType == &quot;item&quot;'> -->
<style type='text/css'>
#comments {display:none;}
</style>
<script type='text/javascript'>
location.pathname !== &#39;/b/post-preview&#39; &amp;&amp;
(function() {
var bloggerjs = document.createElement(&#39;script&#39;);
bloggerjs.type = &#39;text/javascript&#39;;
bloggerjs.async = true;
bloggerjs.src = &#39;//&#39; + disqus_shortname + &#39;.disqus.com/blogger_item.js&#39;;
(document.getElementsByTagName(&#39;head&#39;)[0] || document.getElementsByTagName(&#39;body&#39;)[0]).appendChild(bloggerjs);
})();
</script>
<!-- </b:if> -->
<style type='text/css'>
.post-comment-link { visibility: hidden; }
</style>
<script type='text/javascript'>
location.pathname !== &#39;/b/post-preview&#39; &amp;&amp;
(function() {
var bloggerjs = document.createElement(&#39;script&#39;);
bloggerjs.type = &#39;text/javascript&#39;;
bloggerjs.async = true;
bloggerjs.src = &#39;//&#39; + disqus_shortname + &#39;.disqus.com/blogger_index.js&#39;;
(document.getElementsByTagName(&#39;head&#39;)[0] || document.getElementsByTagName(&#39;body&#39;)[0]).appendChild(bloggerjs);
})();
</script>
</b:includable>

まとめ

DisqusのBloggerへの導入手順を説明した。最新コメント一覧の設置や,固定ページでの有効化など一部HTMLやJavaScriptコードを編集する必要があってやや難しそうな印象をもった。しかし,一度設定してしまえばおしまいなので我慢しよう。

ブログにコメントがつくことはそんなになかった。しかし,コメントがついて返信してやりとりするときに,相手に通知が飛ばなくて手間をとらせてしまうのが恐縮だった。今後はこのようなことがなくなるので,自分の罪悪感もなくなり,お互い効率的に議論ができるようになる。

コメントの返信に対して訪問者へ通知が標準で送られるブログサービスはそうない。標準の機能は使いにくく,Disqusは世界中で使われており,品質が高いのでみんな導入したらよいと思う。

Install Sphinx from source on Linux

$
0
0

Pythonで書かれたドキュメントツールであるSphinxをオフラインのLinuxでソースからインストールしたので方法を記す。今回の方法ではPython2でも3でも対応できる。

Introduction

職場のソースコードの文書化システムとしてSphinxがどんなものか試すことになり,まずはインストールする必要があった。

Pythonは3.4からパッケージマネージャーのpipが標準で付属しており,通常であればこれを使えば簡単にSphinxをインストールできる。

pip install --user sphinx

ただし,pipは不足している依存関係をWebからダウンロードしようとするので,インターネットと繋がっていなければこの手段は使えない。今回は職場のインターネットに繋がっていないマシンにインストールしてみたかったので,この方法ではだめだ。そこで,インターネットに繋がるマシンで必要な依存関係を自分でダウンロードして,インストールしたいマシンにtar.gzを配置する。そこで,tar.gzに対してpipを実行することでインストールする。

必要な依存関係はパッケージのsetup.pyrequires変数に書かれている。これをみると,Sphinxの依存関係は以下であることがわかる。

requires = [
'six>=1.4',
'Jinja2>=2.3',
'Pygments>=2.0',
'docutils>=0.11',
'snowballstemmer>=1.1',
'babel>=1.3,!=2.0',
'alabaster>=0.7,<0.8',
'imagesize',
'requests',
]

さらに,これらの依存関係に存在するパッケージが以下に示す依存関係を必要とする。

babel
pytz
jinja
markupsafe

全部のパッケージを手動でsetup.pyからインストールするのは面倒なので,以下の手順でpipでインストールする。

  1. 最初にpipをソースからインストール
  2. 残りのSphinxの依存関係はpipでソースファイルからインストール

pipの依存関係はsetupttoolsである。これらを整理すると,以下の順番でインストールを行うこととなる。

  1. ソースのsetup.pyからpipをインストール
    1. setuptools
    2. pip
  2. Sphixの依存関係をpipでインストール
    1. pytz, markupsafe
    2. その他(six, Jinja2, Pygments, docutils, snowballstemmer, babe, alabaster, imagesize,requests)
    3. Sphinx
今回はルート権限を使いたくないので,$HOME/.local配下に全てインストールする。便宜のためLOCAL=$HOME/.localとしておく。

Download all dependencies

依存関係を手作業で全てダウンロードするのは面倒なのでwgetでのダウンロードコードを以下に掲載する。

bash
LOCAL="$HOME/.local"
GET="wget -nc"
mkdir -p "$LOCAL/src/python"
cd "$LOCAL/src/python"

## For pip
$GET https://pypi.python.org/packages/6b/dd/a7de8caeeffab76bacf56972b3f090c12e0ae6932245abbce706690a6436/setuptools-28.3.0.tar.gz
$GET https://pypi.python.org/packages/e7/a8/7556133689add8d1a54c0b14aeff0acb03c64707ce100ecd53934da1aa13/pip-8.1.2.tar.gz

## For Sphinx
$GET 'https://pypi.python.org/packages/1f/f6/e54a7aad73e35232356103771ae76306dadd8546b024c646fbe75135571c/Sphinx-1.4.8.tar.gz#md5=5ec718a4855917e149498bba91b74e67'

$GET 'https://pypi.python.org/packages/53/35/6376f58fb82ce69e2c113ca0ebe5c0f69b20f006e184bcc238a6007f4bdb/pytz-2016.7.tar.bz2#md5=8d8121d619a43cf0b38a4195de1cb8a5'
$GET 'https://pypi.python.org/packages/c0/41/bae1254e0396c0cc8cf1751cb7d9afc90a602353695af5952530482c963f/MarkupSafe-0.23.tar.gz#md5=f5ab3deee4c37cd6a922fb81e730da6e'

$GET 'https://pypi.python.org/packages/b3/b2/238e2590826bfdd113244a40d9d3eb26918bd798fc187e2360a8367068db/six-1.10.0.tar.gz#md5=34eed507548117b2ab523ab14b2f8b55'
$GET 'https://pypi.python.org/packages/f2/2f/0b98b06a345a761bec91a079ccae392d282690c2d8272e708f4d10829e22/Jinja2-2.8.tar.gz#md5=edb51693fe22c53cee5403775c71a99e'
$GET 'https://pypi.python.org/packages/b8/67/ab177979be1c81bc99c8d0592ef22d547e70bb4c6815c383286ed5dec504/Pygments-2.1.3.tar.gz#md5=ed3fba2467c8afcda4d317e4ef2c6150'
$GET 'https://pypi.python.org/packages/37/38/ceda70135b9144d84884ae2fc5886c6baac4edea39550f28bcd144c1234d/docutils-0.12.tar.gz#md5=4622263b62c5c771c03502afa3157768'
$GET 'https://pypi.python.org/packages/20/6b/d2a7cb176d4d664d94a6debf52cd8dbae1f7203c8e42426daa077051d59c/snowballstemmer-1.2.1.tar.gz#md5=643b019667a708a922172e33a99bf2fa'
$GET 'https://pypi.python.org/packages/6e/96/ba2a2462ed25ca0e651fb7b66e7080f5315f91425a07ea5b34d7c870c114/Babel-2.3.4.tar.gz#md5=afa20bc55b0e991833030129ad498f35'
$GET 'https://pypi.python.org/packages/71/c3/70da7d8ac18a4f4c502887bd2549e05745fa403e2cd9d06a8a9910a762bc/alabaster-0.7.9.tar.gz#md5=b29646a8bbe7aa52830375b7d17b5d7a'
$GET 'https://pypi.python.org/packages/53/72/6c6f1e787d9cab2cc733cf042f125abec07209a58308831c9f292504e826/imagesize-0.7.1.tar.gz#md5=976148283286a6ba5f69b0f81aef8052'
$GET 'https://pypi.python.org/packages/2e/ad/e627446492cc374c284e82381215dcd9a0a87c4f6e90e9789afefe6da0ad/requests-2.11.1.tar.gz#md5=ad5f9c47b5c5dfdb28363ad7546b0763'

Install setuptools and pip

まず,setuptoolsとpipをソースのsetup.pyからインストールする。setup.pyからインストールするときは,インストール先にPYTHONPATHに設定されているディレクトリを指定しなければエラーとなる。PEP 370によれば,Unixでは~/.local/lib/pythonX.X/site-packages(X.Xはバージョン)が既定で指定されているので,事前にこのディレクトリを用意しておけばよい。なお,この場所が気に入らなければ,PYTHONUSERBASE環境変数に値を指定することで,~/.localの部分を変更できる。

現在のPythonのバージョンをシェルスクリプトのワンライナーで変数に取得してディレクトリを作成しておく。

PYTHONVERSION=$(python -V 2>&1 | grep -E -o '[0-9]+\.[0-9]+')  # Get python version
mkdir -p ~/.local/lib/python$PYTHONVERSION/site-packages

そして,stuptoolsとpipを順番にインストールする。

## setuptools
cd $LOCAL/src/python
tar xf setuptools-*.tar.gz
cd setuptools-*
python setup.py install --prefix=$LOCAL

## pip
cd $LOCAL/src/python
tar xf pip-*.tar.gz
cd pip-*
python setup.py install --prefix=$LOCAL

これでpipのインストールが完了した。

Install Sphinx

続いてpipを使ってSphinxに必要なパッケージをインストールしていく。babelとjinja2だけpytzとMarkUpSafeの依存 関係が必要なので,これらを先にインストールしていく。また,一個ずつpipでインストールするのは煩雑なので,Sphinxだけ最後にインストールする ようにして,for文を使ったシェルスクリプトで一括でインストールする。

cd $LOCAL/src/python
pip install --user pytz-*.tar.*
pip install --user MarkupSafe-*.tar.*

for i in *.tar*
do
[ "${i%%*Sphinx*}" ] && pip install --user "$i"
done

pip install --user Sphinx-*.tar.*

これでSphinxのインストールは完了した。

Conclusion

Sphinxをソースコードからインストールする方法を記した。必要なものは全てソースからインストールしたのでPythonのバージョンに依存せず,2でも3でも対応できた。実際には,Python2.7と3.3で動作を確認できた。

普段はpipを使っていてあまり意識しないが,Sphinxはたくさんの小さなパッケージに依存していることが分かった。1個ずつ手動でインストールするのは面倒なので,pipが便利だと思った。たぶん今回のようにオフラインの環境にインストールすることはこの先もあまりないと思うので勉強になった。


How to loop N times in POSIX Shell script

$
0
0

POSIX原理主義で指定回数forループする方法を記す。

最終的に,awkを使った実装が最も汎用的でベストだろうと結論づけた。

N=10
for i in $(awk "BEGIN{for(i=1; i<=$N; ++i) print i}")
do
echo $i
done

Introduction

シェルスクリプト内で指定回数処理を実行したいときがある。通常であれば,seqコマンドかbashの構文を使って実現される。

N=10
## seq is not POSIX
for i in $(seq $N)
do
echo $i
done
## bash specific syntax
for i in {1..$N}
do
echo $i
done

しかし,どちらもPOSIX非準拠となってしまう。seqは機能の単純さや利用頻度からPOSIXで定義されているかと勘違いしがちだが,実はPOSIXで未定義だ。

2016-10-13追記:Google+のコメントで以下の構文はPOSIX準拠ではないのかと質問された。

for ((i=1; i<=10; ++i))
do
echo $i
done

C系言語のfor文とよく似ており,この方法でも指定回数のループが可能だ。

しかし,このfor文の構文はPOSIXでは未定義であり,bashやzshの独自拡張だ。bashでは2.04からksh-93形式の算術forコマンドとして導入されている。

回数を指定した反復は頻出事項であり,POSIX原理主義的方法を確立する必要があると感じたので検討した。

アプローチとして以下の2種類がある。

  1. ループ方法の工夫
    1. while
    2. printf
  2. seqを代替
    1. bc
    2. awk

ループ方法の工夫

まず,最も簡単な方法はwhileを使うことだ。

N=10
i=0
while [ "$i" -lt $N ]
do
i=$((i+1))
echo $i
done

whileを使えば通常のループの中で自然に組み込めるので,違和感は少なく,連番に必要なコードの文字数は最小となる。

この方法の欠点は以下2点だ。

  • ループ内でループ変数のインクリメントが必要
  • ループの度に評価が行われるので速度の低下の懸念がある。

次の方法は,指定回数文だけ空白区切りの文字列を用意することだ。

N=10
for i in $(printf "%0${N}d\n" | sed 's/0/0 /g')
do
echo $i # 0
done

この方法は少しトリッキーなので仕組みを解説する。

printfコマンドで%0による0パティングにより任意の数の0を一度に出力できることを利用している。最初のprintf "%0${N}d\n"で任意の個数の0を出力して,この内00 に置換して,それぞれの0を空白区切りにすることで指定回数のループを実現している。書籍「 すべてのUNIXで20年動くプログラムはどう書くべきか」で説明されていた方法を応用している。

処理の内容は単純でループ変数のインクリメントが不要なので,早い実行速度が期待できる。

この方法では以下の欠点がある。

  • ループ変数の値が0固定。

seqコマンドの代替

もう片方のアプローチとしては,seqコマンドそのものを代替する。seqコマンドのように連番の数字を別の方法で出力して,それをfor文に使う。bcコマンドとawkコマンドを使う2通りの方法がある。

まず,bcコマンドを使う方法は以下の通りとなる。

N=10
for i in $(echo "for (i=1; i<=$N; ++i) i" | bc)
dso
echo $i
done

欠点

  • whileに比べると複雑。
  • 標準でインストールされていない環境がある。

bcコマンドはPOSIXで定義されているが,標準で付属されていない環境がいくつかある。試しに,Ubuntu16.04で以下のコマンドを実行すると,外部パッケージとしてインストールされていることがわかる。

apt search bc 2>&- | grep -B 1 "GNU bc"
bc/xenial,now 1.06.95-9build1 amd64 [installed,automatic]
GNU bc arbitrary precision calculator language

その他,seqコマンドをPOSIX原理主義で本気で実装する場合,bcコマンドは小数の出力形式がまちまちであるなどいくつかの欠点がある。

続いて,awkで実装する場合以下の通りとなる。

N=10
for i in $(awk "BEGIN{for(i=1; i<=$N; ++i) print i}")
do
echo $i
done

bcとほぼ同様で,C系言語のfor文の書式で連番を出力している。awkだと出力の形式などカスタマイズしやすいという利点がある。

欠点

  • whileに比べると複雑。

参考:portability - Portable POSIX shell alternative to GNU seq(1)? - Unix & Linux Stack Exchange

速度比較

ここまでで,POSIX原理主義に従った合計4通りのループの実装方法を説明した。最後にこれらの実装の速度を比較して,どれが最良であるかの判断材料とする。

以下のコードで示すように,10万回のループを実行して速度を測る。

POSIX原理主義によるループ実装の速度比較コード
N=100000 time -p sh -c 'i=0; while [ "$i" -lt $N ]; do echo $((i=i+1)) >/dev/null; done'
N=100000 time -p sh -c 'for i in $(printf "%0${N}d\n" | sed "s/0/0 /g"); do echo $i >/dev/null; done'
N=100000 time -p sh -c 'for i in $(echo "for (i=1; i<=$N; ++i) i" | bc); do echo $i >/dev/null; done'
N=100000 time -p sh -c 'for i in $(awk -v N=$N "BEGIN{ for(i=1; i<=N; ++i) print i}"); do echo $i >/dev/null; done'

さらに,上記のコードを5回実行して平均をとったものを以下の表にまとめた。

速度比較と欠点のまとめ
方法5回平均user時間[s]欠点
while0.276seqの代替にならない。ループ毎に評価が必要なので速度遅い。
for-printf0.094seqの代替にならない。ループ変数が0固定。
for-bc0.180whileに比べると複雑。インストールされていないことがある。
for-awk0.108whileに比べると複雑。

この結果から,最も実行速度は速いのはprintfを使ったものだった。この方法では,ループ変数のインクリメントなどが不要であり処理が最も単純なので速かったのだと思われる。次点は,awkによるものだった。最も遅かったのはやはりwhileによるものだった。whileではループの度にインクリメントや評価が行われるので,速度が遅くなるだろうという予想通りの結果となった。

printfawkによるものは他の方法の約2倍の実行速度であり有力だと感じた。

まとめ

4通りのループの実装方法を紹介し,速度を計測した。この結果から,awkによる実装がベストだろうと思った。

N=10
for i in $(awk "BEGIN{for(i=1; i<=$N; ++i) print i}")
do
echo $i
done

理由は以下2点だ。

  1. 速度が早い。
  2. 応用が効く。

短いループを何回も行う場合でも,速度が早いので有力な選択肢である。また,awkを使っておけば,ループ変数の出力の形式を調整したり,デクリメントなど複雑な連番処理にも対応でき,seq自体をawkを使って自分で実装して代替することもできそうと感じた。やや記述が長いが,インデックスの出力方法はC形言語のループとほぼ同じでなじみやすい。

今後指定回数のループを行うときは,seqは使わずにawkを使いPOSIX原理主義に従った記述を心がけていこう。

オプションのないseqのawkでの実装

最後に,参考までにawkによるseqの実装コードを記す。オプションのパースが複雑になるので,ひとまずオプションは使わないという前提をおいている。

引数の処理は行い,デクリメントなども対応している。その内勉強も兼ねて,POSIX原理主義によるseqの実装にも挑戦してみたい。

awkによるオプションのないseqの実装例
#!/bin/sh
################################################################################
## \file seq-minimum.sh
## \author SENOO, Ken
## \copyright CC0
################################################################################

set -u

seq()(
readonly HELP_WARN="Try 'seq --help' for more information.\n"

## Check arguments
case $# in
1) LAST="$1";;
2) FIRST="$1" LAST="$2";;
3) FIRST="$1" INCREMENT="$2" LAST="$3";;
0) printf "seq: missing operand\n$HELP_WARN" 1>&2; exit 1;;
*) printf "seq: extra operand '$4'\n$HELP_WARN" 1>&2; exit 1;;
esac

## Set default value
: ${FIRST:=1} ${INCREMENT:=1} ${COMPARISON:=<}
case "$INCREMENT" in -*) COMPARISON='>';; esac

## Execute seq
awk "BEGIN{for(i=$FIRST; i$COMPARISON=$LAST; i+=$INCREMENT) print i}"
)

seq "$@"

How to skip GRUB boot menu

$
0
0

LinuxのブートローダーであるGRUBの起動画面をスキップする方法を記す。

結論としては,/etc/default/grubのパラメーターを以下のとおりに変更すれば実現できる。

# GRUB_HIDDEN_TIMEOUT=0
GRUB_TIMEOUT=0

Introduction

PCの電源を入れると,ブートローダーと呼ばれるプログラムがメモリに読み込まれる。このブートローダーがさらに別のプログラムを呼ぶことを繰り返して最終的にOSがメモリに読み込まれPCの起動が完了する。

GNU GRUB(GRand Unified Bootloader)はこのブートローダーの自由なソフトであり,Linux OSで採用されているブートローダーだ。

PCを起動すると,以下のどのOSで起動するかのGRUBの選択画面が表示されることがある。

GRUBの起動メニュー

マシンのトラブルなどで古いバージョンのOSで起動する必要があるなど,いつもと違うOSを選択する場合はありえる。しかし,普段はOSの選択をする必要はなく,このGRUBの画面は省略したい。

そこで,このGRUBのメニューをスキップする方法を調べた。なお,Ubuntu 16.04のGRUB 2.02-beta2で動作を確認した。

設定

インターネットでざっと調べてみると,どうやら/etc/default/grubファイルにかかれている以下のパラメーターを設定を変更すれば実現できそうというのがわかった。

  • GRUB_TIMEOUT
  • GRUB_HIDDEN_TIMEOUT

ただ,ネットの情報は古かったり間違っていることがよくある。そこで,GRUBの公式マニュアルを確認した。なお,GNUのソフトはinfoコマンドでもマニュアルを確認できる。例えば,以下のコマンドで閲覧できる。

info grub

このマニュアルの「5.1 Simple configuration handling」にパラメーターの説明が書かれている。ここから,GRUB_TIMEOUTGRUB_HIDDEN_TIMEOUTの説明をまとめると以下の表のとおりとなる。

GRUBの画面の待ち時間に関する設定
変数説明
GRUB_TIMEOUTメニューが表示されてからデフォルト項目の起動までの待ち時間
GRUB_HIDDEN_TIMEOUTメニューに入るためのキー入力の受け付け時間

GRUB_HIDDEN_TIMEOUTが少しわかりにくいので補足する。PCの起動直後にF2やF8キーを押下すれば,ユーザーが自分でGRUBの画面を表示させることができる。このキーの入力受付時間がGRUB_HIDDEN_TIMEOUTと考えればよいだろう。

また,マニュアルのGRUB_HIDDEN_TIMEOUTの説明を読むとGRUB_TIMEOUTGRUB_HIDDEN_TIMEOUTのどちらを設定すればよいかはっきりする。

GRUB_HIDDEN_TIMEOUT

Wait this many seconds for a key to be pressed before displaying the menu. If no key is pressed during that time, display the menu for the number of seconds specified in GRUB_TIMEOUT before booting the default entry. We expect that most people who use GRUB_HIDDEN_TIMEOUT will want to have GRUB_TIMEOUT set to ‘0’ so that the menu is not displayed at all unless a key is pressed. Unset by default.

5.1 Simple configuration handling - GNU GRUB Manual 2.00

ここで書かれている通り,画面表示を飛ばしたければ,GRUB_TIMEOUT=0と設定すればよいことがわかる。

設定変更

設定すべき項目が分かったので,設定ファイル/etc/default/grubを修正する。

以下のコマンドで設定を変更して,GRUBの設定を更新する。

sudo sed -i 's/^\(GRUB_TIMEOUT=\)[0-9]\+/\10/'    /etc/default/grub
sudo sed -i 's/^\(GRUB_HIDDEN_TIMEOUT=.*\)/# \1/' /etc/default/grub
sudo update-grub

上記コマンドでは,以下のようにGRUB_TIMEOUTを0に設定して,GRUB_HIDDEN_TIMEOUTをコメントアウトしている。もちろんテキストエディタで編集してもよい。GRUB_HIDDEN_TIMEOUTをコメントアウトした理由は次の節で説明する。

# GRUB_HIDDEN_TIMEOUT=0
GRUB_TIMEOUT=0

これで設定は完了した。

起動が早くなったかどうか気になったので,実際に起動時間を測って確認した。

  • GRUB_TIMEOUT=0:1.25 min
  • GRUB_TIMEOUT=10で即選択:1.25 min

起動画面が表示されて即選択した場合と,起動時間が変わっていないので成功している。

GRUB_HIDDEN_TIMEOUTをコメントアウトした理由

当初はGRUB_HIDDEN_TIMEOUTはコメントアウトしていなかったのだが,update-grubの実行後に以下のメッセージが表示されてしまったからだ。

Generating grub configuration file ...
Warning: Setting GRUB_TIMEOUT to a non-zero value when GRUB_HIDDEN_TIMEOUT is set is no longer supported.

日本語訳:GRUB_HIDDEN_TIMEOUTが設定されている時に,GRUB_TIMEOUTを非0の値に設定することは,もはや対応されない。

この件について調べると,どうやらGRUB_HIDDEN_TIMEOUTの設定は廃止予定事項のようで,GRUB_HIDDEN_TIMEOUTはコメントアウトしたほうがいいらしい。

参考:grub2 - Grub update warning in Ubuntu 14.04 - Ask Ubuntu

公式マニュアルに書かれていなかったので疑問に思ってさらに調べた。使っているGRUBが2.02-beta2だけど,参照していたマニュアルが2.00だったので,マニュアルに更新があったのかもしれないと思い,最新ソースをあたった。最新ソースのマニュアルの元ファイルは以下となっている。

参考:grub.texi\docs - grub.git - GNU GRUB

この確認してみたところ,2013-11-28のこのコミットで廃止予定であることが付け加えられたようだ。リリース版としては,2.02-beta1から,この変更が入っている。

まとめと標準の/etc/default/grub

/etc/default/grubの以下の2パラメーターを変更することで,GRUBの起動が画面を省略できるようになった。

# GRUB_HIDDEN_TIMEOUT=0
GRUB_TIMEOUT=0

これでPCの起動速度が早くなったので,PC作業が少し快適になっただろう。最後に,設定を間違えてしまったときのために,標準の/etc/default/grubを掲載する。

Ubuntu 16.04日本語Remixの標準の/etc/default/grub
# If you change this file, run 'update-grub' afterwards to update
# /boot/grub/grub.cfg.
# For full documentation of the options in this file, see:
# info -f grub -n 'Simple configuration'

GRUB_DEFAULT=0
GRUB_HIDDEN_TIMEOUT=0
GRUB_HIDDEN_TIMEOUT_QUIET=true
GRUB_TIMEOUT=10
GRUB_DISTRIBUTOR=`lsb_release -i -s 2> /dev/null || echo Debian`
GRUB_CMDLINE_LINUX_DEFAULT="quiet splash"
GRUB_CMDLINE_LINUX=""

# Uncomment to enable BadRAM filtering, modify to suit your needs
# This works with Linux (no patch required) and with any kernel that obtains
# the memory map information from GRUB (GNU Mach, kernel of FreeBSD ...)
#GRUB_BADRAM="0x01234567,0xfefefefe,0x89abcdef,0xefefefef"

# Uncomment to disable graphical terminal (grub-pc only)
#GRUB_TERMINAL=console

# The resolution used on graphical terminal
# note that you can use only modes which your graphic card supports via VBE
# you can see them in real GRUB with the command `vbeinfo'
#GRUB_GFXMODE=640x480

# Uncomment if you don't want GRUB to pass "root=UUID=xxx" parameter to Linux
#GRUB_DISABLE_LINUX_UUID=true

# Uncomment to disable generation of recovery mode menu entries
#GRUB_DISABLE_RECOVERY="true"

# Uncomment to get a beep at grub start
#GRUB_INIT_TUNE="480 440 1"

Solution for garbled characters on GNU Screen

$
0
0

GNU Screenを起動すると日本語などが文字化けするようになってしまった。この原因と解決策を記す。

初めはxtermの文字エンコーディングが原因かと思ったが,よく調べていくと違った。GNU Screenの起動中だけ文字化けしていた。そこで,GNU Screenの文字エンコーディング関係のマニュアルを確認してみた。GNU Screenのencコマンドの説明が参考になった。

— Command: encoding enc [denc]
(none)
Tell screen how to interpret the input/output. The first argument sets the encoding of the current window. Each window can emulate a different encoding. The optional second parameter overwrites the encoding of the connected terminal. It should never be needed as screen uses the locale setting to detect the encoding. There is also a way to select a terminal encoding depending on the terminal type by using the ‘KJ’ termcap entry. See Special Capabilities.

Supported encodings are eucJP, SJIS, eucKR, eucCN, Big5, GBK, KOI8-R, CP1251, UTF-8, ISO8859-2, ISO8859-3, ISO8859-4, ISO8859-5, ISO8859-6, ISO8859-7, ISO8859-8, ISO8859-9, ISO8859-10, ISO8859-15, jis.

See also ‘defencoding’, which changes the default setting of a new window.
11.11 Character Processing - Screen User's Manual

ここを参照する限り,GNU Screenは起動時のロケールを端末のロケールとして設定するようだ。

そして,GNU Screenの現在の文字エンコーディングは以下のコマンドで確認できる。

screen -Q info

参考:screen(1) - Linux manual page

実際にLANG環境変数を指定してGNU Screenを起動して,文字エンコーディングを確認すると,以下のとおりになった。

LANG= screen
screen -Q info
(1,4)/(80,41)+1024 -(+)flow G0[BBBB] 0(bash)
LANG=ja_JP-UTF-8 screen
screen -Q info
(1,4)/(80,41)+1024 -(+)flow UTF-8 0(bash)

後ろから2番目のフィールドがGNU Screenの文字エンコーディングを表している。LANG環境変数の設定をなしにすると,G0 (=ASCII)に設定されてしまっている。

問題の起きたマシンでは,LANG環境変数がどこにも設定されておらず,既定のLANG=Cが適用されてしまっていたようだ。.bashrcなどで以下のようにLANG環境変数を設定することで解決した。

export LANG=ja_JP.UTF-8

なお,同種のソフトであるtmuxでもLANG環境変数によって文字化けが発生するか確認したが,問題なかった。tmuxでは起動時のLANG環境変数には依存しないようだ。

How to check if command option is enabled in POSIX Shell script

$
0
0

シェルスクリプトで,コマンドのオプションが有効かどうかの判定方法を記す。

Introduction

複数のマシンを使っていると,マシンによってコマンドオプションが異なることがある。これは,主に以下2点の理由が原因だ。

  1. コマンドのバージョンの違い
  2. 開発元の違い

1のバージョンによるオプションの違いの例としては,GNU grepコマンドがある。GNU grepでは,version 2.5.2から--exclude-dirオプションが追加された。このオプションを使うことで,検索対象外ディレクトリを設定できる。しかし,version 2.5.2以前の古いマシンのGNU grepではこのオプションは存在しない。

2の開発元の違いの例としては,lsコマンドがある。lsコマンドの表示結果に色を付けるオプションが存在している。このオプション名がLinuxたと--colorだが,Macだと-Gである。

これらのコマンドのオプションの有無による問題は,POSIXで未定義のベンダーの独自拡張オプションを使っていることが原因だ。これらのオプションをそもそも使わなければいいという考え方もある。しかし,利便性を考えるならこれらのオプションが使えるなら使いたい。例えば,grepの--exclude-dirオプションで,.gitや.svnなど常に検索対象外にしたいファイルを指定して予めaliasで指定しておいたがほうが便利だ。lsの表示結果に色をつけるオプションについても同様だ。

POSIX原理主義的に考えるならば,オプションが存在するときだけ使うようにきちんどガードすれば問題ないだろう。オプションが存在しなくてもエラーを出さずに処理は通常どおり行い,可用性を維持すればいい。

Method

オプションの存在の有無の判定方法には2通りの方法がある。

  1. コマンドのバージョンから判定
  2. コマンドのオプションの存在の判定

1. のコマンドのバージョンから判定する方法では,--versionオプションにより表示されるバージョン番号からオプションが存在するかどうかを判断する。この方法では,以下2点の欠点があるので不利だ。

  • オプションがサポートされるバージョン番号の把握が必要
  • 開発元の違いによる判定が煩雑

2.の方法では,--helpオプションにより表示されるオプション一覧からオプションの有無を判断する。この方法では,実際にオプションが存在するかどうかを判定するので確実だ。

なお,--versionオプションと--helpオプションは,GNU Coding Standardsで規定されており,存在する可能性が高い。

参考:4.7 Standards for Command Line Interfaces - GNU Coding Standards

しかし,--help--versionオプションはPOSIXで規定されていないので全コマンドに存在するとは限らない。そのため,そのままではPOSIX原理主義に反してしまう。そこで,これらのオプションが存在しないことも考慮して,2>&1により標準エラー出力も含めて判定条件として扱う。これにより,POSIXの範囲内の動作だけでオプション有無の判定ができる。

Coding

それでは,実際にオプションの判定方法を説明しよう。

まず,コマンドのオプションとして想定されるパターンを考える。そのためのコマンドのオプションの挙動としてPOSIXの以下の文書を参照する。

参考:12 Utility Conventions - The Open Group Base Specifications Issue 7, 2016 Edition

上記からオプション有無の判定で必要な項目と,それらの項目が実際に--helpでどのように表示されるかの例を以下の表にまとめた。

コマンドオプションの種類とpr --helpでの表示例
項目--helpでの表示例
ショートオプション-m
ロングオプション--merge
引数なしオプション-m
必須引数ありオプション-N, --first-line-number=NUMBER
任意引数ありオプション-S[STRING], --sep-string[=STRING]

任意引数ありオプションは,POSIXでは非推奨とされており,実装されているコマンドは少ない。prコマンドはこれらの全てのオプションが実装されているPOSIX準拠コマンドなのでとても参考になる。

--helpでの表示される文字列に対して,POSIXで定義されているgrepコマンドでのマッチを用いて,オプションの有無を判定する。オプションの有無を判定する構文は以下となる。

オプションの有無の判定構文
<command> --help 2>&1 | grep -q -- '<option>[=[, ]'

以下で上記の構文の内容を解説する。

  1. コマンドのヘルプを<command> --help 2>&1 |により,--helpが存在しない場合のエラーも含めてパイプで渡す。
  2. コマンドが成功したかどうかの終了ステータスでオプションの有無を判定するので,grepの-qオプションにより,マッチしたときの余計な情報を表示させない。
  3. 引数--により,マッチに使用するオプションをgrepのオプションではなく引数として扱う。
  4. pr --helpの表示例より,オプションの直後に続く文字は", =["の4文字に限られることがわかる。後ろの4文字を付けなければ,他のオプションの部分文字列としてマッチする可能性がある。これらをオプションの直後に置いて'<option>[=[, ]'によりマッチングさせる。

毎回上記の構文を記述するのは少し長ったらしいので,関数にしてしまおう。

コマンドオプションの有無を判定するis_option_enable関数
#!/bin/sh
# \file is_option_enable.sh
# \author SENOO, Ken
# \copyright CC0

set -u

is_option_enable()(
: ${CMD:=$1} ${OPT:=$2}
$CMD --help 2>&1 | grep -q -- "$OPT"'[=[, ]'
)

is_option_enable "$@"

## Test
# is_option_enable ls --test && echo "OK" || echo "NG" # NG
# is_option_enable grep --exclude-dir && echo "OK" || echo "NG" # OK

is_option_enable関数の第1引数にコマンド,第2引数にオプションを指定して実行する。

なお,関数の最後が"$OPT"'[=[, ]'となっている。"$OPT"[=[, ]"としてもshで動くのだが,zshでinvalid subscriptというエラーが表示されてしまうのでこちらを採用した。

Conclusion

POSIX原理主義でコマンドに特定のオプションが存在するかの判定方法について説明した。コマンドの有無に比べたらマイナーで,あまり使う場面がないかもしれない。しかし,aliasの設定においては重要だと思う。実際に,grepの--exclude-dirオプションは常に指定したいので,今回説明した方法でif文でガードをかけてからaliasでgrepを再定義している。

自分のPOSIX原理主義を実践していく上で必要な情報は今後もまとめていきたい。

#VimConf2016参加レポート

$
0
0

VimConf2016に参加したので参加レポートを記す。

概要

VimCon2016開催概要
項目内容
イベント名VimConf2016
URLhttp://vimconf.vim-jp.org/2016/
開催地株式会社ミクシィ(東京都渋谷区東1-2-20 住友不動産渋谷ファーストタワー7F)
開催日2016-11-06
参加費2000 JPY
TogetterVimConf 2016 のまとめ - Togetterまとめ
イベント申し込みVimConf 2016 - connpass

自分が発表していない勉強会は基本的に参加レポート的なものは面倒なので書かないことにしている。今回の会議もそのつもりだったのだけど,気が変わったので書くことにした。理由はこれ。

今まで参加したカンファレンスでも,参加レポートは書いたほうがいいとか・書いてほしいみたいな声をきいていたのだけど,理由がわからなかったので無視していた。今回は理由がわかった。知ってしまったし,お願いされたので書かないのは具合が悪い。

そもそも,テキストエディターのカンファレンスがあるということ自体がすごいことだ。Vimと同じように昔から存在するEmacs,その他日本では有名なSakura Editorや秀丸,最近登場してきたSublime Text,Atom。これらにも相当数の利用者がいると思うが,少なくとも今の日本ではカンファレンスは開かれていない

カンファレンスがあり,それが継続して開催されているのはVimだけ。これはとても素晴らしく,1ユーザーとして誇らしいことであり,参加できて光栄だった。

実は2年前のVimConf2014を一瞬だけチラ見していた。TeX ユーザーの集い2014に参加・発表していてこちらには参加できなかったからだ。

全体の感想

Vimは開発者御用達のテキストエディターであるので,発表内容も開発に関するものが多く,専門性が高かった。特に,たまたまかもしれないが,Go言語に関する発表が3個もあったのが印象的だった。やはり開発者なので先端技術への興味関心が高いということだろうか。

スポンサーが付いているとのことだが,会場を提供しているmixiの紹介くらいで,ほとんど企業色や宣伝などがなくて,大丈夫なのだろうかとちょっと心配になった。

会場はプロジェクターの他に,電源やWiFi,さらにはドリンクや音響設備もあり,とてもよい会場だった。

前日に,カンファレンス当日は地下鉄が改装工事で電車が通っていないということについて管理者さんから注意のメッセージがあった。親切でよかった。

発表

Introduction to Vim 8.0@Ken Takata

Vim 8.0の変更点やVim 8.0リリースまでの経過についての発表だった。発表者のKen Takataさんは日本で最も活発にVim本体に貢献している人の一人だとのこと。Vim 8.0の変更点は,:help version8で確認でき一通り目は通していた。個人的には新しく入った,非同期I/Oとパッケージ管理機能がとても気になっていた。

更新内容がVimのAPIに関するものが大半で,地味な変更が多かったので,もともとは7.5としてリリース予定だったらしい。しかし, パッケージ管理機能が入るのは変更点としてわかりやすいので8.0にしたようだ。

Vim 8.0で導入されたパッケージ機能はパッケージ管理プラグインであるPathogenと似た形式になっているらしい。

ヘルプでは8.0がリリースされる背景や,パッケージ管理機能の元となっている実装のことまで,知ることはできないので,長年Vimに携わってきた人ならではのよい発表だと思った。

現在NeoBundleでVimプラグインの管理を行っている。Vim 8.0で標準機能でパッケージ管理機能が備わったことから,NeoBundleをやめて標準機能に移行しようかなと考えている。GitHubなどからのダウンロードだけ自分でVim scriptを用意して外部プラグインに頼るのはやめようかなとぼんやり考えている。

Vim as the MAIN text editor@bird_nitryn

Vimを使って1年程度の発表者が,Vimをメインのテキストエディターとして使っていくうえでのコツやなぜVS CodeからVimに乗り換えたかなどを紹介していた。

Vimに乗り換えた理由の一つとして,ログイン先のマシンで直接CUIで実行できることをあげていた。VS CodeをWindowsなどからネットワーク経由で使うと重く,作業にならない。CUIで使える高機能テキストエディターはほぼVimとEmacの2択。速度を求めるのならば,このどちらかにたどり着くのは自然な動機だ。

冒頭の自己紹介ででフォントが好きという話があったが,発表ではフォントについては一切触れられなかった。今思えば,懇親会でフォントについて話かけてもよかったなと少し後悔している。

Denite.nvim ~The next generation of unite~@Shougo

暗黒美夢王ことShougoさんによるUnite.vimの後継のDenite.nvimについての発表だった。

Unite.vimは動作が遅いことが欠点で,Google検索のサジェストにも表示されるほど。作者のShougoさんもそれは 気にしていたらしく,その問題を解消すべくDenite.vimの開発始めたようだ。Denite.nvimの特徴としては,Python3 でほぼ実装されていること。非同期のインターフェースもPython3の機能を使っており,Vim 8.0やvimproc.vimの非同期I/Oは使っていないらしい。

Unite.vimやDenite.nvimの比較 対象としてCtrlP.vimがよくあげられる。CtrlP.vimはVim scriptで実装されており依存関係が少なくシンプルで速く,わかりやすいのが利点だと理解している。ShougoさんはCtrlP.vimに対して対 抗心を燃やしており,Denite.nvimとCtrlP.vimの比較やなぜCtrlP.vimが速いのかなど解説していた。

Denite.nvimはNeoVimとVim 8.0以降で動作する。質問してわかったのだけど,Vim 8.0以降で動作するというのはVim 8.0の非同期I/Oを使っているからではなく,Pythonのサポートが安定しているからとのことだ。てっきりVim 8.0の非同期I/Oを使っているのだと思っていたので予想外で驚いた。

Denite.nvimとCtrlP.vimの比 較が印象に残った。UniteとかDeniteは高機能なのかもしれないけれどなんだか複雑でややこしそうだからCtrlP.vimを使うのがいいのかなという印象を持った。Uniteは何回か使おうとしたことがあったのだけど,キーバインドを覚えられなくて結局インストールし たっきり使っていない。自分の身の丈にあっていなかったのかもしれない。

Go、C、Pythonのためのdeoplete.nvimのソースの紹介と、Neovim専用にpure Goでvim-goをスクラッチした話@zchee

Shougoさんが開発したNeoVim用の補完プラグインdeoplete.nvimのカスタマイズ方法の紹介と自作プラグイン?の紹介 だった。NeoVimもGoもよくしらないのでよくわからなかったので「なんか凄そう」という小学生並みの感想しか持てなかった。

NeoVimもGoもリリースされてせいぜい数年程度の比較的新しいものでまだ安定していないと思う。後ろ盾がなくなればすぐに廃 れてしまうだろうし,エッジを追いかけて消耗するのは疲れる。関わるかかどうかは慎重に判断したほうがいいと思っている。それよりかは手元に あるVimやShell scriptなどの理解を深める方が確実なんじゃないかと考えている。

もともとNeoVim自体はVimのリファクタリングのために派生したと理解している。当初は非同期I/OなどVim本体の不足していた実装に不満があったことが理由の一つでもあったが,Vim 8.0でそれは解消された。Vim本体の開発は活発だしまだまだ廃れる様子もない。そもそもVim自体機能が膨大で,密度にもよるが使いこなせるようになるだけでも数年はかかると思う。まだNeoVimに手を出すタイミングではないと思う。

エディタの壁を越えるGoの開発ツールの文化と作成法@tenten

VimというよりはGo言語の紹介というような内容だった。Go言語はGoogleという一企業が開発したプログラミング言語であり,Oracleが管理しているJavaと立ち位置は似ている。

Go言語は周辺ツールが充実しており,いろいろ便利なんだそうだ。個人的には引数解析の方法や,Go言語の文書作成ツールである godocについて興味を持った。コードの文書化としてはDoxygenがあり,個人的にはDoxygenを多くのプログラミング言語で対応 しているので,これを使うと記法を統一できていいと思っている。Doxygenよりも優れているのならその仕組みは参考にしたいなと思った。

vim-mode-plus for Atom editor@t9md

Atomエディターのアドイン?パッケージのvim-mode-plusについての発表だった。会議参加者の中では年上で熟練した印象の人だった。

他のエディターにVimに似た機能を搭載するという話なのかなと思って,あまり興味なかったのだけど,面白かった。Vimの機能を実装するにあたり,Vimの実装をみて勉強する必要があり,その過程でVimのことを深く考えるようになったのこと。

Vimの中ではテキストオブジェクトのようにあるまとまった塊を一度に操作できるという機能が本質に近いと思う。Vimを使いこなす上ではこういった思想について理解を深めていくことが有効なのだろうと思った。

Vimの日本語ドキュメント@MURAOKA Taro

Vimにおける日本語ドキュメントの歴史・経緯について紹介し,今後の展望などについて発表されていた。冒頭で発表の趣旨が話され,日本語 ドキュメントの翻訳作業への参加を呼びかけることが目的とのことだった。

Vimを使い始めて驚いたこととして,日本語ドキュメントの充実さがあった。Vimを使い始めたのは大学院生で,そのときはEmacsもどんなものかと試したりしていた。Emacsは日本語ドキュメントが少なく,いわゆる普通のOSSという感じだった。日本語ドキュメントの量は日本人初学者のとっつきやすさや,利用者拡大において大きな影響があると思っている。

Vim本体に同梱される日本語文書はVimの作者であるBramさんとMURAOKAさんの1対1でのメールのやりとりで受け渡しされているとのことで,最後は人のつながりでできているんだなと少し感動した。

翻訳については賛否もあるが,個人的には必要なことだと思っている。英語だけしか使えないというのは,自分の母語を禁止され,英語を使うことを強制されている奴隷のようなもので,不自由な状態だ。他国の文化を否応なしに押し付けられ,それに従っているのと同じだ。自国の言葉を使う自由もあるべきだろう。

詳しいことはよくわからないが,MURAOKAさんはissueや管理など多大な労力をかけられており,使う側としてはとてもありがたく思っている。

gerritというのはソースコードレビューシステムらしく,ここでのコメントは的はずれだった。言及するならLibreOffice採用されているPootleだった。

Vim script parser written in Go@haya14busa

Go言語でVim scriptのパーサーを作ったという発表だった。

Go言語は周辺ツールが充実しており,クロスプラットフォームのコンパイルやシングルバイナリなど,コマンドラインツールとしてはVimと 相性がよいらしい。Vimの中からsystem()関数で呼び出して使えるようなものを作られたようだった。

GoはVimと相性がよいというのには違和感があった。というのも,それは単にVimから外部コマンドを実行する程度の話 で,Vimから直接使えるわけではないからだ。本当に相性がよいというのは,Vimがインターフェースを用意している,PythonやLuaなどだろう。VimはGoへのインターフェースは提供していないし,できることの限界があるように感じた。単にGo言語が使いたいから作った というような感じももった。

僕の友達を紹介するよ@aiya000

やけに声の高い人で女性かと思ってしまった。普段使われているプラグインを紹介し,また自作のプラグインについても紹介されていた。

以前であれば,こうしたプラグインには興味があったのだけど,Vimのプラグインは数が多く,更新もけっこうされており,つい ていくのが疲れてしまった。プラグインを使えるようになるにも学習期間が必要であり,このコストがだんだんしんどく感じるようになってしまい,あまりプラグインを使わないほうがいいんじゃないかと思えてきた。

Vimの標準機能をよく理解し,本当に必要なものに絞ってプラグインを試していくのが効率がいいのかなと思った。

Best practices for building Vim plugins@thinca

Vimプラグインを開発していくうえでの注意点や好ましい作法について解説されていた。

このようなノウハウは長年の経験が必要になり,他の人が知ることは難しい。今回の発表のようにある程度まとまった単位で,なぜこうすればよいの か,こういうときはどうするのか,といったことを解説した情報を公開していただけるのはありがたい。

懇親会

懇親会ではお寿司とピザを食べた。お寿司はけっこう高そうな感じで美味しかった。

懇親会では発表で気になっていたことと,Vimで自分が抱えている問題に質問した。偶然だが,懇親会の開始時にKen Takataさんと同卓になり,話しかけても大丈夫そうな感じだったので質問してみた。

Q1.今までVimで非同期I/Oを実現するにはvimproc.vimを使うしかなかった。Vim 8.0の非同期I/O APIによりvimproc.vimがいらなくなった?

A1.Yes。

Q2.Vim 8.0の非同期I/Oの導入で例えばvimgrepなどがバックグラウンドで実行できるようになるのかなと思ったけど,あくまでAPIの提供だけで実装は自分でやる必要がある?

A2.Yes。

Q3. Windowsの共有フォルダのファイルをGVimで操作すると遅いのだが,解決方法をしらないか?

A3. 自分が使う限りではそんなに遅いというのを感じたことはなかった。普段はターミナルから使うので実感していないだけかもしれないが。再現性を確立できれば解決できるかもしれない。

その他近くで一人でいる人に話しかけて,今回の発表の感想だったりVimについて世間話などをした。仕事でも仕事以外でも周りにVimを愛用している人は皆無なので,自分にとってはプラグインの話をできるだけで非日常的な時間だった。

こういうカンファレンスなどで,他の人たちの会話の中に割って入っていくのは恥ずかしくて苦手なので,一人でいてくれた方が僕は話しかけやすい。どうしても質問したいことがあれば,割って入ることもあるけれど,できればやりたくない。

そして,最後にMURAOKA Taro(kaoriya)さんにKaoriYa 版Vimにdiff.exeを同梱する件についてお願いをした。具体的には,2016-08-11に出したPull Requestが放置されていたので,それのリマインドだ。kaoriyaさんは忙しい人で,8月はVim 8.0のリリースや夏コミなどで忙しくてたぶん忘れていたのだろう。加えて,あまりモチベーションのあがらない作業でもあるし…

Fix error for directory diff. by lamsh · Pull Request #2 · koron/diffutils

第一印象

僕はVimを2012年から使い始めた。今日までにVimの周辺で活躍されている方で何人か気になっており,どんな感じの人なのかみてみたいというのが一つの参加動機だった。そこで,実際にお目にかかった第一印象を発表順に書いてみる。第一印象は最初しかないのである意味貴重だ。単純に自分が周りの人にどう思われているのかというのは,短くてもけっこう面白いのではないかと思う。

Ken Takataさん

研究者や学者のような印象を持った。落ち着いていて淡々としていて,知性を感じさせる雰囲気を感じた。発表でもあったとおり,この人は日本でVim 本体に最も活発に貢献している内の一人であり,Rubyの正規表現エンジンである鬼雲の開発者だ。とてもすごいお方なのだと思った。

今回のカンファレンスでVim本体に関して質問するなら,この人かkaoriyaさんのどちらかがベストだと思っていたので,懇親会で質問させていただいた。もっとちやほやされて,質問できないくらい人が押しかけていてもおかしくないと思っていたのだが,他の人はあまり気づいていなかったのかもしれない。

Shougoさん

NeoBundleやneocomplete,neosnippetにはお世話になっている。

偶然なのだが,カンファレンスで僕はこの人の隣の座席(VimConf 2016の座席表 - セキココ)に座っていた。単に一番前の奥から詰めて座っただけなのだが…。Shougoさんの発表でそれが分かった。

席に着いたときから,「この人何かやりこんでいるな」という印象を持った。そして,何か他の人と違う雰囲気をまとっているように感じ,少し近づき難かった。隣で作業の様子をチラ見するくらいしかできなかった。さすが暗黒美夢王といったところか。

言動などをみていると何かしっかりとした思想を持っているようで,みていて面白い人だと思った。

MURAOKA Taroさん

autodate.vimを愛用させていただいている。

今年はこの人との絡みが今までで一番あった。今年の6月ごろにKaoriYa版Vimに「配 布物にdiff.exeを同梱してほしい · Issue #10 · koron/vim-kaoriya」というお願いをし て,そこで8月まで何回かメッセージのやりとりがあった。また,同じく8月に「KaoriYa 版Vimの独自設定を無効化する方法」というブログ記事を書き,指摘をいただいた。さらに,同じく8月にVimの日本語help 文書で未翻訳の1文をみつけたので,Pull Requestを「Translate the untranslated paragraph in `:h cterm-colors` by lamsh · Pull Request #21 · vim-jp/vimdoc-ja-working」送った。

もっと古くは2014-09-15に今のところ唯一の自作Vim pluginであるautofname.vim「My first Vim plugin: autofname.vim」の作成にあたって,そのベースとした autodate.vimのライセンスの確認でメッセージのやりとりをした。

偶然だが,参加受付ではkaoriyaさんが担当してくれた。名前を告げると僕のアイコン・名前に見覚えがある思わせぶりな発言があったので,冒頭にも載せた以下のツイートをした。これに対して返信があり本人だったことがわかった。覚えていてくれて光栄だった。

人情味のある気さくな方だと思った。コミュニケーション能力があって,人徳があって,面白い人で,この人のおかげで日本のVimコミュニティは維持されているのだなと思った。会うまでは,なんとなくメガネをかけた少しクールなお兄さんという勝手な印象をもっていたのだけど,想像していたよりかは年齢が上だと思った。

Twitterのタイムラインなどを見ている限り,ゲームや猫が好きなところ,ユーモアがあって物腰やわらかい感じなど,個人的に好印象をもっている。

thincaさん

vim-quickrunとvim-templateを使わせていただいている。

今回のカンファレンスで言葉を交わすことはなかったので本当に見た感じだが,丁寧な印象を持った。発表とも通じるものがあるが,手堅く,礼儀正しく,きっちり丁寧。そういう安定感・安心感を感じさせる雰囲気を感じた。

Vim scriptの構文などで度々ブログや書籍を参照させていただいており,ありがたく思っている。

まとめ

今後もVimを使い続けるつもりでいる。Vimを使い続ける限り,またVimConfには参加することがあると思う。次回参加するときは短くてもよいので何かしら発表してみたい。ゆるく気長に付き合っていきたい。

How to check if script executed as a command in POSIX shell script

$
0
0

#posixismadventこの記事はPOSIX原理主義Advent Calendarの17日目だ。

POSIX原理主義のシェルスクリプトがコマンドとして実行されたか,dot(.)コマンドで読み込まれたかどうかを判定する方法を記す。

結論としては,以下のようなコードでコマンドとして実行されたか判定できる。

#!/bin/sh
## \file script_name.sh

EXE_NAME='script_name.sh'
NOW_EXE=$(ps -p $$ -o args=)

case "$NOW_EXE" in *$EXE_NAME*)
echo "Executed as a file."
esac

Introduction

一般的に,シェルスクリプトでコマンドを自作する場合,コマンドをファイルとして実行することを前提として作られることが多い。例えば以下のようなコードだ。

cat <<- EOT >init.sh
#!/bin/sh
## \file init.sh

init(){
set -eu
umask 0022
export LC_ALL='C' PATH="$(command -p getconf PATH):$PATH"
}

main()(
init
echo 'init: $@'
)

main "$@"
EOT
./init.sh a b c
init: a b c

しかし,上記のinit.shinit関数に見られるように,どのスクリプトでも共通で使うような処理が出てくる。それぞれのスクリプトで同じ内容を記述するのは冗長なので記述を共通化したくなる。つまり,シェルスクリプトをライブラリとして活用できてもいいのではないかと考えた。

シェルスクリプトでは,dot(.)コマンドにより外部ファイルの内容を現在のシェルでそのまま実行することができる。これにより関数を現在のシェルやスクリプトで読み込むことができる。

cat <<- EOT >import.sh
#!/bin/sh
## \file import.sh

. /init.sh
main(){
echo "import: $@"
}
EOT
./import.sh 1 2 3
init: 1 2 3
import: 1 2 3

しかし,上記のimport.shの実行結果でimport.shでは記述していないinit: 1 2 3が出力されていることからわかるように,単にdotコマンドで読み込んだだけだと,当然ながら読み込んだファイルの通常のコードが実行されてしまう。関数定義を読み込みたいだけなので,通常のコマンドを実行したくない

これについて,例えばPythonやRubyではファイルとして実行されたかどうかを判定する記述方法が存在する。

cat <<- EOT >if_main.py
#!/usr/bin/env python3
## \file if_main.py

def foo():
printf(__name__)

if __name__ == '__main__':
foo()
EOT
./if_main.py
__main__
cat <<- EOT >if_main.rb
#!/usr/bin/env ruby
## \file if_main.rb

def foo
puts $0
end

if __FILE__ == $0
foo()
end
EOT
./if_main.rb
./if_main.rb
PythonとRubyでのファイルとして実行されたことの判定方法
Python
__name__変数に文字列'__main__'が入っている。
Ruby
__FILE__変数にファイル名が$0に実行中のスクリプト名が入っている。

PythonとRubyではファイルとして実行されたかどうかの情報を変数で保持している。したがって,この変数を使いファイルとして実行されている場合にだけ,関数を実行することができる。このように記述することで,ライブラリしても,コマンドとしても使える汎用性が高いスクリプトを作成できる。

このようなことをPOSIX準拠のシェルスクリプトでもできないか検討した。

Method

まず,ネット上で提案されている事例を調査した。複数の案があったので,そのうちダメな方法を以下で一覧する。

既に提案されている方法のダメな理由
方法参照元ダメな理由
[ "$_" = "$0" ]
URL$_変数がPOSIX非準拠。過去にはPOSIX規格に存在したが,多重定義時にkshで混乱するため2001年に廃止となった。
[ "$0" = "$BASH_SOURCE" ]
URLBASH_SOURCE変数がbash専用。
$(return >/dev/null 2>&1)

[ "$?" = "0" ]
URL関数以外でのreturnコマンドの挙動がPOSIX未定義。bash,zshとdashでは挙動が違う。
case $(caller) in '0 '*)
echo "main"
esac
URLcallerコマンドがPOSIX未定義。

bashの独自拡張を使ってよいのであれば,上記のようにいくつもやり方があるのだが,POSIX原理主義を通すには他の方法を探るしかない。

調べてわかった重要な前提として,dot(.)コマンドで読み込んだファイル名を保存する変数やアクセスするコマンドなどはPOSIXの範囲では存在しない。そのため,自分でファイルにファイル名を格納する変数をハードコーディング(直打ち)して,このファイル名と一致するかで判定する。

具体的には,以下のコードの$hogeに相当する変数を用意する。

#!/bin/sh
## \file file.sh

EXE_NAME='file.sh'
case "$hoge" in *"$EXE_NAME"
echo "MAIN"
esac

この視点に立って,改めて利用可能な方法を検討する。考えられる方法は以下の2通りだ。

  1. $0に格納される値と比較
  2. psコマンドの実行で得られる現在実行中のプロセス名と比較

使用する変数を以下の表で説明した。

現在実行中ファイルの特定に利用可能な変数
変数説明
$_シェルかシェルスクリプトの起動で使われたフルパス。POSIX非準拠。
$0現在のシェルかシェルスクリプト名。
$$現在のプロセスID
$PPID親プロセスのID

1点目の方法では,スクリプト実行時に$0変数に格納される値と比較する。この方法でうまくいくならこれが一番素直で簡単だ。

2点目の方法では,psコマンドを使ってプロセス名を取得する。

psコマンドを実行すると以下のようにプロセスのIDと実行コマンドが表示される。

ps
  PID TTY      STAT   TIME COMMAND
2275 pts/2 Ss 0:00 bash
22064 pts/9 R+ 0:00 ps w

この出力から[COMMAND]で表示される実行コマンドを取得することでファイル名と比較を行う。

psコマンドのオプションで,-pで表示させるプロセスIDを指定し,-oで出力項目(列)を指定できる。-oオプションで指定できる項目はいくつかあるが,今回は実行コマンド名がほしいので,commargsを試す。commでは実行中のコマンドだけ(C言語のargv[0]相当)を出力する。argsはコマンドだけではなく呼び出しコマンドも表示できる可能性がある。また,-oオプションでは指定対象の末尾に=を付けることで,ヘッダーを省略できる。

psコマンドで対象とするプロセスには現在のプロセス$$と親プロセス$PPIDが考えられる。現在のプロセスはもちろんであるが,親プロセスが何であるかで特定できるかもしれないので検討対象とした。

これらから,以下のpsコマンドで現在実行中のファイル名を取得できる可能性がある。

ps -p $PPID -o comm=
ps -p $$ -o comm=
ps -p $$ -o args=

ここまでで,$0とpsコマンドによるPOSIXに準拠した方法で現在実行中のファイル名の取得方法について検討した。実際にこれらの方法で値を取得できるかを検証していく。

検証にあたって,ファイル名での実行とdot(.)コマンドによる読み込みで考えられる全てのパターンを試す。具体的には以下のrun_if_main.shのコードで示すように,以下の2パターンを考慮した。

  • 現在のシェルでの読み込みと実行
  • 新しいシェルでの読み込みと実行

なお,参考までに$_の値も一緒に確認する。

現在実行中かの表示コマンド(if_main.sh
#!/bin/sh
# \file if_main.sh

echo "\$_: $_"
echo "\$0: $0"
echo 'ps -p $PPID -o comm=: '"$(ps -p $PPID -o comm=)"
echo 'ps -p $$ -o comm=: '"$(ps -p $$ -o comm=)"
echo 'ps -p $$ -o args=: '"$(ps -p $$ -o args=)"
echo ''
現在の状態を正しく表示できるかのテストコマンド(run_if_main.sh
#!/bin/sh
## \file run_if_main.sh

./if_main.sh
. ./if_main.sh
sh ./if_main.sh
sh -c ' ./if_main.sh'
sh -c '. ./if_main.sh'

run_if_main.shif_main.shを同じディレクトリ(/home/senooken/tmp)に配置して,run_if_main.shを実行した。実行はUbuntu 16.04のbashから以下のコマンドで行った。

cd ~tmp
./run_if_main.sh

実行結果を以下の表にまとめた。

bashからの./run_if_main.shの実行結果一覧
表示項目run_if_main.sh内での実行コマンド

./if_main.sh. ./if_main.shsh ./if_main.sh sh -c './if_main.sh'sh -c '. ./if_main.sh'
期待される値if_main.shif_main.sh以外shif_main.shsh
$_./run_if_main.sh./run_if_main.sh./run_if_main.sh./run_if_main.sh./run_if_main.sh
$0./if_main.sh./run_if_main.sh./if_main.sh./if_main.shsh
ps -p $PPID -o comm=run_if_main.shbashrun_if_main.shshrun_if_main.sh
ps -p $$ -o comm=if_main.shrun_if_main.shshif_main.shsh
ps -p $$ -o args=/bin/sh ./if_main.sh/bin/sh ./run_if_main.shsh ./if_main.sh/bin/sh ./if_main.shsh -c . ./if_main.sh

この表の結果からいえることを以下にまとめた。

$0とpsコマンドによる現在実行中コマンド名の特定方法の考察
  1. $_ではスクリプトを間に介した場合,スクリプト名の値が入っているので,ネストさせる場合などで判定に使えない
  2. $PPIDを使った場合は,確かに親のプロセス名が取得できている。しかし,この情報だけでは現在のプロセス名を特定できない
  3. psコマンドの-oオプションでは,commだと直接のプロセス名だけが表示されるが,argsだと実行コマンド全体が表示される。argsの方が情報は多いが,条件判定に不要な情報が多いので,commの方が有利
  4. $0ps -p $$ -o comm=はほとんど同じ期待される結果を返している。しかし,sh ./if_main.shのときだけ結果が異なっている

この4番目のsh ./if_main.sh実行時の結果の違いは重要な論点となる。具体的には,sh ./if_main.shはファイルを読み込んでいるのか,それともコマンドとしてファイルを実行しているのか?だ。

この議論を検証するために,実行コマンドであるPOSIXのshのマニュアルを確認する。

command_file
The pathname of a file containing commands. If the pathname contains one or more <slash> characters, the implementation attempts to read that file; the file need not be executable. If the pathname does not contain a <slash> character:
The implementation shall attempt to read that file from the current working directory; the file need not be executable.

If the file is not in the current working directory, the implementation may perform a search for an executable file using the value of PATH, as described in Command Search and Execution.

Special parameter 0 (see Special Parameters) shall be set to the value of command_file. If sh is called using a synopsis form that omits command_file, special parameter 0 shall be set to the value of the first argument passed to sh from its parent (for example, argv[0] for a C program), which is normally a pathname used to execute the sh utility.
sh - Shell & Utilities

上記で書かれているように,sh ファイルで実行された場合はファイルを実行ではなく読み込んでいる。したがって,$0ではなくps -p $$ -o comm=がベストな判定方法だろう。

なお,仮に$0ps -p $$ -o comm=が同じ正しい結果を返していた場合でも,psコマンドを採用することになる。理由は$0のzshの標準の挙動がPOSIX shellと異なるからだ。

zshではfunction_argzeroというオプションがデフォルトで有効になっている。このオプションを有効にすると,dotコマンドでファイルを読み込んだ場合,自動的にそのファイル名を$0に設定してしまう。

参考:Bash/Zshで'source'するファイルの中でで自分のパスを取得する

この機能が便利になる面もあるのだろうが,この機能のためにzshだけが他のシェルとデフォルトの挙動が異なる。つまり,$0を採用した場合zshだけ特別設定が必要となってしまう。そのため,共通で有効なpsコマンドを使うべきと判断した。

Coding

ここまでの調査で分かったことから,POSIX原理主義のシェルスクリプトでファイルとして実行中かどうかの判定は以下のようにして行える。

#!/bin/sh
## \file script_name.sh

EXE_NAME='script_name.sh'
NOW_EXE=$(ps -p $$ -o comm=)

if [ "$EXE_NAME" = "$NOW_EXE" ]; then
echo "Executed as a file."
fi

実際に,上記ファイルを実行したら,コマンドファイルとして実行したことを判定できている。

. ./script_name.sh
./script_name.sh # Executed as a file

この調査結果を利用して,現在実行中かどうかの判定関数を以下のように作れる。

## コマンドファイルの名前をグローバル変数EXE_NAMEに代入しておき参照
is_main()(
# EXE_NAME='script_name.sh' # またはis_main関数で定義
NOW_EXE=$(ps -p $$ -o comm=)
[ "$EXE_NAME" = "$NOW_EXE" ]
)

この関数を活用すれば,以下のようなライブラリとしても,コマンドファイルとしても利用可能なシェルスクリプトを作成できる。

#!/bin/sh
## \file is_main.sh

EXE_NAME='is_main.sh'

init(){
set -eu
umask 0022
export LC_ALL='C' PATH="$(command -p getconf PATH):$PATH"
}

is_main()(
NOW_EXE=$(ps -p $$ -o comm=)
[ "$EXE_NAME" = "$NOW_EXE" ]
)

main()(
init
echo "MAIN. $@."
)

if is_main; then
main "$@"
fi

## 現在実行中かの判定を関数にしない場合は,上記を以下のように記述する。
# NOW_EXE=$(ps -p $$ -o comm=)
# if [ "$EXE_NAME" = "$CUURENT_EXE" ]; then
# main "$@"
# fi
## もちろん1行で書いてもいい
# is_main && main "$@"
# [ "$EXE_NAME" = "$NOW_EXE" ] && main "$@"

2017-01-15追記:

ps -o comm=では,コマンド名が15文字までしか表示されないことがわかった。

linux - What is the maximum allowed limit on the length of a process name? - Stack Overflow

そこで,15文字以上のコマンド名にも対応するために,ps -p $$ -o args=を採用する。この場合,表示される内容は以下の通りにインタープリターのフルパスとコマンド引数まで表示される。

/bin/sh ./script_name.sh 0

コード中に埋め込んだコマンド名とプロセス上でのコマンド名のマッチさせるため,case文により判定を行う。

## Function
is_main()(
NOW_EXE=$(ps -p $$ -o args=)
case "$NOW_EXE" in *$EXE_NAME*);;
*) return 1;;
esac
)

## Without function
NOW_EXE=$(ps -p $$ -o args=)
case "$NOW_EXE" in *$EXE_NAME*)
main "$@"
esac

Conclusion

POSIX原理主義によるシェルスクリプトをライブラリとしても活用するための第一歩として,スクリプトが現在実行中かどうかの判定方法を検討し,確立できた。この方法を使えば,自作のシェルスクリプトのメイン部分を関数にまとめて,4-5行追加するだけで,コマンドだけでなくライブラリとしても活用できる

実のところ,今のままだとシェルスクリプトをライブラリ化しようが,コマンドのままとたいした違いはない。ただ,変数の命名規則(メソッドやプロパティの区切りを__に見立てたりなど)や関数の構造を工夫することで,シェルスクリプトでもオブジェクト指向的なことができるのではないかと考えている。もしこれがうまくいけば,より高度で効率的,汎用性の高いPOSIX原理主義による開発ができるかもしれない。

今回の調査はこのための第一歩だった。将来の応用に向けた基礎研究的なものだろう。POSIX原理主義なら時空を超えることができる。蓄積が重要になる。だから,今回のような細かい話で,すぐには役に立たなさそうな内容であっても,無駄にはならないだろう。

参考:

How to check if commands enabled in POSIX shell script

$
0
0

#posixismadventこの記事はPOSIX原理主義Advent Calendarの17日目だ。

POSIX原理主義を実践していくうえで,POSIX規格外のコマンドを使う場合は,交換可能性を担保しなければならない。その際にコマンドが利用可能かどうかの判定が必要となる。そこで,POSIXに準拠したコマンドの利用可能判定方法を解説する。

結論としては,用途に応じて以下の2コマンドを利用すればよい。

command -v hoge >/dev/null  && echo "OK"
hash hoge 2>/dev/null && echo "alias OK"

コマンドが存在するかどうかを判定するコマンドとしてwhichコマンドが存在する。しかし,このwhichコマンドはPOSIXで未定義なので,whichコマンド自体が常に使えると保証できない。

実はコマンドの有無を判定できるコマンドはいくつか存在する。これらを説明していく。

コマンドの存在の判定コマンド

コマンドの有無の判定については以下のコマンドが利用可能である。

  • command -v
  • command -V
  • hash
  • type
  • kshとzshの組み込みコマンドのwhence
  • which

これらのコマンドは判定できるコマンドの種類や,出力形式がまちまちである。これらの対応を以下の表にまとめた。

コマンドの存在判定コマンドの出力内容一覧
コマンド出力形式不在時

存在時




外部コマンド組み込みコマンド関数予約後alias
command -v定義済み。不在時は無表示。無表示絶対パス名前名前名前定義
command -Vunspecified。それぞれの種別がわかるように出力。不在時はエラー表示。エラー絶対パス名前名前名前定義
hashaliasは定義もチェックする。存在時は無表示エラー無表示無表示
(zsh:検出不可)
無表示
(zsh:検出不可)
エラー無表示
typeunspecified。それぞれの種別がわかるように出力。不在時はエラー表示。エラー絶対パス名前名前名前定義
whencePOSIX未定義。kshとzsh組み込みコマンド。不在時は無表示。無表示絶対パス名前名前名前定義
whichPOSIX未定義。aliasは展開してチェックする。不在時は無表示。無表示
(zsh:表示)
絶対パス
無表示
(検出不可)
(zsh:名前)

無表示
(検出不可)
(zsh:定義)
無表示
(検出不可)
(zsh:名前)
絶対パス
(zsh:定義)

この結果をまとめる。

コマンドの有無の判定コマンドの表示結果の解説
command -v
POSIX準拠。存在時の出力形式が決まっている。全項目の有無の判別可能。ただし,aliasは定義の有無しか判定できない。aliasで定義されたコマンドが実際に存在するかは判定不能。
hash
POSIX準拠。全項目の有無の判別可能。aliasの実際のコマンドも展開して判定できる。zshの組み込みコマンドの場合,組み込みコマンドと関数の判定不能。
command -Vtype
ともにPOSIXで定義されており,挙動もほぼ同じ。コマンドの存在および種類の識別ができるが,出力形式は未定義。
whence
command -vと同等。ただし,POSIX未定義なので,kshとzshでしか使えない。
which
外部コマンドしか判定できない。zshの組み込みwhichに限り,whenceと同等。

command,hash,typeはPOSIXで定義されている。この内,typeとcommand -Vは出力形式が未定義。command -vのみコマンド存在時の出力形式が決まっている。外部コマンドの絶対パスの取得に適している。

hashコマンドはコマンドが存在する場合に,何も表示しない。代わりに,aliasの定義元のコマンドまでチェックできる。ただし,zshの組み込みコマンドのhashは挙動が異なり,組み込みコマンドや関数の存在を判定できない。

zsh組み込みのhashが組み込みコマンドや関数を判別できないのは,おそらくPOSIX規格の以下の一文のためだろう。

Utilities provided as built-ins to the shell shall not be reported by hash.
hash - Shell & Utilities

シェルでは,最近実行されたコマンドをハッシュテーブルに記憶しており,これを利用することでコマンド検索速度をあげているらしい。hashコマンドは,指定したコマンドをハッシュテーブルに追加したり,現在のハッシュテーブルを表示したりできる。

hash
hits command
1 /bin/grep
8 /usr/bin/xset
5 /usr/bin/vim
1 /usr/bin/xkbcomp

上記一文は,おそらく引数を指定しないハッシュテーブル一覧に組み込みコマンドをを表示させないことを意図していたのだと思うが,zshではこれを誤って解釈して実装したため,hashコマンドで組み込みコマンドや関数が判定できなくなったのだと思われる。

コマンドの存在判定の書き方

実際にこれらのコマンドを使って,if文などでコマンドの有無を判定することを考える。大きく2種類の判定方法がある。

  1. 実行結果の終了ステータスで判定
  2. 出力文字で判定

1.の方法は,例えば以下のように出力結果を捨てて行う。

command -v which >/dev/null && echo "OK"

この方法では,確実に標準出力と標準エラー出力を捨てる必要があるので,>/dev/null 2>&1などといったリダイレクトを記述する必要がある。

2.の方法は,command -vwhencewhichコマンドは対象コマンドが存在しなければ,何も表示しないことを利用している。testコマンドでは,出力文字があればtrue,なければfalseとなるので,コマンド代入$()を使って,コマンドが存在しているかどうかを出力される文字列を使って判定する。

[ "$(command -v which)" ] && echo "OK"

testコマンドを使う場合,入力文字数が少ないという利点があるが,実行コマンドが増えるためパフォーマンスが悪くなる。

この2通りの方法で判定方法を以下の表にまとめた。

2種類のコマンドの存在判定方法
コマンド終了ステータスでの判定方法標準出力の有無で判定
command -v
command -v hoge >/dev/null
[ "$(command -v hoge)" ]
command -V
command -V hoge >/dev/null 2>&1

hash
hash hoge 2>/dev/null
hash hoge 2>&- #(zsh:エラー)
[ ! "$(hash hoge 2>&1)" ] 
type
type hoge >/dev/null 2>&1

whence
whence hoge >/dev/null
[ "$(whence hoge)" ]
which
which hoge >/dev/null
[ "$(which hoge)" ]

POSIXの範囲内で実現する場合,hashコマンドを使う方法が最短記法となる。しかし,hashコマンドはzshで実装が異なっており,汎用性が若干欠けてしまう。hashコマンドで使っている2>&-というリダイレクトは見慣れないと思うので簡単に解説する。これは標準エラー出力を閉じている。これにより,エラーメッセージの表示を強制的に禁止してしている。

同様に>&-という記法で標準出力を閉じることができる。これにより,>/dev/nullよりも手短に標準出力を捨てられてよいと思うかもしれない。しかし,標準出力を閉じた場合,シェルによってはエラーが出てしまうので残寝ながら使えない。

sh
echo "ABC">&-
sh: echo: I/O error

しかし,標準エラー出力を閉じる>&-であれば,そのエラーすら禁止するので問題なく使える。

hoge 2>&- && echo OK || echo NG
NG

しかし,これをzshの組み込みhashコマンドでやる場合に限り,以下のようなエラーが出る。

hash lv 2>&-
zsh: write error

zshがコマンド実行前にパースしてエラーを出しているようだ。

Re: precmd: write error: interrupted

上記で書かれているようにexec 2>&-を先に掛けば回避できる。しかし,これはみにくく,記述の短さの利点がなくなってしまう。

速度比較

これだけコマンドや判定方法があれば,どれを使えばいいのかわからなくなってしまう。そこで,実行速度を計測して判断材料とする。実際に以下のコードで1万回実行して実行速度を計測してみる。

コマンドの存在判定の実行速度比較コード
#!/bin/sh
## \file time_if_exe.sh

set -eu
umask 0022
export LC_ALL='C' PATH="$(command -p getconf PATH):$PATH"

export N=10000
export NULL="/dev/null"

## test vs. >/dev/null
time -p sh -c 'for i in $(yes|head -$N); do command -v which >$NULL; done'
time -p sh -c 'for i in $(yes|head -$N); do [ "$(command -v which)" ]; done'
echo ""

## Vs. all commmand
time -p sh -c 'for i in $(yes|head -$N); do command -v which >$NULL; done'
time -p sh -c 'for i in $(yes|head -$N); do command -V which >$NULL; done'
time -p sh -c 'for i in $(yes|head -$N); do hash which 2>$NULL; done'
time -p sh -c 'for i in $(yes|head -$N); do hash which 2>&-; done'
time -p sh -c 'for i in $(yes|head -$N); do type which >$NULL; done'
time -p zsh -c 'for i in $(yes|head -$N); do whence which >$NULL; done'
time -p sh -c 'for i in $(yes|head -$N); do which which >$NULL; done'

計測結果を以下の表に示す。

コマンドの存在判定の実行速度比較結果


Time [s]
Commandrealusersys
[ “$(command -v )” ]1.170.040.42
command -v0.030.010.01
command -V0.030.020.00
hash 2>/dev/null0.050.010.03
hash 2>&-0.040.010.02
type0.020.010.00
whence0.050.020.01
which5.690.080.53
コマンド存在判定の実行速度比較結果
  • testコマンド[ "$()" ]は,リダイレクトの約40倍時間がかかる。
  • whichコマンドは外部コマンドのため,速度が最も遅い。
  • 最速はtypeコマンドの0.02 s,ついでcommandコマンドの0.03 s。0.01 sの差であり誤差の範囲内と思われる。

whichコマンドとtestコマンドを使う方法以外は,どれも十分速いので,速度差は無視していい。

commandコマンド

ここまででコマンドの存在の有無の判定コマンドや,その判定の記述方法について説明してきた。結局のところ,実行速度も記述量もそんなに違いがない。ではどれを選ぶべきか?答えはcommand -vだ。

なぜ,コマンドの有無の判定でcommand -vを使うべきかは,POSIXで記載されている-vVオプションが追加された経緯を読めばわかる。

The command -v and -V options were added to satisfy requirements from users that are currently accomplished by three different historical utilities: type in the System V shell, whence in the KornShell, and which in the C shell. Since there is no historical agreement on how and what to accomplish here, the POSIX command utility was enhanced and the historical utilities were left unmodified. The C shell which merely conducts a path search. The KornShell whence is more elaborate-in addition to the categories required by POSIX, it also reports on tracked aliases, exported aliases, and undefined functions.


RATIONALE - command - Shell & Utilities

上記内容を簡単に解説する。commandコマンドの-vと-Vは,もともと以下の3種類のコマンドで達成していたことを実現するために2001年に追加された。

  • System V shellのtypeコマンド
  • KornShellのwhenceコマンド
  • C shellのwhichコマンド

これらのコマンド間では,何をどのように実現するかの合意がなかったので,これらのコマンドを修正せずにcommandが拡張された。

上記3種類のコマンドには,それぞれ以下のような欠点や特徴がある。

歴史的なコマンドの有無の判定コマンドの違い
System V shellのtypeコマンド
元々実行時のコマンド名を表示するためのコマンド。出力形式は未定義。
KornShellのwhence
aliasや未定義関数などPOSIXで要求する以上に詳しい。
C shellのwhich
単純なパス検索。組み込みコマンドなどを検出不可。

command -Vはtypeコマンドに相当しており,typeコマンドと同様に出力書式は未定義だが,シェルでどのように解釈されるかユーザーにとって役に立つ情報が多く表示される。-VはSystem VのVを意味しているのだと思われる。

command -vはKornShellのwhenceコマンドに相当しており,組み込みコマンドや関数まで判別できている。また,出力形式も規格て定義されている。

Conclusion

ここまでで,コマンドの存在の判定方法について説明してきた。結論として,コマンドの存在の判定は,原則command -vで判定するのがよいだろう。理由は以下となる。

  • commandコマンドの-vVオプションがシェル共通で使うことを念頭に作られた。
  • POSIXで出力書式が唯一規定されている。
  • トータルの記述量はtypeコマンドで判定する場合より1文字多いだけ。
  • 実行速度も十分速い。

command -vは唯一出力書式が決まっているので,外部コマンドの絶対パスを取得したい場合はほぼこれを使うしかない。ただし,commandコマンドはaliasの本体を判定できないという欠点もある。このときのために,hashコマンドを使うのはありだろう。ただし,hashコマンドはzsh組み込みの場合に挙動が変わるので,あまり使わないほうがいいように思う。zshで使わないというのならありかもしれない。

command -v hoge >/dev/null  && echo "OK"
hash hoge 2>/dev/null && echo "alias OK"
hash hoge 2>&- && echo "OK" # except for zsh

リダイレクトの記述を省略したり,より意図がわかるように以下のように関数にしてしまうのもよいだろう。

コマンドの有無の判別関数
is_exe_enabled(){
command -v "$@">/dev/null
}

POSIX原理主義における交換可能性を担保したシェルスクリプトを作るうえで,極めて重要なコマンドの存在の判定方法について解説した。command -vを使ってコマンドの存在を判定し,POSIX原理主義を実践していこう。


How to get date time & time zone in POSIX shell script

$
0
0

#posixismadventこの記事はPOSIX原理主義Advent Calendarの22日目だ。

POSIX原理主義によるシェルスクリプトを作成した際に,作成日や更新日などの日時情報をスクリプト内や--helpなどに記述することがある。このときの日時の形式はどうすべきか?という議論がある。この記事ではPOSIX原理主義で採用すべき日時形式とタイムゾーンの取得方法について説明する。

結論としては,ISO 8601の拡張形式(YYYY-MM-DDThh:mm±hh:mm)を採用し,以下のコマンドでタイムゾーンを取得すればよい。

get_tz()(
set $(date -u '+%j %H %M'); U_D=${1#0}; U_D=${U_D#0}; U_H=${2#0}; U_M=${3#0}
set $(date '+%j %H %M'); L_D=${1#0}; L_D=${L_D#0}; L_H=${2#0}; L_M=${3#0}

# Fix if year is crossed
IS_CROSSED_YEAR="[ $L_D = 1 -o $U_D = 1 ] && [ $((L_D+U_D)) -gt 3 ]"
eval "$IS_CROSSED_YEAR"&& U_D=$((L_D == 1 ? L_D-1 : L_D+1))

dm=$(( (L_D*24*60 + L_H*60 + L_M) - (U_D*24*60 + U_H*60 + U_M) ))
DIGIT_1=$((dm<0 ? -dm%10 : dm%10))

# Fix if minute is changed during running date command
[ $DIGIT_1 = 1 -o $DIGIT_1 = 6 ] && dm=$((dm<0 ? dm+1 : dm-1))
[ $DIGIT_1 = 4 -o $DIGIT_1 = 9 ] && dm=$((dm<0 ? dm-1 : dm+1))

printf '%+03d:%+03d\n' $((dm / 60)) $((dm % 60)) | sed 's/:[+-]/:/'
)

get_tz # +09:00
echo "$(date +%Y-%m-%dT%H:%M:%S)$(get_tz)" # 2016-12-27T21:57:32+09:00

Introduction

プログラムにおいて,そのバージョンがいつ作られたかという情報が大事になることがある。例えば,依存ソフトウェアのバージョンや,開発の活発さの指標として参考になることがある。

そこで,ソースコードやREADMEなどに,直接そのプログラムの作成日などや更新日時を記述することがある。しかし,日付や時刻の形式は国や地域,人によっていくつかの書き方がある。

以下に比較的よく見かける日時の表記例を示す。

日付の形式
形式
YYYY-MM-DD2016-12-25
YYYYMMDD20161225
YYYY/MM/DD2016/12/25
DD/MM/YYYY25/12/2016
MM/DD/YYYY12/25/2016
YYMMDD161225
YY/MM/DD16/12/26
YYYY/MMM/DD2016/Dec/25
MMM/DD/YYYYDec/25/2016
DD/MMM/YYYY25/Dec/2016
時刻の形式
形式
hh:mm:ss20:30:40
hhmmss203040
TT hh:mm:ssPM 08:30:40
hh:mm:ss TT08:30:40 PM
T.T. hh:mm:ssP.M. 08:30:40
hh:mm:ss T.T.08:30:40 P.M.
t.t. hh:mm:ssp.m. 08:30:40
hh:mm:ss t.t.08:30:40 p.m.
tt hh:mm:sspm 08:30:40
hh:mm:ss tt08:30:40 pm

この他にも年,月,日を空白区切りで並べる方法などもあり,実に多くの日時の表記方法があることがわかる。特に,この中ではDD/MM/YYYYとMM/DD/YYYY表記が紛らわしい。実際にこれらはそれぞれイギリスとアメリカで使われる日付の標識であり,2016-09-07などお互いに1-12の範囲の月日であるときに見分けがつかない

さらに,これとは別にタイムゾーンもある。例えば,13:00という時刻をアメリカと日本とでみると,それぞれの国では時差があるので,実際にはそれぞれ別の時間をみていることになる。例えば,誰が世界で一番最初に公開したかなどのように世界中で時間を競う場合に,この時差を考慮する必要がある。時差を考慮するには,13:00 JSTや13:00+09:00などのようにタイムゾーンも明記しなければ特定できない

こうした日時の表記方法に対して,どのような形式を採用するべきだろうか?

POSIX standard date time

XBD 1.3 Normative ReferencesでPOSIX規格に含んでいる国際規格が掲載されている。この中に,日時形式の国際規格であるISO 8601が存在している。

実際に,POSIX規格内でも以下の場所でISO 8601が参照されている。

また,POSIX以外にもW3CのHTML 5.1のtime要素でもISO 8601の形式しか日時の形式として認められていない。さらに,ECMAScript 2016でも日時として受け付ける文字列にもISO 8601の形式のみが採用されている。

したがって,ISO 8601の日時形式に従うべきだろう。日時形式の国際標準としてはISO 8601しか存在しないので順当な判断だ。

ISO 8601の日時の形式は以下の形式となる。

ISO 8601の書式
項目書式
ISO 8601YYYYMMDDThhmmss±hhmm20161226T230000+0900
ISO 8601拡張形式YYYY-MM-DDThh:mm:ss±hh:mm2016-12-26T23:00:00+09:00

ISO 8601の形式は日付と時刻を文字Tで区切り,タイムゾーンを末尾に付けることで日時を表記する。

1個目の形式は年月日時刻を詰めた形式となっており,可読性は悪いが,Windowsなどでファイル名として利用不可である:がないことから,ファイル名やデータ名などに適した形式である。

2個目の形式は,ISO 8601の拡張形式(extended format)と呼ばれており,各項目の間に-:といった区切り文字を入れることで可読性に優れた形式となっている。そのため,通常の文書ではこちらの形式が適している。

常に日時+タイムゾーンを明記する必要はなく,必要に応じて後ろの部分を省略することが許されている。例えば,2016-12-26のように日付だけ表記したり,23:00といった具合に時刻の部分だけ表記してもよい。また,タイムゾーンを指定しない場合は現地時間を意味する

しかし,ここで一つ疑問が起きる。それは,現在日時を表示するPOSIX準拠コマンドであるdateコマンドの標準の出力形式がISO 8601と異なる点だ。dateコマンドの出力形式は以下となっており,ISO 8601とは異なっている。

When no formatting operand is specified, the output in the POSIX locale shall be equivalent to specifying:
date "+%a %b %e %H:%M:%S %Z %Y"
date - XCU

実際にコマンドを実行すると以下のように出力される。

date
Mon Dec 26 22:39:45 JST 2016

POSIX文書に書かれている以上,これも一つの標準とみなすことができるかもしれない。ただ,この原因は,憶測だが以下のように考えることもできる。

  1. POSIX前のUNIXのデファクトがそうなっていた。
  2. 対話的な用途として視認性がよいフォーマットが優先された。

1点目の理由として,元々の形式がこのようになっていたので,差し障りがないように標準の出力はそのままにしたのではないかと考えることができる。

2点目の理由だが,「今何時かな」と思ってdateの4文字を入力した結果としては,視認性のよいものがよいという思想が働いた可能性がある。例えば,ISO 8601の形式だと曜日は月曜日から始まる1-7の数字で表記されることとなり,現在が何曜日かぱっとわかりにくい。その他,カレンダーを表示するcalコマンドも,標準出力は機械向けというよりは視認性を優先した出力結果となっている。

cal
   December 2016      
Su Mo Tu We Th Fr Sa
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

dateコマンドの標準出力結果がISO 8601でないことについて考察した。この考察した結果としても,やはり基本はISO 8601に従うべきだろう。ISO 8601の拡張形式(YYYY-MM-DD)を使えば,視認性を損なわずに機械可読な日時にできるからだ。

How to get time zone

ここまでで,日時の形式にはISO 8601を採用すべきだと結論づけた。実際にdateコマンドでISO 8601の形式で出力するには,以下のように変換指定子を組み合わせる。

date +%Y-%m-%dT%H:%M:%S
2016-12-26T23:06:48

ただし,POSIXのdateコマンドのオプションでは現在のタイムゾーンを取得できないという問題がある。変換指定子に%Zというのがあるのだが,これはJSTというようなタイムゾーン名が表示されるだけで,残念ながら協定世界時(UTC)からの時差(オフセット)を数字で表示できない。なお,POSIXで規定されるC言語のstrftime関数には%zという変換指定子があり,これでタイムゾーンが±hhmmの形式で取得できる。dateコマンドにも%zの変換指定子が存在すれば簡単だったのだが,ないならばしかたない。

タイムゾーンを省略する場合は,現地時間と解釈できる。この場合,同じ文書に国や地域名を含める必要があり,これはこれで煩雑になる。より汎用性をあげるには日時にタイムゾーンも明記したほうがよいだろう。そこで,自分でタイムゾーンを取得する方法を検討する。

なお,GNU dateであれば,-Iオプションを使えば簡単にタイムゾーンも含めてISO 8601による現在日時を表示できる。当然ながら,GNUの独自拡張に依存すれば交換可能性がなくなるのでPOSIX原理主義では使ってはいけない。

date -I'seconds'
2016-12-26T23:11:06+09:00

タイムゾーンを取得するにあたって,環境変数TZを使えば簡単にできるかと思ったが,TZ環境変数は定義されていない環境もあり,これに頼ることができない。

dateコマンドはTZ環境変数が存在すれば,このタイムゾーンに基づいて日時を表示する。dateコマンドのTZ環境変数の説明をみればわかる通り,TZ環境変数が存在しなければ,システム標準のタイムゾーンが使われることになっている。

システム標準のタイムゾーンとは,実装依存になるのだが,例えばGNU C Libraryでは/etc/localtime/usr/etc/localtimeが参照され,Ubuntuでは/etc/timezoneが参照される。

しかし,当然ながらこれらのファイルはPOSIXでは未定義であり,これらに依存すれば交換可能性はなくなるので使うことはできない。

POSIX規格を調べたが,現在のタイムゾーンを数値で取得する方法はstrftime以外に存在しない。ではシェルスクリプトではどうするか?

date -uで常にUTC-0での日時が表示されることを利用して,datedate -uの差分をとり,タイムゾーンを取得する。

具体的には,以下のような関数により現在のタイムゾーンを取得できるようになる。

#/bin/sh
## \file get_tz.sh

get_tz()(
L_D=$(date +%j); U_D=$(date -u +%j)
is_crossed_year="[ $L_D -eq 1 -o $U_D -eq 1 ] && [ $((L_D+U_D)) -ne 3 ]"
eval $is_crossed_year && [ $L_D -eq 1 ] && U_D=0 || L_D=0

LOCAL_MIN=$(echo "$L_D*24*60 + $(date +%H)*60 + $(date +%M)" | bc)
UTC_0_MIN=$(echo "$U_D*24*60 + $(date -u +%H)*60 + $(date -u +%M)" | bc)
DELTA_MIN=$((LOCAL_MIN - UTC_0_MIN))
printf '%+03d:%+03d\n' $((DELTA_MIN/60)) $((DELTA_MIN%60)) | sed 's/:[+-]/:/'
)

get_tz

上記のget_tz関数を実行すると,日本であれば+09:00と表示される。

get_tzの仕組みを解説する。

まず,dateコマンドの-uオプションでは,常にUTC-0での日時が表示される。一方,dateコマンドはTZ環境変数が設定されていればそのタイムゾーンに従い,TZ環境変数がなければシステム標準のタイムゾーンに従い日時を表示する。つまり,date -udateコマンドの結果の差分で現在のタイムゾーンを取得できる。

ただし,そのまま差分を取ると問題が起こる。時間は12進数であり,分は60進数である。そのまま単純に減算すれば,10進数での減算となってしまい,値が想定と異なる。例えば,現在が日本時間の01:00である場合,単純に協定世界時との差分を取ると,01:00-14:00=-13となり,期待する9と異なってしまう。

この問題を回避するため,日時の単位を分に統一させる。分に単位を統一して減算を行い,得られた差分を時間と分に戻すことでタイムゾーンを取得している。通常であれば,シェルスクリプトでの日時の演算は煩雑な作業であり,1970-01-01からの経過秒であるエポックタイム(UNIX時間)に変換して行うのが汎用的だ。しかし,この変換自体複雑である。幸い今回は2時刻の差分をとるだけで済むので,素直に日時を分に変換することで対応できた。

最後のsedの処理について説明する。イギリスより西の地域など現地時間とUTC-0との差分がマイナスのとき,時間と分に換算するときにもマイナスの符号が付く。このままだと,出力するときに-09:-30のように分の部分に符号が付くため,これをsedで除去した。

なお,当初はLOCAL_MINUTC_0_MINで日時を分に換算する際に,算術展開(Arithmetic Expansion)($(()))を使っていたのだが,これだと問題が起きたのでbcコマンドに切り替えた。算術展開だと数字が0から始まる場合に8進数とみなされてしまう。現在日時の取得に使用しているdateコマンドの%d%H%M変換指定子では,1桁の数字を常に先頭に0を付けて出力する。そのため,00-07までは10進数と同じであるので問題ないが,08-09に関しては問題が起きる。具体的には以下のようなエラーが出る。

sh
echo "$((08*2))"
sh: 1: arithmetic expression: expecting EOF: "08*2"

bcコマンドであれば数字を常に10進数として扱うのでこの問題を回避できる。同様の問題に対応するのに,exprコマンドも利用できるのだが,こちらは演算子の前後を空白で区切る必要があり,記述量が長くなってしまうので今回は避けた。

2016-12-29追記:

当初掲載していたコードでは,月や年をまたぐ場合に日数が不連続になり(例:12/31と1/1),タイムゾーンを算出できていなかった。ユリウス日(通算日付)をdate +jで取得すれば,月またぎは問題ないが,やはり年をまたぐときにタイムゾーンを算出できない。そこで,年をまたぐかどうかの判定を入れた。判定は以下の手順で行った。

  1. 確実に年をまたぐことを判定。
    1. 現地時間とUTC-0のどちらかが1/1で,もう片方が最終日であるかを判定。
    2. うるう年では最終通算日366がありえるので,残りの日にちが1/1か1/2でないという条件,つまり両方の日数の合計が3以上であるで判定。
  2. 差分を取れるように,日を0か366か367に更新。

これで,年をまたぐ場合であってもタイムゾーンを算出できるようになった。以下のコマンドで現在の端末でだけ日付を1/1に変更してget_tzを実行すれば,きちんと現在のタイムゾーンを取得できることを確認できる。

sudo date 01010000
./get_tz.sh # +09:00

2017-01-07追記:

関数内で1回目のdateを実行してから2回目のdateコマンドを実行するまでの間に,万が一分をまたいでしまうと1分ずれてしまい,タイムゾーンが08:59や09:01となってしまう。幸いなことに,タイムゾーンの分は00,30,45しか存在しないので,差分をとった後の分の1桁目が1,6,4,9なら±1することで誤差を調整する。現在のタイムゾーンがUTC+00:00より西か東かで符号が変わるのでその処理を入れている。

また,当初は日時の先頭に0が登場するために,bcコマンドを使って計算していた。今回の対応で,setコマンドで日時分を個別の変数に代入するように処理を変えたので,ついでに先頭の0を削除するようにして,算術展開$(())で計算できるようにした。

get_tz()(
set $(date -u '+%j %H %M'); U_D=${1#0}; U_D=${U_D#0}; U_H=${2#0}; U_M=${3#0}
set $(date '+%j %H %M'); L_D=${1#0}; L_D=${L_D#0}; L_H=${2#0}; L_M=${3#0}

# Fix if year is crossed
IS_CROSSED_YEAR="[ $L_D = 1 -o $U_D = 1 ] && [ $((L_D+U_D)) -gt 3 ]"
eval "$IS_CROSSED_YEAR"&& U_D=$((L_D == 1 ? L_D-1 : L_D+1))

dm=$(( (L_D*24*60 + L_H*60 + L_M) - (U_D*24*60 + U_H*60 + U_M) ))
DIGIT_1=$((dm<0 ? -dm%10 : dm%10))

# Fix if minute is changed during running date command
[ $DIGIT_1 = 1 -o $DIGIT_1 = 6 ] && dm=$((dm<0 ? dm+1 : dm-1))
[ $DIGIT_1 = 4 -o $DIGIT_1 = 9 ] && dm=$((dm<0 ? dm-1 : dm+1))

printf '%+03d:%+03d\n' $((dm / 60)) $((dm % 60)) | sed 's/:[+-]/:/'
)

setコマンドの後に以下の日時を強制的にセットしてちゃんと算出できていることを確認できる。

 # +5:45 Nepal
# U_D=365; U_H=23; U_M=59
# L_D=1; L_H=5; L_M=45

# U_D=1; U_H=5; U_M=45
# L_D=365; L_H=23; L_M=59

# -3:30 Canada
# U_D=1; U_H=3; U_M=28
# L_D=365; L_H=23; L_M=59

# L_D=1; L_H=3; L_M=28
# U_D=365; U_H=23; U_M=59

Conclusion

POSIXにおける日時の表示形式とタイムゾーンの取得方法について説明した。

現在日時は一時ファイルを作る時などで重宝するので,以下のように関数にして~/.bashrcなどに書いておけば,現在日時を即座にISO 8601形式で出力できるので便利だ。

get_tz()(
set $(date -u '+%j %H %M'); U_D=${1#0}; U_D=${U_D#0}; U_H=${2#0}; U_M=${3#0}
set $(date '+%j %H %M'); L_D=${1#0}; L_D=${L_D#0}; L_H=${2#0}; L_M=${3#0}

# Fix if year is crossed
IS_CROSSED_YEAR="[ $L_D = 1 -o $U_D = 1 ] && [ $((L_D+U_D)) -gt 3 ]"
eval "$IS_CROSSED_YEAR"&& U_D=$((L_D == 1 ? L_D-1 : L_D+1))

dm=$(( (L_D*24*60 + L_H*60 + L_M) - (U_D*24*60 + U_H*60 + U_M) ))
DIGIT_1=$((dm<0 ? -dm%10 : dm%10))

# Fix if minute is changed during running date command
[ $DIGIT_1 = 1 -o $DIGIT_1 = 6 ] && dm=$((dm<0 ? dm+1 : dm-1))
[ $DIGIT_1 = 4 -o $DIGIT_1 = 9 ] && dm=$((dm<0 ? dm-1 : dm+1))

printf '%+03d:%+03d\n' $((dm / 60)) $((dm % 60)) | sed 's/:[+-]/:/'
)


now()(
EXE_NAME='now'
dt=$(date +%Y%m%dT%H%M%S)
OPTSTR=':lst-:'
for opt in $(echo $OPTSTR | sed 's/[:-]//g' | fold -w 1); do
eval is_opt_$opt='false'
done

while getopts $OPTSTR opt; do
case "$opt${OPTARG-}" in
l|-long) dt=$(date +%Y-%m-%dT%H:%M:%S);;
s|-short) dt=$(date +%Y%m%dT%H%M%S );;
t|-time-zone) is_opt_t='true';;
\?*) echo "$EXE_NAME: invalid option -- '$OPTARG'">&2; exit 1;;
*) echo "$EXE_NAME: unrecognized option '-$opt$OPTARG'">&2; exit 1;;
esac
done

if $is_opt_t; then
TIME_ZONE=$(get_tz)
[ -n "${dt%%*:*}" ] && TIME_ZONE=$(printf '%s\n'"$TIME_ZONE" | sed 's/://')
dt="$dt$TIME_ZONE"
fi

echo "$dt"
)

上記のnow関数は以下のように使う。

now     # 20161227T232101          Same as now -s or now --short
now -t # 20161227T232119+0900 With time zone, same as now -st or now --time-zone
now -l # 2016-12-27T23:22:07 ISO 8601 extended format. same as now --long
now -tl # 2016-12-27T23:22:36+09:00 Same as now --long --time-zone

上記のように,関数化までする必要がないと感じるならalias程度にしておくのもよいだろう。こちらだと,タイムゾーンや表示形式の自由度はないが,1行で書けるのですっきりしている。

alias now='date +%Y%m%dT%H%M%S'

このnow関数は,例えば,以下のようにコマンド代入(Command Substitution)$()で現在日時をファイル名として使える。

ls > $(now).log

POSIX規格だけでなく,ISOなどの国際規格に準拠することでも交換可能性を担保できる。日頃からこうしたデジュレ標準を意識して,高品質で高寿命なコードや文書の作成を心がけよう。

What is shebang (#!/bin/sh) in POSIX shell script

$
0
0

この記事はPOSIX原理主義Advent Calendarの13日目だ。

シェルスクリプトに限らず,Perl,Python,Rubyといったスクリプト系言語において,プログラム(スクリプト)の1行目に以下のような記述をする。

#!/bin/sh
#!/usr/bin/env python

この#!の文字列をshebang(シバン)と呼ぶらしい。名前の由来はshell bang(!)などいくつかの説があるとのことだ。Shebangに続けてそのスクリプトを実行するインタープリターを絶対パスで指定することができる。このように記述すると,スクリプトファイルの実行時にshebangで指定したインタープリターによりスクリプトが実行される。

ほぼ全てのシェルスクリプトの1行目にこのような記述があり,もはや暗黙のルールと思えるこの記述だが,恥ずかしながら意味をよく理解していなかった。そこで,POSIXにおけるこのshebangの解釈について調査したのでまとめる。

コマンドの実行の流れ

まず,shebangについて説明する前に,コマンドがどのように実行されるかについてみておく。

POSIXにおけるコマンドの実行の流れは,XCUのCommand Search and Executionに書かれている。自作のシェルスクリプトやコマンドは,1.e.i.bか2のどちらかに分類される。この2種類はPATH変数から探すか,相対パスで実行するかの違いであり,中身はほぼ同じだ。以下に1.e.i.bの第1段落を抜粋する。

b. Otherwise, the shell executes the utility in a separate utility environment (see Shell Execution Environment) with actions equivalent to calling the execl() function as defined in the System Interfaces volume of POSIX.1-2008 with the path argument set to the pathname resulting from the search, arg0 set to the command name, and the remaining execl() arguments set to the command arguments (if any) and the null terminator.
1.e.i.b - Command Search and Execution - XCU

ここで書かれている通り,シェルスクリプトやコマンドはexecl()に相当する関数が呼び出されることで実行される。以下にexecl()の定義示す。

int execl(const char *path, const char *arg0, ... /*, (char *)0 */);

シェルスクリプトやコマンド実行時には,pathにシェルスクリプト・コマンドの絶対パス,arg0にシェルスクリプト・コマンドの名前,arg1以降に残りのコマンド引数が適用されて実行される。

execl()実行後の挙動

execl()のPOSIXのぺージによると,execl()が呼び出された後はpathに指定されたファイルによって処理が異なってくる。

execl()にコマンドとその引数が引き渡されて実行されるとき,正常な実行可能バイナリーファイルであればそのまま実行される。しかし,それ以外ではファイルの種類によって以下2通りの処理が行われる。

  1. コマンドが認識できない形式(ENOEXEC)か,認識できてもシステムが未対応(EINVAL)の場合
  2. ファイル先頭の2バイトが文字列#!である場合
コマンドが実行できない場合

1.の場合は,単純なテキストファイルに実行権限が付いた場合が該当する。この場合,execl()はインタープリターとしてshコマンドを使って,まるで以下の形式で実行した環境かのようにexeclp()execvp()を実行する。

execl(<shell path>, arg0, file, arg1, ..., (char *)0);

ここで,<shell path>はshの絶対パス,fileはプロセスのイメージファイル(ファイル名?)arg0,arg1は引数となる(arg0はfileと同じ?)。

There are two distinct ways in which the contents of the process image file may cause the execution to fail, distinguished by the setting of errno to either v[ENOEXEC] or [EINVAL] (see the ERRORS section). In the cases where the other members of the exec family of functions would fail and set errno to [ENOEXEC], the execlp() and execvp() functions shall execute a command interpreter and the environment of the executed command shall be as if the process invoked the sh utility using execl() as follows:

execl(<shell path>, arg0, file, arg1, ..., (char *)0);

where <shell path> is an unspecified pathname for the sh utility, file is the process image file, and for execvp(), where arg0, arg1, and so on correspond to the values passed to execvp() in argv[0], argv[1], and so on.
execl() - XSH

つまり,実行可能なバイナリーファイル以外はすべてshによるシェルスクリプトとみなされる。

この仕様のため,シェルスクリプトの1行目にはshebang(#!/bin/sh)がなくても,shで起動される。試しに以下の内容のhi.shというスクリプトを作って実行すると,shebangがなくてもshによって実行されていることがわかる。

ps -p $$ -o comm=
ps -p $$ -p args=
sh
/bin/sh ./hi.sh
Shebang(#!)がある場合

2.の場合は,一般的なシェルスクリプトのパターンだ。これは,ファイルの先頭2バイト#!であれば,ファイル1行目の#!以降の残りの文字を実行するコマンドインタープリターとみなす。

Another way that some historical implementations handle shell scripts is by recognizing the first two bytes of the file as the character string "#!" and using the remainder of the first line of the file as the name of the command interpreter to execute.

execl() - XSH

つまり,以下のようなシェルスクリプトでは,#!以降で指定される/bin/shによりファイルが実行される。

#!/bin/sh
ps -p $$ -o comm=
ps -p $$ -p args=

ファイルの先頭2バイトをチェックしているので,UTF-8やUTF-16などでBOM(Byte Order Mark)が付いている場合,shebangは認識されず1.のルートでスクリプトが実行される。その場合,BOMがコマンドとみなされて,当然ながらそんなコマンドはないので,line 1: command not foundというエラーが出る。

一応,以下のようにエラー出力を捨てるコードを1行目に書けば,BOMありでも実行できるシェルスクリプトは作れる。しかし,こんなことをするくらいなら最初からBOMなしで書いたほうがよいだろう。

: 2>-

Shebangによりシェルスクリプトを実行する際に,一つだけ問題がある。それは,以下で記載されている通り,sh側で#!から始まるファイルを読み込んだときの挙動が不定なことだ。

1. The shell reads its input from a file (see sh), from the -c option or from the system() and popen() functions defined in the System Interfaces volume of POSIX.1-2008. If the first line of a file of shell commands starts with the characters "#!", the results are unspecified.
2.1 Shell Introduction - Shell Command Language

つまり,大昔のOSなどでは#!/usr/bin/perlから始まるファイルをshで起動した場合,/usr/bin/perlが起動することもありえる。

そのため,真の意味で可搬性が高いといえるのは,shebangを書かない1.の方法だ。ただ,shebangは広く普及しており,現在の実装では#!から始まるファイルをshが読み込んでも,その行は単にコメントとして無視される場合がほとんどだ。また,POSIXのshコマンドのページにおいても,shebang #!が書かれたシェルスクリプトを意識した記述もある。

このことから,わかりやすさと実状を考慮して,shebang行に#!/bin/shを書いても問題ないだろう。

Shebangに関する問題点

ここまででshebangがPOSIX上でどのように規定されているかを説明した。ここからは,shebangに関する問題点について説明する。

Shebangに指定するパス

まずは,shebang行に何を書けばよいかという議論だ。一般的に,シェルスクリプトでは以下のように/bin/shで指定することが多い。

#!/bin/sh

ここで,はたして本当に/bin/shと書いて問題ないのだろうか?実のところ,POSIXでは/binというディレクトリは定義されていない。

さらに,POSIX規格ではsh/bin/shまたは/usr/bin/shにあるとせずに,自分でgetconf PATHコマンドでPATHを取得するように書かれている。

Applications should note that the standard PATH to the shell cannot be assumed to be either /bin/sh or /usr/bin/sh, and should be determined by interrogation of the PATH returned by getconf PATH, ensuring that the returned pathname is an absolute pathname and not a shell built-in.

sh - XCU

ただ,このようになってしまうと結局shebangの指定に互換性がなくなり,環境ごとにshebangの書き換えが必須となってしまう。

ではどう考えるか。これはFilesystem Hierarchy Standard (FHS)への準拠を考える。FHSはLinuxを含むUNIX形OSでの主なディレクトリとその内容を定めたものである。したがって,FHSで定義されているコマンドのパスは前提としてよいと考える。

FHSで存在することが必須とされているコマンドは/bin配下のものが大半であり,残りの/usr/bin配下などに含むコマンドはほとんどオプションだ。/bin配下に存在することが要求されているコマンドは以下の35コマンドだ。

cat, chgrp, chmod, chown, cp, date, dd, df, dmesg, echo, false, hostname, kill, ln, login, ls, mkdir, mknod, more, mount, mv, ps, pwd, rm, rmdir, sed, sh, stty, su, sync, true, umount, uname, test, [

3.4. /bin : Essential user command binaries (for use by all users) Filesystem Hierarchy Standard

この中に,shコマンドが存在している。したがって,FHSに準拠しているOSでは/bin/shは存在することが保証されているので,shebangとして#!/bin/shと書いても問題ない

ここで注意したいのはFHSではbashcshenvコマンドの存在は必須ではなかったり,定義されていないことだ。したがって,#!/bin/bash#!/usr/bin/envから始まるスクリプトは可搬性が低い。実際のところ,/bin/bashは存在することが多いのだが保証はできない。

/usr/bin/envが定義されていないというのは重要だろう。というのも,シェルスクリプト以外のスクリプト言語(Perl,Python,Ruby)では,#!/usr/bin/env pythonのようにenvコマンドを経由して実行させるshebang行がよく書かれるからだ。

スクリプト言語のインタープリターはPOSIX規格でも定義されておらず,どこにインストールされているかは全く不明だ。一応,FHSでは/usr/binにperlとpythonが存在することがオプションで定義されているのだが,あくまでオプションだ。それに,これらのコマンドではシステムにインストールされたものではバージョンが古いため,ユーザーが独自にインストールしたものを優先して使いたい場合がある。

そこで,envコマンドを経由してユーザーのPATHを使うことで,インストール場所の問題を回避することがよくなされる。FHSでは/binはシステム管理者とユーザーの両方に使われるコマンドが含まれる。envコマンドはシステム管理者には不要なので,/usr/binenvコマンドが存在することが多い。

大半の場合/usr/bin/envは存在するが,存在しない実装もある。そこで,確実に実行させるために/bin/shを経由してスクリプト言語のインタープリターを実行する方法がある。

シェルスクリプトのコメントと,スクリプト言語のコメント文字が違うことを利用して,スクリプト言語のコメント内にシェルスクリプトでexecコマンドを記述して,スクリプト言語のインタープリターを起動するというトリックを使っている。

個人的にはそこまでする必要はないと考える。PerlもPythonもRubyもPOSIXに存在しないし,そもそもインストールされていなくてPATH変数に存在しないかもしれない。あまり気にしなくてもいいのではないか?

基本的には,#!/usr/bin/envで記述しておき,万が一envコマンドが無かったり,スクリプト言語のインタープリターが存在しなければ,自分でインストールしたりシンボリックリンクを貼ればよいだろう。

Shebangにおける引数

以下のようにshebang行にオプションや引数を書くことがある。

#!/bin/sh -eu
#!/usr/bin/env python

ここで問題になるのが,これらのオプションや引数がどう解釈されるかだ。

POSIX規格では,ファイルの先頭2バイトが#!である場合,ファイル1行目のそれ以降の文字は「インタープリター名として扱う」としか記載されておらず,オプションや引数がある場合にどうなるかは書かれていない

例えば,shebangに以下のように書いた場合を考える。

#!/bin/sh -e -u

この場合,以下の5パターンが考えられる。

Shebangの引数の解釈
IDPathArg1Arg2
1/bin/sh -e -u

2/bin/sh-e-u
3/bin/sh-e -u
4/bin/sh-e
5/bin/sh

期待するのは全てオプションとして認識されること(表の2番目)だ。しかし,Ububntu 16.04では3番目の挙動をした。つまり,引数-e -uが空白を含めた1個("-e -u")として認識されてしまった。

この挙動については,Linux Programmer's Manualのexecveのマニュアルに書かれている。以下のコマンドでも閲覧できる。

man execve
Interpreter scripts
An interpreter script is a text file that has execute permission
enabled and whose first line is of the form:

#! interpreter [optional-arg]

The interpreter must be a valid pathname for an executable file. If
the filename argument of execve() specifies an interpreter script,
then interpreter will be invoked with the following arguments:

interpreter [optional-arg] filename arg...

where arg... is the series of words pointed to by the argv argument
of execve(), starting at argv[1].

For portable use, optional-arg should either be absent, or be
specified as a single word
(i.e., it should not contain white space);
see NOTES below.
execve(2) - Linux manual page

さらに,上記マニュアルのNOTESを確認すると以下の記載がある。

The semantics of the optional-arg argument of an interpreter script
vary across implementations. On Linux, the entire string following
the interpreter name is passed as a single argument
to the
interpreter, and this string can include white space. However,
behavior differs on some other systems. Some systems use the first
white space to terminate optional-arg. On some systems, an
interpreter script can have multiple arguments, and white spaces in
optional-arg are used to delimit the arguments.
execve(2) - Linux manual page

つまり,Linuxでは引数を全体で1個として扱う(表の3番目)が,他の実装では最初の空白を終端(表の4番目)とみなしたり,複数の引数を受け付ける(表の2番目)こともありえる。

元々,POSIX規格にはshebang行の引数については言及がないので,表の1番目の#!以降が空白を含めて丸ごとインタープリターとみなしてしまっても問題ないように解釈できる。このため,どの環境でも動作させるには,基本的にはshebangには引数を書かず,書いたとしてもshebang行における引数は1個までにすべきだろう。

ショートオプションであれば,グループ化できるので以下のようにまとめればよい。

#!/bin/sh -eu

スクリプト言語をenv経由で実行する場合は,envコマンドの引数としてスクリプト言語のインタープリターを指定してしまっているので,残念だがオプションの指定は諦めるしかない。

#!/usr/bin/env python     # OK
#!/usr/bin/env python -v # NG

本の虫: Shebangという謎な事実上業界標準について

Shebang直後の空白

シェルスクリプトなどを見ていると,以下のようにshebang#!の直後に空白が置かれているshebang行を見かけることがある。

#! /bin/sh

あまりみかけない記述なので,ただの打ち間違いか,Webブラウザの表示や書籍の組版の文字間隔の調整でそうみえているだけだと思っていたのだが,どうやらこのように書く流派が存在するらしい。

このことについては以下のページでの議論とそこから辿れるURLからことの経緯がよくわかる。

unix - #! /bin/sh で、 ! の後にスペースを空けることに理由はあるか - スタック・オーバーフロー

発端としては,GNU autoconfのチュートリアル文書において,「4.2BSDでは先頭4バイト"#! /"をマジックナンバーとして読むのでスペースが必要」と記載されていたためだ。しかし,この記述は誤りで,実際は空白はオプション扱いであり,新しい版のGNU autoconfの文書では修正されている。

さらに,問題の4.1 BSDにおいてもソースコード上でshebangの直後に空白が必要となる修正は入っておらず,空白がなければ動作しない実装が存在したという事実も確認できない。Shebangの直後に空白を入れなければならないというのは,あくまで噂や都市伝説でしかない。

これらの信頼性の欠ける情報に従うべきでないという理由とともに,POSIX規格の観点からも,#!の直後に空白をいれるべきでない

理由は,#!以降の文字を空白を含めてインタープリターのパスとして認識してもPOSIX定義上の動作としてなんらおかしくないからだ

まず,POSIXにおけるshebangによるインタープリターの起動メカニズムはexecl()のマニュアルの以下の1文のみで定義されている。

Another way that some historical implementations handle shell scripts is by recognizing the first two bytes of the file as the character string "#!" and using the remainder of the first line of the file as the name of the command interpreter to execute.

execl() - XSH

ここでは,ファイルの1行目の#!以降の文字をインタプリターのファイル名とみなすとだけ書かれており,POSIXではこれ以上に詳しいルールはなく,引数の扱いがどうなるかなども実装依存となる。

さらに,以下の事実からPOSIXの定義だけだと,Shebangの直後に空白をおくと,相対パス" /bin/sh"でインタープリターが指定されたと解釈することもできる。

  1. Shebangは相対パスも有効,実際にshを./testに配置してShebangを相対パス#!test/shで書いたシェルスクリプトに記述したコマンドps -p $$ -o args=で確認可能。
  2. 名前が空白1個のディレクトリも有効なパス(mkdir '')。

実際には,空白を含むパスを指定するとコマンドが見つからない。

bash: ./test.sh: /test/bin/sh: bad interpreter: No such file or directory

しかし,POSIXの定義上,#!以降の残りを空白や引数丸ごと単一コマンドとみなしても,挙動として問題ない。

このPOSIX規格の解釈上の曖昧さをなくすために,shebangの直後に空白をいれるべきでない。常に#!/bin/shのように#!の直後に空白を入れずにshebang行を書こう。

シェルスクリプトへのsetuid

最後の問題はセキュリティに関するものだ。ファイルのアクセス権限として,セットユーザーID(setuid)がある。この属性のついたファイルはそのファイルの所有者(通常はroot)の権限で実行できる。

setuidの利点はroot権限のパスワードを教えることなくroot権限でしか行えない作業を行える点だ。例えば,passwdコマンドで自分のログインパスワードを変更する場合や,mountコマンドでデバイスを接続する場合は,本来ならroot権限のファイルを操作する必要がある。しかし,これのコマンドにはsetuid属性があるので,root権限のパスワードをしらなくても通常ユーザーのまま実行できる。

setuidをファイルに設定するには以下のコマンドで行う。

chmod u+s file

処理内容が限定される場合には便利だが,一時的にroot権限を与えてしまうため,セキュリティ上の危険性がある。

実際にシェルスクリプトにsetuid属性がある場合の脆弱性を狙った攻撃が過去には行われたらしい。

このことは,以下で言及されている。

上記のUnix FAQで説明されている2個の事例を紹介する。

ファイル名を-iにする問題

これは,root権限で対話シェルが起動してしまうという問題だ。

setuidのついた/etc/setuid_scriptというスクリプトのshebangが以下であるとする。

#!/bin/sh

ここで以下の順序でコマンドを実行する。

cd /tmp
ln /etc/setuid_script -i
PATH=.
-i

この場合,最終的に以下のコマンドが実行されてしまう。

/bin/sh -i

/etc/setuid_scriptの所有者であるroot権限で対話シェルを起動することになってしまう。これは,ファイル名-ishのオプションと誤認されてしまうことによる問題だ。

これを防ぐのは簡単であり,以下のようにshebang行の最後にオプションの終わりを示す-をつければよい。

#!/bin/sh -

この問題については,POSIXのshのページでも以下のように言及がある。

On systems that support set-user-ID scripts, a historical trapdoor has been to link a script to the name -i. When it is called by a sequence such as:

sh -

or by:

#! usr/bin/sh -

the historical systems have assumed that no option letters follow. Thus, this volume of POSIX.1-2008 allows the single <hyphen-minus> to mark the end of the options, in addition to the use of the regular "--" argument, because it was considered that the older practice was so pervasive. An alternative approach is taken by the KornShell, where real and effective user/group IDs must match for an interactive shell; this behavior is specifically allowed by this volume of POSIX.1-2008.
Note:
There are other problems with set-user-ID scripts that the two approaches described here do not resolve.
sh

ただ,この問題は手元のUbuntu 16.04では再現できなかった。実際に上記の手順を行っても,ファイル名の部分が./-iとなっており,きちんとファイルとして認識されている。

さらに,上記の最後に書いてあるとおり,shebang行に#!/bin/sh -と書いても他に重要な問題があるのであまり意味がない。例えば,それは次の問題だ。

実行ファイルの差し替え

これは,root権限で任意のコマンドが実行されてしまうという問題だ。

以下の手順でコマンドを実行する。

cd /tmp
ln /etc/setuid_script temp
nice -20 temp &
mv my_script temp

ここで,3番目のコマンドは以下のように解釈される。

nice -20 /bin/sh - temp

nice -20によりコマンドの実行優先順位を遅くできるので,shtempを読み込む前に,tempファイルを別のファイルで上書きすることができる。つまり,任意のファイルを管理者権限で実行できてしまう

対策

こうしたsetuidに関する問題に対応するには以下の2通りのアプローチがある。

  1. setuidを付与する実行ファイルはC言語などのバイナリーにする。
  2. シェルスクリプトではsetuidを使わずに,管理者権限でないと実行できないようにする。

1.がより確実だろう。バイナリーにしてしまえば,間にshなどがファイルを読み込む隙がないので安全だ。

2.の方法について,管理者権限での実行が必要な場合は,setuidではなくそもそも管理者権限でないと実行できないようにしたほうがいいだろう。

具体的には,以下のコードで管理者権限として実行されているか判定できるので,冒頭に記述しておいてroot権限でなければ終了すればよい。

if [ $(id -u) != 0 ]; then
echo "Please run as root user!">&2
exit 1
fi

My Future Sight for Past: How to check root user in POSIX Shell script

なお,Linuxを始めとした現代のOSではシェルスクリプトへのsetuidは無視するようになっているので,シェルスクリプトにsetuidを付与しても機能しない。

Linux ignores the set-user-ID and set-group-ID bits on scripts.
execve(2) - Linux manual page

セキュリティ上の問題になるのですべきではないが,参考までにシェルスクリプトに付与したsetuidを有効にして実行したい場合の方法を示す。これは,POSIX範囲外になるが-pオプションを付けることでできる。

#!/bin/sh -p

Conclusion

ここまででシェルスクリプトのshebangについて説明してきた内容を以下にまとめる。

  • シェルスクリプトのshebang行には,FHSで保証される#!/bin/shを書く。
  • 基本的にshebang行にオプションや引数を付けない。つけたとしても最大1個までにする。
  • シェルスクリプトにsetuid属性は付与させない。

POSIX規格に厳密に従うならば,shebangを書かないほうがいい。shebangを書かなければ,「#!から始まるファイルがshで結果が不定になる」ことについて悩まなくて済む。

このような曖昧さがPOSIXに残ったのは,POSIX自体が既存の実装ありきだったためだろう。既に存在する実装をカバーできるように,こうした曖昧さをあえて残したのだと思われる。

できることなら,次回のPOSIX規格の改定時にここの曖昧さがなくなってくれたら嬉しい。具体的には,現在2.1 Shell Introductionで「If the first line of a file of shell commands starts with the characters "#!", the results are unspecified.」となっているところを,以下のツイートのように#!から始まる場合にどのように処理すべきか明記してほしい。

Kazuho Okuさんのツイート: "#! について、例えば「○○のように処理すべき、あるいはコメントとして無視しろ」という規定があれば #! /bin/sh は POSIX においてポータブルだと言えるんだろうと思うんだけど"

この記事は元々2016-03-23にまとめた以下のTogetterの記事が頭に残っていたので,これをもとにさらに調査したものだ。

POSIXにおけるShebangの解釈についてのシェルショッカーとの議論まとめ - Togetterまとめ

今回の記事はシェルスクリプトを書く上ではほとんど役に立たず,教養を身に付けるような内容となった。ただ,今後おそらく一生見ることになるので,シェルスクリプトの1行目のshebangとは何であるか,どう書くべきなのかをはっきりさせておきたかった。

これで,これからは胸を張ってシェルスクリプトを書くことができるだろう。

How to work HTA (mshta.exe) in Wine on Linux

$
0
0

HTML Application (HTA)をWineを使いLinux上で実行する方法を記す。

Introduction

ここ数年になって,NW.jsやElectronのように,HTML+CSS+JavaScriptによりデスクトップアプリの開発環境が発展してきた。しかし,15年以上前の1999年から既に似たようなことは可能だった。それがHTAだ。

HTAはMicrosoft Windowsで導入された仕組みである。Internet Explorerを使うことでHTMLをプログラムとして実行することができる。Internet Explorerと一緒にインストールされるmshta.exeがエンジンとなって,HTAは実行される。

HTAには以下の2点の利点があり,非常に魅力的な技術だと感じていた。

  • Windows標準で使える
  • HTAで使えるJScriptやVBScriptが高機能

ただ,一つ残念ところがあった。それはWindowsでしか使えないことだ。せっかくWebの標準技術が使えても,Windowsでしか使えないのはもったいない。

OS標準の開発環境が最も貧弱なのはWindowsであり,このWindowsで使えるHTAが他のLinuxなどのOSでも使えるのなら,HTAは非常に汎用的なプログラムになる。

なんとかして,HTAをLinuxでも実行できるようにしたいと考えていた。幸いなことに,Windows向けのアプリケーションをネイティブ動作させるためのWineというソフトがあり,このソフトでHTAを実行できた。

WineとHTAに関する情報は少ないので,手順を記す。Ubuntu 16.04で動作を確認した。

Method

標準のAPTなどのパッケージマネージャーでインストールしたwine 1.6.2とwineのパッケージマネージャーであるwinetricks 1.8ではバージョンが古いようで,起動しても以下のメッセージが出てしまいHTAを実行できなかった。

wine mshta.exe test.hta
mshta.exe is a stub!

以下のバグレポートで解決策が記載されていたのでこの方法をもとに行う。

WineHQ Bugzilla – Bug 26463 – Multiple applications and games need 'hta' file association, reporting 'There is no Windows program configured to open this kind of file' (18 Wheels of Steel, HTA Examples)

まず,パッケージマネージャー(APT)でインストールしたwinetricksを一旦アンインストールして,最新のwinetricksをインストールする。

sudo apt remove -y winetricks
cd ~/.local/bin
wget https://raw.githubusercontent.com/Winetricks/winetricks/master/src/winetricks
chmod +x winetricks

事前に~/.local/binにPATHを通しておく。winetricksがインストールできたので,mshta.exeの実行元であるInternet Explorerをインストールする。winetricksでインストールできる最新バージョンはIE8である。

winetricks -q ie8

既にIE8がインストールされている場合は,以下のコマンドで強制的にインストールしてもよいかもしれない。

winetrickes --force -q ie8

最後に,以下のコマンドでmshta.exeをレジストリーを登録する。

wine reg.exe ADD HKCU\\Software\\Wine\\DllOverrides /v "mshta.exe" /t REG_SZ /d "native,builtin"

これで動くようになった。この最後のレジストリーの設定が大事なようだ。

試しに以下の内容のtest.htaを作ってみる。

<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<title>HTAのサンプル</title>
<script>
alert("これはHTAであってHTMLではありません");
</script>
</head>
<body>
<a href="http://myfuturesightforpast.blogspot.jp/">My Future Sight for Past</a>
</body>
</html>

test.htaをダブルクリックするか,[右クリック]→[Open With Microsoft (R) HTML Application]により,起動できた。その他,以下のコマンドでも起動できた。

wine start test.hta

起動すると以下の画面が表示される。

test.htaの実行結果

なお,以下のコマンドでは起動できなかった。

wine mshta test.hta

しかし,以下のようなメッセージがでて動いているような印象だ。

fixme:thread:InitializeSRWLock (0x5de6b680): stub
fixme:thread:InitializeSRWLock (0x5de6cfe4): stub
fixme:thread:AcquireSRWLockShared (0x5de6b680): stub

Conclusion

LinuxのWine上でHTAを動かす方法について解説した。

実際のところ,Wine上のHTAでどこまでできるかはまだよくわからない。しかし,HTAは有望な技術だと感じている。

Windows 10でbashが使えるようになったからといっても,まだまだWindows 7は現役だ。標準でシェルスクリプトが使えるようになるには,Windows 8のサポートが切れる2023-01-10まで,少なくともあと6年はかかるだろう。

また,シェルスクリプトだと業務で頻繁に使われるExcelファイルの処理は複雑だ。HTAからJScriptなどでVBAのAPIを操作したほうが簡単だ。

それに,HTAを使えばPOSIX原理主義に基づいて交換可能性を担保したGUIを作れる可能性がある。既存のシェルスクリプトのGUIを作ることもできるかもしれない。

HTAのベースはWeb標準技術だ。学んで無駄になることはない。今後もHTAやWSHなどについて調べてみたい。

Release of BlueGriffon 2.3

$
0
0

HTMLエディターでもあり,EPUBエディターでもあるBlueGriffonの最新版である2.3が2017-02-14にリリースされた。この記事では,2.3での更新内容を解説する。

Revision from 2.2 to 2.3

2.3のリリースは公式サイトと,Google GroupGitHubのタグ(2017-02-14時点ではタグはまだ不在)でアナウンスされる。

まず,2.2から2.3の変更点だが,以下の公式サイトにあるとおり,大きく2点の機能追加・変更がある。

BlueGriffon

BlueGriffon 2.3での変更点
  1. ソースコードとWYSIWYGのデュアルビュー(Dual View (Source + Wysiwyg in sync))
  2. 目次の挿入・更新ボタンの追加(Table of Contents with one-click update)

詳細な変更点は,有料のユーザーマニュアルのSeciont 3に掲載されている。参考として全文を以下に掲載する。

3. Changes from version 2.2
• new preference to hide named anchors' icons in Wysiwig view
new button to insert or automatically update a Table of Contents (if originally created by BlueGriffon)
• the toggle between absolute and relative URIs in Image Insertion dialog was buggy
• the title of the main Window was not centered on OS X
• the link to Opquast from the Opquast Accessibility First Step window was obsolete
• the update manager could fail in some rare circumstances
• let empty inline nodes remain (user request)
• themes ironing
Dual View, including lots of changes everywhere to handle it correctly
• fixed a regression in editorHelper.jsm
fixed a regression in Style Properties panel
added Reset License button to the Preferences panel
• fixed min/max/close buttons on Windows in light theme
BlueGriffon User's Manual v2.3

以下でそれぞれの機能について解説する。

Dual View

まず,1点目の変更点のデュアルビュー(Dual View)について説明する。

これは,今までもBlueGriffonにほしい機能として要望のあがっていたものだ。

従来は,HTMLソースコードとWYSIWYG画面は片方だけしか開くことができなかった。つまり,HTMLコードを編集しながらリアルタイムでそのレンダリング結果を確認することができず,画面を切り替える必要があった。

今回の機能追加では,これが解消され,ソースコードとレンダリング結果を横に並べて閲覧でき,ソースコードの編集結果をリアルタイムで確認でき,またWYSIWYG画面の変更結果のソースコードへの反映結果もリアルタイムで確認できる。

サンプル画面を以下に掲載する。

Sample of Dual View

画面の左下に,[Dual View]と書かれたボタンが追加されている。このボタンを押下すると,[Dual View]モードに切り替わり,左にHTMLソースコード画面,右にWYSIWYG画面が表示される。

実際にソースコード画面やWISYWYG画面を編集すると,1-2テンポほど待つと,もう片方の画面が再読込され,変更結果が反映される。ソースコードの編集結果をWYSIWYG画面で確認する場合は,レンダリングが入るため,文書量によってはやや時間がかかる。

BlueGriffon 2.3のユーザーマニュアルでは,[15. The Dual View]でこの機能について解説されている。そこでの注意点として,BlueGriffonの機能であるリアルタイムスペルチェッカーが有効であると,ソースコードの変更結果をWYSIWYG画面に反映させる場合に,パフォーマンスに影響があると書いてある。

スペルチェッカーの設定は,以下の画像の通りに[Tools]→[Preferences]→[General]タブ→[Spellchecing]→[□Enable real-time spellchecing]から設定を変更できる。

Spell checker configuration

Table of Contents with one-click update

もう片方の変更点は,目次の生成・更新ボタンの追加だ。

この内容は,BlueGriffon 2.3のユーザーマニュアルでは,[10.16. Insert a Table of Contents]でこの機能について解説されている。

BlueGriffonにはもともと文書中のh1-h6要素を抽出して,目次を生成・更新機能があった。[Insert]→[Table of Contents]から実行できた。しかし,普段文書を作成していると見出しを追加することは頻繁にあり,その都度目次を更新するのが面倒だった。

2.3の変更では,この目次の生成・更新機能が以下の画像の通りとおりに,メニューののボタンとして追加された。

Sample of Table of Contents with one-click update

これにより,いちいちメニューを開くことなくこのボタンを押下することで,一瞬で目次を更新できるようになった。さらに,目次を作成すると一緒に生成される<!--mozToc -->というようなコメントをダブルクリックすると,目次の更新画面が表示され,目次の更新がしやすくなった。

普段の文書作成でよく行う作業なので,ありがたい更新だった。

Conclusion

2017-02-14にリリースされたBlueGriffon 2.3の主な更新内容2点について解説した。

まだリリースされたばかりなので,もしかしたらバグがあるかもしれないが,今回のDual View機能は長年BlueGriffonにほしい機能として声のあがっていたものだった。有償の別ライセンスであるが,2.2からEPUBエディターの機能を搭載(マージ)し,Dual Viewの機能も搭載され,まずますCSS組版のエディターとしての機能が充実してきた。

日本語でのBlueGriffonの情報はほとんどないので,今後も動向に注目していきつつ,情報共有していきたい。

参考:せのぺんさんのツイート: "おお!BlueGriffon 2.3がきたか! 今回の目玉は,ソースコードとWYSIWYGのリアルタイム編集。この更新はでかい。今まで画面を行ったり来たりしないとダメだった。 それと目次のワンクリック更新。 試さねば, https://t.co/GbWYbz9gEu"

「ScanSnapを使用することができませんでした。」の対処方法

$
0
0

Windows 10でScanSnap ix500を使っていると以下のエラーが出るようになった。

ScanSnapを使用することができませんでした。
他のユーザーまたは他のアプリケーションがScanSnapを使用していないことを確認してから再度動作してください。

ScanSnapのエラーメッセージ

以前は問題なく使えていたのだが,いつからかUSBケーブルを接続しても認識されず,このエラーが出るようになってしまった。

他のソフトをアンインストールしてみたり,ScanSnap Managerを再起動してみたりしたが,効果がなく困っていた。

以下のページを見つけて,ここに書かれている通り,「接続しているUSBポートを変更する」と解決した。

ScanSnap を使用することができませんでした。他のユーザーまたは他のアプリケーションが ScanSnap を使用していないことを確認してから、再度操作してください。 | Evernote

原因不明だが解決してよかった。

Viewing all 265 articles
Browse latest View live