PINコード(数字のみ)を生成する

暗証番号やPINコードに使えるような、指定された桁数のランダムな整数値を生成したいのである。

英数記号を用いたパスワード文字列の自動生成については知見があるのだが、「数字のみ」という条件が付いた途端に、「あれ? そういえば、どう生成すればよいのかなあ?」とつまづいてしまったのである。

パスワード文字列の生成ではpwgen(1)を愛用している。他のツールを探すのも面倒なので、pwgen(1)で何とかできないだろうか?

man(1)を見ながら検討した結果、手元のpwgen 2.08ではこんな感じで数字のみのパスワードを生成することに成功した。

# 6桁のPINコードを1つ生成する場合:
pwgen -A -r abcdefghijklmnopqrstuvwxyz -s 6 1

オプション-Aを指定して英大文字を除外した上で、オプション-rを使って英小文字を除外している。オプション-yを指定しなければ、記号文字は使われない。消去法で、残された有効文字は数字のみとなる。

他にもっとスマートなツールが存在するはずだが、とりあえず個人的に使う分にはこんなもので十分だろう。

終わりに

最終的には、こんな感じになった。

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

『単体テストの考え方/使い方』ファースト・インプレッション

パッと見の印象:テスト駆動開発あたりの文脈から出てきた「単体テスト」についての、「良いテスト」について論じた本。

単体テストにフォーカスしたソフトウェア・テストの本で、しかも「具体的なテストのやり方」ではなく抽象的な視点で論じているものって、(少なくとも和書では)あまり類書を見ないように思う。

本書を購入した理由は、私の中の「単体テスト」と、最近に世間一般で言われている「単体テスト」の差異を知るためである。

私自身は、どちらかと言えば組み込みソフトウェア開発の文脈で単体テストに取り組んできた人である。組み込み系の単体テストは、本書の「単体テスト」とはまた異なる趣がある。

知らないうちに、自分と相手とで異なる「単体テスト」を思い浮かべたまま会話していて、話がこじれてしまった――みたいなことを避けたい。そのためにも、今の時代に多くの人が思い浮かべるだろうタイプの「単体テスト」について知っておきたい。

こういう用途には、今の「単体テスト」について一通りまとめられた本書はちょうど良い感じである。

ただし、この本は教科書や学術書ではなく、「著者が自分自身の開発経験を通じて会得した考え方をまとめたもの」という面が大きい。そのため、内容の所々に著者の好みが現れているように感じられる。全てを鵜呑みするのは避けた方がよいだろう。

Dockerがファイアウォールに穴を空けるとはどういうことか?

1週間ほど前の下記blog記事が気になったため、少し調べてみた。似たような事例に引っかかった人はそれなりにいるようだ。

jun-networks.hatenablog.com

調べてみての感想は:

  • 「なんでデフォルトがinsecureやねん!」という利用者の心の叫びは真っ当だけど、でもこれ、Dockerの開発者の立場からすれば「せやかて工藤、しゃーないやんけ」案件だろうなあ。
  • 根本的には「Dockerのコンテナ自体はプロセスではない(Linux namespacesの機能を駆使した代物である)」という点に起因するような気がする。
  • 自作Linuxルーター万歳!

――といった具合だ。特に自作Linuxルーター万歳。*1

周知の事実だが今一度繰り返すと、コンテナ自体はプロセスではない。Linux namespacesの機能を駆使して、既定の名前空間とは別の名前空間に諸々のリソースを詰め込んで隔離した代物がコンテナである。

プロセスではないので、Dockerのbridge networkにおいて今回の話題の中心であるポート・マッピングのような機能を実現するにあたり、ユーザーランドでのソフトウェア処理による通信制御――つまりソケット・プログラミングを行っていない。

もしもソケット・プログラミングによるソフトウェア処理で実現していたならば、この件は「Linux上でネットワーク・サーバを直接動かす」のと同じレベルになるので、実運用において問題を引き起こすことはなかっただろう。

でも残念、現実は非情である。コンテナから「ホストの外部ネットワーク」への通信や、ポート・マッピングの機能を実現するために、ホスト側では古の自作Linuxルーターみたいなことをやっているのである。

bind(2)によるIPアドレスの割り当て

Dockerのネットワーク機能について語るにあたり、一見して無関係そうな、迂遠なところから話を始めようと思う。BSDソケットとネットワーク・インタフェースの関係である。

TCPUDPにはポート番号という概念があり*2、クライアント・サーバ・モデルにおけるサーバ側ソフトウェアでは、bind(2)を使用してソケットを「通信を待ち受けるポート番号」に割り当てる――というのがソケット・プログラミングにおける古典的なアプローチである。*3

ポート番号に目が向きがちだが、bind(2)ではソケットにIPアドレスを割り当てることもできる。どういうことだろうか?

例えばコンピュータにLANポートが2つ付属していて、そのうちeth0には192.0.2.1が、eth1には203.0.113.10が割り当てられていたとする。

ここで、TCPポート2195番を使用するネットワーク・サーバを動かしたいのだが、eth0(192.0.2.1:2195)宛ての通信は受信したいけどeth1(203.0.113.10:2195)宛ての通信は無視したい――という場合に、bind(2)にて「192.0.2.1:2195」というIPアドレスとポート番号のペアを割り当てることにより、eth0宛ての通信だけ受信するようになる。

eth0とeth1の両方に届いた通信を受信したいなら、「0.0.0.0」というワイルドカードとして機能するIPアドレス*4を指定すればよい。ワイルドカードアドレスを指定することにより、ローカルループバックを含む「そのコンピュータに割り当てられている全てのIPアドレス」のポート2195番宛ての通信を受信するようになる。

「bind(2)によるIPアドレスの割り当て」の理想と現実

bind(2)によるIPアドレスの割り当ては便利といえば便利なので、大抵のネットワーク・サーバでは「通信を待ち受けるIPアドレス」を設定できるようになっている。

例えば、下記は手元のsshd_configの一部を抜粋したものになるが、「ListenAddress」という項目にてbind(2)するIPアドレスを指定できるようになっている。

# sshd_config より抜粋

#Port 22
#AddressFamily any
#ListenAddress 0.0.0.0
#ListenAddress ::

既定値は0.0.0.0であり、コンピュータの全てのIPアドレス宛ての通信に反応するようになっている。

古典的な「管理者がサーバの隅から隅まで把握している」というスタイルのサーバ構築・管理においては、サーバ上で動かす「TCP/IPを喋るサーバ・デーモン」について、各々の設定を見直して、必要最小限の「IPアドレスのbind(2)」を実現するだろう。つまり、軽々しくワイルドカードアドレスを使用したままにはしておかないのが理想的である。

現実は異なる。ネットワーク・サーバの設定を変更して特定のIPアドレスをbind(2)するようなことは、今となってはあまり多くない。設定ファイルの既定値はワイルドカードアドレスであることが多いし、大半のケースでは既定値であるワイルドカードアドレスのまま運用している。

先に抜粋したsshd_configから分かるように、私自身もワイルドカードアドレスのままネットワーク・サーバの類を動かしていることが多い。

実際のところ、現在のサーバ管理においては、多種多様な「TCP/IPを喋るサーバ・デーモン」を動かしている上に、各種コンポーネント類の入れ替わりも早い。なのでいちいち全てのネットワーク・サーバの設定を精査なんてしていられない。ちょっと間に合わない。

では、bind(2)によるIPアドレスの割り当ての代わりに何をしているかといえば、ファイアウォールで宛先アドレス/ポート番号をチェックしてパケットフィルタリングしているのである。

ネットワーク・サーバの設定ファイルは分散している上に内容も千差万別だが、ファイアウォールなら1ヶ所で集中管理できる。その点は便利だよね。

ローカル・プロセス宛てのパケットをフィルタリングするタイミング

Linux上では、ネットワーク・サーバはプロセスとして動作している。LinuxのNetfilterにおいては、ローカルプロセス宛てのパケットはINPUTチェインでフィルタリングすればよい。

次の図は、NetfilterにおけるL3でのパケット処理の流れを示したものである。

 +-----------------------------------------------------------------+
 |                       Network Interfaces                        |
 |                      (lo/eth/wlan/etc...)                       |
 +-----------------------------------------------------------------+
        |                                                   |
 Incoming packet                                     Outgoing packet
        |                                                   |
 +-------------+                                            |
 | PREROUTING  |                                            |
 | +---------+ |                                     +-------------+
 | |   raw   | |                                     | POSTROUTING |
 | +---------+ |                                     | +---------+ |
 |      |      |                                     | |   nat   | |
 | +---------+ |                                     | +---------+ |
 | | mangle  | |                                     |      |      |
 | +---------+ |                                     | +---------+ |
 |      |      |                                     | | mangle  | |
 | +---------+ |                                     | +---------+ |
 | |   nat   | |                                     +-------------+
 | +---------+ |                                            |
 +-------------+                                            |
        |           +---------------------------+           |
        |           |          FORWARD          |           |
 +-------------+    | +--------+     +--------+ |    +-------------+
 |   Routing   |--->| | mangle |---->| filter | |--->|   Routing   |
 +-------------+    | +--------+     +--------+ |    +-------------+
        |           +---------------------------+           |
        |                                            +-------------+
        |                                            |   OUTPUT    |
        |                                            | +---------+ |
        |                                            | | filter  | |
 +-------------+                                     | +---------+ |
 |    INPUT    |                                     |      |      |
 | +---------+ |                                     | +---------+ |
 | | mangle  | |                                     | |   nat   | |
 | +---------+ |                                     | +---------+ |
 |      |      |                                     |      |      |
 | +---------+ |                                     | +---------+ |
 | | filter  | |                                     | | mangle  | |
 | +---------+ |                                     | +---------+ |
 +-------------+                                     |      |      |
        |                                            | +---------+ |
        |                                            | |   raw   | |
 +-----------------------------------------------+   | +---------+ |
 |      |          Local Process                 |   +-------------+
 |      |                                        |          |
 | +---------------------+ +-------------------+ |   +-------------+
 | | recv(2)/recvfrom(2) | | send(2)/sendto(2) |-|-->|   Routing   |
 | +---------------------+ +-------------------+ |   +-------------+
 |           |                       |           |
 |      Received data            Send data       |
 +-----------------------------------------------+

内部でソケット通信しているネットワーク・サーバは、この図におけるローカル・プロセスに該当する。INPUTチェインは、ネットワーク・インタフェースからローカル・プロセスまでの経路上に位置する。なので、INPUTチェインにてパケットフィルタリングを行うことになる。

VPSなどでサーバを構築する場合、iptablesやnftablesのINPUTチェインにフィルタリングルールを設定するだろう。大概の入門文書にてそう案内されているし、実務的にもその通りだ。

ということで、Linuxサーバを触っている人にとって、INPUTチェインは身近なものである。普通にネットワーク・サーバを動かす分には、割とそれだけで十分に間に合うものだ。

Bridge networkにおけるホストとコンテナのネットワーク構成

残念ながら、Dockerのコンテナそのものはローカル・プロセスではない。ローカル・プロセスではないのだから、コンテナ宛ての通信について「ホスト側のNetfilterのINPUTチェイン」にて制御を試みるのは的外れである。

次の図は、Dockerでネットワーク・ドライバとしてbridge networkを選択した状態で、3つのコンテナを動かした際の、大まかなネットワーク構成を示したものである。

 +----------------------------------------------+
 | Linux box                                    |
 |                                              |
 | +------------------------------------------+ |
 | | Default network namespace                | |
 | |                                          | |
 | | +-------+ +-------+ +-------+ +-------+  | |
 | | |  lo   | | eth0  | | eth1  | | wlan0 |  | |
 | | +-------+ +-------+ +-------+ +-------+  | |
 | |                                          | |
 | |               +---------+                | |
 | |               | docker0 |                | |
 | |               +----+----+                | |
 | |                    |                     | |
 | |     +--------------+--------------+      | |
 | |     |              |              |      | |
 | | +---+---+      +---+---+      +---+---+  | |
 | | | veth0 |      | veth1 |      | veth2 |  | |
 | | +---+---+      +---+---+      +---+---+  | |
 | +-----|--------------|--------------|------+ |
 |       |              |              |        |
 | +-----|------+ +-----|------+ +-----|------+ |
 | | +---+---+  | | +---+---+  | | +---+---+  | |
 | | | veth0 |  | | | veth0 |  | | | veth0 |  | |
 | | +-------+  | | +-------+  | | +-------+  | |
 | | +-------+  | | +-------+  | | +-------+  | |
 | | |  lo   |  | | |  lo   |  | | |  lo   |  | |
 | | +-------+  | | +-------+  | | +-------+  | |
 | |            | |            | |            | |
 | | Container1 | | Container2 | | Container3 | |
 | +------------+ +------------+ +------------+ |
 +----------------------------------------------+

Dockerのホスト側から見ると、実行中の各コンテナはdocker0という仮想ネットワーク・デバイスの先にぶら下がっている。

この構造は、下記のネットワーク構成と非常によく似ている。

 +------------------------------------------+
 | Linux_box0                               |
 |                                          |
 | +-------+ +-------+ +-------+ +-------+  |
 | |  lo   | | eth0  | | eth1  | | wlan0 |  |
 | +-------+ +-------+ +-------+ +-------+  |
 |                                          |
 |                +-------+                 |
 |                | eth2  |                 |
 |                +---+---+                 |
 +--------------------|---------------------+
                      |
       +--------------+--------------+
       |              |              |
 +-----|------+ +-----|------+ +-----|------+
 | +---+---+  | | +---+---+  | | +---+---+  |
 | | eth0  |  | | | eth0  |  | | | eth0  |  |
 | +-------+  | | +-------+  | | +-------+  |
 | +-------+  | | +-------+  | | +-------+  |
 | |  lo   |  | | |  lo   |  | | |  lo   |  |
 | +-------+  | | +-------+  | | +-------+  |
 |            | |            | |            |
 | Linux_box1 | | Linux_box2 | | Linux_box3 |
 +------------+ +------------+ +------------+

この図は自作Linuxルータの実行環境の一例である。Linux_box0がルータである。eth2はプライベート・ネットワークに繋がっている。プライベート・ネットワーク上には3台のコンピュータ(Linux_box1~Linux_box3)が動作している。Linux_box1~Linux_box3は、ルータであるLinux_box0を経由して外部ネットワークと通信する。

ここで、外部ネットワークと繋がっているネットワーク・インタフェースがeth0だと仮定すると、Linux_box0では、何かしらの方法でeth0~eth2間でパケットを橋渡しする必要がある。

Dockerの場合も似たようなもので、やはり外部ネットワークと繋がっているのがeth0だと仮定すると、ホスト側にて何かしらの方法でeth0~docker0間でパケットを橋渡しする必要がある。

どう橋渡しするか? IPフォワードを使用してネットワーク・インタフェース間でパケットを転送するのである。この時、NetfilterのPREROUTINGチェインとPOSTROUTINGチェインにて、パケットの宛先IPアドレスの書き換えを行っている(俗に言うNATとかIPマスカレードとかいうアレである)。

先に示したNetfilterのパケット処理の図で言うと、例えばLinux_box1/Container1が外部ネットワークに向けて送信したパケットは、Linux_box0/Dockerホスト側では次のように処理されるだろう。

  1. Network Interfaces(eth2/docker0) → PREROUTINGチェイン。
  2. PREROUTINGチェイン → FORWARDチェイン。
  3. FORWARDチェインのフィルタリング・ルールが適用される。
  4. FORWARDチェイン → POSTROUTINGチェイン。
  5. POSTROUTINGチェインで送信元IPアドレスが書き換えられる。
  6. POSTROUTINGチェイン → Network Interfaces(eth0)。

反対方向の、外部ネットワークからLinux_box1/Container1に向けてのパケットは、次のように処理されるだろう。

  1. Network Interfaces(eth0) → PREROUTINGチェイン。
  2. PREROUTINGチェインで宛先IPアドレスが書き換えられる。
  3. PREROUTINGチェイン → FORWARDチェイン。
  4. FORWARDチェインのフィルタリング・ルールが適用される。
  5. FORWARDチェイン → POSTROUTINGチェイン。
  6. POSTROUTINGチェイン → Network Interfaces(eth2/docker0)。

どちらのケースも(特に受信方向のパケットについて)INPUTチェインを通過しない。だからINPUTチェインに記述したフィルタリング・ルールは適用されない。

このことは、Dockerのネットワークまわりについて理解していれば、自明のことである。

残念ながら、私を含めて「コンテナはプロセスではない」ということの本当の意味についての理解が浅い人は多い。そのような人が今まで触ってきたネットワーク・サーバと同じような塩梅でコンテナを扱おうとしてしまい、ファイアウォールについてもネットワーク・サーバの時と同じ流儀で適用すれば十分だと判断してしまった時に、INPUTチェインのフィルタリング・ルールが適用されないという事実が牙をむくことになる。

……自作Linuxルータなら、物理的にLANの構成が目に見えて分かる。でもDockerの場合は全てがホスト側のコンピュータの中におさまっていて目に見えない。目に見えないからこそ、混乱を引き起こしやすいのかもしれない。

実際にNetfilterに設定されるルールを見てみる

Dockerは外部ネットワークとコンテナの間をつなぐためにNetfilterのルールを変更する。具体的にはどのような内容なのだろうか?

以下に示す環境(Ubuntu 22.04上のDocker 24.0.3)にてルールを確認してみた。実験用に用意した環境であるため、そもそもパケットフィルタリングのルールは未設定の環境である。

$ lsb_release -a
Distributor ID: Ubuntu
Description:    Ubuntu 22.04.2 LTS
Release:        22.04
Codename:       jammy

$ uname -a
Linux fabrico 5.19.0-46-generic #47~22.04.1-Ubuntu SMP PREEMPT_DYNAMIC Wed Jun 21 15:35:31 UTC 2 x86_64 x86_64 x86_64 GNU/Linux

$ docker --version
Docker version 24.0.3, build 3713ee1

$ docker version
Client: Docker Engine - Community
 Version:           24.0.3
 API version:       1.43
 Go version:        go1.20.5
 Git commit:        3713ee1
 Built:             Wed Jul  5 20:44:55 2023
 OS/Arch:           linux/amd64
 Context:           default

Server: Docker Engine - Community
 Engine:
  Version:          24.0.3
  API version:      1.43 (minimum version 1.12)
  Go version:       go1.20.5
  Git commit:       1d9c861
  Built:            Wed Jul  5 20:44:55 2023
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.6.21
  GitCommit:        3dce8eb055cbb6872793272b4f20ed16117344f8
 runc:
  Version:          1.1.7
  GitCommit:        v1.1.7-0-g860f061
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0

まず、Docker本体が動作している時にnftablesでルールを確認すると、次のようになっている。

# sudo nft -s list ruleset
table ip nat {
    chain POSTROUTING {
        type nat hook postrouting priority srcnat; policy accept;
        oifname != "docker0" ip saddr 172.17.0.0/16 counter masquerade
    }

    chain PREROUTING {
        type nat hook prerouting priority dstnat; policy accept;
        fib daddr type local counter jump DOCKER
    }

    chain OUTPUT {
        type nat hook output priority -100; policy accept;
        ip daddr != 127.0.0.0/8 fib daddr type local counter jump DOCKER
    }

    chain DOCKER {
        iifname "docker0" counter return
    }
}
table ip filter {
    chain DOCKER {
    }

    chain DOCKER-ISOLATION-STAGE-1 {
        iifname "docker0" oifname != "docker0" counter jump DOCKER-ISOLATION-STAGE-2
        counter return
    }

    chain FORWARD {
        type filter hook forward priority filter; policy drop;
        counter jump DOCKER-USER
        counter jump DOCKER-ISOLATION-STAGE-1
        oifname "docker0" ct state related,established counter accept
        oifname "docker0" counter jump DOCKER
        iifname "docker0" oifname != "docker0" counter accept
        iifname "docker0" oifname "docker0" counter accept
    }

    chain DOCKER-USER {
        counter return
    }

    chain DOCKER-ISOLATION-STAGE-2 {
        oifname "docker0" counter drop
        counter return
    }
}

この状態では、コンテナがクライアント側として振る舞う通信が許可されている(なのでapt(8)やyum(8)を使用してネットワーク経由でもコンポーネントをアップデートすることが可能である)。

具体的には、POSTROUTINGチェインの下記ルールにより、コンテナから外部ネットワークに向けて送信したパケットの送信元IPアドレスが「ホスト側のIPアドレス」に書き換えられてから外部ネットワークに送出される。

oifname != "docker0" ip saddr 172.17.0.0/16 counter masquerade

nftablesのルールでは明示されていないが、外部ネットワークからコンテナ宛てに届いたパケットについても、PREROUTINGチェインあたりのタイミングで送信先IPアドレスが「ホスト側のIPアドレス」から「コンテナのIPアドレス」に書き換えられているはずである*5

外部ネットワークからコンテナ宛てに届いたパケットに関しては、FORWARDチェインの下記ルールにより「コンテナが送信したパケットに対する応答パケット」のみを受容するように動作している。

oifname "docker0" ct state related,established counter accept

総じて、送信側も受信側も実に「家庭用のブロードバンドルータ」っぽいルールであるな、という感想である。

次にdocker run -p 8080:8080でコンテナを起動した時のルールはどうなっているだろうか?

# docker run -p 8080:8080 test_image
# sudo nft -s list ruleset
table ip nat {
    chain POSTROUTING {
        type nat hook postrouting priority srcnat; policy accept;
        oifname != "docker0" ip saddr 172.17.0.0/16 counter masquerade
        meta l4proto tcp ip saddr 172.17.0.2 ip daddr 172.17.0.2 tcp dport 8080 counter masquerade
    }

    chain PREROUTING {
        type nat hook prerouting priority dstnat; policy accept;
        fib daddr type local counter jump DOCKER
    }

    chain OUTPUT {
        type nat hook output priority -100; policy accept;
        ip daddr != 127.0.0.0/8 fib daddr type local counter jump DOCKER
    }

    chain DOCKER {
        iifname "docker0" counter return
        iifname != "docker0" meta l4proto tcp tcp dport 8080 counter dnat to 172.17.0.2:8080
    }
}
table ip filter {
    chain DOCKER {
        iifname != "docker0" oifname "docker0" meta l4proto tcp ip daddr 172.17.0.2 tcp dport 8080 counter accept
    }

    chain DOCKER-ISOLATION-STAGE-1 {
        iifname "docker0" oifname != "docker0" counter jump DOCKER-ISOLATION-STAGE-2
        counter return
    }

    chain FORWARD {
        type filter hook forward priority filter; policy drop;
        counter jump DOCKER-USER
        counter jump DOCKER-ISOLATION-STAGE-1
        oifname "docker0" ct state related,established counter accept
        oifname "docker0" counter jump DOCKER
        iifname "docker0" oifname != "docker0" counter accept
        iifname "docker0" oifname "docker0" counter accept
    }

    chain DOCKER-USER {
        counter return
    }

    chain DOCKER-ISOLATION-STAGE-2 {
        oifname "docker0" counter drop
        counter return
    }
}

コンテナから外部ネットワークに向けて送信されたパケットの扱いについては、先ほどと同じだ。違いは外部ネットワークからコンテナ宛てに届いたパケットの扱いに見られる。

外部ネットワークからホスト側のポート8080番に届いたパケットは、PREROUTINGチェインからジャンプした先のDOCKERチェインにおいて、下記ルールによって宛先IPアドレス/ポート番号が書き換えられる。

iifname != "docker0" meta l4proto tcp tcp dport 8080 counter dnat to 172.17.0.2:8080

その後、FORWARDチェインからジャンプした先のDOCKERチェインの下記フィルタリング・ルールでパケットが受容される。

iifname != "docker0" oifname "docker0" meta l4proto tcp ip daddr 172.17.0.2 tcp dport 8080 counter accept

この一連の流れには、家庭用ルータのポート開放機能を彷彿させるものがある。

さて、PREROUTINGチェインで実行される下記ルールを再度見てみる。

iifname != "docker0" meta l4proto tcp tcp dport 8080 counter dnat to 172.17.0.2:8080

このルールでは、ホスト側に届いたパケットの宛先IPアドレスをチェックしていない。そのため、ホスト側の「docker0を除く、全ての『IPアドレスが付与された』ネットワーク・インタフェース」のポート8080番に届いたパケットが、DNATで宛先がコンテナのIPアドレスに書き換えられて、以降のチェインに流れて行ってしまうことになる。

それでは、例えばdocker run -p 192.0.2.100:8080:8080のように、IPアドレスを明示してポート・マッピングした場合には、どのようなルールになるだろうか?

# docker run -p 192.0.2.100:8080:8080 test_image
# sudo nft -s list ruleset
table ip nat {
    chain POSTROUTING {
        type nat hook postrouting priority srcnat; policy accept;
        oifname != "docker0" ip saddr 172.17.0.0/16 counter masquerade
        meta l4proto tcp ip saddr 172.17.0.2 ip daddr 172.17.0.2 tcp dport 8080 counter masquerade
    }

    chain PREROUTING {
        type nat hook prerouting priority dstnat; policy accept;
        fib daddr type local counter jump DOCKER
    }

    chain OUTPUT {
        type nat hook output priority -100; policy accept;
        ip daddr != 127.0.0.0/8 fib daddr type local counter jump DOCKER
    }

    chain DOCKER {
        iifname "docker0" counter return
        iifname != "docker0" meta l4proto tcp ip daddr 192.0.2.100 tcp dport 8080 counter dnat to 172.17.0.2:8080
    }
}
table ip filter {
    chain DOCKER {
        iifname != "docker0" oifname "docker0" meta l4proto tcp ip daddr 172.17.0.2 tcp dport 8080 counter accept
    }

    chain DOCKER-ISOLATION-STAGE-1 {
        iifname "docker0" oifname != "docker0" counter jump DOCKER-ISOLATION-STAGE-2
        counter return
    }

    chain FORWARD {
        type filter hook forward priority filter; policy drop;
        counter jump DOCKER-USER
        counter jump DOCKER-ISOLATION-STAGE-1
        oifname "docker0" ct state related,established counter accept
        oifname "docker0" counter jump DOCKER
        iifname "docker0" oifname != "docker0" counter accept
        iifname "docker0" oifname "docker0" counter accept
    }

    chain DOCKER-USER {
        counter return
    }

    chain DOCKER-ISOLATION-STAGE-2 {
        oifname "docker0" counter drop
        counter return
    }
}

PREROUTINGチェインでDNATする部分のルールのみ変化している。

# IPアドレスを明示しなかった場合:
iifname != "docker0" meta l4proto tcp tcp dport 8080 counter dnat to 172.17.0.2:8080

# IPアドレスを明示した場合:
iifname != "docker0" meta l4proto tcp ip daddr 192.0.2.100 tcp dport 8080 counter dnat to 172.17.0.2:8080

IPアドレスを明示した場合は、DNATする際に宛先IPアドレスをチェックしている。これにより「ホスト側の特定のネットワーク・インタフェースに届いたパケット」のみがDNATされ、以降のチェインに流れていくようになる。

「せやかて工藤、しゃーないやんけ」案件

ポート・マッピングする際にIPアドレスを明示することで、ホスト側の特定のネットワーク・インタフェースに届いたパケットだけがDNATされて、コンテナに転送されるのであった。

さて、Dockerの開発者になった気分で思考実験してみよう。「ホスト側の特定のネットワーク・インタフェースを示すIPアドレス」の既定値(デフォルト値)として相応しい値は何だろうか?

Dockerを動かすホストには、ネットワーク・インタフェースは何個存在するだろうか? それぞれのネットワーク・インタフェースには、どのようなIPアドレスが付与されているだろうか?

答え:ネットワーク・インタフェースが何個あるのか分からないし、付与されているIPアドレスも分からない。

さあ、既定値として相応しいIPアドレスは何だろうか?

――と考えた時に、既存のネットワーク・サーバの待ち受けIPアドレスのように「とりあえず全てのネットワーク・インタフェース宛てのパケットを受容してしまおう」という判断をすることは、まあ、意外とありがちなパターンではないかと思う。

唯一の違いは、普通のネットワーク・サーバならNetfilterのINPUTチェインのフィルタリング・ルールが適用される(だから、その辺のルールさえ適切ならば、ワイルドカードアドレスにbind(2)しても被害はでない)のにたいして、Dockerのポート・マッピングではINPUTチェインのルールが適用されないことである。

ここで、既定値としてローカルループバックを採用することは、Dockerという汎用なソフトウェアとしては「ちょっとやりすぎ」感がある。

例えばRDBMSMySQLとか)の管理ポートなら「既定値=ローカルループバック」でも納得できるかもしれない。なぜならRDBMSを全世界に大公開することは非常に稀だからだ。というか普通は、公開ネットワーク上のサーバで動かしているRDBMSの管理ポートに外からアクセスできてしまったら、真っ先にサーバのセキュリティ対策不足を疑うと思う。

一方でWebサーバの公開ポートについて「既定値=ローカルループバック」だったら「うーん、それってどうなのよ?」みたいなお気持ち表明が出てくるはずである。「既定値はワイルドカードアドレスでいいのでは?」とも突っ込まれるかもしれない。

Dockerは汎用な仕組みである。各コンテナでは、Webサーバ(広く公開されることが多い)が動いていることもあれば、RDBMS(公開範囲はできるだけ狭く)が動いていることもある。

ローカルループバックを既定値とするのは適切だろうか? むしろDockerユーザからのお気持ち表明が出ないだろうか?

非常に悩ましい。私個人の感想としては、仮に開発者が「しゃーないやんけ」とこぼしていたとしても、まあ、むべなるかな……。

対策案

とりあえず以下の4通りが考えられる。オススメは(1)ないし(2)じゃないかしら。

  1. コンテナを起動する時に、ポート・マッピングにて「パケットを受容するネットワーク・インタフェースのIPアドレス」を明示する。
    • 例えばdocker run -p 8080:8080ではなくdocker run -p 127.0.0.1:8080:8080のように明示する。
    • 設定ファイル(例えばdaemon.json)を書き換えて、ポート・マッピングの既定値をワイルドカードアドレス以外に変更しておくのも良いアイデアだろう。
  2. NetfilterのFORWARDチェインにて「コンテナ宛ての通信」のパケットフィルタリングを行う。
    • DockerはDOCKER-USERという名前のチェインを追加する。ここにユーザ定義のフィルタリング・ルールを追加すればよい。
  3. 各々のコンテナ側にてパケットフィルタリングする。
    • docker runする時に--cap-add=NET_ADMINを付与することで、コンテナの中でiptablesやnftablesを利用できるようになる。
    • ある種の「特権モード」でコンテナを動かすことになるので、何となくコンテナの長所の1つを潰してしまうことになる気がする。
  4. DockerがNetfilterのフィルタリング・ルールを勝手に変更しないように設定した上で、自前でフルセットのフィルタリング・ルールを用意する。
    • daemon.jsoniptablesキーをfalseにすればよいのだが、公式マニュアルでは非推奨となっている。

おまけ

コンテナ起動後に追加されるルールのうち、POSTROUTINGに追加される下記内容について:

meta l4proto tcp ip saddr 172.17.0.2 ip daddr 172.17.0.2 tcp dport 8080 counter masquerade

どのような意図のルールなのか分からなかったのだが、Stack Overflow情報によれば、POSTROUTINGのデフォルト・ルールがDENY(つまりpolicy drop)である場合でもコンテナが自分自身に接続できるようにするため、というエッジケース対応用のルールであるらしい。

stackoverflow.com

*1:なお私は自作Linuxルーターエアプ勢である。

*2:SCTPやDCCPなどの後発のL4プロトコルにもポート番号はある。AppleTalkやIPX/SPXやNBF(NetBEUI)などの古いプロトコルについては知らない。

*3:サーバ側のポート番号が固定されているので、クライアント側は「送信先ポート番号」を決め打ちして通信開始する――というアプローチである。これとは別に、VoIPやWebRTCなどのように、仲介者(SIPサーバやシグナリングサーバ)を通じて(SDPなどで)「通信に使用するポート番号」の情報を相互に交換することにより、ポート番号を動的に決定する(≒決め打ちではない)アプローチもある。

*4:IPv6では「::」である。

*5:iptablesのマニュアルを確認した感じでは、IPマスカレードを使用する場合にはPOSTROUTINGチェインにだけ設定しておけばよいみたいである。

今までどのくらいプログラミング言語を触ってきたか(3秒で挫折したものものも含む) Ver.15

2023-06-18現在のステータス。昨年(2022-06-19)から1年経て、こうなっている。

eel3.hatenablog.com

なおCSS、HTML、XMLはひとまず除外する。人工言語ではあるけれども「プログラミング言語」という括りに含められるか否かは議論が分かれる気がする。*1

よく使っている

AWK (Gawk)
単純なテキストレコードの処理はAWKで十分間に合う。今の時代、自作ツールをnawkやGNU awk単体で実装するのは苦行すぎて*2皆無なものの、シェルスクリプトMakefileAWKのコードを埋め込むなどして他のコマンドと組み合わせて使う機会は依然として多い。シェル上でワンライナーでテキスト処理する時にも重宝している。これはこれで十分AWKらしい使い方ではないだろうか?
C++
最近のお仕事の主力言語で、C言語のコードをC++に移行する作業も多いのだが、しかし未だに本職のC++使いのレベルに到達できていない。まだまだC++17止まりではあるが、C++11以降は非常に便利で、better Cでも使う価値があると思っている。C言語使いからすると、C++03時代よりも充実度が進んだ標準ライブラリや、ラムダ式やautoによる型推論に始まるモダンな言語機能は、便利で羨ましい限りだ*3。あと、Swift時代のクロスプラットフォームC++ライブラリ作者は、どうあがいてもARCから逃れられないので、C++11以降のスマートポインタは必須だ*4正規表現とスレッドとファイルシステムが標準ライブラリに加わったので、あとはソケット(低水準ネットワークAPI)をサポートしてくれないだろうか。低水準の処理を行いつつも便利な機能で実装時間を短縮できる点は便利で、少なくともシステムプログラム言語としての利点だと思う。だけど機能多すぎ/複雑すぎなところはなんとかならないものか。強力な反面、使い手を選ぶ言語だ。
C言語
お仕事での主力言語だった――ここ最近は使ってないなあ(C++のコードの一部にC言語寄りのコードを埋め込むことはあるけど)。シンプルかつ低水準の世界が垣間見れるところが割と好きだが、同時にどうしようもなく面倒にも感じる。最近の他の言語と比較すると、シンプルすぎて安全機構が欠けていたり、標準の便利機能が少なかったりするので、入門用の言語としては薦められない。にもかかわらず、かつてはプログラミング未経験者向けのC言語の本が盛んに出版されていた――あれ、何だったのだろうか? 謎だ。クロスプラットフォームなモジュール屋としては、2023年1月にVisual Studio 2012がEOLに到達したことで、ようやく大手を振ってC89からC99に移行できることを喜びたい。あとVisual Studio 2019 version 16.8以降にて本格的にC11/C17のサポートが始まったことも。まあ、まずはC89時代からの手癖をC99向けにアップデートするところから始めたいと思う。
DOSバッチファイル
プログラミング言語に含まれるかどうか不明だが、含めてしまう。ちょっとした自動化や、複数ツールを組み合わせて使うときのラッパーとして、今でもよく使う。コマンドプロンプトはシバン(shebang)に対応していないので、スクリプト言語で書いたツールを起動するラッパーとしても多用している。意外と色々なコマンドが用意されているので、単純にそれらを叩く分には十分だが――言語機能がショボいので、バッチファイルでifやforのような制御構文系コマンドが必要になってきたら、何か決定的に間違えていないか、考え直すようにしている。
make (Makefile)
プログラミング言語に含まれるかどうか不明だが、DSL扱いで……いやGNU Makeはそこそこプログラミング言語的か。GNU Make 4.0はさらにプログラミング言語的だな、特にGNU Guileなところが。GNU MakeとNMAKEが主力。昔は稀にNetBSD Make(pmake)を使うこともあった。3者いずれも独自拡張アリアリで使っていた。もう素のmakeではMakefileを書けない :)
Objective-C, Objective-C++
時代はSwiftだと言われて久しい――どころか場所によっては「Flutter + Dartがメインで、Swiftはサブ」みたいなこともありそうだけど、どっこいObjective-CObjective-C++は生きている。というかSwiftのコードにC++で書かれたライブラリを直接組み込むことができない以上、両者を繋げるグルー言語として生き残ることになるよね。一定以上のリアルタイム性が求められるアプリをSwiftだけで書くのは厳しくて、どうしても部分的にC言語C++を使うことになり、グルー言語としてObjective-Cが召喚されることになる。最近流行の言語と比べると良くも悪くも80年代的だが、アプリケーションプログラミング用としてはC言語よりマシだし、C++ほど複雑怪奇*5ではない。そしてC言語C++で書かれた既存のライブラリをそのまま使える。Objective-Cのハイブリッドな所は好きだが、Objective-C++はハイブリッドすぎて――C++のクラスとObjective-Cのクラスを、C++ラムダ式Objective-Cのブロック構文を同時に使うのは大変だ。便利ではあるんだけどね。
Python
最近、ようやくドキュメンテーション文字列と型ヒントを覚えた。型ヒントは便利で興味深い機能だと思う。Pythonではlazyなスタイルでのコーディングが許されず、整然とコードを記述する必要がある。その辺は、Perl 5やRubyとは随分と雰囲気が異なるように思う。少し気になるのは、インデントが必須な言語仕様であるために、シェルスクリプトに埋め込んで使うのが苦痛な点だ。Pythonだけでコードを書く分には気にならないのだけど……。
Ruby
自作ツールを実装する時、最近はPythonを使うことが多いのだが、時々Rubyを選択することもある。多言語化(文字エンコーディング)が絡むテキスト処理や、処理速度は考慮しなくてよいが桁あふれが気になる数値計算を行う場合だ。あとirb(1)も時々使っている。to_s(16)to_s(2)で基数変換して表示できるところが割と好き。
シェルスクリプト (/bin/sh)
プログラミング言語に含まれるかどうか不明だが……いや、私的にはシェルスクリプトは立派なプログラミング言語だ。基本的な用途は、バッチファイルと同じくちょっとした自動化や複数コマンドを組み合わせて使うときのラッパーだが、実現できる内容は遥かに多い。言語本体(?)がバッチファイルよりも高機能だし、Unixユーザランドはコマンドが充実している。その意味では、WindowsではMSYSよりもCygwinで――いやむしろWSL(Windows Subsystem for Linux)で環境構築すべきだろう。Cygwinでは、主要な処理をシェルスクリプトで記述しておき、bashからはシェルスクリプトを利用し、コマンドプロンプトではラッパーのバッチファイル経由でシェルスクリプトを叩く使い方をしている。ただWindows上では処理速度が妙に遅くなる点が不満だ。まあしかし、Unixのシェルは言語設計もシステム開発技法も未成熟だった大昔に「プアな環境でも問題なく動作する、プログラマブルな対話型コマンドインタプリタ」として開発された代物なので、言語設計の研究が進んでから作られたプログラミング言語と比較してはならない。なお自分自身が落とし穴に嵌らないようにShellCheckを活用すべし。

あまり使っていない

JavaScript(クライアントサイド)
ものすごく久しぶりにクライアントサイドJavaScriptのコード触った――いまどき珍しい、DOM直叩きスタイルだけど。収穫は、最近のECMAScriptのスタイルに触れたぐらいだろうか? フレームワークもTypeScriptも触っていないので、クライアントサイド開発のスキルは依然として賞味期限切れのままだ。
JavaScript(サーバサイド?)
初めてお仕事でNode.js向けのJavaScriptのコードを触った。Web開発の外の人からみた印象としては、ブラウザ以外のJavaScript処理系はNode.jsに収斂しちゃった感があるなあ――Denoもあるけど、エコシステム的に今後どうなるんだろう?
Kotlin
本格的にAndroidアプリ開発に関わるようになったのがGoogle I/O 2017直後の過渡期なので、JavaよりもKotlinでの経験値の方が多い。モダンな「強い静的型付け」の、割とええ感じの言語やね。ただ、使い始めが「Swift 3をつまみ食いして半年以上経ってからKotlinをつまみ食いした」みたいな経緯だったこともあり、未だに両者の概念・機能が頭の中でごった煮になっている。それと、NDK絡みの作業が多いので、C++11/14・Java 7/8・Kotlinを行ったり来たり。泣けるぜ。Swiftもそうだが、最近のメジャーな「強い静的型付け」の言語は「開発環境込み」で高い生産性とコードの安全性を両立させる方向に進んでいる気がする。
Perl 5
時々、やむをえない事情で触ることがある。だが基本的によく分からない。何というか、あの記号の羅列っぽさに中々慣れないというか、自分は余りに自由度が高すぎる言語は苦手だと気づいたというか。(言語仕様に慣れているなら)半ば使い捨てなテキストフィルタとかをさっと書くに分には悪くない言語だと思うのだけど。
Scheme
GaucheWindowsネイティブ環境用バイナリは実験版だが、私が触る分には何の支障もない*6ことに気づいて久しい今日この頃。『Scheme手習い』と『Scheme修行』を購入したので、とりあえずCommon LispではなくGaucheScheme)の勉強をする方向に転換しようか検討*7しているうちに何年たったのやら。Gaucheはフィルタ・ライクな小ツールの実装用としても良い感じだ。しかし最も多い利用方法はREPLを電卓代わりにすることだ*8。うーん、作業環境がmacOSLinuxに移ったなら、大手を振ってGaucheでフィルタを書くのだが。
sed
プログラミング言語に含まれるかどうか不明だが、DSL扱いで*9。テキスト処理用。シェルスクリプトMakefileにて他のコマンドと組み合わせて使う。というか正規表現でのテキスト置換以外の機能を使った記憶が……あったな、dとiとpと=とブレースによるグループ化ぐらいだが。私の技術レベルではsedFizzBuzzを書けないので、sedで難しい処理を記述しないようにしている。
Swift
コンパイラによる強力な型推論と型安全性のチェック」がお仕事用のメジャーな言語にまで降りてきたという点で、Swiftは静的型付け言語界のJavaScript*10だと思っている。でもユーザ数的には、Kotlinが「静的型付け言語界のJavaScript」ポジションなのかもしれない。割と好感が持てる言語だが、知識が中途半端にKotlinとごった煮になっているので、ついうっかりif式を書こうとしてコンパイルエラーになったり、「varval」と「varlet」の振る舞いの差異につまづいたりしてしまう*11
Windows PowerShell
時代はPowerShell Coreらしいが、現行のWindows 10でデフォルトで利用できるv5.1に留まったままである。スクリプト言語としてのPowerShellは、オブジェクト指向.NET Frameworkを叩けてダイナミックスコープでスクリプトブロック(という名の無名関数)と、無茶でピーキーで完全にプログラマ向けな代物だ。Microsoftもよくもこんなエライ代物を出したものだ。残念なことに、コマンドプロンプトの代替という観点では、外部ツールとの親和性が微妙にイマイチだ(特に文字コードとか)。でもPowerShell内で閉じている分には問題ないので、私の手元では「Windows専用のGUI付き小ツールを作るためのスクリプト言語」か「Excel COMとか叩く用のスクリプト言語」か「Windows Serverの管理スクリプトを書くためのスクリプト言語」扱いしている。ところで、いい加減『Windows PowerShell イン アクション』並みの言語解説書の最新バージョン対応版を出版してくれないだろうか。

最近使ってないが、縁は切れてない

bash
最近はデフォルトシェルがbashな環境も多いので、自分用のツールぐらいは素の/bin/shではなくbashで書いても大丈夫な気がしてきた。shよりbashの方が遥かに便利だからなあ――PerlRuby等には負けるけど。bashスクリプトを書くときの唯一の欠点は、メジャーバージョンごとの差異や各ディストリでのビルドオプションの違いにより、同じbashという名前でも実は千差万別なところだと思う。PerlRubyのバージョンは気にするけど、これがシェルになると意外とバージョンに無頓着になってしまう。なんでだろう?
C#
かつて、勉強を兼ねてC# 2.0を少し触ろうとするも未完に終わり、数年後にあらためてVisual Studio 2013をインストールして少しだけ触った*12けどほんの少しだけで終わった過去をもつ私。変数の型推論ラムダ式LINQ・デフォルト引数は便利だなあと思っていたら、いつの間にかC# 8.0になってKotlinやSwiftに見られる流行を取り入れてますな。おっちゃん、付いてくのが大変だよ。.NET Frameworkの機能数は反則ものだが、所々に微妙に抽象化が行き過ぎたAPIが見られるのは気のせいだろうか? それにしても、クラスが必須ではないC言語C++に慣れてしまった弊害か、アプリケーション・メインエントリすらclass内に定義しなくてはならないC#には、なかなか慣れない。
Free Pascal
お試しで触っているのだが、微妙にDelphi/Free Pascal初心者(ただし他言語の経験者)向けの良い資料が少なくて難儀している。玉石混交なのだ。いっそのこと『OBJECT PASCAL HANDBOOK―マルチデバイス開発ツールDelphiのためのプログラミング言語完全ガイド』を買ってしまおうかしら……と思っていたら絶版っぽい。
Go
寡作ながらもいくつか小ツールを書いてみたが、標準ライブラリが充実しているコンパイラ型言語っていいっすね。C言語に比べればC++の標準ライブラリも充実しているが、どちらかといえばプリミティブな機能が中心だ。PythonRubyばりの標準ライブラリを抱えているGoには及ばない。その辺は、やはりCプログラマ(特にCでフィルタやデーモンの類を書く層)には受けそうな言語だと思う。並列処理周り(goroutines)とかARM対応とかが気になる。ソフトリアルタイム限定だが「組み込みLinux + Goで書いたデーモン」とかどうだろう? ただメモリを食うらしいという噂がどうなったか気になる――64bit環境では解消されるという話だったようだが、32bit環境でも解消されるようになったのだろうか? 組み込みでは現時点では逆立ちしたって64bit CPUはありえないからなあ、スマホタブレット以外では。
Java
生まれて初めて触れたプログラミング言語その2。実のところ、職業プログラマとして本格的に使用することは一生ないと思っていた。Androidアプリ開発も、Kotlin採用後に本腰入れて関わるようになったので、Kotlinメインだ。だが、なぜかぬるい感じに時々Javaのコードを触っている。先にコレクションの操作方法が充実した他の言語を学んでからJavaを本格的に触るようになったので、Java 8以降のStream APIが使えないと身体が拒否反応を示す。少なくとも、構文の見た目こそ保守的なオブジェクト指向プログラミング・スタイルで書かれたC++に似ているけど、中身はC++とは似ても似つかない代物だということは体感している。
Lua
Wiresharkのパケット解析スクリプトを書いたことも、C言語で書かれたUnixデーモンの設定ファイル用に処理系を組み込んだこともあった*13。あれから数年経ったが、今はどんな感じなんだろう?
SQL
生まれて初めて触れたプログラミング言語その3ぐらいに位置する。組み込みの人なのでSQLとは無縁だと思っていたが、まさかTransact-SQLを少しだけ触ることになるとは。最近はAndroidアプリ絡みでSQLiteに触れることもあるが、AndroidXのRoom経由だったり、ContentResolverのqueryだったりと、フルセットのSQL文ではなく局所局所でDSL的に使う感じである。
Tcl/Tk
Tclは書き方を忘れた頃にテキスト処理ツールを書いている気がする。Tclは結構独特な言語だ。構文がシェルスクリプトばりに全てコマンドだったり、値が全て文字列だったり、実はリスト構造だったり、意外とTCPソケット通信が得意だったり……。それでも慣れれば結構使いやすい。意外とプロトタイピングに向いている気がする。8.6以降ではオブジェクト指向プログラミングもOKだが、それよりも例外処理用のtry末尾呼び出しの最適化用のtailcallの方が興味深い。しかし、これからメジャーになる可能性は低そうだ。Tkは……小規模なGUIツールをさくっと構築できるところは便利だが、Webアプリ全盛の時代にどれだけ訴求力があるのやら。
Visual Basic .NET
Visual Basic .NET 2003で書かれたコードを時々メンテ中。流石に開発環境はVisual Studio 2013移行したけど。
XSLT
よく考えてみたら生まれて初めて触れたプログラミング言語その4ぐらいに位置する言語だった。縁が切れたと思いきや、仕事でXHTMLから特定要素を抜き出す作業に使うことがあったり……。XMLからテキストレコードに変換してしまえば、後はUnix流テキストフィルタの世界が待っている。餅は餅屋というもので、定型的なXMLの変換はXSLTで記述するべきか。唯一気に入らないのは、xsl:sortでアルファベットの大文字と小文字を区別してソートすることができないこと。ぐぬぬぬ。

これから(また)使うかもしれない

Alloy
形式手法の中では比較的カジュアルに使えそうなので期待中。入門書も処理系も入手した。私の場合、先に何か論理型の言語をかじった方がよいのかも。
bison (yacc)
プログラミング言語に含まれるかどうか不明だが、DSL扱いで。やっぱり構文解析系統のコードを自作するのは割に合わない――だなんてうそぶきつつ、LALR法とか全く知らないままに、既存のyaccのコードを切り貼りして遊んでみた。簡易電卓レベルだが便利さを体感しつつ、さっそくtypo 1文字で痛い目(shift/reduce)に遭った。とりあえず、flexと組み合わせた上でのエラー処理(エラーメッセージの改善)が課題だ。
Common Lisp
2009年に勉強しようと思い立ったものの、未だに進んでいない。階乗とかハノイの塔とかiotaぐらいは書いたが、目標は「ちょっとしたツールを自作する」だ。まだ道は遠い。最近は時々CLISPを簡易電卓代わりにしている。
Coq
ソフトウェアの基礎が気になるので、処理系だけ入手。
F#
OCamlは「Windows上で日本語を扱う」という視点では処理系がちょっと微妙なので、いっそのことF#に乗り換えようかと……。『実践F#』が積読状態になっている。
flex (lex)
プログラミング言語に含まれるかどうか不明だが、DSL扱いで。字句解析用のツールという印象が強かったのだが、よく考えてみたら、flexは「sed(1)のよくある使い方」と同様に「正規表現でパターンマッチング --> 何らかのアクション」という内容を記述するためのツールだった。ただ単に、「何らかのアクション」をC言語で書けることと、flex自体ではパターンマッチングをせずに「パターンマッチングするC言語のコード」を生成することが少々風変わりなだけ。grep(1)やsed(1)その他で小ツールを実装して運用しつつ、性能が求められたらflexで専用ツール化する――とか考えたけど、普通にgrep(1)やsed(1)を使う方が高速だった。
Forth
pForthをMinGWでビルドしたので処理系は手元にある。スタック指向の言語はいつか勉強したい。
Io
プロトタイプベースである点を除けば、何となくSmalltalk的であるような――公式ドキュメントらしきIo Programming Guideでも影響を受けた言語として真っ先にSmalltalkが挙げられているから、あながち思い違いでもないだろう。今更ながら『7つの言語 7つの世界』のIoの章を読み終えたので、ちょっとしたコード片を書いているが……Windows版のバイナリが古いためか、リファレンス通りなのに動作しないコードに直面している。
LOGO
そういえばLOGOを触ったことがない。とりあえずUCBLogo(Berkeley Logo)だろうか? Windows上でUCBLogoばりにGUI無しで動作する処理系はないだろうか?
Object REXX
思うところがあって処理系とIBM謹製のドキュメントを入手したものの、そこから先の進展は無いまま。ReginaでClassic REXXっぽい感じで触っているからなあ。
OCaml
Common Lispを勉強するはずが、いつの間にか触っていた言語。一応、階乗ぐらいは書いた。時間が取れたらもうちょっとしっかりと勉強したいが、面倒なのでF#に移行しようか検討中。
Oz
ふと思い立ってUbuntuにMozartを入れた。『Scheme手習い』の次はCTMCP片手にOzで勉強かなあ。道は遠いな……。
PostScript
これかForthか、どちらに手を出すべきか? 悩ましい。
Processing
入門書も処理系も入手して、あとは弄る時間をつくるだけ。
Prolog
『7つの言語、7つの世界』の地図の色分けプログラムには衝撃を受けた。何というか「正しい問い」を見つけられるか否かが肝なのか。この辺は、根底の部分でAlloyに通じる何かがあるように思う。ひとまず、Prologで論理プログラミングと宣言的なスタイルに慣れておけば、形式手法にて「論理で宣言的に」記述するときに戸惑いが減るのではないかと期待している。
Rust
仕事柄「C/C++の次のシステムプログラミング言語」はそれなりに興味の対象で、Go言語やD言語ほどではないが、Rustも……まあ、気にならなくはない。ちなみに、これら3言語と同列にObjective-CやSwiftが挙げられることもあるようだが、個人的見解としては、システムプログラミング言語としてのこの2言語には全く期待していない。あれは、Appleというしがらみからは逃れられないでしょうな。
VBA (Visual Basic for Applications)
今までVBAから逃げ回っていたのだが、ついに使うことになりそうな予感。たぶん、Access VBA 8割にExcel VBA 2割ぐらい。

今は全く使っていない

Active Basic
VBScripを触りだした影響で、時々思い出しては弄くっていた。ほんの少しだけ触って放置して、すっかり忘れてからまた触る――これを繰り返していた気がする。なので毎度初めて触るのと同じ状態だった。String型をバシバシ使用 :)
bc
その昔、Windows標準の電卓アプリの代わりに使おうとして色々あって挫折した。今はirbclisp/goshで計算しているからなあ。
CASL II
生まれて初めて触れたプログラミング言語その1。何だかんだで、後でCプログラマになってからも低水準での思考ツールとして微妙に役に立っている。まあ考えるための言語であって実用言語ではない。仮に実用的な処理系*14があったとしても余りに命令がシンプル過ぎて悶絶するなあ、なんてFizzBuzzしてみて思った。
Clojure, Scala
JDKがなくてもJava APIを叩くスクリプトを書けるので非常に便利。Scala型推論とか、便利っすね。言語仕様はJavaよりも好みだ。とはいえ、IoT時代にJava VMベースでどこまでメインストリームに居残ることができるのか? ちょっと興味深い。サーバサイドに活路を見出すのだろうか?
COBOL
FizzBuzzするためだけにOpenCOBOL 1.0をWindows上に用意して触ってみた。なんというか、COBOLの名前と生まれた時代が示すように基幹業務(というかお金や帳簿が絡んでくるところ)向けの言語だよなあ、といった感じ。COBOL 2002のフリーフォーマットを採用するだけでも使い勝手が変わる気がしたが、世の中にはまだ広まらないのだろうか。
CoffeeScript
仕事で使う予定はない。RubyPythonその他の影響を受けているだけあり、その手のスクリプト言語っぽい感じでコードを書けるので、慣れれば素のJavaScriptで直接コーディングするよりは楽だ。しかし標準ライブラリ回りや処理系絡みの機能やサードパーティのライブラリなど、結局はJavaScriptを知らないとCoffeeScriptでコードを書けないと思う。それに生成されたJavaScriptのコードを見て「うわぁ、これあまり効率的でないなあ」と感じる時もあって、高速化が必要な部分では生成されるコードを気にしながら記述したりCoffeeScriptを諦めてJavaScriptで書くことになるので、やはりJavaScriptを知らないとマズイ。とはいえ便利なのは確かだ。CoffeeScriptのコードは即Node.jsで実行できるので、その辺りから「CoffeeScriptでテキストフィルタ」的な文化が生まれると面白いかも。気になるのはECMAScript 6の存在で、今までCoffeeScript独自の機能だった部分の多くがES6で取り込まれるので、今後ES6対応が進むにつれてCoffeeScriptの立場がどうなっていくのか、少々興味深い。
D言語 2.x
仕事柄「C/C++の次のシステムプログラミング言語」はそれなりに興味の対象で、Go言語ほどではないが、D言語も気になる存在だ。D言語シンタックスがC・C++に近いだけでなく、コーディングしている時のアプローチ・判断についても、CやC++での流儀がそこそこ通用しやすい気がする。少なくとも、Go言語でコーディングするよりは、文化的背景の違いによるモヤモヤは感じにくい。あと、標準ライブラリを使ってテキストフィルタを書いたところ、エラー処理を1~2ヶ所のtry - catchにスッキリまとめることができて、ちょっと驚いた。throwされる例外のメッセージ文字列が、ちょうどよい塩梅の内容だったため、メッセージを変更する(いったんcatchして、再throwする)必要がなかった。ちょっと残念なのは、マルチバイト対応だが……。
Emacs Lisp
.emacsにコピペ」限定で。Common LispSchemeを触ったためか、何となく内容を追えるようになってきた気がしていたが、勘違いだった。
Fortran
Fortran 90やFortran 95あたりは結構近代的な言語だと思う。用途次第ではC言語よりもFortranの方が遥かにマシな選択だろう。配列がらみの処理はFortranの方が得意だし、言語機能としてのモジュール化の方法はC言語には存在しない。可変長な文字列の扱いに微妙な制限がある点はマイナスな気もするが、まあ基本的に数値計算プログラム用の言語だからなあ。
GDB (GNU Debugger)
……いやGDBはデバッガとして使っているが、GDBスクリプトを書く機会は(FizzBuzz以外に)ない。勉強不足なだけかもしれない。
Groovy
JDKがなくてもJava APIを叩くスクリプトを書けるので非常に便利。動的型付け言語っぽくいくもよし、@CompileStaticや@TypeCheckedで型推論するもよし。言語仕様はJavaよりも好みだ。コンソールアプリを書く人としては、オプション引数解析用の機能を標準で持っている点で、GroovyはClojureScalaよりもポイントが高い*15。個人的には、IoT時代に「Java VMベース」の言語としてどこに活路を見出すのが、興味深く見守りたいところ。やはりサーバサイドだろうか?
HSP (Hot Soup Processor)
FizzBuzzで楽しんでみたが、何というか他言語経験者には受けが悪そうな命令体系だと思う。もっとも初心者がプログラミングという行為に深入りせずにWindows用のGUIな何かを作る分には、あの命令体系でも十分な気がしないでもない。ところで元々は「HSPで職業プログラマ的な良いコードを書くと、どんな感じになるか?」というネタを思いついて処理系を用意したのだけど、そちらは全く進展がないまま。
JScript on WSH
他人が使うテキスト処理ツールの実装に使って以来、時々触ってきた。Windows用の配布可能な小ツールを実装する時の定番言語だった。でもそろそろ潮時だろう。HTAと組み合わせてクライアントサイドJavaScriptなノリで簡易なGUIツールを実装できる点も、PowerShell + WPF + XAMLで代替できそうだ。他のメリットは「JavaScriptECMAScript)でフィルタを書ける」だったが、WSHのなかなか目的にたどり着けないオブジェクト階層にイライラするよりも、Node.jsやPhantomJSを使ったほうが精神衛生的にマシだ。
m4
その昔テキスト処理用に触ろうとして、Windows用のどの処理系も日本語の置換に何かしらの問題を抱えていたので泣く泣く諦めた。思うところがあって改めて少し触ってみたが――なるほど、確かに中毒性のある言語*16だ。
QML
宣伝文句のとおり、QMLはGUIの記述に非常に向いている。それも、単に標準のUI部品(エレメント)を使うだけでなく、少し改造して使うとか、オリジナルのUI部品を作って使うとか、それらを別のアプリケーションに使いまわすとか、そういう時に威力を発揮する。あと、プロパティバインディングやレイアウトのアンカー指定など、画面サイズの変更に追随するUIを作りやすい機能も揃っている。JavaScriptでちょっとした処理も記述できる――とはいえ、やりすぎるとパフォーマンスの罠が……。少なくとも、JavaScriptでゴリゴリコードを書くのはQML的ではない。QMLは宣言的に、シンプルに書くものだ。力技でロジックでゴリ押しすると、色々と罠に嵌る言語だ。
REXX
Open Object REXXの処理系を入手したのに、何故かReginaを入れてClassic REXXっぽい方向に走っていた。何というか、COMコンポーネント.NET Frameworkと無関係でいられるのなら、バッチファイルの代替としてはREXXあたりがほどよい塩梅だと感じる。しかし最近流行の言語とは随分と勝手が違うし、日本語の情報も少ない。メインフレーム以外の世界で流行る可能性は少ないだろう。
Smalltalk (Squeak, Pharo)
Smalltalkは有名な古典的プログラミング言語だというのに、触ったことがない。ということでSqueakとPharoの処理系のみ準備完了。うーん、「環境」付きなのが気になる――言語を弄くる基準が「コンソール上でテキストフィルタ」という変な人種な私だからなあ。
Smalltalk (GNU Smalltalk)
個人の思想信条による理由よりSqueakとPharoにわだかまりを感じてしまう変人なので、邪道だと思いつつもコンソールでテキスト処理もOKなGNU Smalltalkも用意してみた。これで言語としてのSmalltalkの勉強に集中できる……か?
T4 Text Template
「へえ、こんなものがVisual Studioに入っていたのか。機能多すぎで色々と便利なツールを見逃しているんだな、やっぱり」と思いつつ触ってみた。テンプレート変換の用途ではピカ一だと思う。ただ処理系を手に入れる方法が「Visual Studioをインストールする」or「MonoDevelopをインストールする」なので、何となく「単体で手軽に使えるツール」ではないというイメージが……。まあC#VBで処理を記述するので、それらの処理系が必要だという面での制約なのだろう。
VBScript on WSH
JScriptほどではないが「Windows上で他人も使えるツールを書くためのLL」扱いしていた言語。Windows Server管理の関係で触っていた。というかWebで入手可能なWSHのサンプルの大半がVBScriptで書かれていたり、ADSI関連のコレクションをJScriptで舐めれなかったりして、結局は必要に駆られて使用することに。明快に記述できる文法は評価に値するが、スクリプト言語としては少々冗長だ。配列は自動拡張しないし、組み込み関数はプリミティブ気味だし、冗長気味な文法との合わせ技でコードがさらに冗長になっていく……。文法や言語仕様の詳細なドキュメントが見つからないのだが、どこにあるのだろうか?*17
Vim script
少し触ってみた分には、exコマンドの拡張(=コマンドの羅列)とは気づかない程度にはプログラミング言語らしいと思う。とはいえ妙なところで嵌ったり微妙に一貫性のない部分があったりするので、その辺りで好き嫌いが別れる気がする。
秀丸マクロ
7年ほど秀丸エディタを使っていたが、マクロを書く機会はなかった。一念発起してFizzBuzzしてみて感じたのは、最近の便利な言語に慣れた身としては色々とモヤモヤ感がある言語仕様だということ(歴史的経緯的に仕方ないのだが)。とはいえちょっとした拡張ツール的なものを手軽に作れそうではあった。

*1:HTML5 + CSS」の組み合わせなら、チューリング完全の疑惑があったり、JavaScript使わずにCSSでWebチャットを作った猛者がいたりと、色々と怪しいのだけど。

*2:「独立性の高い単体のツールを実装する」という視点では、現代ではAWKよりも便利な言語が山ほどある。

*3:しかし標準ライブラリの充実度をJavaC#.NET Framework含む)と比較したり、型推論まわりをKotlinやSwiftと比較してはいけない。以前よりも随分と便利になったのだけど、だけど、隣の芝生を見てしまうと、うーん……。

*4:SwiftではC++の機能を直接呼び出すことができないので、Objective-Cでラッピングして利用することになる(インタフェースはObjective-Cで、内部実装はObjective-C++)。この時、Objective-Cクラスのインスタンス変数として「C++クラスのインスタンスを保持するスマートポインタ」を持つ構成にしておくと、Objective-Cクラスのインスタンスがdeallocされる時に、スマートポインタ経由でC++クラスのインスタンスもdeleteされる。

*5:少なくともC++の言語/ライブラリ仕様は私の手には余る。自分が把握している範囲の機能でコードを書くのは好きなのだけど。

*6:支障がある部分を触るほど深入りするには、あと20年ぐらい掛かるのではないか?

*7:Schemeの勉強というよりも、再帰の勉強なのか?

*8:現状はirbclispかgoshの3択だ。

*9:これでもsedチューリング完全な言語だと証明されているらしい。

*10:私の認識では、JavaScriptは、第一級関数やクロージャがお仕事用のメジャーな言語に組み込まれて、少なくない人が使う契機となった言語だ。

*11:Kotlinのvalは「再代入不可の変数」だ(定数はconstで定義する)。Kotlinのプリミティブ型以外のデータ型はclass(つまり参照型)なので、valで定義した変数を破壊的操作する行為は割と普通だと思う。一方でSwiftのletは定数であるし、値型が中心の言語仕様である影響かstructやenum(つまり値型として振る舞うデータ型)が多用されるので、letで定義した変数を破壊的操作できるケースとできないケースが生じる。

*12:言語仕様的にはC# 5.0の環境だが、ライブラビまわりはC# 4.0相当だったはず。

*13:Windowsのことを考えなければ、自前でライブラリをビルドしてアプリに組み込むのは結構簡単だった。

*14:――といってもシミュレータだけど。

*15:ClojureScalaに関しては、同様の機能を私が見逃している可能性も高いのだが。

*16:m4はマクロプロセッサなのでプログラミング言語ではないはずだけど……。

*17:MSDNの資料では物足りない。もうちょっと掘り下げた内容のものが欲しい。

改行コードを問わずにテキストファイル中の連続する空白行を1行に縮めて上書きしたい

注文の多いタイトルである。

  1. テキストファイル中の連続する空白行を1行に縮めたい。
  2. 縮めた結果を元ファイルに上書きしたい。元ファイルのバックアップは残さなくてよい。
  3. CR、LF、CRLFのいずれの改行コードのファイルでも使えるようにしたい。
    • 出力結果では元の改行コードを保持したい。
    • 複数種類の改行コードが混在しているファイルは、さすがに想定外とする。

なぜ、こんなに注文が多いのか?

元々は、astyle(1)(Artistic Style)を弄くっていて:

astyle --style=kr \
       --indent=tab=4 \
       --indent-namespaces \
       --indent-labels \
       --indent-preproc-define \
       --break-blocks \
       --pad-oper \
       --pad-header \
       --unpad-paren \
       --add-brackets \
       --align-method-colon \
       --unpad-method-prefix \
       --pad-method-colon=none \
       --max-code-length=120 \
       --errors-to-stdout \
       ${@+"$@"}

――こんなオプションでいい感じになるのだが、唯一気に入らないのが、2行以上の連続する空白行がそのまま残ってしまうこと。1行にしたいのだ。

astyle(1)には--delete-empty-linesというオプションがあるが、これだと1行だけの空白行も消えてしまう上に、関数内でしか有効にならない。そうじゃなくて、1行だけの空白行はそのまま残しつつ、2行以上の連続する空白行を1行にしてほしい。関数の中と外のどちらでも適用されてほしい。

astyle(1)にかけるソースファイルには、改行がCRLFのものとLFのものが混在している。なので、どの改行コードのファイルでも処理できるようにしたい。出力結果の改行コードは元ファイルに準じてほしい。

astyle(1)は、元のファイルのバックアップを作成しつつ、元のファイルの名前で処理結果を出力する。その出力結果を上書きする感じで、空白行の処理を行いたい。大本のファイルはastyle(1)が残しているので、空白行の処理を行う前のファイルは残さなくてもよい。

ということで、まずはこんな感じのコードを(「これ、遅いだろうな」と思いつつ)書いてみた。

for i; do
    cat "$i" | (rm -f "$i"; sed '/./,/^$/!d' >"$i")
done

しかし残念なことに、手元の環境(Cygwinsed(1)を使用)では、CRLFが問答無用でLFになってしまった。どうもsed(1)が原因のようだ。

sed(1)を止めてPerlにすることにした。最初に書いたのがコレ。

perl -0777 -pi -e 's/(\r\n|\n|\r){3,}/\1\1/g' ${@+"$@"}

-0777」で入力ファイルを一気に読み込むことで、正規表現を用いて連続する改行コードにマッチできるようにしている。また、オプションiを拡張子無しで用いて、入力ファイルを出力結果で上書きするようにしている。

これでうまくいくかと思いきや、例えば次のようなファイル(CRLF)にて:

1 aaaaa

2 bbbbb

1行目の末尾から3行目の頭の間の「\r\n\r\n」が「\n\n」になってしまった。つまり、CRLFのファイルを食わせると、CRLFとLFが混在したファイルができあがる可能性がある。

これは正規表現の問題で、本来なら、CRLFのファイルでは「CRLFが3つ以上」に、LFのファイルでは「LFが3つ以上」に……という具合にマッチさせなくてはならないところを、無理やり1つの正規表現にしたために、「\r\n\r\n」が「『CRかLF』が4つ」と解釈されてマッチしてしまう可能性が生じていたのだ。

上記を踏まえて書き直したのがコレ:

perl -0777 -pi -e '
  foreach my $nl ("\r\n", "\n", "\r") {
    s/(?:$nl){3,}/$nl$nl/g;
  }
' ${@+"$@"}

これで意図したとおりに動作するようになった。

ところで、大抵のファイルでは改行コードが混在している可能性は低い。なので、3種類の改行コード全てについてマッチングと置換をする必要もないだろう。

ということで、ファイル中の最初の改行コードを調べて、その改行コードでのみ置換を行うようにしてみた。

perl -0777 -pi -e '
  foreach my $nl ("\r\n", "\n", "\r") {
    if (/$nl/) {
      s/(?:$nl){3,}/$nl$nl/g;
      last;
    }
  }
' ${@+"$@"}

これで多少は速くなった……かもしれない。

Lispマル非入門

コンピュータは電子計算機とも訳されるように計算に用いるのが本流である、という信念*1の元に、私はちょっとした計算にもパソコンを用いている。

当然ながら*2計算にはLisp処理系のREPLを使用する。*3

ちょっとした計算にてLisp処理系のREPLを用いるメリットは、例えば何かを単純に足し引きする時に、足し引きする値ごとに演算子を書かなくて済むのである。

私はよく買い物の計画を練るのにREPLを用いる:

; ルーターのお守りを任されたけど使い方が分からん。
; 入門書でも読もうかな。
(+
 4730   ; 978-4839965402
 )
; => 4730

; そういえば新人向けのLinux入門書を見繕わないと。
; この辺りの本とかどうだろう?
(+
 4730   ; 978-4839965402
 1980   ; 978-4048913928
 2970   ; 978-4797380941
 )
; => 9680

; ついでに同僚のA氏が絶賛してた本も買おうかな……。
(+
 4730   ; 978-4839965402
 1980   ; 978-4048913928
 2970   ; 978-4797380941
 4488   ; 978-4839981723
 )
; => 14168

; ちょっと金額を減らしたいなあ。
; こんな感じでどうだろう?
(+
 4730   ; 978-4839965402
 2970   ; 978-4797380941
 4488   ; 978-4839981723
 )
; => 12188

; 端はポイントで支払おう。
(+
 4730   ; 978-4839965402
 2970   ; 978-4797380941
 4488   ; 978-4839981723
 -188
 )
; => 12000

買い物が1ヶ所のネットショップで済むならば、買い物かごの機能で合計金額を確認すれば済むだろう。だけど時には品揃えと単価の都合より複数のネットショップ(そして稀に実店舗も)に分散して購入を検討することもある。そんな時は、REPL上であれこれ項目をこねくり回して合計金額を計算することが多い。*4

中置記法を用いるプログラミング言語に慣れた身からすると、Lisp処理系での足し算は少し興味深い。

私の普段使いの言語では、加算は二項演算だ*5。そのため加算する際には値が2個必要だ。値が1個だけでは駄目だ。そして値が3個以上あってもいけない(演算子を都度追加しなくてはならない)。

# これはOK。
10 + 3
# => 13

# これはNG……というかREPLでは「右辺となる値の入力待ち」になりそう。
10 +

# これはNG。シンタックスエラー。
10 + 3 5

# 演算子を都度追加すればOK。
10 + 3 + 5
# => 18
10 + 3 + 5 + 2
# => 20

(後置記法/逆ポーランド記法を用いるプログラミング言語ではどうだろうか? Forthの解説文を読んだ感じでは、ワード+はスタックに積まれている値を2個使用するようだが……)

一方で、Lisp処理系では値が1個でも3個以上でも加算した結果を得ることができる。だから、REPLを使ってあれこれ考えながらこねくり回す時に、安心して「取り扱う値」にだけ集中していられる。演算子の書き忘れに起因するシンタックスエラーなどを気にしなくてもよい。

; これはOK。
(+ 10 3)
; => 13

; これもOK。
(+ 10)
; => 10

; これだってOK。
(+ 10 3 5)
; => 18
(+ 10 3 5 2)
; => 20

それと、中置記法の言語では例えば「10 + -3」のような書き方は合法だが、個人的に見た目に違和感があり、どうしても「10 - 3」と書きたくなる。しかし理由は不明だが、Lisp処理系で「(+ 10 -3)」と書いても違和感がない*6。だから安心して負の値のリテラルを加算するコードを書ける。

注意点としては、多くのLisp処理系には分数型があるので、整数値を除算した時に意図せず分数型の値が得られてしまうことがある。

; 72の法則を試してみよう。
; 年利4%だとどうだろう?
(/ 72 4)
; => 18

; 年利5%だと?
(/ 72 5)
; => 72/5

; おおっと、明示的に小数のリテラルで書かないと駄目だった。
(/ 72.0 5)
; => 14.4

変数については、letなどを用いて局所変数を使う分には、Lisp処理系ではワンライナーでも比較的書きやすい気がする。普段使いの言語でも、ブロックスコープを用いれば似たようなことを実現できるが、それをワンライナーで書くのはちょっとだけ面倒に感じる。この辺は、Lispの「式もデータ構造もリスト」という特徴がコードの見た目に及ぼす影響によるものだろう。

; 今はこんな感じ:
(+ 108)
; => 108

; 直近の想定値を加味すると:
(let ((n 21)) (+ 108 n -40 -21.2 -4.8))
; => 63.0

; さらに4回分の想定値を加味すると:
(let ((n 21)) (+ 108 n -40 -21.2 -4.8 (* n 4) 10 -40 -21.2))
; => 95.8

; ……うーん、条件を少し変更してみるか:
(let ((n 22)) (+ 108 n -40 -21.2 -4.8))
; => 64.0
(let ((n 22)) (+ 108 n -40 -21.2 -4.8 (* n 4) 10 -40 -21.2))
; => 100.8

ところで大域変数*7ではなく局所変数を用いるしょうもない理由の1つとして「letあたりならCommon LispSchemeのどちらのREPLでも大体同じ感じに書ける」というものがある。例えばここまでに書いたLispのコード片はCLISPGaucheのどちらでも動作する。*8

ここまで書いてきたように、私は単純な足し引きの計算にLisp処理系のREPLを多用しているのだが、それゆえに、ここまでに書いた内容よりも高度なコードはほぼ確実に書けない自信がある。

今、手癖の範囲で書けるLispのコードは、この程度が限界だ。

; 四捨五入を考慮しないなら、こんな感じ?
(let ((ir (* 0.31 0.66)) (lir 0.05)) (if (>= ir lir) ir lir))
; => 0.2046

if式は便利だ……KotlinのノリでSwiftでif式を書こうとしてシンタックスエラーしちゃう癖を何とかしたいなあ。CやC++ならちゃんと「if文」モードでコーディングできるんだけど、Swiftだとうまく切り替わらないの、何でだろう?

*1:そんなものはない。

*2:いや全くもって当然ではない。

*3:単純な計算を行うだけなのに、電子計算用のハードウェアでOSを動かして、OS上で数値計算用ではなくリスト処理用の言語処理系を動かして数値計算を行う――という大いなる計算資源の無駄遣いを楽しめるのも大人の醍醐味であろう。

*4:この手の計算に表計算ソフトを用いないのは、ひとえに私がへそ曲がりなだけである。

*5:もしかしたら「中置記法」かつ「加算が二項演算ではない」プログラミング言語も存在するかもしれないが、今のところ私はそのような言語に遭遇したことがない。

*6:個人の感想です。

*7:もしくはスペシャル変数。

*8:いい加減、この手の計算に用いるREPLをCommon LispSchemeのどちらか一方に統一したいところである――とか言いだしてから10年以上経過した気がする。

macOS上で削除できないファイルのロックを解除する(ターミナルで)

WindowsmacOSを本格的に併用していた時期に気づいたのだが、Windowsで「読み取り専用」に設定したファイルは、macOSではロックされたファイルとして認識されるようだ。明示的にロックを解除しない限り、macOSのFinderでは削除できない。

Windowsでは誤ってファイルを編集してしまわないように「読み取り専用」に設定することがある。「読み取り専用」のファイルは普通のファイルとほぼ同様にそのまま削除できる(「読み取り専用」のフラグを解除する必要がない)。なので、運用上は特に問題にならない。

これが例えば「Windowsで『読み取り専用』のファイルをリムーバブルメディアにコピー→macOSに持っていく」なんてことをすると、macOS上でファイルを削除できずに慌てることになる。

ロックの解除はGUIでも可能だが、上記の状況ではロックされたファイルが大量にあったりする訳で、GUIで解除するのは面倒だ。普段からbash(1)で操作することが多いので、ターミナルで何とかしたい。

ロックの解除はchflags(1)で可能だが、コマンド名やオプションを忘れた頃にロックされたファイルに遭遇することが多いので、シェルスクリプト化しておくことにした。

#!/bin/sh
# -*- coding: utf-8-unix -*-
# vim:fileencoding=utf-8:ff=unix

PATH=/bin:/usr/bin

exec chflags -R nouchg "$@"

シェルスクリプト化したから、多分しばらくの間ロックされたファイルに遭遇することはないだろう。世の中そういうもの。

chflags(1)はmacOS固有のコマンドかと思ったが、どうもBSD由来のようだ。4.4BSDから追加されたファイルフラグ(パーミッションとは別に、ファイルの追加・変更禁止などを設定するフラグ)を操作するためのコマンドだった。

ということは、Windowsで「読み取り専用」に設定したファイルをFreeBSDOpenBSDに持っていった時にも同じような問題が起きるのだろうか?

終わりに

最終的には、こんな感じになった。

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

SSDのブランドの選び方 2023年春

2022年後半はNANDの価格の下落が進み、その影響か有象無象の格安SSDをよく見かけるようになった。

SSDの中身は、乱暴に言ってしまえば「NANDチップを含む電子部品が実装された基板」だ。HDDとは異なり、モーターのような精密な機械部品を含まない。必要な電子部品さえ揃えられるならば、SSDを設計・製造する難易度はあまり高くない。その影響か、現状では有象無象のブランドまでSSDを手がけている。

玉石混交なブランドがSSDを扱っている影響で、市販のSSDの品質も玉石混交気味だ。

SSDを「一定以上の信頼性のある補助記憶装置」として使いたい場合――例えばOSをインストールするシステム・ドライブとして使用したいならば、玉石の中から信頼性が高めなSSDを選ぶ必要がある。

SSDも工業製品なので、最終的には個体による品質の差(そして時には初期不良の問題)に突き当たるのだが、その点を除外するならば、一般的には「NAND型フラッシュメモリの市場シェア上位のメーカーが展開するブランドのSSD」は比較的信頼性が高いといえる。

NANDメーカーの吸収合併など動きでブランド名が変化することがある。ここに、2023年2月時点での「比較的信頼のおけるSSDブランド」についてメモしておく。

以下、アルファベット順:

  • Crucial (Micron Technology)
    • NANDシェア第5位のMicron Technologyが展開しているブランド。
    • 良くも悪くも「定番のブランド」という印象。
  • KIOXIA (KIOXIA)
    • 東芝メモリ東芝がチャレンジしちゃった結果、東芝半導体メモリ事業部が分社化して東芝メモリになり、商号変更でキオクシアになった。
    • 実はNANDシェア第2位のメーカーだが、SSDについては後発ブランドなためかまだまだ影が薄い気がする。NAND自体は、東芝メモリ時代にKingstonのSSDに使われていたりしたことで(一部の界隈では)有名なのだけど……。
  • Samsung (Samsung Semiconductor)
    • 説明不要。なおSamsungはNANDシェア第1位のメーカーでもある。
    • SamsungSSDは、全体的に価格がちょっとお高めな印象がある(個人の感想です)。
  • Solidigm / SK hynix / Intel (SK hynix)
    • NANDシェア第3位のSK hynixが展開しているブランド。今は3位だけど、NAND市場の悪化が無かったらシェア2位になっていたはず。
    • 元々はIntelとSK hynixがそれぞれNAND事業部を持っていて、SK hynixがIntelのNAND事業を買収して子会社化したのがSolidigm(だから旧Intel SSDみたいな言われ方をすることがある)
    • 2023年2月の時点では、旧来のSK hynixの系譜にある「SK hynix」と、旧Intel系の「Solidigm」の2系統にブランドが分かれていると考えられる。とはいえエンタープライズ向けのP5530のような共同開発製品も出てきているので、これから数年をかけて技術面の統合が進んでいくのかもしれない。
  • Western Digital / WD / SanDisk (Western Digital)
    • NANDシェア第4位。Western Digitalは元々HDDメーカーだったが、SanDiskを買収してフラッシュメモリ事業に乗り出した、という経緯がある。
      • 今もWDとSanDiskの両ブランドでSSD製品を展開しているが、技術的にはどちらのブランドも旧SanDiskの流れを汲んでいると考えられる。
      • WDが2013年にSSHD(WD Blackシリーズ)を出した時、SanDiskNANDフラッシュメモリを使用していた。SanDiskの買収は2015年だが、それ以前より繋がりがあったのかもしれない。
    • 他のNANDシェア上位メーカーとは異なり、HDDを含むストレージ・デバイスを幅広く取り扱っている点が特徴的だと思う。

書籍購入:『スーパーエンジニアへの道 技術リーダーシップの人間学』

プログラミングの壺 I ソフトウェア設計編』に続いて、古典を読んでみようと思い立って購入。

あとで書く。