Hatena::ブログ(Diary)

ザリガニが見ていた...。 このページをアンテナに追加 RSSフィード

2015-08-06

シュシュポポと汽笛を鳴らしながら走り抜けるslコマンドにしておく

蒸気機関車の映像を観ていると心に染み入る。力強く上がる真っ黒な煙、シュシュポポと噴き出す蒸気の音、遠くに響く悲しげな汽笛の音などなど、映像を見ていると癒され、心地良さを感じる。いつまでも飽きない。なぜだろう?

石炭を燃やして蒸気から動力を取り出す、という仕組みは、それなりの規模が必要。安定稼働させるまで手間もかかる。黒煙などの排出物も多い。そして動作音も盛大である。現在のエンジンやモーターと比べると、最もコントロールしにくい動力なのかもしれない。しかし、様々な要因を調和させて安定稼働させることができると、蒸気機関車は見事に走り始める。煙の勢い、蒸気の吹き出し、ピストンと車輪の動き、それらが発する盛大な音、それらは全て蒸気機関車を動かすために必要なものであり、どれか一つでも欠けると、たぶん動けなくなってしまう。

蒸気機関車が走っている時の姿とは、最もコントロールしにくい動力機関を高いレベルで調和させて稼働させている状態なのだ。高いレベルで調和された動きを見ると、人は感動する。そして時折人為的に発せられるあの汽笛、あれにやられる。鳴らしているのは人間だが、その音はまるで蒸気機関車が叫んでいるようだ。必死に何かを伝えようとしている気がしてしまう。哀愁を感じる。

この蒸気機関車の感動を、愛すべきslコマンドにも追加したい衝動に駆られた。slコマンドとは、ターミナルでslと打ち込んで実行すると、アスキーアートのSLが走り抜ける癒しコマンドである。

  • そもそもはlsを間違えてslと打ち込んでしまった時に備えたコマンドらしいのだが、自分はslと打ち間違えたことは多分ない。
  • OSXバージョンをインストールすると、アスキーアートのSLはターミナル画面を飛び出して、デスクトップ全体を駆け抜ける!

サウンド作り

実は(だいぶ間が空いてしまったが)前回のなるべく理解したいffmpegは、上記映像の中からお気に入りの特定の部分のサウンドを切り出したいために調べたことだった。

  • ffmpegでお気に入りのサウンド部分を切り出し、
  • いくつかのお気に入りサウンドを繋げて合成した。
    • ミキシングにはAudacityが便利。
  • アスキーアートのSLが走り抜ける時間に合わせて、再生時間も調整した。

そして、出来上がったサウンドファイルはこんな感じ。

slサウンドを再生

slコマンドにサウンドを再生させる方法をいくつか考えてみた。

  • もっともお手軽なのは、シェルスクリプトでサウンドを再生しながらslコマンドを実行するラッパーを作ってしまうことだ。
$ afplay sl.mp3 & sl
  • OSXには標準でafplayコマンドがインストールされている。
  • これを使って好みのサウンドを再生することができるのだ。

  • しかし、slコマンドとラッパースクリプトに分かれてしまうのがちょっと気に入らない...。
  • という訳で、slコマンドの中でafplayを実行するようにしてみた。

 +  NSTask *task  = NSTask alloc] init];
 +  [task setLaunchPath: @"/bin/sh"];
 +  [task setArguments: [NSArray arrayWithObjects: @"-c", @"cd $(dirname `which sl`); set sl.m*; afplay $1 &>/dev/null", nil;
 +  [task launch];
 +  [task release];

  • 上記コードによって、slコマンドの中で以下のコマンドを実行するのだ。
$ /bin/sh -c "cd $(dirname `which sl`); set sl.m*; afplay $1 &>/dev/null"

これで、slコマンドと同じ階層にsl.mp3とか、sl.m4aなどが存在すれば、それらのファイルをafplayで再生しながら、slコマンドの本来の処理に進む。

ソースコード

作り方と使い方

  • ダウンロードしたら、ターミナルで~/Downloads/SLMacOSXVersion-dev/に移動して、makeコマンドを実行する。
  • コンパイルされたslコマンドを任意のディレクトリに移動する。(推奨:/usr/local/bin/sl)
  • サウンドファイルsl.mp3も同じディレクトリに移動する。(推奨:/usr/local/bin/sl.mp3)
  • ターミナルでslと入力して実行すると...

シュシュポポと汽笛を鳴らしながらアスキーアートのSLがデスクトップを駆け抜けるのだ!

2015-06-19

なるべく理解したいffmpeg

ストリーム・フィルターチェーン・フィルターグラフ・リンクラベルという概念に注目して調べてみた。

ファイル情報を見る

  • ファイルに含まれるビデオとオーディオの形式を確認してみる。
$ ffmpeg -i sample.flv
ffmpeg version 2.5.4 Copyright (c) 2000-2015 the FFmpeg developers

...中略...

Input #0, flv, from 'sample.flv:
  Metadata:
    starttime       : 0
    totalduration   : 1155
    totaldatarate   : 346
    bytelength      : 50005082
    canseekontime   : true
    sourcedata      : B0AFCF105HH1424147114695748
    purl            : 
    pmsg            : 
    httphostheader  : r18---sn-oguesnl7.googlevideo.com
  Duration: 00:19:14.65, start: 0.000000, bitrate: 346 kb/s
    Stream #0:0: Video: flv1, yuv420p, 426x240, 280 kb/s, 29.97 fps, 29.97 tbr, 1k tbn, 1k tbc
    Stream #0:1: Audio: mp3, 22050 Hz, stereo, s16p, 65 kb/s

...中略...

At least one output file must be specified
  • エラー表示*1されるのだけど、ファイル情報が詳細に表示されるのだ。
  • 二つのストリームオレンジ色の部分)が確認できる。
    • ストリーム=ファイル内部で区別される連続するデータの流れ。
      • Stream #0:0 には、ビデオ形式 flv1 の映像が記録されている。
      • Stream #0:1 には、オーディオ形式 mp3 の音楽が記録されている。

ストリームという概念

ストリームという概念は重要である!

  • ファイルは-iオプションで指定した順に0、1、2...と番号管理されている。
  • 同様に、ファイル中のストリームも順に0、1、2...と番号管理されている。
  • つまり、Stream #0:0とは、ファイル0番のストリーム0番という意味。
    • 一般的に、ストリーム0番にはビデオが記録される。
    • ストリーム1番以降に、一つあるいは複数のオーディオが記録される。
  • ffmpegでは、処理対象をストリーム単位で指定できる。
  • Stream #0:0は、コマンドオプション中で[0:0]のように表記できる。

変換する

  • .flvファイルを.mp4ファイルに変換する。
    • ファイルフォーマットは、出力ファイルの拡張子によって判別される。
$ ffmpeg -i sample.flv sample.mp4

変換しないで取り出す

  • .flvファイルからmp3オーディオだけ取り出す。
    • -acodec copyを指定することで、.flvファイル中のmp3をそのままコピーするのだ。(再変換なし・劣化なし)
    • -acodec copyを指定しないと、再変換する手間と時間がかかり、多少劣化すると思う。(再変換あり・劣化あり)
      • codec = コーデック = データをエンコード(符号化・圧縮化)する時の処理方法のこと。
$ ffmpeg -i sample.flv -acodec copy sample.mp3
  • ちなみに、.flvファイル中にmp3オーディオが含まれているからコピーできるのだ。
  • 含まれているのがaacオーディオだったりすると、エラーが発生してコピーできない。

  • もしaacオーディオが含まれている場合は、以下のように指定すればコピーできる。
$ ffmpeg -i sample.flv -acodec copy sample.aac

  • さらに、h264ビデオとaacオーディオが含まれているなら、劣化なしでmp4ファイルに変換できる。
    • h264とaacがmp4の要件に合っているからコピーできる。
    • 以下の3通りの書き方は、すべて同じ処理を指定している。
# ビデオストリームとオーディオストリームをコピーする
$ ffmpeg -i sample.flv -vcodec copy -acodec copy sample.mp4

# -codecオプション=-vcodecと-acodecをまとめて指定する
$ ffmpeg -i sample.flv -codec copy sample.mp4

# -cオプション=-codecの短縮形
$ ffmpeg -i sample.flv -c copy sample.mp4

指定した範囲を切り出す

  • ビデオやオーディオの好みの部分を指定して、切り出せる。

    • 再生時刻1分10秒から2分30秒まで(スタート時刻とエンド時刻を指定)
$ ffmpeg -i sample.mp3 -ss 1:10 -to 2:30 sample_110_to_230.mp3
    • 再生時刻1分10秒から、1分20秒間を切り出す。(スタート時刻と期間を指定)
$ ffmpeg -i sample.mp3 -ss 1:10 -t 1:20 sample_110_t_120.mp3

  • 上記はどちらも同じ部分を切り出し、1分20秒間のmp3オーディオとなる。

複数のファイルを連結する

  • sample_1.mp4とsample_2.mp4を連結して、sample_1_2.mp4に出力する。
    • ビデオの幅と高さ・アスペクト比は事前に合わせておく必要あり。
    • 再エンコードが発生するので多少は劣化する。
$ ffmpeg -i sample_1.mp4 -i sample_2.mp4 -filter_complex "concat=n=2:v=1:a=1" sample_1_2.mp4
  • -filter_complex オプションで連結の指定
    • concat=
      • n=連結するファイル数(デフォルト: 2)
      • v=ビデオ ストリーム数(デフォルト: 1)
      • a=オーディオ ストリーム数(デフォルト: 0)

  • concatの引数を何も指定しないと、デフォルト設定によって以下のように解釈される。
$ ffmpeg -i sample_1.mp4 -i sample_2.mp4 -filter_complex "concat"             sample_1_2.mp4
# 以下のように解釈される
$ ffmpeg -i sample_1.mp4 -i sample_2.mp4 -filter_complex "concat=n=2:v=1:a=0" sample_1_2.mp4
  • デフォルトはa=0なので、二つ目のファイルのオーディオは連結されないのだ。
    • 連結とは、一つ目のファイルにconcatの処理をした二つ目のファイルを接続する処理なので、
    • 一つ目のファイルのオーディオはそのまま残るが、二つ目のファイルのオーディオは削除される。

  • また、-iオプションのファイル数より、nの値が小さくてもコマンドは正常に終了するが、nで指定したファイル数しか連結されない。
  • 以下の例では、sample_1_2_3.mp4には、sample_1.mp4とsample_2.mp4しか連結されない。
$ ffmpeg -i sample_1.mp4 -i sample_2.mp4 -i sample_3.mp4 -filter_complex "concat=n=2:v=1:a=1" sample_1_2_3.mp4
  • すべて連結するには、n=3を指定するのだ。
$ ffmpeg -i sample_1.mp4 -i sample_2.mp4 -i sample_3.mp4 -filter_complex "concat=n=3:v=1:a=1" sample_1_2_3.mp4

よって、正しく連結できたかどうかは、動画の最後まで確認する必要がある!

フェードイン・フェードアウト

ビデオ
  • ビデオの先頭から、30フレーム使ってフェードインさせる。
$ ffmpeg -i sample.mp4 -vf "fade=in:0:30" sample_fade-in.mp4
  • ビデオの300フレームから、30フレーム使ってフェードアウトさせる。
$ ffmpeg -i sample.mp4 -vf "fade=out:300:30" sample_fade-out.mp4

  • ビデオの先頭から、1秒使ってフェードインさせる。
$ ffmpeg -i sample.mp4 -vf "fade=t=in:st=0:d=1" sample_fade-in.mp4
  • ビデオの時刻10秒から、1秒使ってフェードアウトさせる。
$ ffmpeg -i sample.mp4 -vf "fade=t=out:st=10:d=1" sample_fade-out.mp4
オーディオ
  • オーディオの先頭から、1秒使ってフェードインさせる。
$ ffmpeg -i sample.m4a -af "afade=t=in:ss=0:d=1" sample_fade-in.m4a
# あるいは
$ ffmpeg -i sample.m4a -af "afade=t=in:st=0:d=1" sample_fade-in.m4a
    • afadeのみ、ss=Nを指定できる。
    • ssの場合、Nに何を指定しても、常に先頭の指定となる。

  • オーディオの60秒から、1秒使ってフェードアウトさせる。
$ ffmpeg -i sample.m4a -af "afade=t=out:st=60:d=1" sample_fade-out.m4a
    • 仮にss=60と指定しても、いきなりフェードアウトが始まってしまう...。

フィルターチェーン・フィルターグラフ・リンクラベルによる連続技

フィルターチェーン
  • フィルターチェーンを使って、二つのフィルター(フェードイン・フェードアウト)を適用する。
    • フィルターチェーンとは、複数のフィルターをカンマ,で区切って指定する方法。
    • 一つ目のフィルター処理、その結果に対して二つ目のフィルター処理...のように適用される。
    • 以下の処理で、フェードインで始まり、フェードアウトで終わるビデオになる。
# -vf(video filters)で指定した場合
$ ffmpeg -i sample.mp4 -vf "fade=in:0:30,fade=out:300:30" sample_fade-in_fade-out.mp4
# -filter_complexで指定した場合
$ ffmpeg -i sample.mp4 -filter_complex "fade=in:0:30,fade=out:300:30" sample_fade-in_fade-out.mp4
フィルターグラフとリンクラベル
  • フィルターグラフとリンクラベルを使っても、フィルターチェーンと同様の処理を行える。
      • フィルターグラフとは、フィルターやフィルターチェーンをセミコロン;で区切って指定する方法。
  • フィルターグラフとフィルターチェーンは似ているので、よく混乱するのだけど、決定的な違いがある。
    • フィルターチェーンは、リンクラベルがなくても処理結果を次のフィルターに引き継ぐが...
$ ffmpeg -i sample.mp4 -vf "fade=in:0:30,fade=out:300:30" sample_fade-in_fade-out.mp4
    • フィルターグラフは、リンクラベルなしでは処理結果を次のフィルターに引き継がない。
$ ffmpeg -i sample.mp4 -vf "fade=in:0:30;fade=out:300:30" sample_fade-in_fade-out.mp4
...エラー発生...
Simple filtergraph 'fade=in:0:30;fade=out:300:30' does not have exactly one input and output.
Error opening filters!
    • フィルターグラフでは、必ずリンクラベルを設定しておく必要がある。
$ ffmpeg -i sample_1.mp4 -vf "fade=in:0:30[a];[a]fade=out:300:30" sample_fade-in_fade-out.mp4
      • [a]という表記がリンクラベル。(英数文字とアンダースコア_を使って任意の名前を命名する)
      • fade=in:0:30のフィルターの結果に[a]というリンクラベルを設定して、
      • [a]が指し示す結果に対してfade=out:300:30フィルターを適用している。

  • たった二つのフィルターの連続では、フィルターグラフの利用価値をあまり見出せないが...
  • フィルターグラフには、フィルターチェーンの区切りとなって、グループ化する役割がある。

  • 例えば、sample_1.mp4とsample_2.mp4を連結してから、フェードイン・フェードアウトする場合...
    • ビデオだけなら、シンプルなフィルターチェーンのみで処理できる。
$ ffmpeg -i sample_1.mp4 -i sample_2.mp4 -filter_complex "
concat=n=2:v=1:a=0, fade=in:0:30, fade=out:800:30
" sample_1_2.mp4
    • ビデオとオーディオを扱うと、concatの出力は二つのストリームになるので、必ず二つのリンクラベルが必要になる。
$ ffmpeg -i sample_1.mp4 -i sample_2.mp4 -filter_complex "
concat=n=2:v=1:a=1[v][a], [v]fade=in:0:30, fade=out:800:30
" -map [a] sample_1_2.mp4
    • -map [a]は何をしている?
      • フィルター出力がリンクラベルの指定で終了している場合、 -mapにそのリンクラベルを指定して、出力ファイルに書き込む必要がある。
      • 最後のフィルター出力でリンクラベルを指定しなければ、そのフィルター出力は自動的に出力ファイルに書き込まれる。

    • フィルターグラフを使えば、もう少しシンプルに表現できる。
      • フィルターグラフの場合、ビデオは[v]にリンクされ、リンクなしのオーディオは出力ファイルに書き込まれる。
$ ffmpeg -i sample_1.mp4 -i sample_2.mp4 -filter_complex "
concat=n=2:v=1:a=1[v]; [v]fade=in:0:30, fade=out:800:30
" sample_1_2.mp4
      • 一方、フィルターチェーンを使うと、リンクなしのオーディオは次のフィルターにリンクしてしまう。
      • だから二つのリンクを受け取ってfadeフィルターがエラーになってしまうのだ!
$ ffmpeg -i sample_1.mp4 -i sample_2.mp4 -filter_complex "
concat=n=2:v=1:a=1[v], [v]fade=in:0:30, fade=out:800:30
" sample_1_2.mp4
...エラー発生...

リンクラベルを使うなら、フィルターグラフで区切った方が幸せになれそう。


  • フィルターの順序を逆にして、sample_1.mp4をフェードイン・sample_2.mp4をフェードアウトしてから、連結することもできる。
$ ffmpeg -i sample_1.mp4 -i sample_2.mp4 -filter_complex "
[0:0]fade=in:0:30[a];
[1:0]fade=out:300:30[b];
[a][0:1][b][1:1]concat=n=2:v=1:a=1
" sample_1_2.mp4
オプション指定意味
[0:0]fade=in:0:30[a];sample_1.mp4のストリーム0番をフェードインして、リンクラベル[a]を設定する。
[1:0]fade=out:300:30[b];sample_2.mp4のストリーム0番をフェードアウトして、リンクラベル[b]を設定する。
[a][0:1][b][1:1]concat=n=2:v=1:a=1[a]・sample_1.mp4のストリーム1番・[b]・sample_2.mp4のストリーム1番を連結する

難解なffmpegのオプション指定が少しずつ見えてきた!

  • フィルターチェーン・フィルターグラフ・リンクラベルが分かってくると、フィルター処理の流れが見える。
  • そしたら後は、個々のフィルターの動作を調べれるだけで、ffmpegが何をしているのか理解できるのだ。
    • フィルターの動作をすべて覚える必要なんてない。
    • 必要になったらマニュアルを見て調べればいいのだ。

合成

  • ffmpegのフィルター指定は、まるでプログラミングコードである。
  • フィルターチェーン・フィルターグラフ・リンクラベルを理解すると、そのコードの流れが見えてくる。
  • コードの流れが見えてくると、自分が何をわからず、何を調べればいいのか、理解できるようになる。

こうなると、俄然面白くなってくる!

  • 以下をサンプル映像とさせていただき、いろいろなオプション指定を試してみた。
左右に並べる
ffmpeg -i sample_1.mp4  -i sample_2.mp4 -filter_complex "
[0:0]pad=2*iw[a];
[a][1:0]overlay=w
" overlay.mp4

f:id:zariganitosh:20150618132156p:image:w450

  • padは、ビデオの表示領域を設定する。
    • iwは、入力側のビデオの横幅(input width)
    • 2*iwを設定すると、横幅が2倍のビデオになる。
    • 拡大された領域は、背景が黒くなる。
  • overlayは、二つのビデオを重ね合わせる。
    • [a][b]overlayとした場合、ビデオ[a]にビデオ[b]を重ねる。(ビデオ[b]が上になる)
    • [a][b]overlay=x:yで、ビデオ[a]の(x,y)座標を起点にビデオ[b]を配置する。
      • w・hは、ビデオ[b]の幅・高さ解釈される。
      • W・Hは、ビデオ[a]の幅・高さと解釈される。
      • 指定なしは、0と解釈される。
  • つまり、sample_1.mp4の横幅を2倍にして、(640,0)の座標を起点に、sample_2.mp4を重ね合わせるのだ。
    • sample_1.mp4とsample_2.mp4のビデオサイズは、640x360。
上下に並べる
ffmpeg -i sample_1.mp4  -i sample_2.mp4 -filter_complex "
[0:0]pad=iw:2*ih[a];
[a][1:0]overlay=0:h
" overlay.mp4

f:id:zariganitosh:20150618132242p:image:w225

  • padとoverlayの指定を上下に配置する設定に変更してみた。
ピクチャー in ピクチャー
ffmpeg -i sample_1.mp4  -i sample_2.mp4 -filter_complex "
[1:0]scale=iw/2:ih/2[red];
[0:0][red]overlay
" -map 1:1 overlay.mp4

f:id:zariganitosh:20150618140917p:image:w300

  • scaleは、入力されたビデオサイズを拡大・縮小する。
    • scale=iw/2:ih/2は、ビデオの幅と高さを半分に縮小するのだ。
  • map 1:1は、sample_2.mp4のオーディオも合成している。
  • つまり、sample_1.mp4に、幅と高さを半分に縮小したsample_2.mp4を重ねているのだ。
    • overlayの座標指定なしなので、(0,0)を起点として、左上の1/4領域に配置される。
透過合成
ffmpeg -i sample_1.mp4  -i sample_2.mp4 -filter_complex "
[0:0]split[black1][black2];
[black1][1:0]overlay[black1red];
[black1red][black2]blend=c0_mode=average
" -map 1:1 overlay.mp4

f:id:zariganitosh:20150618135948p:image:w300

  • 重ねたビデオ同士を透過する。
      • しかし、blendフィルターをちゃんと理解できていない...。
      • c0_mode、c1_mode、c2_mode、c3_mode、all_modeの違いは?
      • averageは、何をしている?
ピクチャー in ピクチャー + 透過合成
ffmpeg -i sample_1.mp4  -i sample_2.mp4 -filter_complex "
[1:0]scale=iw/2:ih/2[red];
[0:0]split[black1][black2];
[black1][red]overlay[black1red];
[black1red][black2]blend=c0_mode=average
" -map 1:1 overlay.mp4

f:id:zariganitosh:20150618135905p:image:w300

  • 上に重なるビデオサイズを調整してから透過合成すれば、ピクチャーinピクチャーの透過合成となる。
確実にconcat
  • 実は、上記サンプル映像二つは、単純にはconcatできない...。
$ ffmpeg -i sample_1.mp4 -i sample_2.mp4 -filter_complex "concat=n=2:v=1:a=1" sample_1_2.mp4

...エラー発生...
[Parsed_concat_0 @ 0x7fabbbc00360] Input link in1:v0 parameters (size 640x360, SAR 1281:1280) do not match the corresponding output link in0:v0 parameters (640x360, SAR 1:1)
[Parsed_concat_0 @ 0x7fabbbc00360] Failed to configure output pad on Parsed_concat_0
  • 調べてみると、アスペクト比が異なっていると気付いた。
Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 640x360 [SAR 1:1 DAR 16:9], 498 kb/s, 29.97 fps, 29.97 tbr, 30k tbn, 59.94 tbc (default)

Stream #1:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 640x360 [SAR 1281:1280 DAR 427:240], 593 kb/s, 29.97 fps, 29.97 tbr, 30k tbn, 59.94 tbc (default)
  • ならばsample_2.mp4のアスペクト比をsample_1.mp4に合わせてみる。
  • その後にconcatを使えば、ちゃんと連結できた!
$ ffmpeg -i sample_2.mp4 -aspect 16:9 sample_2_16.9.mp4
$ ffmpeg -i sample_1.mp4 -i sample_2_16.9.mp4 -filter_complex "concat=n=2:v=1:a=1" sample_1_2.mp4

  • しかし、上記ではコマンドラインが2行に分かれて、余分な中間ファイルも作成してしまい、あまり嬉しくない。
  • そこで、filter_complexを使って、setdarあるいはsetsarのフィルターを追加することで、一行で連結できた。
$ ffmpeg -i sample_1.mp4 -i sample_2.mp4 -filter_complex "[0:v]setdar=16:9[0v]; [1:v]setdar=16:9[1v]; [0v][0:a][1v][1:a]concat=n=2:v=1:a=1" sample_1_2.mp4
# あるいは...
$ ffmpeg -i sample_1.mp4 -i sample_2.mp4 -filter_complex "[0:v]setsar=1:1[0v]; [1:v]setsar=1:1[1v]; [0v][0:a][1v][1:a]concat=n=2:v=1:a=1" sample_1_2.mp4
    • [0:v]・[0:a]は、ファイル0番のビデオストリーム・ファイル0番のオーディオストリームという意味のようだ。
    • sample_1.mp4の場合、[0:v]は[0:0]と同じストリーム、[0:a]は[0:1]と同じストリームを指す。

参考ページ

以下のページがたいへん参考になりました。感謝です!

*1:At least one output file must be specified(少なくとも一つは出力ファイルを指定しなければならない)

2015-03-29

優先順位の高いローカルIPアドレスを取得するコマンド

iMacのIPアドレスを確認したい。そんなの簡単、簡単。システム環境設定 >> ネットワークで確認すればいい。

上段のネットワークサービスほど優先順位が高くなる。よって、現在のIPアドレスは特に指定しない限り、10.0.1.20が利用されることになる。

f:id:zariganitosh:20150327175025p:image:w450

このようにGUIでは何の苦労もなく確認できるのだけど、コマンドを使って確認しようとすると、途端に深い悩みとなった...。


ipconfig

  • ipconfigにデバイス名(en0、en1など)を指定すると、そのデバイスに割り当てられたIPアドレスを取得できた。
$ ipconfig getifaddr en0
10.0.1.20

$ ipconfig getifaddr en1
10.0.1.102

しかし...

  • デバイス名を指定する必要があり、すべてを網羅する自信がない。
  • また、どちらの優先順位が高いのかは分からない...。

ifconfig

  • ifconfigなら、すべてのデバイスの接続状況が一覧できる。
$ ifconfig
lo0: flags=8049<UP,LOOPBACK,RUNNING,MULTICAST> mtu 16384
	options=3<RXCSUM,TXCSUM>
	inet6 ::1 prefixlen 128 
	inet 127.0.0.1 netmask 0xff000000 
	inet6 fe80::1%lo0 prefixlen 64 scopeid 0x1 
	nd6 options=1<PERFORMNUD>
gif0: flags=8010<POINTOPOINT,MULTICAST> mtu 1280
stf0: flags=0<> mtu 1280
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	options=10b<RXCSUM,TXCSUM,VLAN_HWTAGGING,AV>
	ether xx:xx:xx:xx:xx:xx 
	inet6 xxxx::xxxx:xxxx:xxxx:xxxx%en0 prefixlen 64 scopeid 0x4 
	inet 10.0.1.20 netmask 0xffffff00 broadcast 10.0.1.255
	nd6 options=1<PERFORMNUD>
	media: autoselect (1000baseT <full-duplex,flow-control>)
	status: active
en1: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
	ether xx:xx:xx:xx:xx:xx
	inet6 xxxx::xxxx:xxxx:xxxx:xxxx%en1 prefixlen 64 scopeid 0x5 
	inet 10.0.1.102 netmask 0xffffff00 broadcast 10.0.1.255
	nd6 options=1<PERFORMNUD>
	media: autoselect
	status: active
...中略...
  • リスト中の「inet 」の項目がIPアドレス(IPv4)らしいので、抜き出してみた。
$ ifconfig|grep 'inet '
	inet 127.0.0.1 netmask 0xff000000 
	inet 10.0.1.20 netmask 0xffffff00 broadcast 10.0.1.255
	inet 10.0.1.102 netmask 0xffffff00 broadcast 10.0.1.255
  • awk使って、IPアドレスのみ取り出してみた。
$ ifconfig|awk '/inet /{print $2}'
127.0.0.1
10.0.1.20
10.0.1.102

しかし...

  • どちらの優先順位が高いのかは分からない...という問題は依然残る...。

AppleScript

  • AppleScriptにはsystem infoがあり、その中にIPv4 addressというプロパティがあった。
  • そして、osascriptを使えば、コマンドラインからでもAppleScriptを実行できる。
$ osascript -e 'IPv4 address of (system info)'
10.0.1.20

しかし...

  • Wi-FiとEthernetの両方を接続した時、Wi-Fiを優先しても、常にEthernetのIPアドレスが表示されてしまう...。なぜだろう?

いろいろな方法

  • 調べてみると、多くの方がコマンドでローカルIPアドレスを取得することに苦労しているようだ。
  • こうなったら上記ページで紹介されている方法を片っ端からすべて試してみる。
  • そしてついに「優先順位の高いローカルIPアドレス」を取得することができた!
$ python -c "import socket;print([(s.connect( ('8.8.8.8', 80) ), s.getsockname()[0], s.close()) for s in [socket.socket(socket.AF_INET, socket.SOCK_DGRAM)[0][1])"
10.0.1.20
  • OSX標準で、余分なライブラリをインストールせずに、優先順位の高いローカルIPアドレスを取得する方法は、これ一つだった。

しかし...

  • 8.8.8.8という外部のDNSサーバーへの接続に依存しているところに不満が残る。
    • 8.8.8.8を指定するなら、必ずインターネットに接続している必要がある。
  • 8.8.8.8の部分はLAN内のDNSサーバー10.0.1.1などを指定してもOKなのだけど、
    • 192.168.x.xとか、172.16.x.xなどもあり得る。環境に合わせて変更するのは面倒だ。
  • 自分自身で設定したネットワーク設定なのだから、外部に頼らず、自己解決したい気がする。

networksetup

  • OSXのネットワーク環境を設定するコマンドとして、networksetupが用意されている。
  • networksetupを使えば、システム環境設定 >> ネットワークと同様の設定ができるらしい。

  • 優先度順にネットワークサービス名のリストが表示された!
$ networksetup -listallnetworkservices
An asterisk (*) denotes that a network service is disabled.
Wi-Fi
Ethernet
Thunderbolt Ethernet 2
Thunderbolt ブリッジ

  • Wi-FiのIPアドレスを取得できた!
$ networksetup -getinfo Wi-Fi
Manually Using DHCP Router Configuration
IP address: 10.0.1.102
Subnet mask: 255.255.255.0
Router: 10.0.1.1
IPv6: Automatic
IPv6 IP address: none
IPv6 Router: none
Wi-Fi ID: xx:xx:xx:xx:xx:xx

  • でもWi-Fiが切であっても、IPアドレスが表示されてしまう...。(固定IPアドレスを指定している場合)
$ networksetup -getinfo Wi-Fi
Manually Using DHCP Router Configuration
IP address: 10.0.1.102
IPv6: Automatic
IPv6 IP address: none
IPv6 Router: none
Wi-Fi ID: xx:xx:xx:xx:xx:xx

  • Subnet maskRouterが表示されていなければ、オフラインと考えてよいのだろうか?
  • ならば、オフラインを除外してネットワークサービスの順番にIPアドレスを調べればいいのだ!
$ networksetup -listallnetworkservices|sed '1d'|while read s; do networksetup -getinfo "$s"|grep -q '^Router: ' && networksetup -getinfo "$s"|awk -F': ' '/^IP address: /{print $2}'; done|head -1
10.0.1.20
  • 上記ワンライナーをもう少し分かりやすく書き直せば、以下のようなシェルスクリプトとなる。
networksetup -listallnetworkservices | sed '1d' | while read s
do 
  if networksetup -getinfo "$s" | grep -q '^Router: '; then
    networksetup -getinfo "$s" | awk -F': ' '/^IP address: /{print $2}'
  fi
done | head -1

しかし...

  • 手入力でTCP/IPを設定した場合は、オフラインであってもSubnet maskRouterが表示されてしまった...。
  • 結局、networksetup -getinfoはネットワークサービスのTCP/IPに設定された情報を表示しているに過ぎない。
    • DHCPを利用した場合はオフラインではルーターは未設定となるが、
    • 手入力ではオフラインでも入力済みのルーターアドレスが表示される。

f:id:zariganitosh:20150328173001p:image:w450

  • 上記のように設定しておくと、オフラインでもnetworksetup -getinfo Wi-Fiは以下の情報を出力してしまう...。
$ networksetup -getinfo Wi-Fi
Manual Configuration
IP address: 10.0.1.102
Subnet mask: 255.255.255.0
Router: 10.0.1.1
IPv6: Automatic
IPv6 IP address: none
IPv6 Router: none
Wi-Fi ID: b8:09:8a:b9:c8:5d

ifconfig+networksetup

  • こうなったら、ifconfigとnetworksetupの合わせ技でやってみる。
    • ifconfigでオンラインのIPアドレスを確認して、
    • networksetupでネットワークサービスの優先順位を確認するのだ。
$ networksetup -listallnetworkservices|sed '1d'|while read s; do networksetup -getinfo "$s"|grep -q '^IP address: ' && ifconfig|awk '/inet /{print $2}'|grep `networksetup -getinfo "$s"|awk -F': ' '/^IP address: /{print $2}'`; done|head -1
10.0.1.20
  • 上記ワンライナーをもう少し分かりやすく書き直せば、以下のようなシェルスクリプトとなる。
networksetup -listallnetworkservices | sed '1d' | while read s
do 
  if networksetup -getinfo "$s" | grep -q '^IP address: '; then
    i=`networksetup -getinfo "$s" | awk -F': ' '/^IP address: /{print $2}'`
    ifconfig | awk '/inet /{print $2}' | grep "$i"
  fi
done | head -1

できた!


ipconfig+netstat

  • コメントで教えて頂いた方法が素晴らしい!(nsbyさんに感謝!)
  • きっと経験豊富なネットワーク管理者はこの方法を使っているはず。
$ ipconfig getifaddr `netstat -rn -f inet | awk '/^default/{print $6;exit}'`
10.0.1.20
  • networksetupのようなOSXに依存するコマンドも使わないので、多くのUNIX環境で利用できるはず。

仕組みとしては...

  • 例えばipconfig getifaddr en0を実行すれば、en0に割り当てられたIPv4アドレスが求められる。
  • ならば、en0の部分に、優先順位の高いネットワーク インターフェース名を指定すればいいのだ。
  • それを求めるのが`netstat -rn -f inet | awk '/^default/{print $6;exit}'`の部分。
    • netstat -rn -f inetコマンドでルーティングテーブルを表示している。
      • rオプションは、ルーティングテーブルを表示する指定。
      • nオプションは、ホストやユーザーの名前解決を行わず数字のまま出力する指定。(たぶんコマンドの動作が軽くなる)
      • -f inetオプションは、IPv4のルーティングテーブルのみに限定する指定。
    • 上記出力からawk '/^default/{print $6;exit}'コマンドで、デフォルトのNetifのみ取り出している。
      • exitによって、最初の(優先順位の高い)defaultを見つけたら、その後の処理を中断している。

  • シンプルな動作の組合せなので、このワンライナーを丸暗記しなくても覚えられそう。
  • (忘れやすいので)できる限りオプション指定しないで入力してみると、以下でもOKだった。
ipconfig getifaddr `netstat -r | awk '/^default/{print $6}'`
  • xargsを利用して、以下のように表現してもOKだった。
netstat -r | awk '/^default/{print $6}' | xargs ipconfig getifaddr

2015-02-24

エスケープシーケンスを体感する

エスケープシーケンス(escape sequence)とは、直訳すれば「エスケープに続く数列(文字コード列)」といった意味合いになると思う。広い意味では、特別な文字表記(エスケープ記号)で始まる一連の文字列を連想してしまう。例えば、\nが改行を表現するのも広い意味ではエスケープシーケンスに含まれる。しかし、ここではもっと狭い意味のエスケープシーケンスを扱う。ASCII制御コードを拡張するためのエスケープシーケンスだ。

前回調べたように、ASCII制御コード(\x00〜\x20、\x7f)はテレタイプ端末で通信することを想定した古い規格になっている。その古いASCII制御コードだけでは、時代と共に進化してきたビデオ端末や端末エミュレータを満足に制御できるはずがない。新たな制御コードを追加する必要があったのだ!

ところが、新たな制御コードを追加するにも、たったの128文字しかないASCIIコード表には空きがない。ではどうしたかというと、ASCII制御コードのESC(\x1B)を利用して拡張したのだ。ESC(\x1B)で始まる複数の文字コードの連続によって、様々な制御機能を追加している。拡張されたASCII制御コードを体感してみる。

コード領域の基礎知識

  • ASCIIコードは7ビットの文字コードである。(\x00〜\x7F)
  • 表現できる文字数を増やすため8ビットの文字コードが考案された時、それぞれの領域を以下のように区別するようになった。
    • C0のC = Control(制御)のC
    • GLのG = Graphic(図形)のG

f:id:zariganitosh:20150220075855p:image:w450


  • 一方、従来の7ビットコード体系から拡張されたC1領域を使いたい需要もあった。
  • そこで、制御コード ESCを利用して、以下のように指定する仕組みとなった。
    • ASCII制御コードをControlキー@ A〜Z [ \ ] ^ _で入力する仕組みと同じように、
      • 例:^@、^G、^[
    • C1領域の制御コードは制御コード ESC@ A〜Z [ \ ] ^ _で指定できる。
      • 例:\e@、\eG、\e[
      • ESC = \e = \x1B = \033
  • つまり、8ビットコード体系の\x80\x9Fは、7ビットコード体系では\x1B40\x1B5Fまでの2バイトで表現される!

f:id:zariganitosh:20150219173713p:image:w450

ESC(\x1B)の制御コードで始まる数列なのでエスケープシーケンスと呼ばれるのだ。

C1以外の制御コード

  • さらに、ESC(\x1B)を利用することで、それに続く文字コードは何でもエスケープシーケンスと認識できる。
  • もはや、32文字のC1領域だけの表現にこだわる必要はなくなった。
  • 印字可能な94文字すべてを使って制御コードを拡張できるのだ!
ISO 2022の文字コード制御
  • 例えば、ISO 2022はC1領域以外のエスケープシーケンスを利用して必要な文字集合を切り替えている。
      • 但し、ISO 2022のエスケープシーケンスは独自に文字コードを制御したい需要でもない限り、覚える必要はなさそう。
      • ほとんどの場合、OSやエディタ・メールなどがサポートするテキスト入力環境が、良きに計らいうまく処理してくれる。
      • iconvなどの文字コード変換ライブラリを使えば、このようなエスケープシーケンスを知らなくても変換できてしまう。
      • 文字化けしてしまったメールを自動で直すコンバーターとか作りたくなったら、その時に覚えるだけで十分。

略語意味ESC
シーケンス
SI or LS0GL←G0呼び出し(ロッキングシフト)^N
SO or LS1GL←G1呼び出し(ロッキングシフト)^O
LS2GL←G2呼び出し(ロッキングシフト)\en
LS3GL←G3呼び出し(ロッキングシフト)\eo
LS1RGR←G1呼び出し(ロッキングシフト)\e~
LS2RGR←G2呼び出し(ロッキングシフト)\e}
LS3RGR←G3呼び出し(ロッキングシフト)\e
SS2G2の1文字限りの呼び出し(シングルシフト)\eN
SS3G3の1文字限りの呼び出し(シングルシフト)\eO
GZD4G0←94文字集合の指示(1バイトコード)\e(
G1D4G1←94文字集合の指示(1バイトコード)\e)
G2D4G2←94文字集合の指示(1バイトコード)\e*
G3D4G3←94文字集合の指示(1バイトコード)\e+
G1D6G1←96文字集合の指示(1バイトコード)\e-
G2D6G2←96文字集合の指示(1バイトコード)\e.
G3D6G3←96文字集合の指示(1バイトコード)\e/
GZDM4G0←94のn乗文字集合の指示(複数バイトコード)\e$(
G1DM4G1←94のn乗文字集合の指示(複数バイトコード)\e$)
G2DM4G2←94のn乗文字集合の指示(複数バイトコード)\e$*
G3DM4G3←94のn乗文字集合の指示(複数バイトコード)\e$+
G1DM6G1←96のn乗文字集合の指示(複数バイトコード)\e$-
G2DM6G2←96のn乗文字集合の指示(複数バイトコード)\e$.
G3DM6G3←96のn乗文字集合の指示(複数バイトコード)\e$/
S7C1T7ビットモード。応答に7ビットシーケンスを使う。\eSP F
S8C1T8ビットモード。応答に8ビットシーケンスを使う。\eSP G

カーソル・表示・キー入力制御

略語意味ESC
シーケンス
DECSC保存する(カーソル位置とテキスト属性)\e7
DECRC復元する(カーソル位置とテキスト属性)\e8
DECKPAMアプリケーションキーパッドモードに設定する\e=
DECKPNM数値キーパッドモードに設定する\e>
RIS端末リセット(画面クリア・カーソル左上・タブリセット)\ec
DECDHLカーソル行の表示倍率を設定(縦倍・横倍して上側を表示)\e#3
DECDHLカーソル行の表示倍率を設定(縦倍・横倍して下側を表示)\e#4
DECSWLカーソル行の表示倍率を設定(標準表示)\e#5
DECDWLカーソル行の表示倍率を設定(横倍表示)\e#6
DECALN画面を'E'で埋める\e#8

C1領域の制御コード

  • C1領域の制御コードには、以下のような意味がある。

略語由来語句意味8ビットの
16進数コード
ESC
シーケンス
PADPadding Character \x80\e@
HOPHigh Octet Preset \x81\eA
BPHBreak Permitted Here分割許可\x82\eB
NBHNo Break Here分割禁止\x83\eC
INDIndexカーソルを次の行へ移動\x84\eD
NELNExt Lineカーソルを次の行の先頭に移動\x85\eE
SSAStart of Selected Area \x86\eF
ESAEnd of Selected Area \x87\eG
HTSHorizontal Tabulation Set現在のカーソル位置に水平タブストップを設定する\x88\eH
HTJHorizontal Tabulation with Justification \x89\eI
VTSVertical Tabulation Set現在のカーソル位置に垂直タブストップを設定する\x8A\eJ
PLDPartial Line Down下付き文字にする\x8B\eK
PLUPartial Line Up上付き文字にする\x8C\eL
RIReverse Indexカーソルを前の行へ移動\x8D\eM
SS2Single Shift 2GL または GR へ G2 を次の一文字に限り呼び出す\x8E\eN
SS3Single Shift 3GL または GR へ G3 を次の一文字に限り呼び出す\x8F\eO
DCSDevice Control StringDCSシーケンスを開始、STで終了\x90\eP
PU1Private Use 1 \x91\eQ
PU2Private Use 2 \x92\eR
STSSet Transmit State \x93\eS
CCHCancel Character \x94\eT
MWMessage Waiting \x95\eU
SPAStart of Protected Area \x96\eV
EPAEnd of Protected Area \x97\eW
SOSStart Of StringSOSシーケンスを開始、STで終了\x98\eX
SGCISingle Graphic Character Introducer \x99\eY
SCISingle Character Introducer \x9A\eZ
CSIControl Sequence IntroducerCSIシーケンスを開始\x9B\e[
STString TerminatorDCS, SOS, OSC, PM, APC シーケンスの終端\x9C\e\
OSCOperating System CommandOSCシーケンスを開始、STで終了\x9D\e]
PMPrivacy MessagePMシーケンスを開始、STで終了\x9E\e^
APCApplication Program CommandAPCシーケンスを開始、STで終了\x9F\e_

シーケンスの拡張

  • C1領域の制御コードの中でもシーケンス関連のコードは、それに続く複数のコードを引数にして、さらに機能を拡張する。
DCS = \x1B50
SOS = \x1B58
CSI = \x1B5B
OSC = \x1B5D
PM  = \x1B5E
APC = \x1B5F
  • ESC(\x1B)が制御コードを拡張し、続く2文字目のコードでエスケープシーケンスをさらに拡張している。

CSI(Control Sequence Introducer)

  • 中でも画面出力をコントロールするCSIシーケンス(\e[ = \x1B5B)は重要である。
  • CSIシーケンスを使えば、カーソル位置や文字色・背景色・明るさ・下線・点滅・反転などのテキスト属性を自由に設定できる。

  • CSIシーケンスは、\e[に続けて引数終了文字を指定する。
    • 終了文字が動作を決定する。
    • 引数でその動作の詳細を指定する。

例:

CSI引数終了文字意味
\e[2A2行上にカーソルを移動する
\e[ A1行上にカーソルを移動する(デフォルト値 = 1が利用された)
\e[10;8H10行8列へカーソルを移動する( ; で区切って二つの引数を指定した)

    • 引数は10進数で指定する。
    • 引数は ; で区切って複数指定できる。(最大16個)
    • 引数を省略するとデフォルト値が利用される。
      • n行m列などの位置情報を意味する引数のデフォルト値 = 1
      • 機能を選択する引数のデフォルト値 = 0

  • すべては網羅できないが、よく使いそうなCSIシーケンスを調べてみた。
略語意味ESC
シーケンス
CUUカーソル移動(n行上へ)\e[nA
CUDカーソル移動(n行下へ)\e[nB
CUFカーソル移動(n文字右へ)\e[nC
CUBカーソル移動(n文字左へ)\e[nD
CNLカーソル移動(n行下の行頭へ)\e[nE
CPLカーソル移動(n行上の行頭へ)\e[nF
CHAカーソル移動(n列へ)\e[nG
CUPカーソル移動(nm列へ)\e[n;mH
CHTカーソル移動(カーソル行のn個次のタブ位置へ)\e[nI
CBTカーソル移動(カーソル行のn個前のタブ位置へ)\e[nZ
EDテキストクリア(カーソル位置から画面末尾まで)\e[J
EDテキストクリア(カーソル位置から画面先頭まで)\e[1J
EDテキストクリア(画面全体)\e[2J
ELテキストクリア(カーソル位置から行末まで)\e[K
ELテキストクリア(カーソル位置から行頭まで)\e[1K
ELテキストクリア(1行全体)\e[2K
ECHテキストクリア(カーソル位置右側をn文字、SPに置き換え)\e[nX
DCHテキスト削除(カーソル位置右側をn文字、左にスライド)\e[nP
ILn行挿入\e[nL
DLn行削除\e[nM
TBCタブ削除(カーソル位置のタブ)\e[g
TBCタブ削除(すべてのタブ)\e[3g
DECSTBMスクロール範囲を設定(画面全体)\e[r
DECSTBMスクロール範囲を設定(n行からm行まで)\e[n;mr
SUスクロールする(n行上へ)\e[nS
SDスクロールする(n行下へ)\e[nT
REP直前の文字をn回繰り返す\e[nb
DSR端末の状態を返す(例:^0n = 正常)\e[5n
DSRカーソル位置を返す(例:^54;1R = 54行1列)\e[6n
DECTCEM拡張オプションの設定(カーソルを隠す)\e[?25l
DECTCEM拡張オプションの設定(カーソルを表示)\e[?25h
SGRテキスト属性の設定\e[n1;n2;...m

SGR(Select Graphics Rendition)

  • さらに、テキスト属性の設定は、CSIシーケンスのSGRという設定によって行う仕組みである。
  • SGR(\e[m = \x1B5B6D)に複数の引数を ; で区切って与える(\e[n1;n2;...m)ことで、文字色・背景色・明るさ・下線・点滅・反転などを自由に指定できる。

例:

CSI引数終了文字SGR意味
\e[1;31;46m太字・文字色=赤・背景色=水色に設定する(\e[mなどでリセットするまで効果は続く)
\e[4;38;5;150;48;5;200m下線・文字色=150・背景色=200に設定する

太字・低輝度・下線・点滅・反転・非表示の引数

番号意味番号意味
0すべての属性を解除
1太字21
2低輝度22太字解除・低輝度解除
3 23
4下線24下線解除
5点滅25点滅解除
6 26
7反転27反転解除
8非表示28非表示解除

色の引数

番号意味番号意味番号意味番号意味
30文字色を黒40背景色を黒90文字色を明るい黒(灰色)100背景色を明るい黒(灰色)
31文字色を赤41背景色を赤91文字色を明るい赤101背景色を明るい赤
32文字色を緑42背景色を緑92文字色を明るい緑102背景色を明るい緑
33文字色を黄色43背景色を黄色93文字色を明るい黄色103背景色を明るい黄色
34文字色を青44背景色を青94文字色を明るい青104背景色を明るい青
35文字色を赤紫45背景色を赤紫95文字色を明るい赤紫105背景色を明るい赤紫
36文字色を水色46背景色を水色96文字色を明るい水色106背景色を明るい水色
37文字色を白47背景色を白97文字色を明るい白107背景色を明るい白
38;5;n拡張文字色をn(0〜255)48;5;n拡張背景色をn(0〜255)
39文字色を標準49背景色を標準

  • 一桁目の数値は、色コードを表現している。
  • RGBを3ビットで表現すると、0〜7の数値に変換できるのだ。

BGR番号Color
0000Black
0011Red
0102Green
0113黄色Yellow
1004Blue
1015赤紫Magenta
1106水色Cyan
1117White

体感してみる

以上のエスケープシーケンスを理解して、いよいよ体感してみるのだ!

環境と方法
  • iMac 5K OSX 10.10.2
  • ターミナル.app 2.5.1(343.6)
$ bash --version
GNU bash, バージョン 4.3.33(1)-release (x86_64-apple-darwin14.1.0)
Copyright (C) 2013 Free Software Foundation, Inc.
ライセンス GPLv3+: GNU GPL バージョン 3 またはそれ以降 <http://gnu.org/licenses/gpl.html>

  • ターミナル.app上で、実際にエスケープシーケンスを含む文字列を表示してみるのが一番分かり易いはず。
  • その際、printfコマンドを使った方が、無用な悩みを排除できる。
  • ちなみに、echoコマンドを使う場合は、以下の点に注意する。
    • 通常bashでは、bash内部コマンドのechoが実行される。
    • エスケープシーケンスを書く時は、-eオプションが必要になる。
      • と同時に、-nオプションで行末の改行もキャンセルしたくなることが多かった。
    • それでも、\eをエスケープ制御コードと解釈してくれなかった。(なぜだ?)\033を使う必要がある。
256色を体感する

上記SRGの指定に従えば、ターミナル.appは256色を発色する性能を持っている!

  • すべての色を表示してみた。
for c in {0..255}; do printf "\033[48;5;${c}m%8d\033[m" $c; done; echo

f:id:zariganitosh:20150224141919p:image:w450


  • 実は、0〜255の色コードは3つに分類できる。

0〜 15の基本色「黒・赤・緑・黄色・青・赤紫・水色・白」の暗いセットと明るいセットの16色
16〜231の中間色RGBの中間色6段階の輝度を混ぜ合わせた216色
232〜255のモノクロ黒から白まで輝度を16段階に区切ったモノクロ16色

for c in {0..7}; do printf "\033[48;5;${c}m%6d\033[m" $c; done; echo; for c in {8..15}; do printf "\033[48;5;${c}m%6d\033[m" $c; done; echo
for c in {16..231}; do printf "\033[48;5;${c}m%6d\033[m" $c; done; echo
for c in {232..255}; do printf "\033[48;5;${c}m%6d\033[m" $c; done; echo

f:id:zariganitosh:20150224142017p:image:w450


  • さらに中間色について、数値優先の並べ方をやめて、色の変化優先の並べ方にしてみる。
ruby -e '
(0..4).each{|r|
    g=5;(0..5).each{|b|printf("\e[48;5;#{      r*36 +      g*6 + b + 16}m%11d \e[m",       r*36 +      g*6 + b + 16)};
puts};
(0..5).each{|g|
    r=5;(0..5).each{|b|printf("\e[48;5;#{      r*36 + 30 - g*6 + b + 16}m%11d \e[m",       r*36 + 30 - g*6 + b + 16)};
    b=5;(1..5).each{|r|printf("\e[48;5;#{180 - r*36 + 30 - g*6 + b + 16}m%11d \e[m", 180 - r*36 + 30 - g*6 + b + 16)};
puts}'

f:id:zariganitosh:20150224142056p:image:w450


これで速やかに、好みの色コードを選択できるようになった!

太字・低輝度・下線・点滅・反転・非表示を体感する
  • リセット(\e[m)しなければ、直前のテキスト属性も引き継いで反映される。
for c in 0 30 31 32 33 34 35 36 37
do
  printf "\e[${c}mNormal  \e[${c};1mBold  \e[${c};2mDim  \e[${c};4mUnderLine  \e[${c};5mBlink  \e[${c};7mReverse\e[m  \e[${c};8mHide  \e[m<--Hide\n"
done

f:id:zariganitosh:20150224143821p:image:w450


  • リセット(\e[m)することで、設定したテキスト属性のみ反映される。
for c in 0 30 31 32 33 34 35 36 37
do
  printf "\e[${c}mNormal  \e[${c};1mBold\e[m  \e[${c};2mDim\e[m  \e[${c};4mUnderLine\e[m  \e[${c};5mBlink\e[m  \e[${c};7mReverse\e[m  \e[${c};8mHide  \e[m<--Hide\n"
done

f:id:zariganitosh:20150224144000p:image:w450

クルクル回す
  • CSIシーケンスのカーソル移動を利用して、| を回転させてみた。
for i in `seq 1 10`
do
printf "\e[D|" ; sleep 0.2;
printf "\e[D/" ; sleep 0.2;
printf "\e[D-" ; sleep 0.2;
printf "\e[D\\"; sleep 0.2;
done
  • もっとも、| を回転させるだけなら、ASCII制御コードのバックスペースでも実現できるのだけど...。
for i in `seq 1 10`
do
printf "\b|" ; sleep 0.2;
printf "\b/" ; sleep 0.2;
printf "\b-" ; sleep 0.2;
printf "\b\\"; sleep 0.2;
done
プログレスバーを作る
  • でもプログレスバーを作ろうとすると、バックスペースだけではちょっとキツイ...。
  • カーソル位置の保存と復元、繰り返しのエスケープシーケンスを利用して作ってみる。
printf "\e7|-\e[-98b|"; for i in {0..100}; do printf "\e8|\e[${i}b"; sleep 0.05; done; echo
||||||||||||||||||||||||||||||||||||||||||----------------------------------------------------------|
端末リセット
printf "\ec"
    • いちばん下までスクロールして画面に表示されているテキストのみクリアされた。
    • 画面に表示されていないスクロール領域のテキストは残った。
    • カーソルは画面左上に移動された。
    • タブは8文字ごとにリセットされた。
縦倍・横倍表示
printf "\e#3ABCDEFG\n"
printf "\e#4ABCDEFG\n"
printf "\e#3ABCDEFG\n"; printf "\e#4ABCDEFG\n"
printf "\e#5ABCDEFG\n"
printf "\e#6ABCDEFG\n"

f:id:zariganitosh:20150224155226p:image:w450

    • 縦横倍にする時は、上側と下側に分けて2行で表示する仕組みのようだ。
Eで埋める
printf "\e#8"

f:id:zariganitosh:20150224155204p:image:w450

    • 何じゃこれは?各行のフォーマットの状態でも調べるためか?

参考ページ

  • 以下のページがたいへん参考になりました。感謝です!

ISO 2022について


ANSIエスケープシーケンス


VT-100


xterm


エスケープシーケンスの詳細


Linux console_codesについて

2015-02-09

ASCIIコードの秘密

本当はエスケープシーケンスのことを調べていたのだが、その前にASCIIコードについて調べることになってしまった...。文字コードの基本として知っているつもりだったASCIIコードについて、あらためて見直してみると、実は本当の意味をよく分かっていなかったことに気づいた。

ASCIIコード表

  • ASCIIコードは、7ビット(2進数7桁)の文字コードであり、全部で128のコードが定義されている。
  • 最も基本的な文字コードであり、その他多くの文字コードはこのASCIIコードと互換性を維持している。
00 10 20 30 40 50 60 70 
00NULDLESP0@P`p
01SOHDC1!1AQaq
02STXDC2"2BRbr
03ETXDC3#3CScs
04EOTDC4$4DTdt
05ENQNAK%5EUeu
06ACKSYN&6FVfv
07BELETB'7GWgw
08BSCAN(8HXhx
09HTEM)9IYiy
0ALFSUB *:JZjz
0BVTESC+;K[k{
0CFFFS,<L\l|
0DCRGS-=M]m}
0ESORS.>N^n~
0FSIUS/?O_oDEL
  • ASCIIコードはその性質から二つに分類できる。目に見える文字のコードと見えない文字のコード。
    • 見える文字のコードとは、い項目のコードである。(英数記号などの文字そのもの、印字可能文字)
    • 見えない文字のコードとは、オレンジの項目のコードである。(接続している機器を制御する役割がある)
      • SP(スペース)については、ASCIIコード制定当初は制御コードの扱いだったが、現在は見える文字(印字可能文字)の扱い。

制御コード

  • では、見えない文字のコードは何を意味するのだろうか?
  • 見えない文字のコードは、制御コードと呼ばれている。
  • それらの制御コードだけ抜き出して、その意味を調べてみた。
略語由来語句意味10進数コード16進数コード8進数コードcontrol
コード
\エスケープ
コード
NULNull文字無し0\x00\000^@\0
SOHStart Of Headingヘッダ開始1\x01\001^A
STXStart Of Textテキスト開始2\x02\002^B
ETXEnd Of Textテキスト終了3\x03\003^C
EOTEnd Of Transmission伝送終了4\x04\004^D
ENQEnquery問い合わせ5\x05\005^E
ACKAcknowledgement肯定応答6\x06\006^F
BELBell警告音を鳴らす7\x07\007^G\a
BSBack Space一文字後退8\x08\010^H\b
HTHorizontal Tabulation水平タブ9\x09\011^I\t
LFLine Feed / New Line改行10\x0A\012^J\n
VTVertical Tabulation垂直タブ11\x0B\013^K\v
FFForm Feed / New Page改ページ12\x0C\014^L\f
CRCarriage Return行頭復帰13\x0D\015^M\r
SOShift Outシフトアウト / ISO2022:GLへG1を呼び出す14\x0E\016^N
SIShift Inシフトイン / ISO2022:GLへG0を呼び出す15\x0F\017^O
DLEData Link Escapeデータリンク拡張(バイナリ通信開始)16\x10\020^P
DC1Device Control 1装置制御1(XON)17\x11\021^Q
DC2Device Control 2装置制御218\x12\022^R
DC3Device Control 3装置制御3(XOFF)19\x13\023^S
DC4Device Control 4装置制御420\x14\024^T
NAKNegative Acknowledgement否定応答21\x15\025^U
SYNSynchronous idle同期22\x16\026^V
ETBEnd of Transmission Block伝送ブロック終了23\x17\027^W
CANCancel取り消し24\x18\030^X
EMEnd of Medium記録媒体終端25\x19\031^Y
SUBSubstitute / End Of File文字置換 / ファイル終端26\x1A\032^Z
ESCEscapeエスケープシーケンス開始27\x1B\033^[\e
FSFile Separatorファイル区切り28\x1C\034^\
GSGroup Separatorグループ区切り29\x1D\035^]
RSRecord Separatorレコード区切り30\x1E\036^^
USUnit Separatorユニット区切り31\x1F\037^_
SPSpace空白文字32\x20\040
DELDelete削除マーカー127\x7F\0177^?
  • 制御コードの呼称は、便宜的に2文字か3文字の略語で表現されることが多い。
  • その由来となる語句から、なんとなくその意味は想像できるのだけど、
  • 実際にどんな制御しているのか真剣に考え始めると、ちゃんと理解できているものはほとんど無いことに気付く。

ASCIIコードが想定していた制御機器

  • ASCIIコードが制定されたのは1963年である。(ASA X3.4)
  • 当初は大文字しか登録されていなかったり、制御コードの体系も現在とは違っていた。

00 10 20 30 40 50 60 70 
00NULDC0SP0@P
01SOMDC1!1AQ
02EOADC2"2BR
03EOMDC3#3CS
04EOTDC4$4DT
05WRUERR%5EU
06RUSYN&6FV
07BELLEM'7GW
08FE0S0(8HX
09HTS1)9IY
0ALFS2 *:JZ
0BVTS3+;K[
0CFFS4,<L\ ACK
0DCRS5-=M]
0ESOS6.>N ESC
0FSIS7/?O DEL

よって、ASCII制御コードをちゃんと理解するには、テレタイプやタイプライターがどのように動作するのか想像すれば良い。

ASCII制御コードの意味

  • ASCII制御コードの詳細について、素晴らしい解説を見つけた。
  • 自分の知識ではこれ以上わかりやすい説明はできそうにないので、ここにそのまま引用させて頂くことにした。

以下は小林龍生、安岡孝一、戸村哲、三上喜貴編「bit別冊─インターネット時代の文字コード」(共立出版)から引用した。ただし、一部修正してある。

  • SPSpace(空白)

    空白を表す制御文字である。タイプライターやテレタイプでは空白を「打つ」のではなく、単に印字ヘッドを右に移動させるだけの動作であることから、制御文字として定義されている。なお、SPは単に空白を表しているだけではなく、単語と単語を隔てているものだとASCIIでは考えている。この点についてはFSとGSとRSとUSの項を参照されたい。

  • BSBack Space(1文字後退)

    SPの逆の動作を表す制御文字である。タイプライターやテレタイプでは印字ヘッドを左に移動させる制御文字であり、アクセント記号の合成やアンダーラインに用いられることが想定されていた。すなわち、タイプライターを打つときのように、アクセント記号を打ってからBSで1文字戻して、さらに必要な文字を打てばよいと考えられていたのである。アクセント記号の使い方として想定されていたものの例を図1に示す。

    f:id:zariganitosh:20150129160523p:image:w450

    図1.アクセント合成とアンダーライン

  • CRとLFCarriage Return(復帰)、Line Feed(改行)

    CRは、印字ヘッドを行の最初に戻すための制御文字である。LFは、紙を1行分送るための制御文字である。タイプライターやテレタイプにおいては、ピンチローラーを右端まで動かす(印字ヘッドが行の最初に来るようにする)動作と行送りの動作とが独立に行えるようになっているので、それぞれに制御文字が用意されているのである。

  • HTHorizontal Tabulation(水平タブ)

    印字ヘッドをタブマージンと呼ばれるところまで右に移動する制御文字である。タイプライターやテレタイプにおいては、通常8文字ごとにタブマージンが設定されていて、そこまで印字ヘッドを移動させるためのタブキーが準備されていた。このタブキーと同じ動作をさせるための制御文字である。

  • VTVertical Tabulation(垂直タブ)

    垂直タブマージンと呼ばれるところまで行送りを行う制御文字である。水平方向のHTに対応する垂直方向の移動を想定していたようだが、実際にはあまり使用されなかったようである。

  • FFForm Feed(改ページ)

    紙を1枚分送ってしまって、次の紙の最初の行に印字ヘッドを合わせる制御文字である。

  • BELBell(ベル)

    ベルを鳴らすための制御文字である。テレタイプなどのタイプライター型の端末では、端末の近くの人間を呼ぶ際に非常に重宝したようである。

  • SOとSIShift Out(シフトアウト)、Shift In(シフトイン)

    印字フォントの切り換えを意図して準備された制御文字である。すなわち、2つの印字ボールが搭載できるようなテレタイプにおいて、SOで特殊用途の印字ボールに切り換えて、SIで通常の印字ボールに戻すことを想定していたものである。これによって、たとえば強調文字への切り換えや、あるいはまったく別の記号印字ボールなどを使用できるように考えられていたものである。ただし、SOとSIの意味は、ISO 2022の登場によって変更されてしまった。

  • NULとDELNull(空文字)、Delete(削除)

    NULとDELは同一の意味を持つ制御文字である。これらの制御文字がデータ中に入っていても、データの内容には何の影響も与えず、無視してよいということになっている。では、このような無視してよい制御文字が2種類もあることにどのような意味があるのだろうか。紙テープ上のデータを例に考えてみよう。

    f:id:zariganitosh:20150129160633p:image:w450

    図2.紙テープ上の「This is pen.」

    図2は「This is pen.」という文字列を紙テープ上に開けたものである。2進数で左から順に11010100、11101000、01101001、11110011、10100000、01101001、11110011、10100000、11110000、01100101、11101110、00101110と並んでおり、一番上の偶数パリティビットを除けば16進数で54、68、69、73、20、69、73、20、70、65、6E、2Eと並んでいることがわかる。ところが、この紙テープ上に記されているデータはこれだけではない。紙テープという媒体(25.4mm幅のテープに最初から繰り出し孔が開けられている)の性質上、上記のデータの前後に16進数で00がずらっと並んでいるように見えるのである。すなわち、この紙テープをテープリーダーで読み込ませると、データの前後に00が読み込まれてしまうことになるのである。そこで、ASCIIでは00を無視してよい制御文字NULと定義しているのである。これによって、紙テープ上にいくらNULがあったとしても、データとしては「This is pen.」の部分だけが有効となるので、特に問題にはならないわけである。

    f:id:zariganitosh:20150129160632p:image:w450

    図3.紙テープ上の「This is a pen.」

    次に、図2の紙テープ上の「This is pen.」というデータを「This is a pen.」に書き換えることを考えてみよう。図3が書き換えた結果である。すなわち、元の紙テープの「pen.」の部分の孔を全て開けてしまって、その後に「a pen.」というデータを追加したわけである。孔を全て開けた部分には、実際には16進数で7Fが並んでいることになる。ここで、図3の紙テープが「This is a pen.」というデータだとみなされるためには、孔を全て開けた文字、すなわち7Fを、NULと同様に無視してよいことにしなければならない。そこで、ASCIIでは7Fも無視してよい制御文字DELと定義しているのである。

    NULとDELの両方を無視してよい制御文字としている理由は、実は他にもある。紙テープのように各ビットの0→1の操作が不可逆である媒体においては、データは最初全てNULで埋まっていて、誤ったデータの削除にはDELが用いられる。ところが、もし1→0の操作が不可逆であるような媒体があったなら、そのような媒体においてはデータは最初全てDELで埋まっていて、誤ったデータの削除にはNULが用いられるだろう。そこまでを考慮した上で、ASCIIではNULとDELの両方を無視してよい制御文字としているのである。

  • EMEnd of Medium(媒体終端)

    紙テープなどの媒体の終端を表すための制御文字である。EMを用いることで、テープリーダーなどのデバイスに媒体の終わりを知らせることができ、テープの交換要求などが出せることになるのである。なお、EMは本当の媒体の終端に入っている必要はなく、実際、紙テープにおいてはEMの後にNULが続くのはやむを得ない。

  • SOHとSTXとETXとEOTStart of Heading(ヘッダ開始)、Start of Text(テキスト開始)、End of Text(テキスト終了)、End of Transmission(伝送終了)

    伝送路上の通信を制御するための制御文字である。SOHは通信の開始を意味し、EOTは通信の終了を意味する。通信中には1つあるいはそれ以上のテキストが含まれており、各テキストの最初にはSTXを、最後にはETXを用いる。通信中のテキスト以外の部分にはテキストに付帯するデータを送ることが可能になっており、特にSOHから最初のSTXまでの間のデータはヘッディング、最後のETXからEOTまでの間のデータはトレーラーと呼ばれる。

  • ETBEnd of Transmission Block(ブロック転送終了)

    伝送路の都合で、1つの通信を複数に分割しなければならなくなった場合に、各分割の末尾を表す制御文字である。

  • ACKとNAKAcknowledgement(肯定応答)、Negative Acknowledgement(否定応答)

    送られてきた通信データに対し、正しく受け取ることができたかどうかを答えるための制御文字である。正しく受け取れた場合はACKを、どうも正しく受け取れなかった場合にはNAKを答えることになっている。

  • ENQEnquiry(問い合わせ)

    伝送路が接続されたときに、最初に送る「あんた誰?」を表すための制御文字である。

  • SYNSynchronous Idle(同期信号)

    伝送路がシリアル伝送路であるときに、伝送信号を同期させるための制御文字である。0010110というビットパターンを持つことから、ストップビット数やパリティの有無にかかわらず、正しく同期が取れるようになっている。

  • CANCancel(キャンセル)

    それまでに送ったデータをキャンセルするための制御文字である。どこまで遡ってキャンセルするかは、通信の当事者同士で事前に決めておかなければいけない。

  • SUBSubstitute Character(置換)

    送られてきた文字に誤りがあることが発見された場合、その文字の代わりにメモリなどを埋めておくための制御文字である。実際にはテープリーダーがパリティエラーを発見した際などに、エラーのあった文字の代わりにSUBを送る、というような使われ方をしたようである。

  • FSとGSとRSとUSFile Separator(ファイルセパレータ)、Group Separator(グループセパレータ)、Record Separator(レコードセパレータ)、Unit Separator(ユニットセパレータ)

    ASCIIでは、単語の集まりがユニットであり、ユニットの集まりがレコードであり、レコードの集まりがグループであり、グループの集まりがファイルであり、ファイルの集まりがテキストである、という情報階層が想定されていた。この階層において、FSはファイルの区切りを表す制御文字であり、GSはグループの区切りを表す制御文字であり、RSはレコードの区切りを表す制御文字であり、USはユニットの区切りを表す制御文字であり、SPは単語の区切りを表す制御文字である。なお、これらの制御文字は1C〜20に連続して配置されている。

  • DC1〜DC4Device Control 1(装置制御1、XON)、Device Control 2(装置制御2)、Device Control 3(装置制御3、XOFF)、Device Control 4(装置制御4)

    補助装置の起動・停止を行うための制御文字である。DC1とDC2は補助装置の起動を、DC3とDC4は補助装置の停止を行うものとされていたが、詳細は通信の当事者同士で事前に決めておかなければいけない。

  • ESCとDLEEscape(エスケープ)、Data Link Escape(データリンク拡張)

    ESCは制御機能の追加に用いる制御文字である。ESCに続く何文字かで、新たな制御機能を表すことになっている。DLEは、ESCと同じだが、主に伝送機能の追加に用いることになっていた。ただし、ESCの実際の使用方法は、ISO 2022の登場によって細かく規定されてしまった。

@nifty:@homepage:エラー

上記引用を元に、理解したこととか、想像したことなど...

印字位置のコントロール

  • タイプライターはローラーに巻き付けた紙に対して活字を打ち付けて印字する。
  • 活字が打刻される位置は常に同じ。固定されている。
  • 紙が巻き付けられたローラーを上下に回転、左右に移動することで印字する位置を調節しているのだ。
    • 紙が巻き付けられたローラー = キャリッジとか、プラテンと呼ばれている。
    • より正確には、ローラー単体をプラテン、ローラーを含む左右に可動するユニット全体をキャリッジと呼ぶらしい。
  • よって、印字位置を調整する制御コードは、キャリッジの位置を制御しているのだ。
  • 印刷の最初はキャリッジを右端にセットしておく。(紙の印字位置としては左端にセットされることになる)
  • 一文字打刻するごとに、キャリッジは1文字分左に移動する。(紙の印字位置としては1文字右に移動することになる)
  • 制御文字が入力されると、キャリッジは以下のように制御される。
略語由来語句意味10進数
コード
16進数
コード
8進数
コード
control
コード
\エスケープ
コード
SPSpace空白文字 (キャリッジを1文字分左へ移動させる)32\x20\040
BSBack Space一文字後退(キャリッジを1文字分右へ移動させる)8\x08\010^H\b
CRCarriage Return行頭復帰 (キャリッジを右端に戻す)13\x0D\015^M\r
LFLine Feed / New Line改行   (キャリッジを1行分上方向へ回転させる)10\x0A\012^J\n
FFForm Feed / New Page改ページ (キャリッジを回転させて、1ページすべて送る)12\x0C\014^L\f
HTHorizontal Tabulation水平タブ (キャリッジを行の途中に設定したタブ位置まで移動させる)9\x09\011^I\t
VTVertical Tabulation垂直タブ (キャリッジを設定した行数上方向へ回転させる)11\x0B\013^K\v
  • キャリッジの動きに注目してみると、制御コード本来の意味が見えてきた!
  • SpaceやBack Spaceはキャリッジを1文字分左右に移動するだけ。
    • 何も印字していない部分を移動すれば、そこにスペースが生まれる。
    • 既に印字してある部分を移動するなら、それはカーソル移動キーのような動きとなる。
  • Line FeedやForm Feedしても必要な行数が送られるだけであり、キャリッジの左右の位置は変化しない。
  • 次の行頭に移動させるには、上記に加えてCarriage Returnしておく必要があったのだ。*1
  • タイプライターは1文字ずつ印字して、一旦印字した文字は削除できないという物理的制約から上記のような制御を行うしかなかったのである。

  • ところが、ASCIIコードを採用したテレタイプがコンピューターに接続されるようになって事情が変わってきた。
  • コンピューターに接続されたテレタイプで入力した文字は、コンピューターのメモリに保存されるようになったのだ。
    • また、テレタイプ自身も入力した文字をバッファメモリに一旦保持するようになった。
  • メモリは紙と違って、書き換え・挿入・削除が自由自在に何度でもできる。
  • すると"helloworld"と書いてしまっても、helloとworldの間でスペースキーを押して"hello world"に修正可能な仕様が当然となる。
  • 今までキャリッジを移動する制御コードの意味しかなかったSpaceが、空白" "という視覚的な間隔を生み出す文字となったのだ!

  • 同様に、もはやキャリッジの存在しないメモリ上でCarriage Returnする必要もなくなった。
  • 仮にメモリ上で旧来のタイプライターのLine Feedに相当する動作をさせるなら、
  • 行末でLine Feedした瞬間に、次の行をNullまたはSpaceで満たす必要が生じてしまう...。
    • メモリは1次元配列に情報を保持する記録メディア。
    • 最初から2次元のテキスト領域を持つ紙と違って、何も書き込まれなければテキストを保存する領域も存在しない。
    • Line Feedして前行と同じ位置をキープするならば、行頭からカーソル位置までに何らかの情報が必要になるのだ。
    • その情報として、何もないという意味のNullか、視覚的な間隔であるSpaceを使うのが妥当と思われる。
  • Line Feedの動作を忠実に再現しようとすると、メモリ領域も浪費するし、無駄なメモリアクセスも増やしてしまうのだ。

それはとても不合理なことに思えてくる。

  • そこでUNIX系のOSでは、Line FeedにCarriage Returnの役割も含めてしまった。
    • Line Feed = New Lineと解釈して、カーソルを次の行の先頭に移動する制御コードとしてしまった。
    • メモリ領域で改行するということは、新しく1行追加するという意味合いが強い。だからNew Line。
  • 一方、Windows系のOSでは、Carriage Return・Line Feedの順に並んでいる時のみ改行する仕様となった。
    • Line Feed単独の時は、何もしない。

  • UNIXにおいて、Line FeedはNew Lineになってしまったが、Form Feedは従来の動作を再現しようとしているように思える。
  • たとえば、ターミナル.appでForm Feedを使うと、次の行に移動するが、前の行のカーソル位置を保持している。
    • 本来、Form Feedは用紙を1ページ分進める制御コードなのだが、
    • ターミナル.appにはページ概念のない連続したスクロールエリアしかないので、1行進める仕様なのかもしれない。
$ echo -e "hello\fworld"
hello
     world
  • echoコマンド自体はForm Feedの文字コードを出力しているのだけど、
$ echo -e "hello\fworld" | xxd
0000000: 6865 6c6c 6f0c 776f 726c 640a            hello.world.
  • 画面に出力されたhello worldをコピーして、文字コードを確認してみると、
hello
     world
$ pbpaste | xxd
0000000: 6865 6c6c 6f0a 2020 2020 2077 6f72 6c64  hello.     world
0000010: 0a                                       .

それは改行とスペースの文字コードに変換されている!

  • Form Feedをどのように扱うかはアプリケーションによって変わってくるかもしれないが、
  • 少なくともターミナル.appにおいては、1個のLFと5個のSPに変換されているのだ。

区切り文字

  • 元々SPはキャリッジを1文字分左へ移動させる制御コードなのだが、
  • 同時に、単語と単語を区別する区切り文字としての役割もあった。
  • ASCIIコードにおいて、単語は情報の最小単位であり、
    • 単語が集まってUnitという単位となり、
    • Unitが集まってRecordという単位となり、
    • Recordが集まってGroupという単位となり、
    • Groupが集まってFileという単位となる。
    • データベースにおけるフィールド・レコード・テーブル・ファイルの関係に似ている。
  • 上記の階層情報を区切る制御コードが、10進数コードで28〜32まで順に並んで登録されているのだ。
略語由来語句意味10進数コード16進数コード8進数コードcontrol
コード
\エスケープ
コード
FSFile Separatorファイル区切り28\x1C\034^\
GSGroup Separatorグループ区切り29\x1D\035^]
RSRecord Separatorレコード区切り30\x1E\036^^
USUnit Separatorユニット区切り31\x1F\037^_
SPSpace空白文字32\x20\040

DELの真実

  • NUL(\x00)は現在でもよく使う概念である。何もない・存在しないことを意味する。数学的な0と似ているので理解しやすい。
  • 一方、DEL(\x7F)は、検索してみると「1文字削除・削除・抹消」などの説明がヒットする。
  • ある意味どれも正しいのだけど、当時の削除がどのようなものだったか知っておかないと、誤解を生む。

DELには、現在のdeleteキーの働きはないのだ!

  • 自分も誤解していた。てっきり、deleteキーは文字コード\x7Fを生成して、文字を削除しているものだと思っていた。
  • しかし、試してみると分かるが、\x7Fは何も削除しない。
  • 以下のように入力すれば、前後の1文字どちらかが消えることを期待してしまうが、実は何も消えない...。
$ echo -e "hello\x7fworld"
helloworld
  • echoコマンド自体は\x7Fの文字コードを出力しているのだけど、
$ echo -e "hello\x7fworld" | xxd
0000000: 6865 6c6c 6f7f 776f 726c 640a            hello.world.
  • 画面に出力されたhelloworldをコピーして、文字コードを確認してみると、
helloworld
$ pbpaste | xxd
0000000: 6865 6c6c 6f77 6f72 6c64 0a              helloworld.

そこには\x7Fのコードは存在しない!


  • 実は、これこそが\x7F DELの働きである。
  • \x7Fという文字コードは何も存在しないと解釈されるのだ!
  • 当時、テレタイプには紙テープリーダー/ライターが付属していた。
  • 紙テープに穴を開けて、穴のある部分は1、ない部分は0と解釈して、データを読み書きするのだ。
  • テレタイプで受信したデータを文字に印刷しただけでは、その文字データは再利用できなくなってしまうが、
  • 紙テープにも記録しておけば、後で印刷もできるし、別な場所にも転送できる。だから紙テープは必須である。
  • ところが、通信中の文字化けや誤字脱字を見つけてしまった場合、どうするべきか?
  • 物理的に穴を開けてしまう紙テープを修正することはできない...。
  • 特に、長い長いデータの最後の方で間違いを見つけてしまった場合、もう一度すべてを紙テープに出力し直すのは、切ない思いになる...。

そこで\x7Fの出番である!

  • 7ビットのASCIIコード\x7Fをビット列で表現すると 1111111 となる。
  • 7ビットすべてが1の状態。つまり、紙テープではすべて穴の開いた状態となる。
  • \x7Fは何も存在しないと解釈されるので、例えば「This is pen.」を「This is a pen.」に修正したい場合は、
  • 以下のように穴を開ければ、すべてを書き換えなくても修正できるのだ。

f:id:zariganitosh:20150129160633p:image:w450

f:id:zariganitosh:20150129160632p:image:w450

@nifty:@homepage:エラー

つまり、DELには日常で使う二重線による取り消しのような意味がある!

  • よって、DEL = 削除マーカーと表現した方が分かりやすいと思った。
  • 一方、現在のdeleteキーは文字コードそのものを取り除く削除である。

  • 以上の仕組みを知ると、DEL一つだけ別のコード領域にある理由も理解できる。
    • 他の制御コードはすべて\x00〜\x20の連続した領域*2にある。
  • 削除マーカーとしてすべてのビット位置に穴を開ける必要があったので、物理的な理由から\x7Fにするしかなかったのだ。

controlコード

  • キーボードには、実際に見える文字のキーが並んでいる。
  • 例えば、aというキーを押せば、aの文字が画面に出力される。
  • では、見えない文字の制御コードはどのように入力したら良いのだろうか?
  • それは、controlキーを押しながら、見える文字@ a〜z [ \ ] ^ _ ?のどれかを押すのだ。
    • 例:control @は、NUL \x00の文字コードを生成する。
    • 例:control aは、SOH \x01の文字コードを生成する。
  • どのような仕組みになっているかというと、controlキーには、同時に押されたキーの文字コードの最上位7ビット目を反転する機能があるのだ。
@ = \x40 = 100 0000 最上位7ビット目を反転すると... 000 0000 = \x00 = NUL
A = \x41 = 100 0001 最上位7ビット目を反転すると... 000 0001 = \x01 = SOH
B = \x42 = 100 0010 最上位7ビット目を反転すると... 000 0010 = \x02 = STX
...中略...
_ = \x5F = 101 1111 最上位7ビット目を反転すると... 001 1111 = \x1F = US
? = \x3F = 011 1111 最上位7ビット目を反転すると... 111 1111 = \x7F = DEL
  • controlには「制御」という意味がある。
  • 制御コード、つまりcontrol codeを生成する役割のキーだからcontrolキーと呼ばれているのだ。
  • 昔からcontrolキーが何をコントロールするのか疑問だったのだが、その疑問がようやく解けた!
  • ASCIIコード表を見直してみると、以下のような対応になっている。
  • 制御コードから右に4マス移動する(\x40を加算する)と、controlキーを併用する文字を見つけられる。
  • 右端のDELの場合は、最初の列に戻って右に4マス移動すればいい。

00 10 20 30 40 50 60 70 
00NULDLESP0@P`p
01SOHDC1!1AQaq
02STXDC2"2BRbr
03ETXDC3#3CScs
04EOTDC4$4DTdt
05ENQNAK%5EUeu
06ACKSYN&6FVfv
07BELETB'7GWgw
08BSCAN(8HXhx
09HTEM)9IYiy
0ALFSUB *:JZjz
0BVTESC+;K[k{
0CFFFS,<L\l|
0DCRGS-=M]m}
0ESORS.>N^n~
0FSIUS/?O_oDEL

実際の入力方法
  • ターミナル.appにおいて、上記controlキーを併用して制御コードを入力するには、control-Vの操作に続け行う。
  • 例えば、control-GでBELを入力したい時は、control-V control-Gとキー操作して入力するのだ。
$ echo -e "^G"
  • control-V control-Gは、制御コード\x07をダイレクトに入力する。
  • "^G"は、文字イメージのない制御コード\x07を表現する記号である。(^はcontrolキーを意味するようだ)
  • control-V control-Gで入力した"^G"の場合は、文字コードは\x07となる。
# control-V control-Gと入力した場合
$ echo -ne "^G" | xxd
0000000: 07                                       .
  • 目に見える文字"^"と"G"の場合は、それぞれの文字コード2文字分となる。
# ^とGを入力した場合
$ echo -ne "^G" | xxd
0000000: 5e47                                     ^G
  • まったく同じコマンドに見えても、入力方法によっては全然意味が違ってしまうのだ。気を付けよう!
    • control-V control-Gで画面に表示された^Gをでコピー・ペースト(command-C・command-V)したとしても、^とGの2文字を入力した扱いになってしまう...。
    • 但し、キル・ヤンク(control-K・control-Y)の場合は、制御コード1文字の扱いとなった。
control-Vの力
  • control-Vには、直後入力された文字を純粋なASCIIコードとして解釈させる効果があるようだ。
  • control-V無しでは、コマンドの入力途中でcontrol-Gした瞬間にBEL(ベル音・警告音)が鳴ってしまう。
  • control-V control-Gとすることで、カーソル位置に\x07の制御コードが入力され、BELは鳴らない。
  • また、bashでコマンド入力中のcontrol-A・control-Eには、カーソルを行頭・行末に移動する、というASCII制御コードとは別の機能が割り当てられている。
  • control-Vによって、そのようなコマンド入力中の別の機能を発現させずに、純粋なASCII制御コードのみを入力できる。
  • 入力された制御コードは、ターミナル.appの画面上でcontrolコード、あるいはescキーのcontrolコード^[で始まるエスケープシーケンスで表現されるのだ。
  • 例:
    • control-V、delete(バック デリート)してみると、制御コード DELの\x7Fが入力された。
      • ターミナル >> 環境設定 >> プロファイル >> 詳細 >> 入力:DeleteキーでControl+Hを送信チェックなしの設定の場合
$ echo -en ^? | xxd
0000000: 7f                                       .
    • control-V、fn-delete(フォワード デリート)してみると、エスケープシーケンス\x1B 5B 33 7Eが入力された。
$ echo -en ^3~ | xxd
0000000: 1b5b 337e                                .[3~

\によるエスケープコード

  • controlキーを使えばすべての制御コードは入力できるのだけど、
  • よく使う制御コードは、さらに\によるエスケープコードも用意されている。

略語由来語句意味10進数コード16進数コード8進数コードcontrol
コード
\エスケープ
コード
NULNull文字無し0\x00\000^@\0
BELBell警告音を鳴らす7\x07\007^G\a
BSBack Space一文字後退8\x08\010^H\b
HTHorizontal Tabulation水平タブ9\x09\011^I\t
LFLine Feed / New Line改行10\x0A\012^J\n
VTVertical Tabulation垂直タブ11\x0B\013^K\v
FFForm Feed / New Page改ページ12\x0C\014^L\f
CRCarriage Return行頭復帰13\x0D\015^M\r
ESCEscapeエスケープシーケンス開始27\x1B\033^[\e

ASCII制御コードは相当古くなってしまった...。

  • ASCIIコード制定当時の環境(テレタイプ・コンピューター・紙テープ・プリンターなどの利用環境)では必要な手続きや制御だったかもしれないが、
  • 現在の利用環境では、実は多くの制御コードがほとんど使われなくなってしまったと思っている。
  • 覚える価値のある制御コードは上記9コードだけで十分なのかもしれない。(それ以外使った記憶がない)
  • 例えばタブを入力したい場合は、以下のようにエスケープコード\tをそのまま書き込むだけでいい。
$ echo -e "abc\tefg"
abc	efg
  • 上記のような\エスケープを使った書き方は、現在も多くの環境でサポートされている。
    • 正規表現などでもよく使う。
  • controlコードよりも書きやすく覚えやすいので、利用できる時は\エスケープをよく使う。
    • 但し、コマンドによってはcontrolコードでないと解釈してくれない場合もある。
8進数と16進数の指定方法
  • また、8進数や16進数で制御コードを直接指定する場合も\エスケープを利用することになる。
    • 例:8進数でタブを指定。
$ echo -e "abc\011def"
abc	def
    • 例:16進数でタブを指定。
$ echo -e "abc\x09def"
abc	def
\の仕様の違いに注意
  • \エスケープの書き方は、利用するシェルの仕様によって決まる。
  • また、実行するコマンドが\をどのように解釈するかによっても動作は違ってくる。
  • \エスケープを誰が解釈しているのか?常に意識しておくことが大事。(シェルが解釈しているのか?コマンドが解釈しているのか?)
    • 順序としては、まずシェルが\エスケープを解釈する。
    • その後コマンド引数に\エスケープが残る場合は、コマンドが解釈する。
  • 例:
    • bashの$'XXXX'フォーマットの8進数表現は、\nnnである。
    • 一方、bashの内部コマンドecho -eの8進数表現は、\0nnnである。
# \177は、制御コードDELと解釈される
$ echo $'abc\177def'
abcdef
    • 同じ\177がecho -eでは単なる文字列と解釈されてしまう。
# \177は、4つの文字コードと解釈される
$ echo -e 'abc\177def'
abc\177def
    • echo -eの8進数表現は\0で始まる必要がある。
# \0177は、制御コードDELと解釈される
$ echo -e 'abc\0177def'
abcdef
    • ちなみに$'\0177'と書いてしまうと、\に続く3桁まで制御コード、4桁目は単なる数字と解釈される。
# $'\0177'は、制御コード\017(\x0F)と数字の7と解釈される
$ echo $'abc\0177def'
abc7def

$ echo $'abc\0177def' | xxd
0000000: 6162 630f 3764 6566 0a                   abc.7def.
  • その違いを知らずに\エスケープすると、思い通りの動作をしてくれず、悩む。
  • そして文字として目に見えないので、深い悩みにとなってハマることが多い...。
    • 自分がよくハマってる。

XONとXOFF

  • Device Controlの1〜4は、接続された機器(デバイス)の起動・停止を制御する。
  • 事前に当事者間でどのような制御を行うか取り決めが必要らしいが、
  • 一般的によく使われたのが、XON(DC1)・XOFF(DC3)という制御でだった。
  • 文字データ通信において、受信側の処理が遅く、一時的に文字データを保存しておくバッファが一杯になってしまうことがある。
    • 受信側のバッファから溢れた文字は失われてしまうので、送信側にXOFF(DC3)の制御コードを送る。
      • XOFFを受けた送信側は、文字データの送信を一時的に停止する。
    • 受信側の処理が捗りバッファの空きに余裕が出たら、送信側にXON(DC1)の制御コードを送る。
      • XONを受けた送信側は、文字データの送信を再び開始する。
  • 以上のような通信制御が、かつて行われていたのだ。

  • そして、その名残は現在のターミナル.appにも残る。
  • 例えば、1秒ごとに日時を出力する(10回繰り返す)以下のコマンドを実行中に...
$ for i in `seq 1 10`; do date; sleep 1; done
  • control-Sを押すと、画面出力を一時停止する。
    • 出力が停止しても、コマンドの処理は途切れることなく進んでいる。
  • control-Qを押すと、停止していた画面出力を再開する。
    • 最近のターミナル.appはcontrol-Qに限らず、何らかのキーを押すと再開するかも。
  • control-Sは、XOFFのcontrolコードであり、
  • control-Qは、XONのcontrolコードなのだ。
  • 果たして、ターミナル.appのこの制御が何の役に立つか分からないが、
  • ASCIIコードのXON・XOFFに由来する機能であることは想像できる。

\x1A = ファイル末尾の真相

  • ASCII制御コードのSUB \x1Aは本来、Substitute (代理・置換)という意味であった。
  • 紙テープリーダーや通信データからパリティエラーを検出して、間違った文字データと思われる場合、
  • とりあえず、その文字コードをSUB \x1Aに置き換えて保存しておくような運用が行われていたようだ。
  • ところで、かつてCP/Mという8ビットCPU時代のOSが、ファイル末尾を\x1Aで埋めていた。
  • CP/Mはファイルを128バイトのブロック単位でしか管理できなかったため、
  • ブロック中の未使用領域は、\x1Aというコードで埋める仕様となっていた。
  • 128の倍数でないサイズのファイルは、ファイル末尾に\x1Aを書き込むことになる。
    • 確率的には、127/128の確率で末尾が\x1Aとなるはず。
  • つまりCP/Mは、ほとんどすべての場合ファイル末尾が\x1Aになっていたことになる。
  • その後、CP/M用のソフトウェアがMS-DOSに移植されるようになった。
  • 本来MS-DOSはファイルを1バイト単位で管理できるので、ファイル末尾に\x1Aは不要なはずであった。
  • ところが、移植されたCP/Mアプリのファイル末尾のほとんどすべてが\x1Aとなっていたために、
  • \x1A = EOF(End Of File)= ファイル末尾を表現する文字コードと誤解されてしまったらしい。
      • CP/M用アプリのEOF判定ロジックで\x1Aを利用していた可能性もある。DOS用に移植する時もそのロジックはそのまま使われた可能性大。
      • また、移植されたDOS用アプリで作ったファイルをCP/M側で読み込む需要もあったはず。そのようにして\x1A = EOFの呪縛から逃れられなくなったのかもしれない。
  • 現在、テキストファイルの末尾に\x1Aが必要なOS環境は存在しない!

\x1A = ファイル末尾は、CP/M時代の古き遺産なのである。

現在も覚えておくべき終端の扱い
  • #include <stdio.h>を宣言しているC言語環境においては、文字列の末尾はNull \x00であることが求められている。

  • UNIX系のOSでは、ターミナル.appでcontrol-Dを押すと、ログアウトしてしまう。*3
  • control-Dは、ASCII制御コードのEOT(End of Transmission)\x04に由来していると思われる。
  • 意図しないログアウトを防止したい場合は、例えば IGNOREEOF=2 のように設定しておくと良い。
    • 連続2回のcontrol-Dまで警告を出力してくれる。(指定した数値の回数だけ警告される)
    • 連続3回のcontrol-Dを押した時だけ、ログアウトを実行する。
$ IGNOREEOF=2
$ Use "logout" to leave the shell. # control-D 1回目
$ Use "logout" to leave the shell. # control-D 2回目
$ logout                           # control-D 3回目

[プロセスが完了しました]
  • IGNOREEOF=2を.bashrcとか.bash_profileに書いておくと常に警告してくれる。


参考ページ

  • 以下のページがたいへん参考になりました!
  • すべてのページに深ーく感謝です!

タイプライター・テレタイプについて


ASCIIの誕生とその時代背景など


ASCIIコードと日本の文字コードについて


テレタイプが接続されていた通信方式について

*1:ちなみに、テレタイプによる文字通信では、Carriage ReturnしてからLine Feedする、というのがお決まりの手順のようだ。当時、キャリッジ(あるいはプリンタヘッド)を移動させるにはある程度の時間がかかったので、先にCarriage Returnを開始させて、その最中にLine Feedも行っていたようだ。さらに時間を稼ぐために、CR・LF・NULとか、CR・CR・LFなどの改行コードを送信することもあったらしい。

*2:但し8ビット目の桁あふれを無視すれば、\x7F + 1 = \x00であり、\x00 - 1 = \x7Fなので、\x7Fも連続した領域と考えることもできる。

*3:logoutするのは、ログインシェルの場合。別のシェルを起動していたり、別のユーザーでログインしている場合はexitする。

2015-01-24

pentominoコマンドをgemに公開する最小の手順

前回のpentominoコマンドをgemに公開するまでの手順のメモ。次回公開する時に、悩まず・素早く・苦労最小限で公開できますように!

必要な環境

  • RubyGems.orgで1度でも何らかのgemを公開した環境であること。
  • hubコマンドをインストールした環境であること。
    • コマンドラインからGithubに便利にアクセスする仕組みを提供してくれる。

gem名を決める

  • 候補の名前をremote検索してみた。
$ gem search -r pentomino

 *** REMOTE GEMS ***
  • pentominoは存在しないようだ。
  • よって、pentominoに決定する。

bundlerでgemの雛形を作る

  • 既存のプロジェクト名をpentominoに変更しておいた。
  • bundle gem pentominoコマンドで雛形を生成した。
    • ちなみに~/Desktop/pentomino_rbは、以前からGitでバージョン管理されている。
    • その状況でbundle gemを実行しても、既存のファイルが破壊されることはなかった。
$ mv ~/Desktop/pentomino_rb ~/Desktop/pentomino
$ cd ~/Desktop
$ bundle gem pentomino
      create  pentomino/Gemfile
      create  pentomino/Rakefile
      create  pentomino/LICENSE.txt
      create  pentomino/README.md
      create  pentomino/.gitignore
      create  pentomino/pentomino.gemspec
      create  pentomino/lib/pentomino.rb
      create  pentomino/lib/pentomino/version.rb
Initializing git repo in /Users/zari/Desktop/pentomino
$ cd pentomino

  • 生成された雛形ファイルはすでにステージング済みなので、そのままコミットしておいた。
$ git status
# On branch master
# Changes to be committed:
#   (use "git reset HEAD <file>..." to unstage)
#
#	new file:   .gitignore
#	new file:   Gemfile
#	new file:   LICENSE.txt
#	new file:   README.md
#	new file:   Rakefile
#	new file:   lib/pentomino.rb
#	new file:   lib/pentomino/version.rb
#	new file:   pentomino.gemspec
#
$ git commit -m 'bundle gem pentominoを実行'

gemspecファイルを編集

  • pentomino.gemspecのTODO項目を編集しておく。

 @@ -8,8 +8,8 @@ Gem::Specification.new do |spec|
    spec.version       = Pentomino::VERSION
    spec.authors       = ["zariganitosh"]
    spec.email         = ["XXXX@example.com"]
 -  spec.summary       = %q{TODO: Write a short summary. Required.}
 -  spec.description   = %q{TODO: Write a longer description. Optional.}
 +  spec.summary       = %q{Find answer of pentomino puzzle.}
 +  spec.description   = %q{Find answer of pentomino puzzle on the board(3x20, 4x15, 5x12, 6x10, 7x9-3, 8x8-4).}
    spec.homepage      = ""
    spec.license       = "MIT"

READMEを編集

  • README.mdのTODO項目も編集しておいた。

 # Pentomino
  
 -TODO: Write a gem description
 +Find answer of pentomino puzzle on the board(3x20, 4x15, 5x12, 6x10, 7x9-3, 8x8-4).
  
  ## Installation
  
 @@ -19,9 +19,29 @@ Or install it yourself as:
      $ gem install pentomino
  
  ## Usage

 -TODO: Write usage instructions here
 +````
 +Usage: pentomino [options] [board size(3-8)]
 +    -q                               Hide progress putting a piece on board.(quiet mode)
 +
 +board size:
 +    [3] x 20
 +    [4] x 15
 +    [5] x 12
 +    [6] x 10 (Default)
 +    [7] x  9 - 3
 +    [8] x  8 - 4
 +
 +example:
 +            6 x 10                        7 x 9 - 3                   8 x 8 - 4
 +11 12 13 14 15 16 17 18 19 1A    11 12 13 14 15 16 17 18 19    11 12 13 14 15 16 17 18
 +21 22 23 24 25 26 27 28 29 2A    21 22 23 24 25 26 27 28 29    21 22 23 24 25 26 27 28
 +31 32 33 34 35 36 37 38 39 3A    31 32 33 34 35 36 37 38 39    31 32 33 34 35 36 37 38
 +41 42 43 44 45 46 47 48 49 4A    41 42 43          47 48 49    41 42 43       46 47 48
 +51 52 53 54 55 56 57 58 59 5A    51 52 53 54 55 56 57 58 59    51 52 53       56 57 58
 +61 62 63 64 65 66 67 68 69 6A    61 62 63 64 65 66 67 68 69    61 62 63 64 65 66 67 68
 +                                 71 72 73 74 75 76 77 78 79    71 72 73 74 75 76 77 78
 +                                                               81 82 83 84 85 86 87 88
 +````

  ## Contributing
  
  1. Fork it ( https://github.com/[my-github-username]/pentomino/fork )

コマンドファイルを用意

  • binフォルダを作成して、その中にpentomino.rbを入れた。
  • ファイル名をコマンド名と同じpentominoに変更した。
$ mkdir bin
$ mv pentomino.rb bin/pentomino

バージョンファイルを設定

  • lib/pentomino/version.rbのバージョンを設定しておく。
    • デフォルトは"0.0.1"が設定されている。
module Pentomino
  VERSION = "0.0.1"
end

Githubに公開しておく

  • Githubにも公開しておくことにした。
  • hubコマンドでGithubにpentominoリポジトリを作った。
$ hub create
Updating origin
created repository: zarigani/pentomino
  • Webブラウザで開いて、Githubにpentominoリポジトリが作成されていることを確認した。
$ hub browse

  • pentomino.gemspecのspec.homepageに上記GithubのURLを追記しておいた。

 @@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
    spec.email         = ["XXXX@example.com"]
    spec.summary       = %q{Find answer of pentomino puzzle.}
    spec.description   = %q{Find answer of pentomino puzzle on the board(3x20, 4x15, 5x12, 6x10, 7x9-3, 8x8-4).}
 -  spec.homepage      = ""
 +  spec.homepage      = "https://github.com/zarigani/pentomino"
    spec.license       = "MIT"


  • ここまでのgemの設定を一旦コミット。
$ git add .
$ git commit -m 'gemの設定'

  • Githubに最初のpushをしておく。
$ git push -u origin master
Counting objects: 46, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (32/32), done.
Writing objects: 100% (46/46), 8.90 KiB | 0 bytes/s, done.
Total 46 (delta 11), reused 0 (delta 0)
To git@github.com:zarigani/pentomino.git
 * [new branch]      master -> master
Branch master set up to track remote branch master from origin.
  • これ以降のpushはgit pushのみでOK。

ローカルのgem環境にインストールして動作確認

  • bundlerのrakeタスクを利用すると便利。
$ rake -T
rake build    # Build pentomino-0.0.1.gem into the pkg directory
rake install  # Build and install pentomino-0.0.1.gem into system gems
rake release  # Create tag v0.0.1 and build and push pentomino-0.0.1.gem to Rubygems

  • gemをパッケージングして、インストールしてみる。
$ sudo rake install
 ** Invoke install (first_time)
 ** Invoke build (first_time)
 ** Execute build
pentomino 0.0.1 built to pkg/pentomino-0.0.1.gem.
 ** Execute install
pentomino (0.0.1) installed.

  • gemがインストールされていることを確認。
  • pentominoコマンドが使えることを確認。
$ gem list pentomino

 *** LOCAL GEMS ***

pentomino (0.0.1)

$ pentomino -h
Usage: pentomino [options] [board size(3-8)]
    -q                               Hide progress putting a piece on board.(quiet mode)

board size:
    [3] x 20
    [4] x 15
    [5] x 12
    [6] x 10 (Default)
    [7] x  9 - 3
    [8] x  8 - 4

example:
            6 x 10                        7 x 9 - 3                   8 x 8 - 4
11 12 13 14 15 16 17 18 19 1A    11 12 13 14 15 16 17 18 19    11 12 13 14 15 16 17 18
21 22 23 24 25 26 27 28 29 2A    21 22 23 24 25 26 27 28 29    21 22 23 24 25 26 27 28
31 32 33 34 35 36 37 38 39 3A    31 32 33 34 35 36 37 38 39    31 32 33 34 35 36 37 38
41 42 43 44 45 46 47 48 49 4A    41 42 43          47 48 49    41 42 43       46 47 48
51 52 53 54 55 56 57 58 59 5A    51 52 53 54 55 56 57 58 59    51 52 53       56 57 58
61 62 63 64 65 66 67 68 69 6A    61 62 63 64 65 66 67 68 69    61 62 63 64 65 66 67 68
                                 71 72 73 74 75 76 77 78 79    71 72 73 74 75 76 77 78
                                                               81 82 83 84 85 86 87 88

$ pentomino
Pentomino 6x10

No. 1 (TRY: 22902)
...中略...

問題ないことを確認した。

gemに公開

  • rakeタスクのreleaseで素早く公開する。
$ rake release
pentomino 0.0.1 built to pkg/pentomino-0.0.1.gem.
Tagged v0.0.1.
Pushed git commits and tags.
Pushed pentomino 0.0.1 to rubygems.org.
  • タスクの内容は以下のとおり。
    • gemパッケージをbuildして、
    • 現在のバージョンを元に、タグ v0.0.1 を自動的に設定してくれた。
    • Githubへのpush、
    • rubygems.orgへのpush。(gemの公開)

確認

  • 公開完了した。
$ gem search pentomino

 *** REMOTE GEMS ***

pentomino (0.0.1)
  • インストール。
$ sudo gem install pentomino
Fetching: pentomino-0.0.1.gem (100%)
Successfully installed pentomino-0.0.1
Parsing documentation for pentomino-0.0.1
Installing ri documentation for pentomino-0.0.1
1 gem installed

$ gem list pentomino

 *** LOCAL GEMS ***

pentomino (0.0.1)

これで素早くpentominoコマンドが使えるようになった!

2015-01-23

途中経過を表示しながら解くpentominoコマンドに仕上げる

前回までにc言語なら0.9秒(さらなる高速化を追記した)でペントミノパズル(6×10)の全解を出力できるようになった。1秒切ったので、高速化についてはこれで満足。しかし、何かまだ満たされないものがある。

最速を狙ったコマンドは1度試せばそれで満たされてしまう。たぶん、もう二度と実行することはなくなる。そうではなく、何度も使いたくなるpentominoコマンドにしておきたい。

追加する機能

使って楽しいコマンドを目指して、以下の機能を追加するのだ。

  • 味気ないローマ字表現はやめて、カラフルなブロックにしたい。
  • ブロックを置きながら試行錯誤する途中経過を見せたい。
  • コマンドらしく、オプション指定やboardサイズも指定できるようにしておく。

面倒な処理がありそうなので使い慣れているRubyで書いてみる。

現状のコード

# coding: utf-8
BROW, BCOL = 10, 6

# すべてのピース形状をPieceオブジェクトの配列に保存する
class Piece
  attr_accessor :used, :form, :loc_form, :letter

  def initialize(a, m, n, l)
    @used = false
    @form = []
    @loc_form = []
    @letter = l
    for i in (1..m)
      for j in (1..n)
        @form << [a, a.flatten.index(1)]
        a = a.transpose.reverse # rotate L
      end
      a = a.map(&:reverse) # flip LR
    end
  end
end
  
pp = [Piece.new([[0,1,0], [1,1,1], [0,1,0]], 1, 1, :X),
      Piece.new([[1,1,1], [1,0,1]]         , 1, 4, :U),
      Piece.new([[1,1,0], [0,1,1], [0,0,1]], 1, 4, :W),
      Piece.new([[1,1,0], [0,1,1], [0,1,0]], 1, 2, :F),
      Piece.new([[1,1,0], [0,1,0], [0,1,1]], 2, 2, :Z),
      Piece.new([[1,1,1], [1,1,0]],          2, 4, :P),
      Piece.new([[1,1,1,0], [0,0,1,1]],      2, 4, :N),
      Piece.new([[1,1,1,1], [0,1,0,0]],      2, 4, :Y),
      Piece.new([[1,1,1], [0,1,0], [0,1,0]], 1, 4, :T),
      Piece.new([[1,1,1,1], [1,0,0,0]],      2, 4, :L),
      Piece.new([[1,1,1], [1,0,0], [1,0,0]], 1, 4, :V),
      Piece.new([[1,1,1,1,1]],               1, 2, :I)]

pp.each_with_index do |piece, i|
  piece.form.each do |form|
    a = []
    form[0].each_with_index do |row, r|
      row.each_with_index do |col, c|
        a << r * (BCOL + 1) + c - form[1] if col == 1
      end
    end
    piece.loc_form << a
  end
end



# boardの初期化
board = Array.new((BROW + 1) * (BCOL + 1), 0)
board.each_with_index do |b, i|
  board[i] = 100 if ((i + 1) % (BCOL + 1)) == 0 || i >= ((BCOL + 1) * BROW)
end



# パズルの解を求める
def display_board(board, pp)
  $counter += 1
  puts "No. #{$counter}"
  a = []
  board.each_slice(BCOL + 1) do |line|
    a << line.reject {|i| i == 100}.map {|i| pp[i - 1].letter}
  end
  a[0..-2].transpose.each {|line| puts line.join}
  puts
end

def try_piece(board, pp, lvl)
  $try_counter += 1
  x = board.index(0)
  pp.each_with_index do |piece, i|
    next if piece.used
    piece.loc_form.each do |blocks|
      next if board[x + blocks[0]]>0 || board[x + blocks[1]]>0 || board[x + blocks[2]]>0 || board[x + blocks[3]]>0 || board[x + blocks[4]]>0
      # ピースを置く
      blocks.each {|b| board[x + b] = i + 1}
      piece.used = true
      # すべてのピースを置ききったらTrueを返す(recursiveコールの終了)
      if lvl == 11 then
        display_board(board, pp)
        # ピースを戻す
        blocks.each {|b| board[x + b] = 0}
        piece.used = false
        return
      end
      # 次のピースを試す
      try_piece(board, pp, lvl + 1)
      # ピースを戻す
      blocks.each {|b| board[x + b] = 0}
      piece.used = false
    end
  end
end

$counter = 0
$try_counter = 0
try_piece(board, pp, 0)
puts "解合計: #{$counter}"
puts "操作数: #{$try_counter}"

カラー表示

  • ターミナルでカラー表示するためには、エスケープシーケンスで色を指定する必要がある。
  • 例えば、以前jcalコマンドを作った時の色指定はこんな感じ。
$ ruby -e 'puts "\e[31mRed"'
Red
  • "\e[31m"の部分がエスケープシーケンスによる色の指定部分。
    • 30-37の数値によって、8色の色が指定できる。
  • ところが、今回のペントミノは全部で12ピースある。8色では足りない...。
  • しかも、白と黒を除くと、使えるカラーは6色のみ。
  • 減光の指定をすればどうにか12色の区別はできるが、色の選択の余地がない。
  • カラフルにしたいのだけど、このままでは残念なカラーになりそうな予感...。

  • 調べてみると、なんと今時のターミナルは256色表示に対応しているらしい!
  • 以下のように指定すると、0-255の値でカラー指定ができる。
    • 38は、フォアグラウンドカラーを意味し、(ちなみに、48=バックグラウンドカラーを意味する)
    • 5は、0-255のカラーパレットコードを使うことを意味するようだ。
    • 211は、ピンク色のカラーコード。
$ ruby -e 'puts "\e[38;5;211mPink"'
Pink

  • 256色すべてのカラーサンプルは、以下のワンライナーで出力された。
$ for c in {0..255}; do printf "\e[48;5;${c}m%8d\e[m" $c; done; echo

f:id:zariganitosh:20150122180147p:image:w450

  • これで色選択の自由は格段に向上した!自分好みの色を選択できそう。

  • ピース形状をカラーブロックで表示するように変更してみた。

 @@ -3,13 +3,14 @@ BROW, BCOL = 10, 6
  
  # すべてのピース形状をPieceオブジェクトの配列に保存する
  class Piece
 -  attr_accessor :used, :form, :loc_form, :letter
 +  attr_accessor :used, :form, :loc_form, :letter, :color
  
 -  def initialize(a, m, n, l)
 +  def initialize(a, m, n, l, c)
      @used = false
      @form = []
      @loc_form = []
      @letter = l
 +    @color = c
      for i in (1..m)
        for j in (1..n)
          @form << [a, a.flatten.index(1)]
 @@ -20,18 +21,18 @@ class Piece
    end
  end
    
 -pp = [Piece.new(0,1,0], [1,1,1], [0,1,0?, 1, 1, :X),
 -      Piece.new(1,1,1], [1,0,1?         , 1, 4, :U),
 -      Piece.new(1,1,0], [0,1,1], [0,0,1?, 1, 4, :W),
 -      Piece.new(1,1,0], [0,1,1], [0,1,0?, 1, 2, :F),
 -      Piece.new(1,1,0], [0,1,0], [0,1,1?, 2, 2, :Z),
 -      Piece.new(1,1,1], [1,1,0?,          2, 4, :P),
 -      Piece.new(1,1,1,0], [0,0,1,1?,      2, 4, :N),
 -      Piece.new(1,1,1,1], [0,1,0,0?,      2, 4, :Y),
 -      Piece.new(1,1,1], [0,1,0], [0,1,0?, 1, 4, :T),
 -      Piece.new(1,1,1,1], [1,0,0,0?,      2, 4, :L),
 -      Piece.new(1,1,1], [1,0,0], [1,0,0?, 1, 4, :V),
 -      Piece.new(1,1,1,1,1?,               1, 2, :I)]
 +pp = [Piece.new(0,1,0], [1,1,1], [0,1,0?, 1, 1, :X, 141),
 +      Piece.new(1,1,1], [1,0,1?         , 1, 4, :U,   6),
 +      Piece.new(1,1,0], [0,1,1], [0,0,1?, 1, 4, :W, 104),
 +      Piece.new(1,1,0], [0,1,1], [0,1,0?, 1, 2, :F, 172),
 +      Piece.new(1,1,0], [0,1,0], [0,1,1?, 2, 2, :Z, 211),
 +      Piece.new(1,1,1], [1,1,0?,          2, 4, :P,  70),
 +      Piece.new(1,1,1,0], [0,0,1,1?,      2, 4, :N, 121),
 +      Piece.new(1,1,1,1], [0,1,0,0?,      2, 4, :Y, 170),
 +      Piece.new(1,1,1], [0,1,0], [0,1,0?, 1, 4, :T,  42),
 +      Piece.new(1,1,1,1], [1,0,0,0?,      2, 4, :L,   3),
 +      Piece.new(1,1,1], [1,0,0], [1,0,0?, 1, 4, :V,  75),
 +      Piece.new(1,1,1,1,1?,               1, 2, :I, 217)]
  
  pp.each_with_index do |piece, i|
    piece.form.each do |form|
 @@ -45,6 +46,8 @@ pp.each_with_index do |piece, i|
    end
  end
  
 +BLOCK_COLOR = [250] + pp.map{|i| i.color} + [0]
 +
  
  
  # boardの初期化
 @@ -55,13 +58,20 @@ end
  
  
  
 +# エスケープシーケンス定義
 +def bgcolor(nnn); "\e[48;5;#{nnn}m" ; end # 色指定の開始(nnn=0..255
 +def reset       ; "\e[m"            ; end # 色指定の終了
 +
 +# 出力コード生成
 +def create_block(color); bgcolor(BLOCK_COLOR[color]) + "  " + reset; end
 +
  # パズルの解を求める
  def display_board(board, pp)
    $counter += 1
    puts "No. #{$counter}"
    a = []
    board.each_slice(BCOL + 1) do |line|
 -    a << line.reject {|i| i == 100}.map {|i| pp[i - 1].letter}
 +    a << line.reject {|i| i == 100}.map {|i| create_block(i)}
    end
    a[0..-2].transpose.each {|line| puts line.join}
    puts


f:id:zariganitosh:20150123080457p:image:w450

カラーブロック表示の完了!

途中経過を見せる

  • 途中経過を見せるには、pieceが置けたらその状態を出力すれば良いだけなので、以下のように変更してみた。

 @@ -67,7 +67,6 @@ def create_block(color); bgcolor(BLOCK_COLOR[color]) + "  " + reset; end
  
  # パズルの解を求める
  def display_board(board, pp)
 -  $counter += 1
    puts "No. #{$counter}"
    a = []
    board.each_slice(BCOL + 1) do |line|
 @@ -87,8 +86,10 @@ def try_piece(board, pp, lvl)
        # ピースを置く
        blocks.each {|b| board[x + b] = i + 1}
        piece.used = true
 +      display_board(board, pp)
        # すべてのピースを置ききったらTrueを返す(recursiveコールの終了)
        if lvl == 11 then
 +        $counter += 1
          display_board(board, pp)
          # ピースを戻す
          blocks.each {|b| board[x + b] = 0}

  • しかし、これでは大量の途中経過によって、もの凄い速さでスクロールしてしまう...。
  • 大量の途中経過の中に解が埋もれてしまって、肝心な解が見えなくなってしまう...。

f:id:zariganitosh:20150123082034p:image:h450

途中経過の出力位置を固定する

  • そこで、途中経過は位置を変えずに常に同じ位置で出力して、解が見つかった時だけ次の出力位置に移動するようにしたい。
  • 通常、echoやputsで出力すると、出力位置は自動的に次の文字位置となるようにコントロールされている。
    • 改行があれば、次の行の先頭に移動するようにコントロールされている。
  • この出力位置を自分でコントロールできれば、途中経過の位置を変えずに常に同じ位置に出力できるのだ。

  • 出力位置もエスケープシーケンスを使ってコントロールできる。
  • 例えば以下のコマンドは、最初 ABC と出力して、1秒後に xyz と上書きする。
    • 1秒後に ABC が消えて、xyz に変更されるのだ。
$ echo ABC; sleep 1; echo -e "\033[Axyz"
  • \033[A の部分がエスケープシーケンス。
    • \033 = \e、エスケープシーケンスの8進数表記。
    • echoの場合\eではエスケープしてくれなかった。
    • A = 出力位置(カーソル位置)を1行上に移動する。
    • 2行上に移動したい時は2Aと書く。
    • 3行上に移動したい時は3Aと書く。
  • 必要なエスケープシーケンスはあと二つ。
    • \033[2J = 画面全体を消去、
    • \033[H = 画面左上にカーソル移動。

  • 以上のエスケープシーケンスを追加して、以下のように修正してみた。

 @@ -59,23 +59,28 @@ end
  # エスケープシーケンス定義
 +def home        ; "\e[H"            ; end # カーソル位置を画面左上へ移動(ホームポジション)
 +def clear(n=2)  ; "\e[#{n}J"        ; end # n=(0:画面先頭からカーソル位置まで消去, 1:カーソル位置から画面末尾まで消去, 2:画面全体を消去)
 +def moveup(n)   ; "\e[#{n}A"        ; end # カーソルを上方向へn行移動
  def bgcolor(nnn); "\e[48;5;#{nnn}m" ; end # 色指定の開始(nnn=0..255)
  def reset       ; "\e[m"            ; end # 色指定の終了
  
  # 出力コード生成
  def create_block(color); bgcolor(BLOCK_COLOR[color]) + "  " + reset; end
 +def reset_screen       ; home + clear                              ; end
 +def next_screen        ; "\n" * (BCOL + 2)                         ; end
  
 -# パズルの解を求める
 +# boardを表示
  def display_board(board, pp)
 -  puts "No. #{$counter}"
 +  puts moveup(BCOL + 1) + "No. #{$counter}"
    a = []
    board.each_slice(BCOL + 1) do |line|
      a << line.reject {|i| i == 100}.map {|i| create_block(i)}
    end
    a[0..-2].transpose.each {|line| puts line.join}
 -  puts
  end
  
 +# パズルの解を求める
  def try_piece(board, pp, lvl)
    $try_counter += 1
    x = board.index(0)
 @@ -91,6 +96,7 @@ def try_piece(board, pp, lvl)
        if lvl == 11 then
          $counter += 1
          display_board(board, pp)
 +        puts next_screen
          # ピースを戻す
          blocks.each {|b| board[x + b] = 0}
          piece.used = false
 @@ -107,6 +113,7 @@ end
  
  $counter = 0
  $try_counter = 0
 +puts reset_screen
  try_piece(board, pp, 0)
  puts "解合計: #{$counter}"
  puts "操作数: #{$try_counter}"

コマンドオプションを解析する

  • ここまで基本的な機能はほぼ完成したので、コマンドらしくオプション指定できるようにしておく。
    • -qオプションは途中経過を表示しない指定。解だけを素早く出力してくれる。
    • また、3から8の引数を指定することで、boardのサイズも変更できる。
      • 3×20、4×15、5×12、6×10、7×9-3、8×8-4。
      • 7×9-3、8×8-4は中央に穴が空いたドーナツ型のboard。
  • 以上のオプション仕様にして、以下のように追記した。

 @@ -1,5 +1,46 @@
 +#!/usr/bin/ruby
  # coding: utf-8
 -BROW, BCOL = 10, 6
 +
 +require 'optparse'
 +
 +# オプション解析
 +$options = {}
 +OptionParser.new do |opt|
 +  opt.banner = 'Usage: pentomino [options] [board size(3-8)]'
 +  opt.on('-q', 'Hide progress putting a piece on board.(quiet mode)') {|v| $options[:quiet] = v}
 +  opt.separator('')
 +  opt.on('board size:',
 +         '    [3] x 20',
 +         '    [4] x 15',
 +         '    [5] x 12',
 +         '    [6] x 10 (Default)',
 +         '    [7] x  9 - 3',
 +         '    [8] x  8 - 4',
 +         )
 +  opt.separator('')
 +  opt.on('example:',
 +         '            6 x 10                        7 x 9 - 3                   8 x 8 - 4',
 +         '11 12 13 14 15 16 17 18 19 1A    11 12 13 14 15 16 17 18 19    11 12 13 14 15 16 17 18',
 +         '21 22 23 24 25 26 27 28 29 2A    21 22 23 24 25 26 27 28 29    21 22 23 24 25 26 27 28',
 +         '31 32 33 34 35 36 37 38 39 3A    31 32 33 34 35 36 37 38 39    31 32 33 34 35 36 37 38',
 +         '41 42 43 44 45 46 47 48 49 4A    41 42 43          47 48 49    41 42 43       46 47 48',
 +         '51 52 53 54 55 56 57 58 59 5A    51 52 53 54 55 56 57 58 59    51 52 53       56 57 58',
 +         '61 62 63 64 65 66 67 68 69 6A    61 62 63 64 65 66 67 68 69    61 62 63 64 65 66 67 68',
 +         '                                 71 72 73 74 75 76 77 78 79    71 72 73 74 75 76 77 78',
 +         '                                                               81 82 83 84 85 86 87 88',
 +         )
 +  begin
 +    opt.parse!(ARGV)
 +    BCOL = (ARGV[0] || 6).to_i
 +    BROW = 64 / BCOL
 +    raise "Invalid board size: #{BCOL}" if BCOL < 3 || 8 < BCOL
 +  rescue => e
 +    puts e
 +    exit
 +  end
 +end
 +
 +
  
  # すべてのピース形状をPieceオブジェクトの配列に保存する
  class Piece
 @@ -55,6 +96,8 @@ board = Array.new((BROW + 1) * (BCOL + 1), 0)
  board.each_with_index do |b, i|
    board[i] = 100 if ((i + 1) % (BCOL + 1)) == 0 || i >= ((BCOL + 1) * BROW)
  end
 +board[30], board[31], board[39], board[40] = 13, 13, 13, 13 if BCOL == 8
 +board[27], board[35], board[43]            = 13, 13, 13     if BCOL == 7
  
  
  
 @@ -91,7 +134,7 @@ def try_piece(board, pp, lvl)
        # ピースを置く
        blocks.each {|b| board[x + b] = i + 1}
        piece.used = true
 -      display_board(board, pp)
 +      display_board(board, pp) if !$options[:quiet]
        # すべてのピースを置ききったらTrueを返す(recursiveコールの終了)
        if lvl == 11 then
          $counter += 1


  • 実行権限を追加して、ヘルプ表示してみた。
$ chmod a+x pentomino.rb
$ ./pentomino.rb -h
Usage: pentomino [options] [board size(3-8)]
    -q                               Hide progress putting a piece on board.(quiet mode)

board size:
    [3] x 20
    [4] x 15
    [5] x 12
    [6] x 10 (Default)
    [7] x  9 - 3
    [8] x  8 - 4

example:
            6 x 10                        7 x 9 - 3                   8 x 8 - 4
11 12 13 14 15 16 17 18 19 1A    11 12 13 14 15 16 17 18 19    11 12 13 14 15 16 17 18
21 22 23 24 25 26 27 28 29 2A    21 22 23 24 25 26 27 28 29    21 22 23 24 25 26 27 28
31 32 33 34 35 36 37 38 39 3A    31 32 33 34 35 36 37 38 39    31 32 33 34 35 36 37 38
41 42 43 44 45 46 47 48 49 4A    41 42 43          47 48 49    41 42 43       46 47 48
51 52 53 54 55 56 57 58 59 5A    51 52 53 54 55 56 57 58 59    51 52 53       56 57 58
61 62 63 64 65 66 67 68 69 6A    61 62 63 64 65 66 67 68 69    61 62 63 64 65 66 67 68
                                 71 72 73 74 75 76 77 78 79    71 72 73 74 75 76 77 78
                                                               81 82 83 84 85 86 87 88
  • ./pentomino.rb 引数なしで6×10のペントミノパズルを解き始める。
  • ./pentomino.rb 8 とすれば、8×8-4のドーナツ型のペントミノパズルを解き始める。
  • 途中経過を見せずに素早く解を知りたい時は、./pentomino.rb -q 8

修正後のコード

  • さらに幾つかの細々した修正をして、

 *       zariganitosh    37b694c 最初と最後にboardサイズを出力する
 *       zariganitosh    320cdce boardサイズの算出式を修正
 *       zariganitosh    ecc0da9 display_boardにタイトル表示の引数を追加する
 *       zariganitosh    94c4616 8x8正方形のboardは90度回転対称の重複解も除外する

  • 現在のコードは以下の姿になった。
#!/usr/bin/ruby
# coding: utf-8

require 'optparse'

# オプション解析
$options = {}
OptionParser.new do |opt|
  opt.banner = 'Usage: pentomino [options] [board size(3-8)]'
  opt.on('-q', 'Hide progress putting a piece on board.(quiet mode)') {|v| $options[:quiet] = v}
  opt.separator('')
  opt.on('board size:',
         '    [3] x 20',
         '    [4] x 15',
         '    [5] x 12',
         '    [6] x 10 (Default)',
         '    [7] x  9 - 3',
         '    [8] x  8 - 4',
         )
  opt.separator('')
  opt.on('example:',
         '            6 x 10                        7 x 9 - 3                   8 x 8 - 4',
         '11 12 13 14 15 16 17 18 19 1A    11 12 13 14 15 16 17 18 19    11 12 13 14 15 16 17 18',
         '21 22 23 24 25 26 27 28 29 2A    21 22 23 24 25 26 27 28 29    21 22 23 24 25 26 27 28',
         '31 32 33 34 35 36 37 38 39 3A    31 32 33 34 35 36 37 38 39    31 32 33 34 35 36 37 38',
         '41 42 43 44 45 46 47 48 49 4A    41 42 43          47 48 49    41 42 43       46 47 48',
         '51 52 53 54 55 56 57 58 59 5A    51 52 53 54 55 56 57 58 59    51 52 53       56 57 58',
         '61 62 63 64 65 66 67 68 69 6A    61 62 63 64 65 66 67 68 69    61 62 63 64 65 66 67 68',
         '                                 71 72 73 74 75 76 77 78 79    71 72 73 74 75 76 77 78',
         '                                                               81 82 83 84 85 86 87 88',
         )
  begin
    opt.parse!(ARGV)
    BCOL = (ARGV[0] || 6).to_i
    BROW = (60.0 / BCOL).round
    raise "Invalid board size: #{BCOL}" if BCOL < 3 || 8 < BCOL
  rescue => e
    puts e
    exit
  end
end



# すべてのピース形状をPieceオブジェクトの配列に保存する
class Piece
  attr_accessor :used, :form, :loc_form, :letter, :color

  def initialize(a, m, n, l, c)
    @used = false
    @form = []
    @loc_form = []
    @letter = l
    @color = c
    for i in (1..m)
      for j in (1..n)
        @form << [a, a.flatten.index(1)]
        a = a.transpose.reverse # rotate L
      end
      a = a.map(&:reverse) # flip LR
    end
  end
end

r = BCOL == 8 ? 1 : 2
pp = [Piece.new([[0,1,0], [1,1,1], [0,1,0]], 1, 1, :X, 141),
      Piece.new([[1,1,1], [1,0,1]]         , 1, 4, :U,   6),
      Piece.new([[1,1,0], [0,1,1], [0,0,1]], 1, 4, :W, 104),
      Piece.new([[1,1,0], [0,1,1], [0,1,0]], 1, r, :F, 172),
      Piece.new([[1,1,0], [0,1,0], [0,1,1]], 2, 2, :Z, 211),
      Piece.new([[1,1,1], [1,1,0]],          2, 4, :P,  70),
      Piece.new([[1,1,1,0], [0,0,1,1]],      2, 4, :N, 121),
      Piece.new([[1,1,1,1], [0,1,0,0]],      2, 4, :Y, 170),
      Piece.new([[1,1,1], [0,1,0], [0,1,0]], 1, 4, :T,  42),
      Piece.new([[1,1,1,1], [1,0,0,0]],      2, 4, :L,   3),
      Piece.new([[1,1,1], [1,0,0], [1,0,0]], 1, 4, :V,  75),
      Piece.new([[1,1,1,1,1]],               1, 2, :I, 217)]

pp.each_with_index do |piece, i|
  piece.form.each do |form|
    a = []
    form[0].each_with_index do |row, r|
      row.each_with_index do |col, c|
        a << r * (BCOL + 1) + c - form[1] if col == 1
      end
    end
    piece.loc_form << a
  end
end

BLOCK_COLOR = [250] + pp.map{|i| i.color} + [0]



# boardの初期化
board = Array.new((BROW + 1) * (BCOL + 1), 0)
board.each_with_index do |b, i|
  board[i] = 100 if ((i + 1) % (BCOL + 1)) == 0 || i >= ((BCOL + 1) * BROW)
end
board[30], board[31], board[39], board[40] = 13, 13, 13, 13 if BCOL == 8
board[27], board[35], board[43]            = 13, 13, 13     if BCOL == 7



# エスケープシーケンス定義
def home        ; "\e[H"            ; end # カーソル位置を画面左上へ移動(ホームポジション)
def clear(n=2)  ; "\e[#{n}J"        ; end # n=(0:画面先頭からカーソル位置まで消去, 1:カーソル位置から画面末尾まで消去, 2:画面全体を消去)
def moveup(n)   ; "\e[#{n}A"        ; end # カーソルを上方向へn行移動
def bgcolor(nnn); "\e[48;5;#{nnn}m" ; end # 色指定の開始(nnn=0..255)
def reset       ; "\e[m"            ; end # 色指定の終了

# 出力コード生成
def create_block(color); bgcolor(BLOCK_COLOR[color]) + "  " + reset; end
def reset_screen       ; home + clear                              ; end
def next_screen        ; "\n" * (BCOL + 2)                         ; end

# boardを表示
def display_board(board, pp, title='')
  puts moveup(BCOL + 1) + title
  a = []
  board.each_slice(BCOL + 1) do |line|
    a << line.reject {|i| i == 100}.map {|i| create_block(i)}
  end
  a[0..-2].transpose.each {|line| puts line.join}
end

# パズルの解を求める
def try_piece(board, pp, lvl)
  $try_counter += 1
  x = board.index(0)
  pp.each_with_index do |piece, i|
    next if piece.used
    piece.loc_form.each do |blocks|
      next if board[x + blocks[0]]>0 || board[x + blocks[1]]>0 || board[x + blocks[2]]>0 || board[x + blocks[3]]>0 || board[x + blocks[4]]>0
      # ピースを置く
      blocks.each {|b| board[x + b] = i + 1}
      piece.used = true
      display_board(board, pp) if !$options[:quiet]
      # すべてのピースを置ききったらTrueを返す(recursiveコールの終了)
      if lvl == 11 then
        $counter += 1
        display_board(board, pp, "No. #{$counter} (TRY: #{$try_counter})")
        puts next_screen
        # ピースを戻す
        blocks.each {|b| board[x + b] = 0}
        piece.used = false
        return
      end
      # 次のピースを試す
      try_piece(board, pp, lvl + 1)
      # ピースを戻す
      blocks.each {|b| board[x + b] = 0}
      piece.used = false
    end
  end
end

$counter = 0
$try_counter = 0
puts reset_screen
puts "Pentomino #{BCOL}x#{BROW}"
puts next_screen
try_piece(board, pp, 0)
puts "Pentomino #{BCOL}x#{BROW}"
puts "解合計: #{$counter}"
puts "操作数: #{$try_counter}"

ひとまず完成。あとでgemに公開する予定。

2015-01-15

RubyとC言語でもペントミノバズルを解いてみる

前回からの続き。

ペントミノパズルを解くPythonコードは、順調に高速化の道を歩んできた。

  • 3時間以上 → 50分 → 20分 → 3分。

ところで、現在はnumpyにほとんど依存しないコードになっている。ならば、他のプログラミング言語でも同じアルゴリズムでペントミノパズルを解けるはず。ふと、使い慣れているRubyで書いてみたらどうなるのだろう?と思った。やってみた。

Rubyで解く

  • 完全にPython脳になっていたので、endが必要な書き方に激しく無駄を感じてしまった。
  • いくつかのエラーに悩まされながら、どうにか以下のRubyコードを完成させた。
  • 実行してみると...
# coding: utf-8
BROW, BCOL = 10, 6

# すべてのピース形状をPieceオブジェクトの配列に保存する
class Piece
  attr_accessor :used, :form, :loc_form, :letter

  def initialize(a, m, n, l)
    @used = false
    @form = []
    @loc_form = []
    @letter = l
    for i in (1..m)
      for j in (1..n)
        @form << [a, a.flatten.index(1)]
        a = a.transpose.reverse # rotate L
      end
      a = a.map(&:reverse) # flip LR
    end
  end
end
  
pp = [Piece.new([[0,1,0], [1,1,1], [0,1,0]], 1, 1, :X),
      Piece.new([[1,1,1], [1,0,1]]         , 1, 4, :U),
      Piece.new([[1,1,0], [0,1,1], [0,0,1]], 1, 4, :W),
      Piece.new([[1,1,0], [0,1,1], [0,1,0]], 1, 2, :F),
      Piece.new([[1,1,0], [0,1,0], [0,1,1]], 2, 2, :Z),
      Piece.new([[1,1,1], [1,1,0]],          2, 4, :P),
      Piece.new([[1,1,1,0], [0,0,1,1]],      2, 4, :N),
      Piece.new([[1,1,1,1], [0,1,0,0]],      2, 4, :Y),
      Piece.new([[1,1,1], [0,1,0], [0,1,0]], 1, 4, :T),
      Piece.new([[1,1,1,1], [1,0,0,0]],      2, 4, :L),
      Piece.new([[1,1,1], [1,0,0], [1,0,0]], 1, 4, :V),
      Piece.new([[1,1,1,1,1]],               1, 2, :I)]

pp.each_with_index do |piece, i|
  piece.form.each do |form|
    a = []
    form[0].each_with_index do |row, r|
      row.each_with_index do |col, c|
        a << r * (BCOL + 1) + c - form[1] if col == 1
      end
    end
    piece.loc_form << a
  end
end



# boardの初期化
board = Array.new((BROW + 1) * (BCOL + 1), 0)
board.each_with_index do |b, i|
  board[i] = 100 if ((i + 1) % (BCOL + 1)) == 0 || i >= ((BCOL + 1) * BROW)
end



# パズルの解を求める
def display_board(board, pp)
  $counter += 1
  puts "No. #{$counter}"
  a = []
  board.each_slice(BCOL + 1) do |line|
    a << line.reject {|i| i == 100}.map {|i| pp[i - 1].letter}
  end
  a[0..-2].transpose.each {|line| puts line.join}
  puts
end

def try_piece(board, pp, lvl)
  $try_counter += 1
  x = board.index(0)
  pp.each_with_index do |piece, i|
    next if piece.used
    piece.loc_form.each do |blocks|
      next if board[x + blocks[0]]>0 || board[x + blocks[1]]>0 || board[x + blocks[2]]>0 || board[x + blocks[3]]>0 || board[x + blocks[4]]>0
      # ピースを置く
      blocks.each {|b| board[x + b] = i + 1}
      piece.used = true
      # すべてのピースを置ききったらTrueを返す(recursiveコールの終了)
      if lvl == 11 then
        display_board(board, pp)
        # ピースを戻す
        blocks.each {|b| board[x + b] = 0}
        piece.used = false
        return
      end
      # 次のピースを試す
      try_piece(board, pp, lvl + 1)
      # ピースを戻す
      blocks.each {|b| board[x + b] = 0}
      piece.used = false
    end
  end
end

$counter = 0
$try_counter = 0
try_piece(board, pp, 0)
puts "解合計: #{$counter}"
puts "操作数: #{$try_counter}"

1分切ってる!

$ time ruby pentomino.rb

解合計: 2339
操作数: 10385817

real	0m59.035s
user	0m58.632s
sys	0m0.285s
  • なんと!同じアルゴリズムをRubyで書いたら、3倍も高速化されてしまった!
  • 昔からPythonは早くて、Rubyはノロマな子、と思い込んでいたので驚いた。
  • ちなみに、各言語のバージョン。
$ python --version
Python 2.7.6

$ ruby --version
ruby 1.9.3p194 (2012-04-20 revision 35410) [x86_64-darwin12.2.0]

C言語で解く

  • 高速化の話はもう終わりと思っていたのに。
  • Rubyで書き直した途端3倍も早くなってしまった!
  • こうなったらC言語で最速を狙いたい気分になる。
  • 完全なスクリプト言語な脳でC言語を触り始めると、もうエラーと警告が嵐のように出まくる。
    • メモリ構造を想像しながら構造体を定義して、
    • 可変長な要素の配列を作ることをあきらめつつ、配列を定義する。
    • 変数や関数は事前にすべて宣言しておかなければならない。
  • いくつもの試練を乗り越えて、ようやく以下のコードが完成する。
  • コンパイルして、実行してみると...
#include  <stdio.h>

typedef struct {
  int       pos[5];
} MinoPos;

typedef struct {
  char      name;
  int       used;
  int       formsize;
  MinoPos   form[8];
} Piece;

Piece pieces[] =
{
    {'X', 0, 1, {{0,6,7, 8,14}}},
    {'U', 0, 4, {{0,1,2, 7, 9},{0,1, 7,14,15},{0,2, 7, 8, 9},{0,1, 8,14,15}}},
    {'W', 0, 4, {{0,1,8, 9,16},{0,1, 6, 7,13},{0,7, 8,15,16},{0,6, 7,12,13}}},
    {'F', 0, 2, {{0,1,8, 9,15},{0,6, 7, 8,13}}},
    {'Z', 0, 4, {{0,1,8,15,16},{0,5, 6, 7,12},{0,1, 7,13,14},{0,7, 8, 9,16}}},
    {'P', 0, 8, {{0,1,2, 7, 8},{0,7, 8,14,15},{0,1, 6, 7, 8},{0,1, 7, 8,15},{0,1,2,8, 9},{0,1, 7, 8,14},{0,1,7,8, 9},{0,6, 7,13,14}}},
    {'N', 0, 8, {{0,1,2, 9,10},{0,6, 7,13,20},{0,1, 8, 9,10},{0,7,13,14,20},{0,1,2,6, 7},{0,7,14,15,22},{0,1,5,6, 7},{0,7, 8,15,22}}},
    {'Y', 0, 8, {{0,1,2, 3, 8},{0,7,14,15,21},{0,5, 6, 7, 8},{0,6, 7,14,21},{0,1,2,3, 9},{0,7, 8,14,21},{0,6,7,8, 9},{0,7,13,14,21}}},
    {'T', 0, 4, {{0,1,2, 8,15},{0,7, 8, 9,14},{0,7,13,14,15},{0,5, 6, 7,14}}},
    {'L', 0, 8, {{0,1,2, 3, 7},{0,7,14,21,22},{0,4, 5, 6, 7},{0,1, 8,15,22},{0,1,2,3,10},{0,1, 7,14,21},{0,7,8,9,10},{0,7,14,20,21}}},
    {'V', 0, 4, {{0,1,2, 7,14},{0,7,14,15,16},{0,7,12,13,14},{0,1, 2, 9,16}}},
    {'I', 0, 2, {{0,1,2, 3, 4},{0,7,14,21,28}}},
};

int board[77];
int counter, try_counter;

void init_board(void);
void print_board(void);
void try_piece(int level);
int board_index(int find_num);



void init_board(void)
{
  int i;

  for(i=0; i<77; i++){
    if(((i + 1) % 7) == 0 || i >= 70){
      board[i] = 100;
    }
  }
}

void print_board(void)
{
  int row, col;

  printf("No. %d\n", counter);
  for(row=0; row<6; row++){
    for(col=0; col<70; col += 7){
      printf(" %c", pieces[board[row + col] - 1].name);
    }
    printf("\n");
  }
  printf("\n");
}

int board_index(int find_num)
{
  int i;

  for(i=0; i<77; i++){
    if(board[i] == find_num){
      return i;
    }
  }
  return 0;
}

void try_piece(int level)
{
  int i, j, k, x;

  try_counter++;
  x = board_index(0);
  for(i=0; i<12; i++){
    if(pieces[i].used == 1){continue;}
    for(j=0; j<pieces[i].formsize; j++){
      if(board[x + pieces[i].form[j].pos[0]] ||
         board[x + pieces[i].form[j].pos[1]] ||
         board[x + pieces[i].form[j].pos[2]] ||
         board[x + pieces[i].form[j].pos[3]] ||
         board[x + pieces[i].form[j].pos[4]]){continue;}
      // ピースを置く
      for(k=0; k<5; k++){board[x + pieces[i].form[j].pos[k]] = i + 1;}
      pieces[i].used = 1;
      // すべてのピースを置ききったらTrueを返す(recursiveコールの終了)
      if(level == 11){
        counter++;
        print_board();
        // ピースを戻す
        for(k=0; k<5; k++){board[x + pieces[i].form[j].pos[k]] = 0;}
        pieces[i].used = 0;
        return;
      }
      // 次のピースを試す
      try_piece(level + 1);
      // ピースを戻す
      for(k=0; k<5; k++){board[x + pieces[i].form[j].pos[k]] = 0;}
      pieces[i].used = 0;
    }
  }
}

int main(int argc, char **argv)
{
  init_board();
  try_piece(0);
  printf("解合計: %d\n", counter);
  printf("操作数: %d\n", try_counter);
}

わずか1.2秒で完了した!

  • Rubyの1分からC言語1.2秒へ桁違いの高速化。
$ gcc -O2 pentomino.c
$ time ./a.out
解合計: 2339
操作数: 10385817

real	0m1.256s
user	0m1.237s
sys	0m0.017s
なしreal 0m2.789s
-O1real 0m1.276s
-O2real 0m1.249s
-O3real 0m1.292s

-O2が最速だ!


1秒切りたい

  • ここまで来ると、どうしても1秒を切りたくなる。
  • 工夫すべきところは、以下のコード部分だと思う。
...中略...
int board_index(int find_num)
{
  int i;

  for(i=0; i<77; i++){
    if(board[i] == find_num){
      return i;
    }
  }
  return 0;
}

void try_piece(int level)
{
  int i, j, k, x;

  try_counter++;
  x = board_index(0);
  for(i=0; i<12; i++){
    if(pieces[i].used == 1){continue;}
    for(j=0; j<pieces[i].formsize; j++){
      if(board[x + pieces[i].form[j].pos[0]] ||
         board[x + pieces[i].form[j].pos[1]] ||
         board[x + pieces[i].form[j].pos[2]] ||
         board[x + pieces[i].form[j].pos[3]] ||
         board[x + pieces[i].form[j].pos[4]]){continue;}
      // ピースを置く
...中略...
  • この部分はtry_counterが示すとおり1000万回以上も繰り返すのだ。
  • わずかな無駄も1000万回繰り返すと、結構な時間になってしまう...。
board_index()関数の修正
  • まず、board_index()関数の動きに注目してみる。
  • 次にピースを置く場所を決めるために、board配列の先頭から検索して、最初に0が出現するインデックス値を返している。
  • しかし、毎回board配列の先頭から検索するのは無駄である。
  • 少なくとも前回のインデックスxまでは、Pentominoピースで埋まっているはずなので、
  • 次回の検索位置は x+1 から探し始めた方が無駄がないはず。
  • 以下の修正をして、コンパイルして、実行してみると...

 @@ -32,8 +32,8 @@ int counter, try_counter;
  
  void init_board(void);
  void print_board(void);
 -void try_piece(int level);
 -int board_index(int find_num);
 +void try_piece(int x, int level);
 +int board_index(int find_num, int start_pos);
  
  
  
 @@ -62,11 +62,11 @@ void print_board(void)
    printf("\n");
  }
  
 -int board_index(int find_num)
 +int board_index(int find_num, int start_pos)
  {
    int i;
  
 -  for(i=0; i<77; i++){
 +  for(i=start_pos; i<77; i++){
      if(board[i] == find_num){
        return i;
      }
 @@ -74,12 +74,12 @@ int board_index(int find_num)
    return 0;
  }
  
 -void try_piece(int level)
 +void try_piece(int x, int level)
  {
 -  int i, j, k, x;
 +  int i, j, k;
  
    try_counter++;
 -  x = board_index(0);
 +  x = board_index(0, x);
    for(i=0; i<12; i++){
      if(pieces[i].used == 1){continue;}
      for(j=0; j<pieces[i].formsize; j++){
 @@ -101,7 +101,7 @@ void try_piece(int level)
          return;
        }
        // 次のピースを試す
 -      try_piece(level + 1);
 +      try_piece(x + 1, level + 1);
        // ピースを戻す
        for(k=0; k<5; k++){board[x + pieces[i].form[j].pos[k = 0;}
        pieces[i].used = 0;
 @@ -112,7 +112,7 @@ void try_piece(int level)
  int main(int argc, char **argv)
  {
    init_board();
 -  try_piece(0);
 +  try_piece(0, 0);
    printf("解合計: %d\n", counter);
    printf("操作数: %d\n", try_counter);
  }

ほぼ1秒で完了した!

$ gcc -O2 pentomino.c
$ time ./a.out
解合計: 2339
操作数: 10385817

real	0m1.066s
user	0m1.049s
sys	0m0.016s
ループで回す
  • でもまだ1秒切れてない...。
  • あともう少しなんだけど。
  • board_index()関数を最適化してしまったので、
  • 工夫の余地が残るのは、以下の部分しかない。
  • しかしどう考えても、(A)から(B)まで、これ以上効率的な書き方は想像できない...。
...中略...
void try_piece(int x, int level)
{
  int i, j, k;

  try_counter++; // ......(A)
  x = board_index(0, x);
  for(i=0; i<12; i++){
    if(pieces[i].used == 1){continue;}
    for(j=0; j<pieces[i].formsize; j++){ // ......(B)
      if(board[x + pieces[i].form[j].pos[0]] || // ......(C)
         board[x + pieces[i].form[j].pos[1]] ||
         board[x + pieces[i].form[j].pos[2]] ||
         board[x + pieces[i].form[j].pos[3]] ||
         board[x + pieces[i].form[j].pos[4]]){continue;}
      // ピースを置く
...中略...
  • 残る部分は、(C)の長い条件判定のコード。
  • ピースが置けるかどうかを判定している。
  • 高速化を狙って、あえてループを使わずに展開したのだけど、試しにループで処理してみる。(半分ヤケクソ)
  • 以下の修正をして、コンパイルして、実行してみると...

 @@ -77,17 +77,16 @@ int board_index(int find_num, int start_pos)
  void try_piece(int x, int level)
  {
    int i, j, k;
 +  int sum;
  
    try_counter++;
    x = board_index(0, x);
    for(i=0; i<12; i++){
      if(pieces[i].used == 1){continue;}
      for(j=0; j<pieces[i].formsize; j++){
 -      if(board[x + pieces[i].form[j].pos[0 ||
 -         board[x + pieces[i].form[j].pos[1 ||
 -         board[x + pieces[i].form[j].pos[2 ||
 -         board[x + pieces[i].form[j].pos[3 ||
 -         board[x + pieces[i].form[j].pos[4){continue;}
 +      sum = 0;
 +      for(k=0; k<5; k++){sum += board[x + pieces[i].form[j].pos[k;}
 +      if(sum){continue;}
        // ピースを置く
        for(k=0; k<5; k++){board[x + pieces[i].form[j].pos[k = i + 1;}
        pieces[i].used = 1;

1秒切った!

$ gcc -O2 pentomino.c
$ time ./a.out
解合計: 2339
操作数: 10385817

real	0m0.974s
user	0m0.958s
sys	0m0.014s

  • 高速化の経過: 3時間以上 → 50分 → 20分(Python) → 3分(Python) → 1分(Ruby) → 1.2秒(C) → 1秒(C) → 0.97秒(C)!
  • 教訓: 場合によっては、コード展開するより、ループ回した方が早い!
    • ループ処理で高速化したわけではなく、条件判定から加算命令に変更した効果であった。
    • その証拠にループを展開した加算命令の方がさらに高速化された。(わずか0.02秒だけど)

 -      sum = 0;
 -      for(k=0; k<5; k++){sum += board[x + pieces[i].form[j].pos[k;}
 -      if(sum){continue;}
 +      if(board[x + pieces[i].form[j].pos[0 +
 +         board[x + pieces[i].form[j].pos[1 +
 +         board[x + pieces[i].form[j].pos[2 +
 +         board[x + pieces[i].form[j].pos[3 +
 +         board[x + pieces[i].form[j].pos[4){continue;}

$ gcc -O2 pentomino.c
$ time ./a.out
解合計: 2339
操作数: 10385817

real	0m0.949s
user	0m0.933s
sys	0m0.014s

  • さらなる高速化に気付いた。
  • board[ x + pieces[i].form[j].pos[0] ] が0であることは明白なので、計算を省略できる。

 @@ -83,8 +83,7 @@
    for(i=0; i<12; i++){
      if(pieces[i].used == 1){continue;}
      for(j=0; j<pieces[i].formsize; j++){
 -      if(board[x + pieces[i].form[j].pos[0 +
 -         board[x + pieces[i].form[j].pos[1 +
 +      if(board[x + pieces[i].form[j].pos[1 +
           board[x + pieces[i].form[j].pos[2 +
           board[x + pieces[i].form[j].pos[3 +
           board[x + pieces[i].form[j].pos[4){continue;}

$ gcc -O2 pentomino.c
$ time ./a.out
解合計: 2339
操作数: 10385817

real	0m0.909s
user	0m0.884s
sys	0m0.014s

最終的なC言語コード

#include  <stdio.h>

typedef struct {
  int       pos[5];
} MinoPos;

typedef struct {
  char      name;
  int       used;
  int       formsize;
  MinoPos   form[8];
} Piece;

Piece pieces[] =
{
    {'X', 0, 1, {{0,6,7, 8,14}}},
    {'U', 0, 4, {{0,1,2, 7, 9},{0,1, 7,14,15},{0,2, 7, 8, 9},{0,1, 8,14,15}}},
    {'W', 0, 4, {{0,1,8, 9,16},{0,1, 6, 7,13},{0,7, 8,15,16},{0,6, 7,12,13}}},
    {'F', 0, 2, {{0,1,8, 9,15},{0,6, 7, 8,13}}},
    {'Z', 0, 4, {{0,1,8,15,16},{0,5, 6, 7,12},{0,1, 7,13,14},{0,7, 8, 9,16}}},
    {'P', 0, 8, {{0,1,2, 7, 8},{0,7, 8,14,15},{0,1, 6, 7, 8},{0,1, 7, 8,15},{0,1,2,8, 9},{0,1, 7, 8,14},{0,1,7,8, 9},{0,6, 7,13,14}}},
    {'N', 0, 8, {{0,1,2, 9,10},{0,6, 7,13,20},{0,1, 8, 9,10},{0,7,13,14,20},{0,1,2,6, 7},{0,7,14,15,22},{0,1,5,6, 7},{0,7, 8,15,22}}},
    {'Y', 0, 8, {{0,1,2, 3, 8},{0,7,14,15,21},{0,5, 6, 7, 8},{0,6, 7,14,21},{0,1,2,3, 9},{0,7, 8,14,21},{0,6,7,8, 9},{0,7,13,14,21}}},
    {'T', 0, 4, {{0,1,2, 8,15},{0,7, 8, 9,14},{0,7,13,14,15},{0,5, 6, 7,14}}},
    {'L', 0, 8, {{0,1,2, 3, 7},{0,7,14,21,22},{0,4, 5, 6, 7},{0,1, 8,15,22},{0,1,2,3,10},{0,1, 7,14,21},{0,7,8,9,10},{0,7,14,20,21}}},
    {'V', 0, 4, {{0,1,2, 7,14},{0,7,14,15,16},{0,7,12,13,14},{0,1, 2, 9,16}}},
    {'I', 0, 2, {{0,1,2, 3, 4},{0,7,14,21,28}}},
};

int board[77];
int counter, try_counter;

void init_board(void);
void print_board(void);
void try_piece(int x, int level);
int board_index(int find_num, int start_pos);



void init_board(void)
{
  int i;

  for(i=0; i<77; i++){
    if(((i + 1) % 7) == 0 || i >= 70){
      board[i] = 100;
    }
  }
}

void print_board(void)
{
  int row, col;

  printf("No. %d\n", counter);
  for(row=0; row<6; row++){
    for(col=0; col<70; col += 7){
      printf(" %c", pieces[board[row + col] - 1].name);
    }
    printf("\n");
  }
  printf("\n");
}

int board_index(int find_num, int start_pos)
{
  int i;

  for(i=start_pos; i<77; i++){
    if(board[i] == find_num){
      return i;
    }
  }
  return 0;
}

void try_piece(int x, int level)
{
  int i, j, k;

  try_counter++;
  x = board_index(0, x);
  for(i=0; i<12; i++){
    if(pieces[i].used == 1){continue;}
    for(j=0; j<pieces[i].formsize; j++){
      if(board[x + pieces[i].form[j].pos[1]] +
         board[x + pieces[i].form[j].pos[2]] +
         board[x + pieces[i].form[j].pos[3]] +
         board[x + pieces[i].form[j].pos[4]]){continue;}
      // ピースを置く
      for(k=0; k<5; k++){board[x + pieces[i].form[j].pos[k]] = i + 1;}
      pieces[i].used = 1;
      // すべてのピースを置ききったらTrueを返す(recursiveコールの終了)
      if(level == 11){
        counter++;
        print_board();
        // ピースを戻す
        for(k=0; k<5; k++){board[x + pieces[i].form[j].pos[k]] = 0;}
        pieces[i].used = 0;
        return;
      }
      // 次のピースを試す
      try_piece(x + 1, level + 1);
      // ピースを戻す
      for(k=0; k<5; k++){board[x + pieces[i].form[j].pos[k]] = 0;}
      pieces[i].used = 0;
    }
  }
}

int main(int argc, char **argv)
{
  init_board();
  try_piece(0, 0);
  printf("解合計: %d\n", counter);
  printf("操作数: %d\n", try_counter);
}

2014-12-16

タブを閉じてもログアウトしても実行し続けるジョブにしておく

前回までにバックグラウンドでジョブを複数管理できるようになった。ところで...

  • ターミナルでタブを開く度に、あるいはsshでどこかの端末にログインする度に、
  • シェルが起動して、コマンドを入力・実行・出力する環境を整えてくれる。
  • そして、タブを閉じたり、ログアウトすると、シェルは終了する。
  • と同時に、通常はそのシェル上で実行されていたジョブも終了してしまう。
  • 不要なバックグラウンドジョブを漏れなく終了できるので、多くの場合これで良いのだけど、
  • 稀に、シェルが終了しても処理を続けて欲しい状況もある。
  • 特にsshでログインして時間のかかる処理(ダウンロードとか)をしている時などは、
  • 一旦ログアウトしたいんだけど、処理はそのまま続けて欲しいことが多い。

そんな時は、nohupコマンドを使うと幸せになれる。

環境

  • MacBook Pro Retina15 OSX 10.9.5
$ bash --version
GNU bash, version 3.2.53(1)-release (x86_64-apple-darwin13)
Copyright (C) 2007 Free Software Foundation, Inc.

基本

  • 新規タブを開いて、xclockコマンドで秒針付きの時計を表示してみる。
$ xclock -update 1

f:id:zariganitosh:20141212164311p:image:h194

  • タブを閉じると(command-W)、xclockも終了した。

  • 再びタブを開いて、xclockコマンドで秒針付きの時計を表示してみる。但し、コマンド先頭にnohudを付加する。
$ nohup xclock -update 1
appending output to nohup.out

f:id:zariganitosh:20141212164311p:image:h194

  • すると今度はタブを閉じても(command-W)、xclockは終了しない!
タブを閉じても終了しない理由
  • タブを閉じると(command-W)、そのタブ環境下のジョブには一斉にSIGHUPシグナルが送信されるらしい。
  • SIGHUPは、端末の回線が切断したという合図。(端末を操作しても何も反応しなくなる状態=hang upがシグナル名の由来と思われる)
  • SIGHUPを受け取ったジョブは、一般的に終了する。(但し、必ずしも終了する訳ではない。実装によっては別の動作をすることもある)
  • 一方、nohupを付加して起動されたジョブは、SIGHUPを無視するように振る舞う。
  • SIGHUPを無視したジョブはそのまま起動し続けようとする。だから、終了しない。

nohupがなくてもOK

  • シェルの環境にもよるが、実はOSXデフォルトのbashは、ログアウトしてもバックグラウンドジョブをそのまま維持してくれる。
    • 但し、exitあるいはlogoutコマンドでログアウトした場合のみ。(ログアウトが完了するとタブは自動的に閉じる)
    • command-WなどGUI操作でタブを閉じた場合は終了してしまう。

実験

  • 先程のxclockコマンドをバックグラウンドジョブとして実行してみる。
$ xclock -update 1 &
  • そして、exitコマンドで終了してみる。
$ exit
  • ログアウト完了と同時にタブも自動的に閉じる*1のだけど、xclockは表示され続けるのだ!
  • もちろん、秒針もちゃんと動き続けている。

f:id:zariganitosh:20141212164311p:image:h194


  • 一方、command-Wでタブを閉じた時は、残念ながら、xclockは終了してしまう...。
バックグラウンドジョブが終了しない理由
  • bashの設定項目であるhuponexitがオフ(デフォルト設定)になっていると、
$ shopt huponexit
huponexit      	off
  • ログアウトした時にSIGHUPシグナルは送信されなくなる。
      • 但し、command-WなどGUI操作でタブを閉じた場合は、SIGHUPシグナルは送信されるようだ。
      • exitあるいはlogoutコマンドでログアウトする時と、command-WなどGUI操作でタブを閉じる時で、送信されるシグナルが異なるのだ。

shopt huponexit=offの場合、ログアウトしてもSIGHUPシグナルが送信されない。だからジョブは終了しない。


  • 一方、huponexitがオンになっていると、
$ shopt -s huponexit
$ shopt huponexit
huponexit      	on
  • exitやlogoutでもSIGHUPシグナルは送信されてしまう。
  • つまり、ログアウトするとジョブは終了してしまう...。

nohupでもジョブが終了してしまう場合

  • ところで、nohubを付加しても、ジョブが終了してしまう場合がある。
  • nohupを付加して、xclockをフォアグラウンドジョブとして起動して、その後control-Zで一時停止しておく。
$ nohup xclock -update 1
appending output to nohup.out
^Z
[1]+  Stopped                 nohup xclock -update 1

$ exit
logout
There are stopped jobs.
  • この状態でログアウトしようとしても、stopped状態のジョブが存在するとして、ログアウトできない...。
  • そこでcommand-WなどのGUI操作で強引にタブ閉じてしまうと、xclockは終了してしまうのだ!

stopped状態のジョブは、nohupを付加しても終了してしまう。

nohupしたのにログアウトで終了してしまう理由
  • 実は、タブが閉じる時にジョブに送信されるシグナルは、SIGHUPだけではない。
  • stopped状態のジョブに対しては、SIGCONTとSIGTERMシグナルも送信されるらしい。
  • nohupコマンドが無視するのは、SIGHUPシグナルのみであり、
  • SIGCONTとSIGTERMシグナルについては無視されず、有効となるのだ。

stopped状態のジョブは、SIGTERMによって終了していたのだ。

nohup中の出力

  • nohupを付加した時のジョブが何らかの出力を伴う場合、その出力はnohup.outに追記されていく。
    • 標準出力・標準エラーの両方とも、デフォルトではカレントディレクトリのnohup.outに追記されるのだ。
    • "appending output to nohup.out"はそれを伝えてくれるメッセージだったのだ。
$ nohup date
appending output to nohup.out

$ cat nohup.out
2014年 12月15日 月曜日 15時57分33秒 JST

$ nohup date
appending output to nohup.out

$ cat nohup.out
2014年 12月15日 月曜日 15時57分33秒 JST
2014年 12月15日 月曜日 15時57分51秒 JST

  • もちろん、出力先に任意のファイルを指定することもできる。
$ nohup date > std.out

$ cat std.out
2014年 12月15日 月曜日 16時07分09秒 JST

  • 標準出力と標準エラーを分けることもできる。
$ nohup date -x > std.out 2> err.out

$ cat err.out
date: illegal option -- x
usage: date [-jnu] [-d dst] [-r seconds] [-t west] [-v[+|-]val[ymwdHMS ... 
            [-f fmt date | [mm]dd]HH]MMcc]yy][.ss? [+format]

複数のコマンドをnohup

各コマンドの前にnohupを付加する方法
$ nohup command 1 & nohup command 2 &
  • command 1とcommand 2は、二つのバックグラウンドジョブとして並列処理される。
sh -c でスクリプトを渡す方法
$ nohup sh -c 'command 1 ; command 2' &
  • 一つのバックグラウンドジョブとして、command 1が完了してから、command 2が処理される。

$ nohup sh -c 'command 1 | command 2' &
  • 一つのバックグラウンドジョブとして、command 1とcommand 2をパイプで連携させる。

コマンド実行後にタブを閉じても終了しないジョブにする

  • nohupを付加しておけば、タブを閉じてもジョブは終了せず、処理は継続される。そのことはよく分かった。
  • nohupさえ付加しておけばすべてうまくいく。とても簡単なことだ。

しかし、それができない...。

  • コマンドを実行してなかなか処理が終わらずに、あとでnohupを付加しておけば良かったと後悔することがしばしば。
  • せっかく途中まで処理が進んでいるのに、nohupのために途中で終了して、最初からやり直すなんて...。

そんな後悔をしないために、disownコマンドを覚えておく!

  • コマンドを実行して、予想外に時間がかかると気付いたら、disownを実行しておくだけでOK。
    • 引数にはジョブを指定する。(複数指定OK)
    • 引数を省略すると、カレントジョブ(%%または%+)が指定されたことになる。
$ xclock -update 1 &
[1] 58303

$ disown
  • disownを実行するだけで、タブを閉じてもログアウトしても、このジョブは終了せずに処理を継続するのだ!

  • ちなみに-aオプションを指定すると、シェル管理配下のすべてのジョブを指定したことになる。
$ disown -a
disownコマンドは何をしているのか?
  • disownしたジョブは、シェルのジョブ管理テーブルから取り除かれるようだ。
    • だから、disownしてしまうとjobsコマンドに表示されなくなる。
    • fg・bgなどのコマンドで操作できなくなる。
  • タブを閉じた時のSIGHUPシグナルは、ジョブ管理テーブルに存在するジョブだけに送信されるらしく、
  • そのため、disownしたジョブには、タブを閉じてもSIGHUPシグナルが送信されない。
  • だからタブを閉じても終了しないのだ。

disownコマンドは、シェルの管理配下のジョブとの関係を断つ!

      • disownには「自分の所有物であると認めない」「縁を切る」「勘当する」といった意味合いがある。
      • その意味のとおり、まさにシェルが指定したジョブと縁を切った状態になるのだ。
disownしたその後
  • 一旦disownしてしまったジョブを、再びシェルの管理配下に組み入れる方法を、自分は知らない。
  • よって、disownしたジョブをやっぱり終了させたくなったら...

プロセスIDを指定してkillコマンドを実行するしかないのだ。

$ xclock -update 1 &
[1] 65595

$ disown

$ kill 65595

  • しかし、タブを閉じてしまったら、プロセスIDさえ分からないかも...

その場合はプロセス名を思い出してkillallコマンドを実行してみる。

$ xclock -update 1 &
[1] 72053

$ xclock -update 1 &
[2] 72158

$ killall xclock
[1]-  Terminated: 15          xclock -update 1
[2]+  Terminated: 15          xclock -update 1
    • killコマンドはプロセスIDを必要とするが、killallコマンドはプロセス名を指定するのだ。
    • 但し、一致するプロセス名が複数あると、そのすべてを終了してしまう...。

  • だからkillallする前に、指定した名前にヒットするプロセスを調べておいた方が良いかもしれない。
$ ps -ax|grep xclock
72581 ttys005    0:00.00 grep xclock
ジョブ管理テーブルに残したままdisownする
  • 以上のように、一旦シェルの管理配下から取り除かれてしまったジョブを操作するのは非常に面倒である。
  • そこで、-hオプションを指定してdisownコマンドを実行しておけば、
  • タブを閉じるまでは、そのシェルでジョブ管理を続けられるのだ!
$ xclock -update 1 &
[1] 74522

$ disown -h

$ jobs
[1]+  Running                 xclock -update 1 &

$ fg
xclock -update 1
^C
      • ジョブ管理を続けながらSIGHUPシグナルのみ無視するので、おそらくnohupコマンドと同等?
      • 結局タブを閉じてしまえばプロセスIDで管理するしかないのだけど、タブを開いている限りジョブ管理を続けられるのだ。

disownコマンドは-hオプションを指定しておいた方が幸せになれそう!

まとめ

  • exitあるいはlogoutでログアウトした場合は、バックグラウンドジョブはそのまま継続される。
$ xclock -update 1 &
$ exit
  • 但し、shopt huponexitがoffの場合(デフォルト設定)
    • `shopt -s huponexit`を実行すると、onになる。
    • `shopt -u huponexit`を実行すると、offになる。
$ shopt huponexit
huponexit      	off

  • タブを閉じてもジョブを継続したい場合は、nohupを付加して実行する。
$ nohup xclock -update 1 &

  • nohupを付加しても、stoppedなジョブは終了してしまう。
$ nohup xclock -update 1
appending output to nohup.out
^Z
[1]+  Stopped                 nohup xclock -update 1

  • 出力を伴うコマンドをnohupすると、カレントディレクトリのnohup.outに出力される。
$ nohup date
appending output to nohup.out

$ cat nohup.out
2014年 12月16日 火曜日 13時44分24秒 JST

  • コマンドを実行してから処理に時間がかかることに気付いたら...
  • control-Z、bg、disown -hによって、タブを閉じても継続するジョブとなる。
  • 特に理由がない限り、disownには-hオプションを指定しておく。
  • そうしておけば、タブを閉じるまではジョブ管理を続けられる。
$ xclock -update 1
^Z
[1]+  Stopped                 xclock -update 1

$ bg
[1]+ xclock -update 1 &

$ disown -h

$ fg
xclock -update 1

参考ページ

以下のページがたいへん参考になりました。感謝です!


タブを閉じた後のプロセスの行方

  • ターミナル.appでタブを開くと、タブごとに対応するシェルが起動した環境になる。
  • そこで何らかのコマンドを実行すると、コマンドはシェルの子プロセスとして実行されることになる。

f:id:zariganitosh:20141216152051p:image:w450

  • ところで、nohupやdisownされた子プロセスは、シェルが終了しても、処理を継続する。
  • すると、それらの子プロセスは、親なしのプロセスとなってしまう...。どうなってしまうのか?

  • 実はそのような場合、すべてのプロセスの源流であるプロセスID=1のlaunchdが里親となり、面倒を見てくれるのだ!

f:id:zariganitosh:20141216152135p:image:w450

*1:タブが自動的に閉じる理由は、ターミナル.app >> 環境設定 >> 設定 >> シェル >> シェルの終了時: 「シェルが正常に終了した場合は閉じる」という設定のため。

2014-12-12

裏と表のジョブを使い分ける

かつて...

  • 今どきのターミナルはタブやウィンドウをいくつでも開けるから、
  • フォアグラウンドでコマンド実行中に何か別のことがしたくなったら、新たにタブを開いて、そこでコマンドを実行すれば十分用が足りる。
  • バックグラウンドでコマンドを実行できることも知っているけど、体系的に理解してないのでうっかりミスを繰り返した。
    • うっかりバックグラウンドで処理していることを忘れていた...。
    • 処理中だと思っていたら停止していた...。
    • 終了したと思っていたら停止していた...。
    • 停止中のプロセスを再開するにはどうする?
    • やっぱり終了したいんだけど、どうすればいいのか?
  • そんなうっかりを繰り返すうちに、見えないバックグラウンドで実行して悩むくらいなら、常に状態が見えるフォアグラウンドにしておいた方が益し。
  • 好きなだけタブを開いてしまえ、という方針になってしまった。

しかし...

  • 何かの補佐で一時的にバックグラウンドにしておきたいことって、よくある。
    • manでマニュアル見ながら、コマンドオプションを指定したいとか、
    • git commitしてviでメッセージ編集中に過去のコミットメッセージの書き方を見たくなったとか、
    • Rubyで開発中はgem serverをずっと起動しておきたい(ログ不要)とか、
  • そんな時でもフォアグラウンド一筋な自分は、じゃんじゃんタブを開いて調子良く処理するのだけど、
  • その後閉じるの忘れて、ターミナルはタブだらけ...。
  • 元々どのタブで作業していたのかも見失ってしまったりする。

という訳で、フォアグラウンドとバックグラウンドを的確に切り替えて使いこなす技を身に付けるのだ。

七つ道具

  • たった7つのコマンドとキー操作を覚えるだけで、フォアグラウンドとバックグランドを自在に行き来できる。
&コマンドに&を付加することで、バックグラウンドで実行する
jobs稼働中のジョブを表示する
fg [ジョブ指定]ジョブをフォアグラウンドで実行する
bg [ジョブ指定]一時停止中のジョブをバックグラウンドで実行する
control-Zフォアグラウンドジョブを一時停止する
control-Cフォアグラウンドジョブを終了する
kill ジョブ指定orプロセス指定ジョブを強制終了・一時停止するなど
      • fg bgコマンドは、[ジョブ指定]を省略するとカレントジョブを指定したことになる。
      • killコマンドは、ジョブ指定orプロセス指定を省略できない。必ず指定すること。

ジョブとプロセスの違い

ジョブとプロセスという言葉が入り混じってきたので、その意味を明確にしておく。

  • プロセス=OSが管理する実行中のプログラムの単位
  • ジョブ=シェルが管理する実行中のプログラムの単位
  • プロセスは、OS環境の中で重複しない番号が割り振られる。どのシェルからも参照可能。
  • ジョブは、起動したシェルごとに番号管理される。起動したシェルのみ参照可能。(他のシェルは参照できない)
  • 複数のプロセスが連携して、一つのジョブを構成することもある。
    • 例:history|awk '{ print $2 }'|sort|uniq -c|sort -r|head
    • 上記ワンライナー(よく使うコマンドのベスト10を表示)は、計6つのプロセスが次々と連携して、一つのジョブを処理するのだ。

jobsコマンド

  • 実験用に3つのジョブをバックグラウンドで起動しておく。
$ gem server -l &>/dev/null &
$ git help log &
$ top &
  • jobsコマンドは、稼働中のジョブリストを返す。
$ jobs
[1]   Running                 gem server -l >&/dev/null &
[2]-  Stopped                 git help log
[3]+  Stopped                 top
  • 出力されたリストには以下のような意味がある。
JobIDカレント記号状態起動コマンド
[1] Runninggem server -l >&/dev/null &
[2]-Stoppedgit help log
[3]+Stoppedtop

ジョブの指定方法

  • JobID・カレント記号・起動コマンドのどれかを使って、任意のジョブを指定できる。
  • %に続けて入力することで、ジョブ指定になるのだ。
%番号
  • %番号で、JobIDを指定する。
$ fg %1 # gem server -l >&/dev/null &をフォラグラウンドへ
$ fg %2 # git help logをフォラグラウンドへ
$ fg %3 # topをフォラグラウンドへ
%カレントジョブ記号
  • %+は、カレントジョブを意味する。(%%もカレントジョブ)
  • %-は、一つ前のカレントジョブを意味する。
    • カレントジョブ=いちばん直近に操作(起動・停止・fg bgの切替など)されたジョブ
    • 一つ前のカレントジョブ=カレントジョブの前に操作されたジョブ
$ fg %- # git help logをフォラグラウンドへ
$ fg %+ # topをフォラグラウンドへ
$ fg    # topをフォラグラウンドへ(fg・bgコマンドでは、省略するとカレントジョブと見なされる)
%コマンド名
  • %コマンド名では、コマンド名で始まるジョブを指定できる。(前方一致検索)
  • ジョブを区別可能な最小限の入力でOK。
$ fg %gem # gem server -l >&/dev/null &をフォラグラウンドへ
$ fg %git # git help logをフォラグラウンドへ
$ fg %top # topをフォラグラウンドへ

  • %?コマンド名なら、コマンド名を含むジョブを指定できる。(部分一致検索)
$ fg %?server # gem server -l >&/dev/null &をフォラグラウンドへ

対話的なコマンドのバックグラウンド実行はできない

  • topやman・helpのページャーなど、対話的なコマンド実行中は、バックグラウンドに移行すると自動的に一時停止状態になる。
  • bgでバックグラウンド実行のシグナルを送っても、一時停止状態のまま。
  • 対話的なコマンドを実行状態にするには、フォアグラウンドに移行する必要があるのだ。

だから、上記のgit help logやtopコマンドは、バックグラウンドでは常にStopped状態になってしまうのだ!

ジョブ操作の例

バックグラウンドのプロセスを終了する
  • gem serverをバックグラウンドで起動しておく。
$ gem server -l &>/dev/null &
[1] 72740
  • [1]はジョブID、72740はプロセスID。
      • ちなみにバックグラウンドであっても、gem server -l &だけでは、動作中の出力がフォアグラウンドに表示されてしまう。
      • &>/dev/nullを付けることで、動作中の出力はすべて破棄される。
      • 必要ならファイルに出力することもある。例:gem server -l &> gem_server.log
  • gem serverをフォアグラウンドに呼び戻す。
$ fg
  • gem serverを終了する。
[control-C]
viでコミットメッセージ編集中にgit logを確認したい
  • git commitでviが起動してコミットメッセージ編集中、
$ git commit
  • 以前のコミット履歴を確認したくなったので、一時停止して、git logを確認して、
[control-Z]
$ git log --oneline
  • 再びviのコミットメッセージ編集中の状態に戻る。
$ fg

killコマンド

  • killコマンドはジョブやプロセスを強制終了する時によく使うのだけど、
  • より正確には、ジョブやプロセスに「シグナル」を送るコマンドなのだ。
  • 強制終了するシグナルを送信すれば、ジョブは終了する。
  • 強制終了以外にも様々なシグナルがある。
$ kill -l|awk '{print $1 $2"\n"$3 $4"\n"$5 $6"\n"$7 $8}'
 1) SIGHUP
 2) SIGINT	端末からの割込終了(control-C)
 3) SIGQUIT	終了とコアダンプ(control-\)
 4) SIGILL
 5) SIGTRAP
 6) SIGABRT
 7) SIGEMT
 8) SIGFPE
 9) SIGKILL	無視できない強制終了(kill -KILL)
10) SIGBUS
11) SIGSEGV
12) SIGSYS
13) SIGPIPE
14) SIGALRM
15) SIGTERM	強制終了(kill)
16) SIGURG
17) SIGSTOP	無視できない中断(kill -STOP)
18) SIGTSTP	端末からの中断(control-Z)
19) SIGCONT	再開(bg・fg)
20) SIGCHLD
21) SIGTTIN
22) SIGTTOU
23) SIGIO
24) SIGXCPU
25) SIGXFSZ
26) SIGVTALRM
27) SIGPROF
28) SIGWINCH
29) SIGINFO
30) SIGUSR1
31) SIGUSR2
  • 上記のとおり、control-Zやcontrol-C、bg、fgは、ジョブにシグナルを送っている。
      • fgについては、加えてフォアグラウンドジョブにする処理も行っていると思う。
  • 実行中のバックグラウンドジョブを一時停止するには、fgしてから、control-Zするのだけど、
  • kill -TSTP %ジョブ番号を指定すれば、いちいちフォアグラウンドせずに一時停止できるのだ。
$ sleep 100 &
[1] 78927

$ jobs
[1]+  Running                 sleep 100 &

$ kill -TSTP %%
[1]+  Stopped                 sleep 100
  • ジョブ番号は省略できないけど、stopコマンドをエイリアスで設定してみた。
$ alias stop='kill -TSTP'

図解

ここまでのジョブ操作を図にまとめると、以下のようになる。

  • 太字は、コマンドまたは[キー操作]。

f:id:zariganitosh:20141215110911p:image:w450

  • [control-Z]・[control-C]はキー操作。
  • キー操作はフォアグラウンドのジョブに対してのみ作用する。
  • 一方、フォアグラウンドで実行中はコマンドを受け付けてくれないので、
  • 一旦[control-Z]で一時停止してから、bgなどのコマンドで操作する。
  • ジョブ実行中に...
    • シェルのコマンド入力ができない状態=フォアグラウンドジョブ
    • シェルのコマンド入力ができる状態=バックグラウンドジョブ
  • よって、フォアグラウンドジョブは[control-Z]・[control-C]などのキー操作のみ受け付ける。コマンドは使えない。
  • 逆に、バックグラウンドジョブは常にコマンド(bg・fg・kill)で操作する。キー操作は使えない。

参考ページ

以下のページがたいへん参考になりました。感謝です!