unameコマンドでGNUコマンドかBSDコマンドかを判別してはいけない理由

## はじめに

**この環境では `uname` コマンドで Darwin と出力されたから、FreeBSD 版のコマンドを使っているはずだ**。そう考えてはいないでしょうか？ 残念ながらそれは間違いです。`uname` コマンドを使って環境の情報を調べることはできませんし、環境がわかってもコマンドの種類はわかりません。そもそも `uname` コマンドで知ることができるのはカーネルの情報です。環境の名前ではありません。

## Ubuntu上のDockerでAlmaLinuxを起動してみろ！

`uname` コマンドで知ることができるのはカーネルの情報です。Docker はその仕組み上ホストのカーネルを共有しています。つまり Ubuntu で AlmaLinux を起動してもカーネルは Ubuntu のままです。

```console
$ uname -a
Linux server 5.15.0-112-generic ↩
    ↪ #122-Ubuntu SMP Thu May 23 07:48:21 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux

$ docker run -it almalinux
[root@1dd18009780f /]# uname -a
Linux 1dd18009780f 5.15.0-112-generic ↩
    ↪ #122-Ubuntu SMP Thu May 23 07:48:21 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux
              ↑
         Ubuntuを使っている
```

`uname` コマンドは環境の名前を知るためのコマンドではありません。

Alpine Linux というディストリビューションがあります。これは主に BusyBox を使っており GNU コマンドを使用していません。もちろん Alpine Linux を起動しても Ubuntu だと判断されますが、使われているコマンドは GNU コマンドよりも大きく簡略化された機能しか持ち合わせていません。

```console
$ docker run -it alpine

# uname -a
Linux d220b64aebdc 5.15.0-112-generic ↩
    ↪ #122-Ubuntu SMP Thu May 23 07:48:21 UTC 2024 x86_64 Linux

# ps --help
BusyBox v1.36.1 (2023-11-07 18:53:09 UTC) multi-call binary.

Usage: ps [-o COL1,COL2=HEADER] [-T]

Show list of processes

        -o COL1,COL2=HEADER     Select columns for display
        -T                      Show threads
```

少し面白いディストリビューションとして [Chimera Linux](https://chimera-linux.org/) というものがあります。これはカーネルに Linux を使っているものの、ユーザーランドはすべて FreeBSD のものとなっています。
```console
$ docker run -it chimeralinux/chimera

# uname -a
Linux 2ed1dc55bfe2 5.15.0-112-generic #122-Ubuntu SMP Thu May 23 07:48:21 UTC 2024 x86_64

# sort --version
2.3-FreeBSD

# sort --help
Usage: sort [-bcCdfigMmnrsuz] [-kPOS1[,POS2] ... ] [+POS1 [-POS2]] [-S memsize]
[-T tmpdir] [-t separator] [-o outfile] [--batch-size size] [--files0-from file]
[--heapsort] [--mergesort] [--radixsort] [--qsort] [--mmap] [--parallel thread_no]
[--human-numeric-sort] [--version-sort]] [--compress-program program] [file ...]

# date --help
date: unrecognized option: -
usage: date [-jnRu] [-I[date|hours|minutes|seconds|ns]] [-f input_fmt]
            [ -z output_zone ] [-r filename|seconds] [-v[+|-]val[y|m|w|d|H|M|S]]
            [[[[[[cc]yy]mm]dd]HH]MM[.SS] | new_date] [+output_fmt]
```

歴史的な理由から、**FreeBSD 版の `sort` コマンドは、GNU 版と同じように `--version` オプションや `--help` オプションを持っている**という点も目の付け所です。

## よりマシな os-release ファイル

環境の名前を知る場合、`os-release` ファイルを参照するのがよりマシな方法です。`os-release` ファイルのパスは `/etc/os-release` または `/usr/lib/os-release ` のどちらかです。前者がある場合は前者を優先して使い、後者と内容をマージしてはいけません。詳細は https://www.freedesktop.org/software/systemd/man/latest/os-release.html を参照してください。

`os-release` は systemd 関連のファイルですが、単なるテキストファイルなので FreeBSD や Solaris 11 でも採用されています。ナンセンスな `lsb_release` コマンド（Debian版はPythonスクリプト）に代わるものです。ただしすべての環境に存在するわけではないことに注意してください。

`os-release` はシェルスクリプトで読み込めるようになっており、先のリンク先でも以下のようなサンプルが提示されているので、`.` コマンドで読み込んでも構わないでしょう。`/etc/os-release` もしくは `/usr/lib/os-release` に限ればシステムファイルなので不正に書き換えられる心配はありません（もし不正に書き換えられているのであれば、すでにシステムは侵入されておりそれどころではない）。他の言語での読み込み方も書いてあります。

```sh
#!/bin/sh -eu
# SPDX-License-Identifier: MIT-0

test -e /etc/os-release && os_release='/etc/os-release' || os_release='/usr/lib/os-release'
. "${os_release}"

echo "Running on ${PRETTY_NAME:-Linux}"

if [ "${ID:-linux}" = "debian" ] || [ "${ID_LIKE#*debian*}" != "${ID_LIKE}" ]; then
    echo "Looks like Debian!"
fi
```

しっかし、このコード無駄にややこしいし、`os-release` ファイルが存在しないシステムのことが考慮されてないですね。代わりに以下のように書き換えました。こういうのでいいんだよ、こういうので。

```sh
#!/bin/sh

set -eu

if [ -e /etc/os-release ]; then
  . /etc/os-release
elif [ -e /usr/lib/os-release ]; then
  . /usr/lib/os-release
fi

echo "Running on ${PRETTY_NAME:-Linux}"

case "${ID:-linux}:${ID_LIKE:-}" in
  *debian*) echo "Looks like Debian!"
esac
```

さて、`os-release` ファイルは環境を調べるのに便利なファイルですが、`os-release` ファイルを使って使うコマンドを調べてもいけません。`uname` コマンドを使うよりもマシなだけです。

## 各環境のコマンドの種類を判別する方法

そもそも各環境で使われているコマンドは、OSの名前なんかと関係ありません。macOS に GNU コマンドをインストールできますし、ユーザーは自由にコマンドをインストールできます。環境変数 `PATH` を変更することで、macOS なのにまるで Linux のような GNU コマンドベースの環境を作ることも可能でしょう。

つまり、`uname` コマンドや `os-release` ファイルを使って、GNU コマンドか BSD コマンドかを判別してはいけないということです。それに現在 **BSD コマンドなんてものはもう存在しない**んです。BSD Unix という名前の Unix の開発は終了しました。FreeBSD 版、NetBSD 版、OpenBSD 版、全部違います。macOS 版は FreeBSD 版に近いですが、独自の修正が行われているため、FreeBSD 版と同じという前提もできません。

**Alpine Linux は GNU コマンドではなく BusyBox コマンド**です。
**Chimera Linux  は GNU コマンドではなく FreeBSD コマンド**です。

じゃあ、どうやって各環境で使われているコマンドを判別するのか？

## おわりに

正月で記事を書くのがめんどくさいので、おしまい。

いや別にこれらの方法を使っても構わないんですよ。今どき「どの環境でも動くシェルスクリプト」の価値はそれほど高くありません。自分が使う環境でだけ動けば十分です。ただ `uname` コマンドで雑に環境を判別しておいて、**どの環境でも動くシェルスクリプトを書いた**という主張が、あまりにも「移植性のあるシェルスクリプト」の基本を知らないと言いたかっただけです。

移植性の高いシェルスクリプトを書くには、シェル関数を使って移植性問題を解決するコードと、本質的な処理を行うコードを分離します。移植性問題を解決するコードを本質的な処理を行うコードの中に混ぜ込んで、**あちこちに処理を分岐するコードを書いてはいけません**。シェル関数を使って移植性問題を封じ込め、本質的な処理を行うコードはまっすぐに処理を実行できるようにします。シェル関数を使って移植性問題を局所化しておけば、当初に想定していない環境にあとから対応するのも簡単です。シェル関数を使わなければ全体を精査しなくてはならなくなります。

POSIX に準拠した移植性のあるシェルスクリプトを書くには**シェルプログラミング**の知識が必要です。