f

アーカイブ

2016-10-23

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

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

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>[[:blank:],=[]'

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

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

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

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

set -u

is_option_enabled()(
 CMD="$1"; OPT="$2"
 $CMD --help 2>&1 | grep -q -- "$OPT"'[[:blank:],=[]'
)

is_option_enable "$@"

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

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

なお,関数の最後が"$OPT"'[[:blank:],=[]'のように,$OPTとそれ以降を別の引用符で囲んでいる。"$OPT[=[, ]"としてもbashで動くのだが,zshでinvalid subscriptというエラーが表示されてしまうのでこちらを採用した。これは,zshでは"$OPT[]"のブロックで配列と誤認されてしまうからのようだ。

Conclusion

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

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

Solution for garbled characters on GNU Screen

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環境変数には依存しないようだ。

2016-10-22

How to skip GRUB boot menu

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"

2016-10-10

How to loop N times in POSIX shell script

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

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

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

ただし,ループ変数が不要な場合はfor-yesを使う方法が最速で手短な方法だ。

N=10
for i in $(yes | head -n $N); do
echo $i
done

また,速度が重要でない場合や記述の簡潔さを重視する場合はwhileによる方法も検討に値する。

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

なお,処理内容が単純な場合は,yes-shが最短の記述となる。

N=10
yes 'echo $((i+=1))' | head -n $N | i=0 sh

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文とよく似ており,この方法でも指定回数のループが可能だ。

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

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

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

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

ループ方法の工夫

while

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

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

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

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

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

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年動くプログラムはどう書くべきか」のp. 104「同じ文字が連続した文字列を作る」で説明されていた方法を応用している。

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

欠点
  • ループ変数の値が0固定
  • コードがトリッキーで覚えにくい
yes

また,printfと似た方法でyesコマンドで任意の文字を出力し続け,それをheadコマンド任意の行数取得する方法もある。

N=10
for i in $(yes | head -n $N); do
echo $i # y
done

yesコマンドの引数に任意の文字列を指定することで,ループ変数に任意の固定文字列を使うことができる。この方法は,printfよりも記述が簡単である。この方法も書籍「 すべてのUNIXで20年動くプログラムはどう書くべきか」のp. 104「同じ文字が連続した文字列を作る」に書かれていた方法を利用している。

欠点
  • ループ変数の値が任意の文字列で固定

yesで固定文字列を出力する都合,ループ変数の連番を使えないのが欠点となる。

2017-04-15追記:

yes | head -n $Nの形式だとループ変数の連番を保存できない欠点があったが,後述のshを間に経由することで,ループ変数の連番も利用できる。

N=10
for i in $(i=0; yes 'echo $((i+=1))' | head -n $N | sh); do echo $i done

文字列'echo $((i+=1))'を指定回数だけshに渡して実行させて連番を生成している。環境変数としてiに数値が設定されていると,その値が使われるので初期化している。

ただし,記述が冗長になるので,いまいちな方法だ。

2017-01-21追記:

ネットで調べものをしていたらyesコマンドを使った方法で,forwhileすら使わない実現方法が見つかったので追記する。

N=10
yes 'echo $((i+=1))' | head -n $N | i=0 sh

Linux・UNIXでコマンドを定期的(数秒ごとなど)に連続実行させる方法 | 俺的備忘録 〜なんかいろいろ〜

上記コードは以下の手順に解釈される。

  1. yesコマンドで実行するコマンドをテキスト('echo $((i+=1)')として生成
  2. headコマンドで実行する回数分(-n $N)だけ抽出
  3. shでテキストをコマンドとして実行

環境変数としてiに数値が設定されていると,初期値がその値になるのでshの実行前に初期化している。

この方法はforwhileを使わないため,ループ全体を含めた指定回数の実行コードとしては最短となる。

欠点
  • パイプを使うのでループ変数を実行後に維持できない
  • ループ中での条件分岐など複雑な処理に向かない

変数を維持させる場合は,以下のようにevalで展開すれば一応可能だ。

N=10
i=0 eval "$(yes 'eval i=$((i+1)); echo $i' | head -n $N)"
echo $i

変数や環境変数としてループ変数iが使われていると,初期値がその値になってしまうので,先頭のi=0で明示的に初期化している。

seqコマンドの代替

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

bc

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

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

この方法では,bcコマンドが計算式と認識できる式をパイプで渡してbcコマンドで式を実行している。

欠点
  • コードが複雑
  • 標準でインストールされていない環境がある

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

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

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

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

欠点
  • コードが複雑

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

速度比較

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

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

POSIX原理主義によるループ実装の速度比較コード
N=100000 \time -p sh -c 'while [ "$((i+=1))" -le $N ]; do echo $i >/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 $(yes | head -n $N); 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)
N=100000 \time -p sh -c "yes 'echo \$((i+=1))' | head -n \$N | i=0 sh >/dev/null"

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

速度比較と欠点のまとめ
方法5回平均user時間[s]欠点
while0.240seqの代替にならない。ループ毎に評価が必要なので速度遅い。
for-printf0.094seqの代替にならない。ループ変数が0固定。
for-yes0.076seqの代替にならない。ループ変数が任意文字列固定。
for-bc0.180whileに比べると複雑。インストールされていないことがある。
for-awk0.108whileに比べると複雑。
yes-sh0.124複雑な処理に向かない。ループ変数が保存されない。

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

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

Conclusion

6通りのループの実装方法を紹介し速度を計測した。この結果から,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形言語のループとほぼ同じでなじみやすい。

しかし,awkで行う場合は記述が長くなってしまう。ループ変数がそもそも不要であったり,デクリメントなどループ変数の処理が不要な場合は,記述の手軽さからyesコマンドを使うのも悪くない。

N=10
for i in $(yes | head -n $N)
do
echo $i # y
done

2017-01-09追記:

また,実際にコードを書く場合に,awkでの連番の出力方法はやや冗長で複雑なのでぱっと思い出しにくい。そういう場合はwhile文を使うのもありだろう。

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

速度は劣るが,記述が簡単であり,ループ変数から番号も取得できる。デクリメントや2個飛ばしなども簡単にできる。

2017-01-21追記:

ループ全体の記述量に関していえば,yes-shを使ったものが最短となる。

N=10
yes 'echo $((i+=1))' | head -n $N | sh

awkやfor-yesの方法より速度が遅くなるが,単純にコマンドだけを指定回数実行する場合に有力な選択肢となりえる。ただし,この方法だとループ中でのif文による条件分岐などは記述が難しくなるので,処理内容が単純な場合にのみ使うべきだろう。

用途に応じて,awkfor-yesyes-shwhileを使い分けるのがよいだろう。個人的には,以下の優先順位で利用を検討するのがよいと思った。

seqに頼らないループの実現方法
yes-sh
処理が単純で短い場合
for-yes
ループ変数が不要な場合
while
ループ回数が少ない場合,実行速度が重要でない場合
awk
実行速度が重要な場合,汎用性を高める場合

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

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

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

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

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

set -eu
umask 0022
export LC_ALL='C' PATH="$(command -p getconf PATH):$PATH"
seq()( 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 "$@"

Install Sphinx from source on Linux

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が便利だと思った。たぶん今回のようにオフラインの環境にインストールすることはこの先もあまりないと思うので勉強になった。

2016-10-02

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

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_items 5 表示するコメント数
hide_mods 0 1にすれば管理者のコメントを非表示
excerpt_length 100 1コメントの表示文字数
hide_avatars 0 1にすればアバターアイコンを非表示
avatar_size 32 アバターアイコンのサイズ[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は世界中で使われており,品質が高いのでみんな導入したらよいと思う。