macOS の bash で空きポートを取得するワンライナー（ランダムな未使用ポート検索）

## 被らないポート番号を取得したい

macOS のターミナルからワンライナーもしくは `bash` のシェル・スクリプトで、**ランダムな空きポート番号（未使用のポート番号）を取得したい**のです。

- ランダムで未使用の空きポート番号を取得したい
- macOS のデフォルト状態で使いたい（`brew` などで別途インストールできない）
- サーバのポート指定のために、空きポート探しに使いたい
    - PHP や Golang などのビルトイン WEB サーバー
    - [Gitea](https://gitea.io/en-us/) のビルトイン SSH サーバ
    - [Vargant で（プロビジョンされた）サーバーで使う SSH ポート](https://qiita.com/Sanche/items/43d615beef05cd9417e2)

※ コピペピピックが怖い方は TS; DR の詳細をご覧ください

## TL; DR

### ワンライナー

```bash:bash
while :; do PORT="$(jot -w %i -r 1 1 65536)" ; RESULT="$(lsof -i :$PORT)"; if [ -z "$RESULT" ] ; then break ; fi ; done; echo "$PORT"
```

### スクリプト（速いタイプ）

使い捨てで、**いま現在利用されていないポート番号が欲しいだけ**なら `lsof` コマンドで取得できます。80番ポートの場合 `lsof -i :80` で、戻り値のあり・なしで判断できます。

しかし、他のアプリで予約済みのポートなどであっても未使用の場合は空きポートと判断してしまいます。SSH や WEB サーバーなどの長く使う可能性のある場合は次の「確実なタイプ」をご利用ください。

```bash:getport.sh
#!/usr/bin/env bash

port_min=1
port_max=65535

while :
do
    port="$(jot -w %i -r 1 "$port_min" $((port_max+1)))"
    result="$(lsof -i :$port)"
    if [ -z "$result" ] ; then
        break
    fi
done
echo "$port"
```
### スクリプト（確実なタイプ）

```bash:getport.sh
#!/usr/bin/env bash

function getPortUsed() {
    # この取得処理が一番重いので変数に入れて使い回すために用意
    echo `lsof -i -P | grep -i "tcp" | sed 's/\[.*\]/IP/' \
                   | sed 's/:/ /' | sed 's/->/ /'| awk -F' ' '{print $10}' \
                   | awk '!a[$0]++'`
}

function getPortRandom() {

    # 使用中のポート一覧がセットされていなければ関数内で取得
    if [ -z ${port_list+x} ]; then
        port_list="$(getPortUsed)"
    fi

    # 検索ポートの範囲指定（デフォルト範囲）
    port_min=${1:-1}
    port_max=${2:-65535}

    # 希望するポートがセットされていない場合や範囲外の場合は初期化
    if [ -z "$port" ] || [ $port_min -gt $port ] || [ $port_max -lt $port ]; then
        port="$(jot -w %i -r 1 $port_min $((port_max+1)))"
    fi
    
    port=$((port+0)) #文字列の数字を数値に変換
    
    while :
    do
        # ポートの衝突フラグをリセット（１なら使用中）
        port_collide=0

        # 使用中のポート一覧と $port を比較
        for port_used in $port_list
        do
            if [ "$port" = "$port_used" ]; then
                port_collide=1 # 使用中のポートと同じ
                break          # 処理を抜けてランダム番号を再取得
            fi
        done

        # 現在の $port が使用中ポートと重ならない場合は処理を抜ける
        if [ $port_collide -eq 0 ]; then
            break
        fi

        # ランダムな番号を取得
        port="$(jot -w %i -r 1 $port_min $((port_max+1)))"
    done

    echo "$port"
}

# [使用例] -------------------------------------------------------------------------

# 使用中のポート一覧取得（この取得処理が一番重いため変数に入れておく。なくても動く）
port_list="$(getPortUsed)"

# 使用例１（シンプル）
# -------
port="$(getPortRandom)" # ランダムな空きポート番号を取得
echo "$port"            # 取得結果表示

# 使用例２（希望ポート指定）
# -------
port=8080               # 希望するポート番号
port="$(getPortRandom)" # ランダムな空きポート番号を取得
echo "$port"            # 取得結果表示

# 使用例３（ポート範囲指定）
# -------
port="$(getPortRandom 10 500)" # ポート番号の範囲を指定（10〜500）
echo "$port"                   # 取得結果表示

# 使用例４（希望ポートと範囲指定）
# -------
port=400                       # 希望するポート番号
port="$(getPortRandom 10 500)" # ポート番号の範囲を指定（10〜500）
echo $port                     # 取得結果表示
```

```shell-session:実行結果
$ ./getport.sh
27690
8080
169
400
```

### 動作検証済み環境

- macOS HighSierra（OSX 10.13.6）
    - `$ bash --version`： GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin17)
    - `$ man lsof`： Revision-4.89
    - `$ grep --version`： grep (BSD grep) 2.5.1-FreeBSD
    - `$ man sed`： BSD General Commands, May 10, 2005
    - `$ awk --version`： awk version 20070501
    - `$ man jot`： BSD General Commands, February 19, 2010

### 関連記事

- [PHP版 macOS のランダムな空きポートを取得するスクリプト](https://qiita.com/KEINOS/items/be7b769f64d5f154388f) @ Qiita
- [Who is listening on a given TCP port on Mac OS X?](https://stackoverflow.com/questions/4421633/who-is-listening-on-a-given-tcp-port-on-mac-os-x) @ StackOverflow


## TS;DR

サーバの空きポート探しで [CentOS7 用のスクリプト](https://blog.keinos.com/20180125_3145)が macOS に使えなかったので、[PHP で Mac の空きポート探しをするスクリプトの Qiita 記事](https://qiita.com/KEINOS/items/be7b769f64d5f154388f)を書きました。しかし、やはり Mac のシェルでも使えるようにしたいと思い、勉強がてら調べてみました。

### 使用中のポート一覧の取得

「空きポート探し」と言っても、**使用中のポート一覧を取得して、ランダムに作成した番号と重複していなければ空きポート**という、単純な処理です。

以下のコマンドが、**使用中の TCP のポート番号を一覧で取得するコマンド**です。

> `lsof -i -P | grep -i "tcp" | sed 's/\[.*\]/IP/' | sed 's/:/ /' | sed 's/->/ /'| awk -F' ' '{print $10}' | awk '!a[$0]++'`

上記の処理を分解すると以下の７ステップになり、最初の`lsof` コマンドの結果をパイプで次々とコマンドに渡し、ふるいに掛けながらポート番号を抜き出すという、ゴリゴリとした処理です。[`Netstat` コマンド](https://ja.wikipedia.org/wiki/Netstat)を利用しなかったのは、どうも別のプロセスで PHP や GO などのビルトインサーバーのポートが出てこなかったためです。

また、[`LISTEN`（待機）](https://qiita.com/DQNEO/items/7027fda87a1ecf44b9f2)しているポートだけでなく `ESTABLISH` （接続）しているものなどの、全ての TCP ポートが含まれます。 

1. **`lsof -i -P`**<br><p>ポート一覧を取得しています。今回の要（かなめ）とも言えるコマンドです。</p><p>`lsof` コマンドは「`List Open Files`」の略で、現在開いているファイルの情報を得るコマンドです。「ポートなのにファイル？」と~~思いました~~思われるかもしれませんが、**Linux/UNIX ではファイル、ディレクトリ、ストリーム、ソケットなどは全て「ファイル」として扱う**という、シンプルかつ柔軟な概念があります。`ls` コマンドでカレントディレクトリや親ディレクトリが `.` や `..` としてファイル一覧に表示されるのも、ディレクトリをファイルとして扱っているからです。</p><p>`-i` オプションはインターネット（やネットワーク関連）の接続ファイルを表示し、`-P` オプションでポート番号を[ウェルノウンのポート名](https://ja.wikipedia.org/wiki/TCP%E3%82%84UDP%E3%81%AB%E3%81%8A%E3%81%91%E3%82%8B%E3%83%9D%E3%83%BC%E3%83%88%E7%95%AA%E5%8F%B7%E3%81%AE%E4%B8%80%E8%A6%A7)に変換するのを防いでいます。つまりポート番号は番号のまま表示さるオプションです。`-P` オプションを付けないと 80 番ポートの場合 `http` と置き換わってしまい~~ました~~ます。</p><p>実はページ冒頭にあるように「`lsof -i :${PORT_NUM}`」でポート番号を渡し、戻り値があったら使用中、なかったら未使用という処理も試したのですが、圧倒的に速い反面、予約済みのポート関係なく使用中か否かの判断になるらしく、長期的に使う予定のポートとしては正確性に欠けた（のちのち他のアプリとバッティングする可能性が高かった）ので「`lsof -i -P`」で一覧情報を取得し、そこにないものを空きポートとしました。</p>
2. **`grep -i "tcp"`**<p>`tcp` の文字を含む行のみピックアップしてフィルターしています。</p>
3. **`sed 's/\[.*\]/IP/'`**<p>IPv6 の `[ffff:ffff::ffff:ffff:ffff:ffff]` をダミー文字 `IP` に置換しています。これをしておかないと、この後に出てくる `:` の置換でポート番号が記載される列がズレてしまうからです。</p>
4. **`sed 's/:/ /'`**<p>最初に出現した `:` をスペースに置換しています。これで、各行の項目の区切りがスペース区切りになります。最初の `:` の置換だけで十分なので `s/:/ /g` とはしていません。</p>
5. **`sed 's/->/ /'`**<p>接続されている（`ESTABLISH`）ポートの接続元を示す `->` をスペースに置換しています。この `->` とポート番号の間にスペースがないため、置換しておかないと（スペースで区切られないため）意図しない情報も一緒に表示されるからです。</p>
6. **`awk -F' ' '{print $10}'`**<p>ここまでの過程で、各行の、スペース区切りになった項目から 10 列目の値（ポート番号）を取得しています。</p>
7. **`awk '!a[$0]++'`**<p>`lsof -i` では同じポートが（IPv4 と IPv6 のぶんの）２回表示されるため、重複するポート番号をユニークな（一意の）番号にフィルターしています。</p><p>具体的には `a[$0]++` で `$a` の連想配列に次々と渡されるポート番号 `$0` をキー名として登録し、登録済みのキーがあった場合は `++` で先へ進むという処理を `!` で反転させることで、「同じキー名でなかった場合は次へ進む」という処理にしています。結果、[ソートしないで重複行を削除する](https://qiita.com/arcizan/items/9cf19cd982fa65f87546)ことができます。こんなの普通は思い付かないですよね。</p>

### ランダムな番号の取得（範囲指定）

macOS（UNIX）や Linux などの場合、[ランダムにアレをするには `$RANDOM` が便利](https://qiita.com/piroor/items/6300e665bc8ac314211b)ではあるのですが、0 から 32767 までの間という制限があるため、[65536 個あるポート数](https://ja.wikipedia.org/wiki/%E3%83%9D%E3%83%BC%E3%83%88%E7%95%AA%E5%8F%B7)には足りません。

`$RANDOM` を２回足すなどの工夫をしてもいいのですが、やはり [PHP の `rand( min, max )` 関数](http://php.net/manual/ja/function.rand.php) くらいシンプルに範囲指定したいのです。

そこで、まずは以下の様な **`$RANDOM` 変数と剰余演算（`%`,パーセント）を使った範囲指定可能な式**を考えました。

```bash:$RANDOMを使ったサンプル（範囲指定型の乱数値取得サンプル）
MIN=50000
MAX=65535
PORT_RANDOM=$RANDOM % ($MAX + 1 - $MIN)
```

上記で $RANDOM の値がどんなにランダムな値であっても `$MIN` から `$MAX` の範囲で返ってくるので一見動いているように見えます。

しかし `$MAX` が 32767 より大きい場合に偏りが出てしまいます。`$RANDOM` の値が 0 〜 32767 の範囲でしか動かないためです。これは `$((RANDOM+RANDOM))` などとしても（小さい値が出にくいという意味でも）同じでした。

有名な `shuf` コマンドは macOS にはデフォルトで入っていないため、標準のシェル・コマンドで替わりとなるコマンドないかな、と調べてみると [StackExchange でドンピシャの質問](https://unix.stackexchange.com/questions/140750/generate-random-numbers-in-specific-range)が。「[`jot`を使うと良いよ](https://unix.stackexchange.com/a/241199/302858)」とのこと。

調べてみたところ、macOS のシェル・コマンドには **`jot` という「連番もしくはランダムな値を出力するコマンド」**が標準でインストールされています。標準で入っているのが良いところです。

[`jot` コマンドの基本的な使い方を調べてみる](https://www.networkworld.com/article/3200222/linux/what-the-jot-command-can-do-for-you.html)と、引数を順に「X A B」とすると値 A から値 B の範囲で X 回出力してくれます。

```bash:jot基本サンプル
$ # 5回 11 から 13 の値を取り出す
$ jot 5 11 13
11
12
12
12
13
$ # 5回 24 から 2 の値を取り出す
$ jot 5 24 2
24
18
13
8
2
```

一見ランダムに取得しているように見えるのですが、`jot 100 1 10` とすると分かるように「与えられた範囲の数値のリスト（配列）を指定された回数にわけて**順番に取り出している**」だけなので、ランダムではありません。この動きを「シーケンシャル」（`sequential`）と呼ぶそうです。

先の StackExchange のコメントを見ると `-r` オプションを付けているので、`jot` のマニュアルを見てみました。

```text:「man␣jot」より抜粋
The following options are available:
-r      Generate random data instead of the default sequential data.
```

`-r` オプションを付けるとランダムにしてくれるらしいので、以下のようにすると良い様に思えます。

```shell-session
$ jot -r 1 50000 65535
65310
```

こちらも、一見ランダムに取得しているように見えるのですが、実は、これには落とし穴があって、完全なランダムではなくシャッフルに近い動作をするようです。

マニュアル（`man jot`）を読むと以下の様に書いてあります。

> Random numbers are obtained through arc4random(3) when no seed is specified, and through random(3) when a seed is given.
> When jot is asked to generate random integers or characters with begin and end values in the range of the random number generator function and no format is specified with one of the -w, -b, or -p options, jot will arrange for all the values in the range to appear in the output with an equal probability.

以下は筆者の意訳です。

> 「シード」が指定されている場合は [`random(3)`](https://nxmnpg.lemoda.net/ja/3/random)、指定されていない場合は [`arc4random(3)`](https://nxmnpg.lemoda.net/ja/3/arc4random) を使い、ランダム値を取得します。
> `jot` コマンドが `-r` オプションによりランダムな出力を求められた時、開始と終了の範囲が指定され、かつ `-w`, `-b`, `-p` の書式オプションが付かない場合は、出力される整数もしくは文字の**出現率は均等になるように調整されます**。

つまり、「一度出現した値は再出現しづらいという判断が出来る」ということであり、結果を絞り込めるため、完全なランダム値にならないということのようです。

そこで、`-w` の書式オプションを使って、`-w %i` で使用する文字に数値を指定して、そこから `-r` オプションでランダムに取得するのが王道のようです。以下が最終的なサンプル。

```bash:JOTを使ったサンプル（範囲指定型の乱数値取得サンプル）
MIN=50000
MAX=65535
jot -w %i -r 1 $MIN $((MAX+1))
```

以下は $RANDOM 変数と上記 `jot` コマンドを 10 万回繰り返した際の出現回数をグラフに出力したものです。（X=回数, Y=出現した数値）

![COMP_JOT-AND-RAND.png](https://qiita-image-store.s3.amazonaws.com/0/78776/ff49321a-9c53-52a7-2113-0408aa7ae26f.png)

このように $RANDOM の方は出現する値が偏り、比べて `jot` は広い範囲で出現することがわかります。

まぁ、たかがポート番号なのでそこまでシビアに乱数にこだわる必要はないのですが、気になってしまいました。おそらくこれ以上ランダム性にこだわるなら、他のプログラム言語を叩いて得るか、別途専用のコマンドをインストールした方がコストは安いのかもしれません。


## 参考文献

- [awkで重複行を排除する方法](https://qiita.com/pocket8137/items/a6de22d0be6b26f7bcac) @ Qiita
- [シェルスクリプトでランダムにアレをやる](https://qiita.com/piroor/items/6300e665bc8ac314211b) @ Qiita
- [Generate random numbers in specific range](https://unix.stackexchange.com/questions/140750/generate-random-numbers-in-specific-range) @ StackExchange