日々之迷歩

世の中わからんことだらけ

ITが複雑で難しくなっていく様に翻弄される日々です。微力ながら共著させていただいた「シェル・ワンライナー160本ノック」をよろしくお願い申し上げます。

「第66回シェル芸勉強会」リモート参加レポート

2023-09-30(土)「第66回シェル芸勉強会」が開催されました。まずは勉強会主催者の上田さんへ、問題作成と勉強会運営ありがとうございました。参加者の皆様もお疲れ様でした。東京と大阪サテライトで会場が設けられました。今回福岡サテライトは開催せず、自宅からリモート参加しました。

勉強会情報

シェル芸勉強会リンク集

勉強会主催者の上田さんが公開されているリンク集をご覧ください。 出題された問題や解答例の投稿は、上田さんが運用されているMisskeyを使っています。 Misskeyの勉強会に関するクリップ(作成中)も記載されています。

b.ueda.tech

会場情報

東京会場 usptomo.doorkeeper.jp

大阪サテライト shell-nagasaki.connpass.com

勉強会の内容について

今回の問題は珍しくデータファイルが無く、Bashの機能そのものを問う問題でした。

問題と解答

問題や解説は、出題者上田さんによるYoutubeライブ配信の録画でご確認ください。 動画のリンクは、上記の「jus共催 第66回シェル芸勉強会リンク集」をご覧ください。 以下、私なりに考えた解答などを書きました。

Q1

コマンドラインに数字を使わず0123456789と出力する問題です。下記2つの解答を考えました。

1つ目は0から9までの数字を表示するのにbashのブレース展開を使いました。具体的にはブレース展開の文字列{0..9}を作成しますが、0と9はシェルのプロセスIDを表示する$$を使って表現します。「難読化シェル芸」でよく使われる手法ですね。

# 対話シェルのプロセスIDを表示

$ echo $$
622770

# 0を表示
$ echo $(($$-$$))
0

# 9を表示
$ echo $((($$+$$+$$+$$+$$+$$+$$+$$+$$)/$$))
9

上記を使ってブレース展開の文字列{0..9}を作成し、空白を削除すれば完成です。

# ブレース展開の文字列{0..9}を作成
$ echo "echo {$(($$-$$))..$((($$+$$+$$+$$+$$+$$+$$+$$+$$)/$$))}"
echo {0..9}

# bashに渡してブレース展開
$ echo "echo {$(($$-$$))..$((($$+$$+$$+$$+$$+$$+$$+$$+$$)/$$))}" | bash
0 1 2 3 4 5 6 7 8 9

# 空白を削除して完成
$ echo "echo {$(($$-$$))..$((($$+$$+$$+$$+$$+$$+$$+$$+$$)/$$))}" | bash | tr -d ' '
0123456789

# evalを使っても良い
$ eval "echo {$(($$-$$))..$((($$+$$+$$+$$+$$+$$+$$+$$+$$)/$$))}" | tr -d ' '
0123456789

もう一つはawkで乱数を使った解答です。ただしこの解答例は、運が悪いと(0から9まで数字が出揃わない場合)失敗します。 まずawkのrand関数を使って0から1までの少数を出力し、headコマンドで最初の10行を切り出します。

$ yes | awk 'BEGIN{srand()}{print rand()}' | head
0.78839
0.156347
0.715727
0.0713433
0.940707
0.51625
0.364574
0.523542
0.419437
0.782732

あとは.と改行を削除し、0から9まで揃えて表示します。

$ yes | awk 'BEGIN{srand()}{print rand()}' | head | tr -d '.\n' | grep -o . | sort | uniq | tr -d '\n'

Q2

問題で想定される動きは下記の通りです。

# 想定する動作
$ ls a 3>file 2>&3
$ cat file
ls: 'a' にアクセスできません: そのようなファイルやディレクトリはありません

この問題の意図としては、要するに3という文字を作る方法でしょうか?下記2つの解答を考えました。 1つはQ1と同様にシェルのプロセスID$$を使う方法です。

# シェルのプロセスIDを使って3を表示
$ echo $((($$+$$+$$)/$$))
3

$ ls a 3>file 2>&$((($$+$$+$$)/$$))
$ cat file
ls: 'a' にアクセスできません: そのようなファイルやディレクトリはありません

もう一つはseqの出力を加工して3を切り出す方法です。

# seqの出力を10行目まで切り出して改行を削除
$ seq inf | head | tr -d '\n' 
12345678910

# 頭2文字と後ろ8文字を消して3を切り出す
$ seq inf | head | tr -d '\n' | sed 's/^..//' | sed 's/........$//'

# 上記を使って3を表現する
$ ls a 3>file 2>&$(seq inf | head | tr -d '\n' | sed 's/^..//' | sed 's/........$//')
$ cat file
ls: 'a' にアクセスできません: そのようなファイルやディレクトリはありません

Q3

bashの内部コマンドを実行して、終了ステータスが1になるものを探しました。下記は全て終了ステータスが1にな流ようです。

$ bg
-bash: bg: カレント: そのようなジョブはありません

$ fg
-bash: fg: カレント: そのようなジョブはありません

$ test

$ popd
-bash: popd: ディレクトリスタックが空です

Q4

trueコマンドの終了ステータスを0以外にする問題です。 以下を考えましたが、これは残念ながらtrueコマンドではなくbashでエラーが発生しているようです。

$ true > /dev/aa
-bash: /dev/aa: 許可がありません

$ echo $?
1

問題の意図は、ulimitを使ってシェルのコマンド実行環境に制限をかける事だったようです。

Q5

ファイル識別子の最大番号でファイルを開く問題です。

出題者上田さんの解説で、ulimitを使ってファイルを開く最大数を確認する方法が確認が出てきました。 しかし私はulimitの存在をすっかり忘れていたため、下記のような力技の解答になってしまいました。

ファイル記述子の番号を増やしながら、使っている対話シェルを使って順次/dev/nullを開いて失敗するまで繰り返す方法です。 0から2はデフォルトで開いているので、3以上を指定しています。

まずファイル記述子の数字を順次指定して/dev/nullを開くコマンド列を作成します。

$ seq -f 'exec %g> /dev/null' 3 10000
exec 3> /dev/null
exec 4> /dev/null
exec 5> /dev/null
(略)
exec 9998> /dev/null
exec 9999> /dev/null
exec 10000> /dev/null

上記のコマンド列をsourceコマンドで実行します。ファイル記述子の番号が1024以降で/dev/nullを開くのを失敗しました。 ファイル記述子の最大値が1023なのが確認出来ました。

$ source <(seq -f 'exec %g> /dev/null' 3 10000)
-bash: /dev/null: Too many open files
-bash: /dev/null: Too many open files
-bash: /dev/null: Too many open files
(略)

$ ls /proc/$$/fd/????
-bash: start_pipeline: pgrp pipe: Too many open files
/proc/623419/fd/1000  /proc/623419/fd/1005  /proc/623419/fd/1010  /proc/623419/fd/1015  /proc/623419/fd/1020
/proc/623419/fd/1001  /proc/623419/fd/1006  /proc/623419/fd/1011  /proc/623419/fd/1016  /proc/623419/fd/1021
/proc/623419/fd/1002  /proc/623419/fd/1007  /proc/623419/fd/1012  /proc/623419/fd/1017  /proc/623419/fd/1022
/proc/623419/fd/1003  /proc/623419/fd/1008  /proc/623419/fd/1013  /proc/623419/fd/1018  /proc/623419/fd/1023
/proc/623419/fd/1004  /proc/623419/fd/1009  /proc/623419/fd/1014  /proc/623419/fd/1019

ちなみにulimitコマンドで開けるファイルの最大数を確認したら1024でした。 上記の解答ではファイル記述子番号の最大値が1023だったのであっていますね。

$ ulimit -n
1024

Q6

許可された上限までファイルを開く問題です。

Q5とやり方がほとんど同じ解答になりました。 ファイル記述子の番号を10以上で指定しながら増やし、使っている対話シェルで順次/dev/nullを開いて失敗するまで繰り返す方法です。 1024以降でファイルを開くのを失敗しました。開けるファイル数上限のファイル記述子まで利用可能のようです。

$ source <(seq -f 'exec %g> /dev/null' 10 10000)
-bash: 1024: 不正なファイル記述子です
-bash: 1025: 不正なファイル記述子です
-bash: 1026: 不正なファイル記述子です
(略)
-bash: 9998: 不正なファイル記述子です
-bash: 9999: 不正なファイル記述子です
-bash: 10000: 不正なファイル記述子です

$ ls /proc/$$/fd | sort -n | xargs
0 1 2 10 11 (略) 1021 1022 1023

出題者上田さんの解答例では、下記のようなリダイレクトを使っていました。 この場合はファイル記述子の番号が変数aに代入されます。割り当てられるファイル記述子の番号は10以上になるようです。 勉強会参加者の方からbashマニュアルのリダイレクトに記載があると教えていただきました。man bashの中で{varname}を検索してみてください。

$ : {a}>file; echo $a; ls /proc/$$/fd
10
0  1  10  2  255

$ : {a}>file; echo $a; ls /proc/$$/fd
11
0  1  10  11  2  255【←実行する度にファイルを開く数が増えていく】

$ : {a}>file; echo $a; ls /proc/$$/fd
12
0  1  10  11  12  2  255【←実行する度にファイルを開く数が増えていく】

またbashがデフォルトで開いているファイル記述子で255があり、これが何なのか?という謎があります。 少し調べてみましたが詳細が分かっていません。bash独特のようで、zshなど他のシェルでは無いようです。

終わりに

今回はデータを使わずにBashの機能自体に着目した問題でした。数字を使わないという制限で久しぶりに難読化でよく使う手法を思い出したり、ファイル記述子やシェルのulimitなどに関する問題があったりで、色々と知見がありました。

TLはたいちょーさんの呪文詠唱が更に進化していてとても面白かったですね。また、ぷるさんによるMisskeyでのシェル芸bot運用もありがとうございました。

シェル芸勉強会福岡サテライトを開催する会場については、今後は新しくなったAIP Cafeを使うかもしれません。福岡サテライトを今後どうするかは、改めて考えてみたいと思います。