仕様書・設計書の必要論と不要論の不毛な議論を避けるためのヒント

仕様書や設計書の「要る/要らない」で議論になることがあるが、往々にして不毛なのでここに記しておく。

実のところ「仕様書や設計書は必要だ」という意見も「仕様書や設計書なんて不要だ」という意見も、根本的には同じ問題を出発点としている。

それは「必要な仕様書・設計書を、必要なときに、必要なだけ」というジャスト・イン・タイムの原則から外れている、ということだ。

「仕様書や設計書は必要だ」という人は、「必要なドキュメントが存在しない」「必要なときに手に入らない」「内容が不足している」という経験をした人であることが多い。

「仕様書や設計書なんて不要だ」という人は、「不要なドキュメントを作らされた」「まだ必要ではないのに作らされた/不要になった後に作らされた」「必要量を超えて作らされた/不要な内容まで書かされた」という経験をした人であることが多い。

結局はどちらも「必要な仕様書・設計書を、必要なときに、必要なだけ」が成立していないのだ。

問題の根っ子は同じなのに、適正レベルからの逸脱の方向の違いというコンテキストの差異にしか目を向けていないために、必要論と不要論に分かれてしまうのだ。不毛だと言わざるを得ない。

そろそろ「美しいコード」って言うの止めようぜ

――と題名の通りの感想を抱いたのだが、「美しいコード」の代替となる上手い言い方が思い浮かばないのである。

いや、記事の内容に異論はないというか、私自身は汚いコードに拒否反応を示す人であるし、未だに「もっと良いコードを書きたいなあ」という欲を持っている。

なんだけど、こう、何というか、「美しいコード」の「美しい」という言葉ゆえに誤解されてしまっている側面があるように思うのだ。

どうも「美しさなんか関係ないだろ!」と否定する人は、「美しい」という言葉ゆえに脊髄反射的に「気難しい職人のこだわり」みたいな解釈をしているように感じられる。どことなく「常のものではない」という意識がある。だから「(常の側にいる私たちには)関係ない」と切り捨ててしまう。

でも実のところ、「美しいコード」の初級〜中級というのは、製造現場に例えると「4S5Sが定着している状態」だと思うのだ。目的は「安全性・効率化・品質向上」で、手段の1つが「4Sないし5S」。これがプログラミングでは、目的は「RASIS」であり、手段の1つが「美しいコード」、という感じだろうか。

「3M(ムダ・ムラ・ムリ)を解消する手段の1つとしての4S/5S」からのアナロジーとして「システム開発の3Mを解消する手段の1つとしての『美しいコード』」と捉えると、「美しいコード」は割と基礎レベル寄りの手法であるし、日常的に定着していないとマズい代物だということになる。

つまり「美しいコード」は常のものだ。それどころか4Sや5Sのような土台・基礎であり、出来てないと「え、そんなこともできないの?」と言われて恥をかくレベルのモノだ。

だから「『美しいコード』って、実は製造現場でいう4Sや5Sなんですよ。基礎中の基礎ですよ」と明確化すると同時に、今後似たような誤解が起こらないように、対外的には「美しいコード」という言葉を封印して、別の言い方をすべきだと思う。

問題は「美しいコード」に代わるパワーワードが思い浮かばないことだ。「良いコード」だとインパクトに欠けるしなあ。

2〜3世代前のOS X/macOSの実行環境を整えるためのメモ

最新版を含む過去4世代のOS X/macOSをbootできる環境を整えているのだが、環境構築の過程で得られた知見を残しておく。

なお、このメモはOS X Mavericks(10.9)以降を前提としている。執筆時点での最新のOSはmacOS High Sierra(10.13)である。

どこまで古いOSをインストールできるか?

Windowsの場合、例えばWindows 10プリインストールPCであっても、Windows 7に対応したデバイスドライバが公開されているなら、Windows 7でも問題なく動作する可能性が高い。しかしMacはそうではない。

現在のMacはハードウェアとOS双方を含めてクローズドなシステムだ。そのため、Mac本体の各デバイス用のドライバを独自に入手する術がない。OS付属のデバイスドライバを使用するしかないのである。

そのため論理的には、あるMacの機種に、その機種にプリインストールされていたものよりも古いOSを入れようとした場合、必要なデバイスドライバが古いOSに含まれていないために正常に動作しない、という可能性がある。

(もしかしたら、OSのインストーラ側にてガードがかけられていて、古いOSをインストールできないようになっているかもしれないが、試していないので不明である)

少なくとも、当該機種の発表時に搭載されていたOS以降のものを入れるべきだろう。例えば、購入時にOS X El Capitan(10.11)がインストールされていたMacには、El Capitan以降のOSのみ入れるべきだ。間違ってもMavericksを入れようとするべきではない。もしMavericksの動作環境を構築したいなら、古いMacを入手してインストールするか、ParallelsVMwareなどの仮想環境にゲストOSとしてインストールするべきだろう。

古いOSのインストール先

用途次第ではあるが、過去数世代のOSを入れるとなると、各バージョンごとにそこそこのディスクスペースが必要となる。なので内蔵ディスクではなく外付けHDDなどに入れるべきだろう。*1

古いOSのインストーラの入手方法

El Capitan以前のOSのインストーラは、過去にApp Storeからダウンロードしたことがあるのなら、App Storeの「購入済み」タブから再ダウンロードできる。

過去にダウンロードしたことがないために「購入済み」タブから再ダウンロードできない場合は、OS X El Capitan へアップグレードするには - Apple サポートに書かれているように、このリンク経由でApp StoreOS X El Capitanのページを開いて、そこからダウンロードすることができる。MavericksOS X Yosemite(10.10)のページは既に存在しないようなので、過去にダウンロードしたことがない場合、通常の手段で入手することはできなさそうだ。

macOS Sierra(10.12)以降は、App Storeからダウンロードしても、「購入済み」タブに表示されなくなった。

Sierraインストーラを入手したい場合は、macOS Sierra にアップグレードする方法 - Apple サポートに書かれているように、このリンク経由でApp StoremacOS Sierraのページを開いて、そこからダウンロードすること。

High SierramacOS High Sierra にアップグレードする方法 - Apple サポートに書かれているようにこのリンク経由でダウンロードできるが、El CapitanないしSierra上のApp Storeからダウンロードしないと、全コンポーネントが含まれた状態のインストーラを入手できないようだ。

MojaveはmacOS Mojave にアップグレードする方法 - Apple サポートに書かれているようにこのリンク経由でダウンロードできる。

なお上記のいずれか場合も、過去にダウンロードしたインストーラがディスク上に残っていると、再ダウンロードができない可能性がある。この場合は、ディスク上のインストーラを削除してから再ダウンロードを試みること。

特にありがちなのは、例えばSierraの公開直後に10.12のインストーラをダウンロードしていて、1年後のHigh Sierra公開前後にSierraの最終版である10.12.6のインストーラをダウンロードしようとして、10.12のインストーラが残っていたために再ダウンロードできない――というパターンである。注意すること。

インストール用メディアの作成

ダウンロードしたインストーラmacOS上でアプリケーションとして実行可能だが、インストーラに付属するcreateinstallmediaを使用してインストール用メディアを作成することも可能だ。対象メディアはUSBメモリや外付けHDDだ。

作成方法についてはmacOS の起動可能なインストーラを作成する方法 - Apple サポートを参照のこと。

(前述のサポート情報にも書かれているが)High Sierraインストーラは、El CapitanないしSierra上のApp Storeからダウンロードしないと、全コンポーネントが含まれた状態のものが入手できず、結果としてインストール用メディアを作成できないので、注意すること。

*1:Windowsユーザ向け補足:Windows PCでは、外付けディスクにWindowsを入れることは不自然というか、Microsoftのポリシーの関係で普通にはできない所業なのだが、一方でMacでは割と普通なことである。

書籍購入:『Optimized C++』

あとで書く。

Optimized C++ ―最適化、高速化のためのプログラミングテクニック

Optimized C++ ―最適化、高速化のためのプログラミングテクニック

書籍購入:『micro:bitではじめるプログラミング』

あとで書く。

micro:bitではじめるプログラミング ―親子で学べるプログラミングとエレクトロニクス (Make:PROJECTS)

micro:bitではじめるプログラミング ―親子で学べるプログラミングとエレクトロニクス (Make:PROJECTS)

ALSAでMIDIをループバックさせたい――ただしALSA RawMidi APIで!

ALSA C libraryのAPIを使用してMIDIの入出力を行うLinuxアプリがあるのだが、出力したMIDIをループバックさせて入力として読み込みたい――という話である。

macOSなら標準のIACドライバを、Windowsならサードパーティの仮想MIDIケーブル*1を使うパターンである。

今回のポイントは、ALSAのRawMidi interfaceのAPIを使用しているアプリである、という点だ。

ALSAMIDI APIは、RawMidi interfaceとSequencer interfaceの2種類が存在する。RawMidi interfaceはベタにMIDIバイスのポートを読み書きするためのOSS互換のAPIだ。一方のSequencer interfaceは高水準のAPIで、MIDIバイスだけでなくTimidity++のようなアプリケーション間でもMIDIの入出力を行えるようにデザインされている。そのため、MIDIシーケンサという抽象化されたオブジェクトのポートを読み書きするようになっている。

Linux上でのMIDIの取り扱いについての解説資料――例えばThe Linux MIDI-HOWTOとかLinuxビンボ道〜明日がALSA設定編とかArchLinuxのWikiに書かれているTimidity++の話題などは、Sequencer interfaceを使用しているアプリ向けの話が大半だ。

Sequencer interfaceを使用しているアプリなら、カーネルモジュールsnd-seq-dummyをロードすることで使用可能となるMidi Throughという仮想MIDIシーケンサを使用して、MIDIをループバックさせることができる。「Midi Through」という名前の通り、入力ポートに書き込まれたMIDIをそのまま出力ポートから読み出せるMIDIシーケンサで、macOSのIACドライバやWindowsの仮想MIDIケーブルと似たような要領で扱うことができる。

# snd-seq-dummyの組み込み方。
# 末尾の「ports=1」は、ループバック用のポートの数を指定する。
sudo modprobe snd-seq-dummy ports=1

UbuntuやRaspbian(Raspberry Pi)ではデフォルトで組み込まれているので、再度ロードする必要はない。

$ # 手元のRaspberry Pi 3 Model B(Raspbian 9.3 stretch)の場合。
$
$ aconnect -i   # 入力ポートを表示
client 0: 'System' [type=カーネル]
    0 'Timer           '
    1 'Announce        '
client 14: 'Midi Through' [type=カーネル]
    0 'Midi Through Port-0'
$
$ aconnect -o   # 出力ポートを表示
client 14: 'Midi Through' [type=カーネル]
    0 'Midi Through Port-0'
$ _

上記の例では、入力・出力ポートともに14:0を指定すればよい。

しかしRawMidi interfaceを使用しているアプリはMidi Throughを使えない。RawMidi interfaceで扱えるのはMIDIバイスだが、Midi Throughは仮想MIDIシーケンサだ。厄介なことに、RawMidi interfaceのMIDIバイスとSequencer interfaceのMIDIシーケンサは全く別の概念で、APIからして全く異なるのである。

$ # 手元のRaspberry Pi 3 Model B(Raspbian 9.3 stretch)の場合。
$
$ # aplaymidi -lでは、Sequencer interfaceで扱うことが可能な、
$ # MIDIシーケンサの入力ポートが表示される。
$ aplaymidi -l
 Port    Client name                      Port name
 14:0    Midi Through                     Midi Through Port-0
$ # Midi Throughが見つかる。
$
$ # amidi -lでは、RawMidi interfaceで扱うことが可能な、
$ # MIDIデバイスの入力ポートが表示される。
$ amidi -l
Dir Device    Name
$ # Midi Throughはおろか、何のMIDIデバイスの入力ポートも見つからない。
$ _

ではどうすればよいか? カーネルモジュールsnd-virmidiを組み込んで仮想MIDIバイスを使用できるようにした上で、aconnect(1)で仮想MIDIバイスの入力ポートを出力ポートを接続すればよい。これで、仮想MIDIバイスの入力ポートに書き込んだMIDIコマンドを出力ポートから読み出すことが可能となる。

# snd-virmidiの組み込み方。
#
# 末尾の「index=1」は、仮想MIDIデバイスに付与するカード番号である
# (RawMidiのデバイスは「カード番号,デバイス番号,サブデバイス番号」で指定する仕組み)。
# カード番号は0から始まる通し番号で、
# システムが認識したMIDIデバイスごとに自動的に付与されている。
# 「amidi -l」などでシステムが認識しているMIDIデバイスを確認したうえで、
# それらと重複しない番号を割り当てること。
sudo modprobe snd-virmidi index=1

snd-virmidiを組み込むと、既定では4個の仮想MIDIバイスが使用可能となる。

$ # 手元のRaspberry Pi 3 Model B(Raspbian 9.3 stretch)の場合。
$
$ # snd-virmidiをロードする前。
$ amidi -l
Dir Device    Name
$
$ # snd-virmidiをロード。
$ sudo modprobe snd-virmidi index=1
$
$ # snd-virmidiをロードした後。
$ amidi -l
Dir Device    Name
IO  hw:1,0    Virtual Raw MIDI (16 subdevices)
IO  hw:1,1    Virtual Raw MIDI (16 subdevices)
IO  hw:1,2    Virtual Raw MIDI (16 subdevices)
IO  hw:1,3    Virtual Raw MIDI (16 subdevices)
$ # 「index=1」を指定したのでhw:1に割り当てられている
$ # (もし「index=2」だったらhw:2,0〜hw:2,3が割り当てられただろう)。
$ # 「16 subdevices」なので、例えばhw:1,0,0〜hw:1,0,15が使用できる。
$ _

興味深いことに、snd-virmidiの仮想MIDIバイスは、RawMidi interfaceで扱えるMIDIバイスであると同時に、Sequencer interfaceで扱えるMIDIシーケンサとしても振る舞う。

$ # 手元のRaspberry Pi 3 Model B(Raspbian 9.3 stretch)の場合。
$
$ aconnect -i   # 入力ポートを表示
client 0: 'System' [type=カーネル]
    0 'Timer           '
    1 'Announce        '
client 14: 'Midi Through' [type=カーネル]
    0 'Midi Through Port-0'
client 20: 'Virtual Raw MIDI 1-0' [type=カーネル,card=1]
    0 'VirMIDI 1-0     '
client 21: 'Virtual Raw MIDI 1-1' [type=カーネル,card=1]
    0 'VirMIDI 1-1     '
client 22: 'Virtual Raw MIDI 1-2' [type=カーネル,card=1]
    0 'VirMIDI 1-2     '
client 23: 'Virtual Raw MIDI 1-3' [type=カーネル,card=1]
    0 'VirMIDI 1-3     '
$
$ aconnect -o   # 出力ポートを表示
client 14: 'Midi Through' [type=カーネル]
    0 'Midi Through Port-0'
client 20: 'Virtual Raw MIDI 1-0' [type=カーネル,card=1]
    0 'VirMIDI 1-0     '
client 21: 'Virtual Raw MIDI 1-1' [type=カーネル,card=1]
    0 'VirMIDI 1-1     '
client 22: 'Virtual Raw MIDI 1-2' [type=カーネル,card=1]
    0 'VirMIDI 1-2     '
client 23: 'Virtual Raw MIDI 1-3' [type=カーネル,card=1]
    0 'VirMIDI 1-3     '
$ _

上記の例では、RawMidi interfaceのデバイスhw:1,0がSequencer interfaceのポート20:0に、hw:1,1が21:0に――という具合に対応している。

仮想MIDIバイス自体はMIDIをループバックする機能を持たないが、aconnect(1)を使用してSequencer interfaceの入力ポートと出力ポートを連結させると、RawMidi interface側でも有効となる。

例えばaconnect(1)で入力ポート20:0を出力ポート20:0に連結すると:

aconnect 20:0 20:0

入力ポート20:0に書き込んだMIDIを出力ポート20:0から読み出せるだけでなく、デバイスhw:1,0の入力ポートに書き込んだMIDIをhw:1,0の出力ポートから読み出せるようになる*2

あとは、RawMidi interfaceのAPIを使用するアプリケーションにて、デバイスhw:1,0を使用すればよい。同じサブデバイス番号同士(例えば入出力ともにhw:1,0,0)にてMIDIがループバックされる。

*1:昔はMIDI YokeHubi's Loopback MIDI Driverを使っていたものだが、64bit Windowsを使うようになってからは縁遠くなった。最近はloopMIDIを使えばよいのだろうか?

*2:試してはいないが、おそらく入力ポート20:0に書き込んだMIDIをhw:1,0の出力ポートから読み出すことや、hw:1,0の入力ポートに書き込んだMIDIを出力ポート20:0から読み出すことも可能なはずだ。というのも、The Linux MIDI-HOWTOを読む限り、元々snd-virmidiはRawMidi interfaceのMIDIバイスとSequencer interfaceのMIDIシーケンサを橋渡しするために開発されたと推測できるからだ。

低速で無駄のある小規模ツール開発を実現する方法

「低速で無駄のある」はツール本体なのか開発手法なのか、はたまた保守フェーズか。

例1:外部コマンドを何度も実行するシェルスクリプト/バッチファイルとして実装する

シェルスクリプトもバッチファイルも個人的に愛憎相半ばする気持ちを抱いてしまう言語というかツールだけど、その有効性は否定しない。自分もよく使っているし。

個々の小さなコマンドを組み合わせて目的を達成する――大抵のテキスト処理はその方法で何とかなるし、私が普段扱うデータ量は高が知れているので実行速度が問題となることもない。そして今の所はハードウェア性能は足りている。

とはいえコマンドを実行すれば(それが組み込みコマンドやシェル関数でなければ)その度にプロセスが生成されるし、各コマンドが個別にファイルにアクセスすればその度にファイルIOが発生する。気をつけないとこれらのコストが嵩んで信じられないほど低速なツールに仕上がってしまうことがある。というか覚えている限り2回ほど痛い目にあった。Windowsはセキュリティツールの影響もあってプロセスの生成コストが高いからなあ。

ループの中で外部コマンドを実行していたり、更にそこで毎回ファイルを引数指定していたりしたら要注意だ。テストデータでは気にならない時間で終了しても、現実的なデータ量ではウンザリするほど遅くなることがある。ループ数が増加すればするほど遅くなる。

該当する箇所をシェル組み込み機能に変えたり、テキストフィルタに置き換えることで済むなら良いが、それが駄目なら他の言語で書き直す羽目になるかもしれない。

例2:本格的に内部構造を変更するリスクを避けて無茶苦茶小手先の手法で既存ツールを改造する

作業コストや自分の力量の兼ね合いで、時として小手先の手法に頼らざるをえないこともある。しかし大抵は後でしっぺ返しをくらう気がする。何でだろう。

MVC (Model View Controller) や、それに微妙に似たDocument/Viewアーキテクチャなんて言葉がある。これに基づくと、例えばデータの画面表示に関して、

  • 表示したい元データはModelが保持している。
  • Modelからデータを取得し、表示用に加工してViewに突っ込む。
  • Viewには加工後のデータが存在する。

――こんな感じになると思う。実はデータの加工をどこでやるべきか分かっていない。

ある時遭遇したプログラムはViewerの類で、Model側に元データが残らない設計になっていた。Viewに加工後のデータが残っているだけだが、表示方法が1種類だけだったので何の問題もなかった……当初は

これがある時、表示方法を2種類に拡張して動的に切り替えられるように改造することになった。Model側に元データが残っているなら簡単だけど、残っていない! ピンチッ!

ここでModel側に元データを残すように修正すればよかったのに、何故か「Viewに残っている加工後のデータを使う」という力任せな方針を採ってしまった。表示方法を切り替える際にModelからデータを取得するのではなく(残っていないので不可)、Viewに残っているデータを使うのだ。この方法で何とかなってしまったのが悪かった。

その後、更に表示方法が増えたのだ! 私はこの対応の時に初めて駆り出された……。

表示を切り替える際に「Viewに残っている加工後のデータを使う」という実装の為、例えば次のような問題が噴出してしまった。

  1. 表示を切り替える度に、異なる加工済みデータが後に残る。
  2. 表示方法が3種類以上になると、表示を切り替えるときに複数の異なる種類のデータが元データとなる。つまり一つの表示方法に対して2種類以上の変換処理を実装する必要が生じる。
  3. データによっては、表示方法を色々と切り替えていくうちに誤差が生じる可能性がある。

ああ、こういうのが積み重なって「誰も弄りたがらない負債コード」と化してコールタールの沼のように踏み入れたプログラマを引きずりこんでいくのだろうなあ。

例3:よく分からんけどネット上の色々なサンプルをざっくり切り貼りして実装する

別にネット上のサンプルコードを使うことは……ライセンス絡みで問題ないなら構わないと思っている。

しかしですね、何の整理もせずに大雑把に切り貼りされると困るのですよ。低速になるか否かは運次第でもソースコード自体は確実に無駄の多い代物になりますから。ええ私も経験がありますあの全体の3分の1ぐらいが全く不要なコードでバラバラなコーディングスタイルがキメラのように組み合わさっていてライブラリ関数を使わずに自前で処理を書いてたりして当然のごとく関数化なんて申し訳程度にしかされてない一枚岩でこれまたバラバラな命名スタイルの変数が山のようにあるアレですよあのたった400行足らずのコードの整理に暇を見てこつこつ実施したとはいえ2週間近く掛かったのは災難なのかそれともその程度で済んだ幸運をかみ締めるべきなのかは未だに分かりません。

世間からすればまだまだ軽い事例だとは思うけど、嫌なものは嫌だ。

ある程度まとまった量のコードを外部から持ってくる――例えば関数単位やクラス単位でサンプルコードをコピーするのならともかく、ステートメント単位で切り貼りする時に何の整理もしないのは死亡フラグだ。

  • ネット上のサンプルコードを切り貼りする時はコードのスタイルを直すべし。
    • 大概のサンプルコードのスタイルは組み込む先のコードのそれと異なる。
    • コードの一貫性を保つためにも、スタイルは直すこと。まあ後で整形ツールにかけるという前提があるのなら話は別かもしれない。
  • ネット上のサンプルコードを使う時は、自分が欲しい部分以外は徹底的に取り除くべし。
    • 残っていると、後でそのコードを解読する羽目になった人が発狂します。

あとステートメント単位でコピーする場合もまとまった量のコードをコピーする場合も、何も考えずに使うのはダメだ。

  • ネット上のサンプルコードを使う時は、その内容の是非を問うこと。必要なら書き換えるべし。
    • バグもあれば質の悪いコードだってあるさ。
    • 例えばVBScriptのサンプルコードにて文字列を格納した配列をForループで舐めて中身を連結していることに深く高度な理由があるのか、それとも単に作者が組み込み関数Joinを知らなかっただけなのか?
  • ネット上のサンプルコードを使う時は、そのコードが前提としている実行環境・開発環境・依存ライブラリ等に留意すること。
    • サンプルコード中で新しく便利なAPIを使っていて、しかしターゲット環境が古いためにまだそのAPIは使えなかった……なんてことがある。
    • あと「このクールなライブラリを組み込めば一発だぜ」という記事を鵜呑みにして実装したが、実はライブラリのライセンスが……なんてこともある。

結局のところ、ネット上のサンプルコードを読んで内容を理解してからでないと、適切に切り貼りすることは不可能なのだ。

まとめ

  1. IOの頻度に注意。
    • 例ではファイルIOを挙げたが、TCPのようにタイムアウトが起こりうる通信処理などでも注意すること。
  2. 急がば回れ。その方法は「小手先の対応」ではないか、手を付ける前に考えること。
    • ただし、本質的な対応を実施するに足るコストを賄えるか否かは別の話。
  3. ネット上のサンプルコードは、よく読んで内容を理解した上でコピペしてプログラミングすること。
    • コピペプログラミングで楽できるのは「ゼロから考えなくて済む」という点だけ。
    • 何も考えずにコピペプログラミングで一定以上の品質のモノが出来上がるはずがない。出来上がるのはウンコなコードだ。

リンク切れしたシンボリックリンクを探す

id:eel3:20121112:1352651058 でシンボリックリンクを張りなおすMakefileを書いたのだが、デッドリンク(リンク切れしたシンボリックリンク)がそのまま残ってしまう問題があった。

この問題に対処するために、デッドリンクを探す方法を――『覚えて便利 いますぐ使える!シェルスクリプトシンプルレシピ54』から持ってくることにした。ただ、ちょっと理由があって、デッドリンクを消すのではなく列挙することにした。単純に列挙するだけなら、他のツールと組み合わせて色々なことができるからだ。

Version 1

初期バージョン。

#!/bin/sh
# deadlink: select dead symlinks from arguments
# bug:
#   ----
#   xxxx -> yyyy (File xxxx is symlink to yyyy.)
#   yyyy -> zzzz (File yyyy is deadlink! file zzzz is not exist.)
#   ----
#   In above case, this script assumes that file xxxx is deadlink.

PATH=/bin:/usr/bin

for i; do
    [ -L "$i" -a ! -e "$i" ] && echo "$i"
done

考え方は単純で、シンボリックリンクかつ存在しないファイルを探せばよい。-hや-Lではファイルがシンボリックリンクか否かのみチェックするだけで、シンボリックリンクを辿ることはない。一方でファイルの存在をチェックする-eはシンボリックリンクを辿る。もしデッドリンクだったなら、-hや-Lは真を返すが、-eはシンボリックリンクを辿るのに失敗して偽を返す。

ただ、この方法は「シンボリックリンクを指し示すシンボリックリンク」では意図しない挙動となるかもしれない。xxxxがyyyyを指し示すシンボリックリンクで、yyyyがzzzzを指し示すシンボリックリンクで、zzzzが存在しなかったと仮定する。zzzzが存在しないのでyyyyはデッドリンクだ。xxxxは、yyyyが存在するのでデッドリンクではないはずだが、シンボリックリンクを辿った先の最終地点であるzzzzが存在しないので、このスクリプトアルゴリズムではデッドリンクと見なされてしまう。

もっとも今回の最終目的はあくまで「デッドリンクを消す」だ。デッドリンクであるyyyyが消された時点でyyyyを指し示していたxxxxはデッドリンクになる――削除対象となるのだから、最初の時点でxxxxまでデッドリンクとして列挙されても問題はないだろう。

ちなみに使い方はこんな感じ。

$ deadlink *
xxxx
yyyy
$ deadlink * | xargs -d '\n' rm
$ _

引数として渡したファイル名に対してデッドリンクか否かチェックを行い、1行1ファイル名の形式で標準出力に表示する。この例ではカレントディレクトリのファイルをチェックした結果、デッドリンクとしてxxxxとyyyyが列挙されている。もしデッドリンクを削除したいのなら、xargs(1)を使ってrm(1)の引数に渡せばよい。

ところで、なぜ「引数に指定したフォルダの中身をチェックしてデッドリンクを列挙する」みたいな仕様にしなかったのか? 理由は、それをやろうとすると面倒だから。再帰的にチェックするか否かとか、再帰レベルを調整できるように云々とか、そんな機能は他のツールに任せれば十分だ。具体的にはfind(1)とか。

Version 2

最初に書いたdeadlinkの実装では、チェック対象のファイルを引数として渡す仕様になっていた。

仮にあるディレクトリ以下にて再帰的にデッドリンクをチェックしたい場合、どうするのか? find(1)とxargs(1)を使えばよい。

find ./work -type l -print0 | xargs -0 deadlink

シンボリックリンクか否かのチェックを二重に行っているが、気にしない方向で。find(1)は列挙したファイルを標準出力に書き出すので、xargs(1)を使ってdeadlinkの引数に渡すようにする。

ただxargs(1)を使う方法はファイル数が多くなったときに効率の面が気になる。というのも、引数として許される上限までファイルが列挙されないとdeadlinkが実行されないのだ。なので下手をすればfind(1)によるファイルの列挙が完了してからdeadlinkが実行されるだろうし、そうなる可能性は高いと思う。

しかし、もしdeadlinkが標準入力からファイル名を読み込みつつデッドリンクか否かのチェックを行うのなら、find(1)などによるファイルの列挙とdeadlinkによるチェックが並行して実行される。マルチコア環境では高速化が期待できる。実際にはfind(1)もdeadlinkもファイルシステムにアクセスするので、効果は低いかもしれないが。

そんな訳で標準入力からもファイル名を読み込むことが可能なように改造することにした。ただ元々の仕様も捨て難い。元の仕様を残すのなら「引数が無かったら標準入力から読み込む」という仕様では都合が悪い(メタキャラクタであるアスタリスクを引数に指定したら、ファイルが無かったので引数ゼロになった――なんて可能性があるので)。そこで引数が `-' の1つのみだった場合にのみ標準入力から読み込むようにしてみた。

#!/bin/sh
# deadlink: select dead symlinks from arguments
# bug:
#   ----
#   xxxx -> yyyy (File xxxx is symlink to yyyy.)
#   yyyy -> zzzz (File yyyy is deadlink! file zzzz is not exist.)
#   ----
#   In above case, this script assumes that file xxxx is deadlink.

PATH=/bin:/usr/bin

if [ $# -eq 1 -a "$1" = '-' ]; then
    # Trims IFS character from the beginning and end of the $i.
    while read i; do
        [ -L "$i" -a ! -e "$i" ] && echo "$i"
    done
else
    for i; do
        [ -L "$i" -a ! -e "$i" ] && echo "$i"
    done
fi

実はチェック対象のファイルが1つだけで、且つファイル名が `-' だった場合に困るのだが……そこは運用でカバー、ということで。

Version 3

標準入力からもファイル名を読み込むことが可能なようにしたものの、今度はwhile readを使っている点が気になる。高速化のために追加した部分なのに、低速な機能を使うというのも妙な話だ。

そこでwhile readによるループの部分をスクリプト言語で書くことにした。

最初はawkを使おうとしたのだが、system()を使ってtest(1)を呼び出す部分の外部コマンドの組み立てが面倒だった(特にファイル名のエスケープ処理が……)。そこでPerl5にしてみた。Perlが入っている環境は結構多いはずなので、問題になることは少ないだろう。

#!/bin/sh
# deadlink: select dead symlinks from arguments
# bug:
#   ----
#   xxxx -> yyyy (File xxxx is symlink to yyyy.)
#   yyyy -> zzzz (File yyyy is deadlink! file zzzz is not exist.)
#   ----
#   In above case, this script assumes that file xxxx is deadlink.

PATH=/bin:/usr/bin:/usr/local/bin

if [ $# -eq 1 -a "$1" = '-' ]; then
    perl -n -e 'chomp; -l $_ && ! -e $_ && print "$_\n";'
else
    for i; do
        [ -L "$i" -a ! -e "$i" ] && echo "$i"
    done
fi

うん、非常に短い。もういい加減sayを使うべきなんだろうけどprintを使用している。

最近はデフォルトでPerl5が入っていない(!)事態も起こりそうなので、頑張ってPythonでも書いてみた。何が「頑張って」なのかというと、インデント位置である。

#!/bin/sh
# deadlink: select dead symlinks from arguments
# bug:
#   ----
#   xxxx -> yyyy (File xxxx is symlink to yyyy.)
#   yyyy -> zzzz (File yyyy is deadlink! file zzzz is not exist.)
#   ----
#   In above case, this script assumes that file xxxx is deadlink.

PATH=/bin:/usr/bin:/usr/local/bin

if [ $# -eq 1 -a "$1" = '-' ]; then
    python -c '
import os, sys
for line in sys.stdin:
  i = line.strip()
  if os.path.islink(i) and not os.path.exists(i):
    print i'
else
    for i; do
        [ -L "$i" -a ! -e "$i" ] && echo "$i"
    done
fi

インデント位置で怒られたので、仕方なくPythonのコードを行頭から書き始めることにした。うーん、見栄えが良くない……。なおPython 2.7にてそれっぽく動作することは確認したが、Python 3で動作するか否かは不明だ。

(念のため書いておくと、Pythonのインデントに関する仕様は特に気にしていない。少なくとも、Python単体でコードを書く分には好ましいと思っている。シェルスクリプトに埋め込むという使い方が邪道気味なのだ)

個人的にはRubyが好みなのだが――PerlPythonは入っていないがRubyは入っている、という環境は考えにくい。むしろ逆にRubyが入ってない環境の方が多そうなので、Ruby版は書いていない。

しかし、ここまでくると、シェルスクリプトPerlPythonのコードを埋め込むのではなく、全てPerlPythonで書いてしまった方がスッキリするような気が……。

終わりに

最終的には、上記のPerl5版に手を加えて、こんな感じになった。

https://github.com/eel3/eel3-scripts/blob/master/bin/deadlink

初めて生PCMを触る人には『WAVプログラミング C言語で学ぶ音響処理 増補版』を推薦します

久しぶりに生のPCMデータを弄る機会があった。生PCMまわりは独学で、割と知識が歯抜けだったので、基本的なところを学び直そうとしたところ、意外とまとまった情報が見つからなかった。

色々と探し回って、ようやくたどり着いたのがこの本だった。

WAVプログラミング―C言語で学ぶ音響処理

WAVプログラミング―C言語で学ぶ音響処理

本書を見せた同僚の感想は「生PCMを取り扱う業務にアサインされた若手・新人に自習用に渡すのにちょうど良い本」だった。

この本は生PCMを触る際のものすごく基本的なことがサンプルコード付きで説明されているというか、むしろサンプルコードで説明されている。そのため、読み解くにはC/C++系統の言語知識が必要となるのだが、逆に考えれば、その辺の言語が分かっている若手のプログラマに参考資料として渡しやすい本だと言える。

内容自体も「ステレオのLとRを反転」とか「モノラル化」とか「16bitから8bitに変換」とか「ボリューム変更」とか、そういう簡単なところから始まっている。意外なことに、この辺の内容がまとまっている資料があまり無いのである。そういう意味では、本書はあまり類書の無い本だと言える(ニッチすぎるのか絶版寸前っぽい感じである)。

おそらく本書の次にやさしい本は『C言語ではじめる音のプログラミング―サウンドエフェクトの信号処理』だが、こちらの本はサウンドエフェクトの入門書なので、生PCMの入門書としては厳しいというか、そもそも分野が少々異なるといえる。

本書のサンプルコードはWAVファイルの生PCMを加工するものが大半で、最後の1章のみWAVファイルからPCMを取り出して加工してWindows APIで再生させるものである。

なのでリアルタイム処理まわりはサッパリなのだが、その辺になると各OSのマルチメディアAPIによって流儀が全く異なってくるので、1冊の本にまとめるのは厳しいだろう。しかも実のところ、どんなAPIを使おうとも、生PCMを直接加工する部分は、WAVファイルで生PCMを加工する場合と基本的な考え方は同じである(そのAPIに用意されている便利な機能を使う場合は別だけど)。

あとサンプルコードが16bitよりも大きな分解能には対応していないのだが……「8bitがOffset Binaryで16bit以上が2の補数」という点さえ押さえておけば、サンプルコードを片手に自前でコードを書くのは難しくないはずだ。というかサンプルコードは中身を理解してから流用するもの(※ただしライセンス等の問題が無い場合に限る)で、何も考えずに流用しちゃうのはプログラマとしてアウトだ。

ともかく、PCMまわりの知識ゼロの状態で生PCMを触るのなら、1冊目として本書はオススメだ。後は、実際に使うAPIのドキュメントとか、サウンドエフェクト系の作業ならそちらの入門書とか、もう1〜2冊ほど併読すれば何とかなるだろう。

書籍購入:『WAVプログラミング C言語で学ぶ音響処理 増補版』

あとで書く。

WAVプログラミング―C言語で学ぶ音響処理

WAVプログラミング―C言語で学ぶ音響処理