第32回シェル芸勉強会 Q3 の解答

前置き

昨日に続いて第32回シェル芸勉強会のQ3を解いていきます。

第32回シェル芸勉強会の問題は次のURLにあります。
【問題のみ】jus共催 第32回全くインスタ映えしないシェル芸勉強会 | 上田ブログ
そういえば、当時の様子は一旦全部解いてから確認する予定です。
また、Q1・Q2の別解も思いついてるので終わってから書きます。

思考を忠実にアウトプットすることを目標にしています。

Q3 問題(上記URLからコピー)

/etc/servicesから、TCPのポート番号が素数のサービス一覧を作ってください。

問題を見た感想・補足

シェル芸人が大好きな素数問題です。これは1時間かかりませんね。

解答1(不足あり)

cat /etc/services | grep -o '[0-9]*/tcp' | cut -d/ -f1 | factor | awk 'NF==2{print $2}'

解説

まず /etc/services とはなにかと言う話ですが、UNIX系がよく使われるポート番号を管理しているファイルです。

$ cat /etc/services | sed '/^#/d' | head

tcpmux          1/tcp                           # TCP port service multiplexer
echo            7/tcp
echo            7/udp
discard         9/tcp           sink null
discard         9/udp           sink null
systat          11/tcp          users
daytime         13/tcp
daytime         13/udp
netstat         15/tcp

上記のように、サービス名[TAB]ポート番号/プロトコルという形式で記載されています。
ちなみに、私は待受しているところを見たことはありませんが、echoは送ったリクエストをそのまま返してくるサービス(シェルにおけるcatですね)で、discardはただ受け取るだけのサービス(≒ /dev/null)です。

今回出力するポート番号は、ポート番号/プロトコル の形で記載されているので、その形でTCPのものだけgrep -oで引っこ抜くのが楽でしょう。

$ cat /etc/services | grep -o '[0-9]*/tcp' | head
1/tcp
7/tcp
9/tcp
11/tcp
13/tcp
15/tcp
17/tcp
18/tcp
19/tcp
20/tcp

sed 's#\t\t#\t#' | tr \\t \ | cut -d\ -f2などでもできますが、だいぶめんどくさいです。
今回は付けなくても影響なかったので付けていませんが、コメントに同様の形が記載されていることを危惧するなら、先に sed 's/#.*//' でコメントを除いておくと安心できます。

ここから数字だけにすれば、対象のポート番号が得られます。パッと思いつくだけでも、以下のいずれかで得られます。

  • awk -F/ '$0=$1', cut -d/ -f1
    • / を区切り文字として、第1フィールドを得る
  • tr -dc 1-9\\n
    • 数字と改行コード以外を消す
  • grep -o '[0-9]*'
    • 連続する数字だけ残す
  • sed 's#/.*##'
    • / 以降を消す
$ cat /etc/services | grep -o '[0-9]*/tcp' | tr -dc 0-9\\n | head
1
7
9
11
13
15
17
18
19
20

あとは、いつものやつ(factor | awk 'NF==2{print $2}')で結果が得られます。
見たことない人に対して補足すると、素因数分解した(factor)結果、素因数が自身のみ(awk 'NF==2'。(factorはラベルと結果を出すため対象行は2列になる。))だったものを表示しています。

$ cat /etc/services | grep -o '[0-9]*/tcp' | cut -d/ -f1 | factor | awk 'NF==2{print$2}' | head
7
11
13
17
19
23
37
43
53
67

補足

完全に余談ですが/etc/servicesはポート番号とサービス名の名前解決?をするためのファイル(IPにおける/etc/hosts的な存在)なので編集すると、システムが表示する待受サービス名を変えられます。

以下は、ssで出てくる待受サービス名を編集しています。(普段はss -tnaで名前解決なしで呼ぶのでサービス名見ませんが。。。)

# cat /etc/services | grep ssha
ssha            22/tcp                          # SSH Remote Login Protocol
# ss -ta
State      Recv-Q Send-Q             Local Address:Port                              Peer Address:Port
LISTEN     0      128                            *:ssha                                         *:*
ESTAB      0      0                      10.0.2.15:ssha                                  10.0.2.2:63942
LISTEN     0      128                           :::ssha                                        :::*

解答1(修正)

cat /etc/services | grep -o '[0-9]*/tcp' | cut -d/ -f1 | factor | awk 'NF==2{print "\\<"$2"/tcp\\>"}' | grep -f - /etc/services

解説

問題を見返すと、「TCPのポート番号が素数サービス一覧を作ってください。」と書いてて焦りました。
上記で作成したポート番号一覧でサービス名を拾ってくる必要があります。

ポート番号/プロトコルの形に直して、 /etc/services に再度 grepをかければ実現できます。
grepのパターンにする際に \<7/tcp\>などと記載すると、77/tcpのような文字を弾いて、\t7/tcp\tだけを抜き出せます。
つまり、\<pettern\>で、前後が空白や記号のデリミタっぽいもので分けられた文字列だけをマッチさせられます。

$ cat /etc/services | grep -o '[0-9]*/tcp' | cut -d/ -f1 | factor | awk 'NF==2{print "\\<"$2"/tcp\\>"}' | head
\<7/tcp\>
\<11/tcp\>
\<13/tcp\>
\<17/tcp\>
\<19/tcp\>
\<23/tcp\>
\<37/tcp\>
\<43/tcp\>
\<53/tcp\>
\<67/tcp\>

上記で出てきた結果を用いて、/etc/servicesgrepし直せば、サービス一覧が得られます。
ワンライナーの3神器たるgrepsedawkは普段は引数にスクリプトを書きますが、-fオプションでファイルを渡すことができます。
また、UNIX系コマンドで良くなされている実装で、ファイル名に- を指定すると標準入力になります。

つまり、... | grep -f - <filename> とすると、パイプで渡ってきた文字列を検索条件として、ファイル内を検索できます。

$ cat /etc/services | grep -o '[0-9]*/tcp' | cut -d/ -f1 | factor | awk 'NF==2{print "\\<"$2"/tcp\\>"}' | grep -f - /etc/services | head
echo            7/tcp
systat          11/tcp          users
daytime         13/tcp
qotd            17/tcp          quote
chargen         19/tcp          ttytst source
telnet          23/tcp
time            37/tcp          timserver
whois           43/tcp          nicname
domain          53/tcp                          # Domain Name Server
bootps          67/tcp                          # BOOTP server

これでサービス一覧が得られました。1列目だけ残すのはお好きにどうぞ。

まとめ

知っているからよく使う手法が多かったです。
具体的には以下のものです。

  • フィールドを切り出す
  • 素数判定する:factor | ...
  • 標準入力をgrepの検索条件として扱う

感想

知ってれば解ける系でした。と思いきや問題をちゃんと読まずスタートしており、10分前に気づいて焦りました。。。
記事自体を書くのには結局1時間かかっています。余談が楽しくなってしまうので仕方ありませんね。


改訂履歴

  • 2017/12/08: 「まとめ」を「感想」にして、きちんとした「まとめ」を書きました。