実行環境によって利用するコマンドを選択する(シェルスクリプト)

課題

ウェブコンテンツをダウンロードするツールとして wget コマンドや curl コマンドが代表的だが、スクリプト実行環境でどちらが利用可能かあらかじめわからない。 このようなときはどのようにスクリプトを記述すれば良いか。

解決策

実行環境で利用できるコマンドをスクリプトの先頭で確認し、利用できるコマンドを変数として保存しておく。

以下のリポジトリスクリプトで実際にこの手法を用いている。 https://github.com/altarterminal/hobbytool

この問題が発生する原因は wget コマンドや curl コマンドが「標準的ではない」ことだ。 もっと具体的に言うと、wget コマンドや curl コマンドは POSIX に規定されていないからだ(※現実では POSIX に規定されていれば必ず安全というわけでもないのだが...)。 例えば Ubuntu を利用しているなら、wget を使うためのパッケージや curl を使うためのパッケージをインストールすることになるだろう。 どのコマンドを利用するかはその環境の作成者に依存してしまう。

それならば、どのコマンドを利用していてもスクリプトが動くように、スクリプト内で利用可能なコマンドを確認しよう。 コマンドの利用可否を確認するためには type コマンドを利用できる。 利用可能であれば type コマンドはゼロで終了し、利用不可能であればゼロ以外で終了する。 これをもって条件分岐を行うのだ。

if   type curl >/dev/null 2>&1; then
  wcmd="curl -sS -f -L -o"
elif type wget >/dev/null 2>&1; then
  wcmd="wget -q -O"
else
  echo "${0##*/}: wget or curl is needed" 1>&2
  exit 11
fi

上記のスクリプトではまず curl コマンドの存在を確認し、次に wget コマンドの存在を確認している。 どちらも存在しない場合はエラーメッセージを出力してスクリプトをエラー終了するようにしている。 curl コマンドまたは wget コマンドが存在する場合は、それを呼び出すためのコマンドラインを wcmd 変数に保存する。 コマンドごとに必要なオプションは異なるので、どちらが選択されても動作が同じになるようにそれぞれで適切なオプションを設定する。

上記の変数は以下のように評価してコマンドを呼び出している。

...
$wcmd "コンテンツ保存先ファイル名" "コンテンツURL"
...

ここで「$wcmd」をクォートしていないことに注意したい。 シングルクォーテーションは言わずもがな、ダブルクォーテーションもつけてはならない。 クォートをしてしまうと、シェルによる単語分割がスキップされてしまい、「curl」や「wget」の単語がそれとして認識されないからだ。

他の方法として、eval コマンドを利用してシェルの評価を2段階で行うようにしても良い。

所感

この記事に記載している内容は POSIX 原理主義における交換可能性に関するものである。 標準的ではない機能に関して、単一の実装に依存しないような記述とすることで、移植性を高めている。 実行環境に存在するか不安なコマンドを利用している場合は、スクリプトの先頭で確認を行い、利用が不可能ならばその場でエラー終了すると安全だ。

映画『かがみの孤城』米国版でのこころの願いごと

はじめに

今までにいくつかの媒体でかがみの孤城を鑑賞しました。 最近鑑賞したのは米国版映画(Blu-ray)になりますが、その中でのこころの願いごとの表現が気に入ったので、他の媒体と比較して紹介します。

比較

媒体 表現
日本版映画 どうかアキちゃんを助けて下さい。ルール違反をなかったことにしてください。
日本版漫画 どうか、アキを助けてください。アキのルール違反を、なかったことにしてください。
日本版小説 どうか、アキを助けてください。アキのルール違反を、なかったことにしてください。
英国版小説 Please, save Aki. Please forget that she broke the rules.(どうかアキを助けてください。ルール違反を忘れてください。)
米国版小説 Please, save Aki. Please forget that she broke the rules.(どうかアキを助けてください。ルール違反を忘れてください。)
米国版映画 My wish is for Aki to be saved from her fate. Undo the fact that she broke the rules, please.(わたしの願いは、アキがその運命から救われることです。 彼女のルール違反をなかったことにしてください。)

所感

米国版映画だけ他とは明らかに表現に違いがあります。

他の媒体では「アキを助けてください」とだけ述べています。 状況を鑑みると「オオカミに食べられたこと」に関して述べているものと受け取れます。

一方で米国版映画では「アキが運命から救われること」を願っています。 これはもちろん「オオカミに食べられたこと」も含んでいるとは思いますが、その先の未来のことへ言及しているようにも聞こえました。 つまり「オオカミに食べられたこと」を単純になかったことにするだけでは、アキが生きていくのは難しかったのではないかということです。 ある意味、他のメンバの「エゴ」でアキを取り戻したとしても、以前と同じようにアキが再び現実に戻ってしまったら、悲劇が繰り返されたかもしれません。

しかし、そのような結果にはなりませんでした。 全員の真実を垣間見ることで、こころは全員の時間がズレていることに気づき、同じ世界に生きていることに気づき、お互いに助け合えることを確信しました。 そしてアキが希望を持って生きているよう、その事実を必死に訴えて、アキを呼び戻すのでした。 こころがアキを救うことができたのは、願いの鍵のおかげというよりも、こころ自身のチカラがあってこそだったのかもしれません。

米国版映画では、上記のことを明確に示してくれていたのかなあと思いました。 もちろん他の媒体においても、このことを暗黙的に読み取ることは十分に可能でしょうけれど。

映画『かがみの孤城』米国版Blu-ray

はじめに

かがみの孤城が好きすぎて、米国版のBlu-rayを購入し、本日手元に到着しました。 米国での発売は2023/09/26で、その前に予約購入をしていました。 到着するまでにけっこうな日数がかかってしまいましたが、無事に到着して鑑賞できたので、一安心です。

購入方法

面倒な手続きはあまり自分ではやりたくなかったので、諸々をケアして日本語で対応してくれるサイトで購入しました。 以下のサイトになります。

https://www.fantasium.com

米国版DVDオンラインショップと銘打ってありますがBlu-rayもあります。 サイトにアクセスしてわかる通り、日本語で検索・購入することができます。 一部、届け先情報の登録時等、自身で英語で入力する必要はありますが、基本的に日本語で手続きできるので便利です。

購入時に発送方法の選択肢が複数用意されています。 今回は最も安い「USPS(国際航空郵便)」を選択しました。 他の方法がけっこう高かったので...(汗)。 その結果、発売から2週間ほど経過してからの遅い到着になりました。

気になるお値段は約23ドルです(定価)。お買い得です。円安?シラナイデスネ...

視聴しての所感

やはり気になっていたのは声優さんでしたが、それほど違和感なく視聴することができると思います。 こころちゃんの声優は、日本版の當真さんと同じく、儚い性質の声を表現してくれている感じがしました。 オオカミ様は、日本版の芦田さんよりもやや子供っぽさが出ている感じでしょうか。

英語で鑑賞していても、字幕を見ればなんとなく内容は追っていけると思います。 難しい英単語や英語表現はあまりありません。 気になる方はぜひご購入ください。

メニュー画面

排他制御を実現する(シェルスクリプト)

課題

同時に複数のプロセスが処理をしてはならないクリティカルセクションを用意したい。 シェルスクリプトではどのように排他制御を実現することができるのか。

解決法

mkdir コマンドを利用する。

排他制御と mkdir コマンドには一見して関連がないように思えるかもしれない。 排他制御として利用できるのは mkdir コマンドの「すでに同名のディレクトリ(ファイル)が存在しているとき失敗する」性質である。


最初に、比較として、排他制御を行わないために意図通りに動作しない例を示す。

以下のスクリプトでは、2つのバックグラウンドプロセスを作成し、それらが同一のファイルに対して書き込みを行う。 sleep コマンドは本記事用に「意図通りに動作しない」確率を上げるために挿入した。

touch target.txt

(
  echo "start from p1" >> target.txt
  sleep 0.01
  echo "end   from p1" >> target.txt
) &

(
  echo "start from p2" >> target.txt
  sleep 0.01
  echo "end   from p2" >> target.txt
) &

wait

本来意図した動作結果は以下のどちらかだ。 つまり、片方のプロセスがファイルへの出力を完全に終えたあとに、もう片方のプロセスがファイルへの出力を行うということを意図していた。

$ cat target.txt 
start from p1
end   from p1
start from p2
end   from p2
$ cat target.txt 
start from p2
end   from p2
start from p1
end   from p1

しかし、最初に示したスクリプトを実行すると、以下のようになってしまう。 このようになる原因は、もちろん、排他制御を行っていないからだ。 片方のプロセスが「start ~」の書き込みを行って sleep している間に、もう片方のプロセスが書き込みを始めてしまっている。

$ cat target.txt 
start from p1
start from p2
end   from p1
end   from p2


上記の状況に対して、今度は排他制御を行って、意図通りの動作を実現しよう。

さっそくだが排他制御を実現するスクリプトを以下に示す。 キーとなるのは冒頭で説明した mkdir コマンドの存在だ。

ちなみにこのスクリプトでは動作状況を把握できるようにいくつか echo コマンドを挿入している。 また、今回は mkdir コマンドの引数になっている lockdir 変数の値については特に触れない。 ごちゃごちゃしているが、既存のディレクトリ名と被らないようにするための記述である。

lockdir="${TMPDIR:-/tmp}/${0##*/}.$(date +%Y%m%d_%H%M%S).$$"

touch target.txt

(
  while true
  do
    if mkdir "$lockdir" 1>/dev/null 2>&1; then
      break;
    else
      echo "p1: lock failed"
    fi
  done

  echo "p1: lock succeed"

  echo "start from p1" >> target.txt
  sleep 0.01
  echo "end   from p1" >> target.txt

  rmdir "$lockdir"
) &

(
  while true
  do
    if mkdir "$lockdir" 1>/dev/null 2>&1; then
      break;
    else
      echo "p2: lock failed"
    fi
  done

  echo "p2: lock succeed"

  echo "start from p2" >> target.txt
  sleep 0.01
  echo "end   from p2" >> target.txt

  rmdir "$lockdir"
) &

wait

それぞれのプロセスの先頭部分に「ロックを獲得する」ことを表すコードを配置している。 それはつまり mkdir コマンドを内包した while ループだ。 lockdir を作成することに成功したら、while ループを抜けてファイル出力のコードに進むことができる。 逆に、lockdir を作成することに失敗すると、while ループを抜けることができず、何度も lockdir の作成を試みることになる。 何度も試みるという意味では、この排他制御はスピンロック式であるということに注意したい。

先に「ロックを獲得した」プロセスはファイル出力を行い、最後に、先頭部分で作成した lockdir の削除を行う。 これはつまり「ロックを解放する」ことに対応する。 その後、もう片方のプロセスが「ロックを獲得する」ことに成功する。 そして先のプロセスと同様の処理をたどる。

排他制御を実現するこのスクリプトを実行した結果を以下に示す。 スクリプト実行時の標準出力は、それぞれのプロセスが「ロックを獲得する」までの過程を表している。 とくに後続のプロセス(p2)が 11 回も「ロックの獲得」を試みたことがわかるだろう。 そしてファイルの内容を見ると、意図通りの結果となっていた。

$ ./mutex.sh 
p1: lock succeed
p2: lock failed
p2: lock failed
p2: lock failed
p2: lock failed
p2: lock failed
p2: lock failed
p2: lock failed
p2: lock failed
p2: lock failed
p2: lock failed
p2: lock failed
p2: lock succeed
$ cat target.txt 
start from p1
end   from p1
start from p2
end   from p2

所感

排他制御は大変なので、なるべくなら使わない方法を探しましょう

入力データの追加を監視する(シェルスクリプト)

課題

次々と入力されるデータを監視し、そのデータに対して処理を行いたい。

解決策

tail コマンドの -f オプションを利用する。 これはファイル(または名前付きパイプ)に追加されるデータをリアルタイムで監視して出力するものである。

tail コマンドは POSIX 規定のコマンドであり、その -f オプションも同じく POSIX 規定のオプションである(tail - The Open Group Publications Catalog)。 tail コマンドの基本的な機能はファイル末尾の n 行を出力することである。 その延長線上の意味で、ファイル末尾に次々と追加されるデータを出力するのは自然なことだろう。


まず tail コマンドの基本的な使い方を確認してみる。 以下のようなファイルがある。

$ cat seq.txt 
1
2
3
4
5
6
7
8
9
10
11

オプション無しで tail コマンドを呼び出したとき、-n 10 のオプションが指定されたものとみなされる。 つまりファイル末尾の10行が表示される。

$ tail seq.txt
2
3
4
5
6
7
8
9
10
11


次に本題の -f オプションの動作を確認する。 まずは -f オプションの説明を引用しておく。

If the input file is a regular file or if the file operand specifies a FIFO, do not terminate after the last line of the input file has been copied, but read and copy further bytes from the input file when they become available.

先程と同様のファイルに対して、今度は -f オプション付きの tail コマンドを呼び出してみる。 実際のターミナルでないとわかりにくいかもしれないが、ファイル末尾の10行が表示された後にプロンプトが表示されず、待機状態になる。 この状態でシェルに対してさらなるコマンドを入力することはできない。

$ tail -f seq.txt
2
3
4
5
6
7
8
9
10
11
  (← プロンプトは表示されない)

この状態で別のターミナルを開く。 当該のファイルに対してデータを追加するために以下のようにコマンドを打つ。

$ seq 12 15 >> seq.txt

この瞬間、もとのターミナルの表示は以下のように変化しているはずだ。

$ tail -f seq.txt 
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  (← プロンプトは表示されない)

つまり新しくファイルに追加した12 13 14 15の行が tail コマンドから出力されたのだ。 このように tail コマンドの -f オプションはターゲットに追加されたデータをリアルタイムで出力してくれる。


この tail コマンドの後ろに別のコマンドをパイプラインで接続すれば、リアルタイムで入力されるデータに対して処理を施すことが表現できる。

例えば以下のような状況を考えてみよう。 名前付きパイプを作成して、そのパイプへの入力を tail コマンドによって監視し、後ろに grep コマンドを繋げてみる。

$ mkfifo logfifo
$ tail -f logfifo | grep error 
  (← プロンプトは表示されない)

別のターミナルを開いて以下のように数列を入力してみよう。 このとき、もとのターミナルには何も変化はない。

$ seq 1 10 >> logfifo

次に以下のような error の文字列を含んだ行を入力してみよう。

$ echo '11 error' >> logfifo 

今度はお察しの通り、もとのターミナルの表示に変化があった。

$ tail -f logfifo | grep error
11 error
  (← プロンプトは表示されない)

この例では次々と入力されるデータに対して、特定の文字列を含む行を grep コマンドで抽出した。 これは稼働中のシステムのログのエラー情報を抽出することを模擬したものだ。 抽出したデータのこのあとの用途はもちろん自由だ。

所感

その瞬間のファイル末尾を表示するという tail コマンドの基本機能は他のコマンドでも代替可能だ。 しかし -f オプションを指定した tail コマンドの挙動は、他のコマンドで再現することは困難であり、覚えておく価値は大いにある。

ターミナルに蒸気機関車を走らせる(シェルスクリプト)

課題

会議中に暇になったのでターミナルでアニメーションを表示したい。

解決策

sl コマンドを利用する。

sl コマンドはジョークコマンドの一種である。 sl コマンドを実行するとターミナル上にアスキーアート蒸気機関車が走る。

sl コマンド実行時の様子

たいていの場合はインストールが必要である。 Mac の Homebrew を利用している人は以下のようにしてインストールできる。

brew install sl


sl コマンドが有効になっている場合、頻繁に利用される ls コマンドの打ち間違いによってアニメーションが流れてしまうので、非常にやっかいである。

sl コマンドの使い方を調べようとして sl --help と打ち込んでもアニメーションが実行されてしまうので、使い方を知りたい場合は man sl とすること。

さらに sl コマンドは SIGINT シグナルの割り込みが無効になっているため、実行中に Ctrl + C を押しても実行を停止することができない。 困ったときは SIGTSTP シグナル、つまり Ctrl + Z を押して一時的にその場を収めることが望ましい。


sl コマンドの開発の発端は日本人らしい。この精神は見習いたい。

所感

わたしは社用のPCにはインストールしていません。

行番号を付加する(シェルスクリプト)

課題

後続の処理で利用するためにパイプラインを流れるデータに行番号を付加したい。

解決法

以下のような方法がある。

  • nl コマンドを利用する
  • awk コマンドを利用する
  • sed コマンドを利用する
  • cat コマンドを利用する

nl コマンドを利用する

nl コマンドは正真正銘の POSIX 規定のコマンドである(nl - The Open Group Publications Catalog)。 しかし、イマイチ知名度が低いせいか、自分の周囲ではこれを使う人をあまり見ない。

以下は利用例である。 ただし、データに空行が含まれている場合、デフォルトでは空行での行番号の付加がスキップされてしまう。 それでは困るというときは -b オプションで a (Number all lines)を指定すると良い。

$ find /usr/bin | nl | head
     1 /usr/bin
     2 /usr/bin/ptargrep5.30
     3 /usr/bin/uux
     4 /usr/bin/cpan
     5 /usr/bin/loads.d
     6 /usr/bin/htmltree5.30
     7 /usr/bin/write
     8 /usr/bin/lwp-mirror5.30
     9 /usr/bin/indent
    10 /usr/bin/bzip2recover

awk コマンドを利用する

もちろん awk は行番号を付加する専用のコマンドではない。 awk の機能のひとつとして行番号の付加に役に立つものがある。

awk 組み込み変数「NR」は現在の入力行の番号を保持している変数である。 一行入力されるごとにこの変数の値は自動的にセット(インクリメント)される。 したがって、この変数の値と入力行の内容を合わせて出力すれば良い。

$ find /usr/bin | awk '{print NR, $0}' | head
1 /usr/bin
2 /usr/bin/ptargrep5.30
3 /usr/bin/uux
4 /usr/bin/cpan
5 /usr/bin/loads.d
6 /usr/bin/htmltree5.30
7 /usr/bin/write
8 /usr/bin/lwp-mirror5.30
9 /usr/bin/indent
10 /usr/bin/bzip2recover

sed コマンドを利用する

あまり有名ではないが sed コマンドには行番号を付加するコマンド(サブコマンド)がある。 ただし、出力の形式は以下のようになっているので、求める形式に変形するためにはひと工夫が必要だ。 つまり、その行の直前の行に行番号が出力されるのだ。

$ find /usr/bin | sed '=' | head
1
/usr/bin
2
/usr/bin/ptargrep5.30
3
/usr/bin/uux
4
/usr/bin/cpan
5
/usr/bin/loads.d

これの処理方法もいくつかあるが、せっかく sed コマンドを利用しているので同じく sed コマンドで整形してみよう。 sed のパターンスペースに2行のデータ(つまり行番号と行コンテンツ)を読み込んで、それらを連結している改行コードをスペースで置換すれば、求める形式となる。

$ find /usr/bin | sed '=' | sed 'N;s/\n/ /' | head
1 /usr/bin
2 /usr/bin/ptargrep5.30
3 /usr/bin/uux
4 /usr/bin/cpan
5 /usr/bin/loads.d
6 /usr/bin/htmltree5.30
7 /usr/bin/write
8 /usr/bin/lwp-mirror5.30
9 /usr/bin/indent
10 /usr/bin/bzip2recover

cat コマンドを利用する

一部の実装では cat コマンドで行番号を付加するようなオプションがあるらしい。 ただしこのオプションは POSIX 非規定であるので推奨しない。

$ find /usr/bin | cat -n | head
     1 /usr/bin
     2 /usr/bin/ptargrep5.30
     3 /usr/bin/uux
     4 /usr/bin/cpan
     5 /usr/bin/loads.d
     6 /usr/bin/htmltree5.30
     7 /usr/bin/write
     8 /usr/bin/lwp-mirror5.30
     9 /usr/bin/indent
    10 /usr/bin/bzip2recover

所感

行番号をつけることは、それそのものの値より、入力順のマーカーをつけるという意味で重要である。 UNIX的なシェルスクリプトの動作は行指向で設計することが基本だ。 行番号を付加することは、行指向の処理に寄り添うツールの一つとして必ず役に立つだろう。