LoginSignup
9
9

More than 3 years have passed since last update.

実験:シェルスクリプトに自分自身を削除させると?少し変わったクワインの作り方。

Last updated at Posted at 2019-06-30

当方執筆の技術情報に関するライセンス、免責事項、禁止事項


今回お届けするのは……なんだろう、これ。

執筆時参考とした文献

クワイン (プログラミング) - Wikipedia

シェルスクリプトで変数に改行コードを含める方法 | 俺的備忘録 〜なんかいろいろ〜

シェルの入出力制御あれこれ
https://qiita.com/tag1216/items/7ce35b7c27d371165e56

ヒアストリングとヒアドキュメント
https://qiita.com/ogiw/items/ac77a3bbb813351099a1

実験環境

Slackware 14.2 x86_64

スクリーンショット_2019-06-30_18-22-26.png

自分自身を削除するシェルスクリプト

suicide.sh
#!/bin/sh
rm ./suicide.sh

これは問題なく動作する。

次に、

suicide2.sh
#!/bin/sh
rm ./suicide2.sh
echo "hoge"
echo "fuga"
echo "piyo"
ls ./suicide2.sh

結果はこう。

$ ash suicide2.sh
hoge
fuga
piyo
ls: './suicide2.sh' にアクセスできません: そのようなファイルやディレクトリはありません

不思議に思えるが、これも問題なく動作する。

自分自身を一旦削除した上で再生成するシェルスクリプト

ghost_in_the_shell.sh
#!/bin/sh
# a strange shell script which remove oneself and regenerate oneself.
# Maybe, this is a kind of quine (self-printing program).
GHOST_IN_THE_SHELL=$(cat ./ghost_in_the_shell.sh)
rm ./ghost_in_the_shell.sh
ls ./ghost_in_the_shell.sh
echo "$GHOST_IN_THE_SHELL" >./ghost_in_the_shell.sh
ls ./ghost_in_the_shell.sh

結果はこう。

$ ash ghost_in_the_shell.sh
ls: './ghost_in_the_shell.sh' にアクセスできません: そのようなファイルやディレクトリはありません
./ghost_in_the_shell.sh

コメント行や動作確認用の ls を外すと shebang 含めて 4 行。

ghost_in_the_shell.sh
#!/bin/sh
GHOST_IN_THE_SHELL=$(cat ./ghost_in_the_shell.sh)
rm ./ghost_in_the_shell.sh
echo "$GHOST_IN_THE_SHELL" >./ghost_in_the_shell.sh

bash ならヒアストリングで echo いらず。

ghost_in_the_shell.bash
#!/bin/bash
GHOST_IN_THE_SHELL=$(cat ./ghost_in_the_shell.bash)
rm ./ghost_in_the_shell.bash
cat <<< "$GHOST_IN_THE_SHELL" >./ghost_in_the_shell.bash

アルゴリズム

変数にスクリプトをまるごと格納

自分自身を記述したファイルを削除

変数から自分自身を再生成しファイル化

終了

要点

シェルにおいて、変数は文字列を格納する。
格納される文字列には改行が含まれていてもよい。
シェルスクリプトは改行を含む文字列として変数に格納出来る。

利点

上述のアルゴリズムを用いることで、様々なシェルスクリプトに自分自身を出力させることが可能になる(クワインをいくらでも長く出来るようになる?)。
※ 「自分自身を削除」の部分はなくても良さそう。

蛇足

自分自身の削除と再生成の間に sleep を挟むとより不気味に。

追記1

関連する情報をググりました。
今回の実験では ls が失敗していますが、この時点で実体は残っているのか否か。
後ほどプログラムの修正を試みます(rm に替えて echo >./ghost_in_the_shell.sh などと挟むと動作不能になるのかどうか等)。
→動作しました……わけわからん。
→追記2にまとめました。

ファイル、フォルダと inode に関する参考文献一覧

Tips: 実行中のシェルスクリプトを書きかえるときには
https://qiita.com/kitsuyui/items/d0048eeaa50293a92a60

Linux: ハードリンクと inode
https://qiita.com/lnznt/items/6178e1c5f066f22fe9c2

Linuxの文字化けファイルを削除する方法
https://qiita.com/nekkoneko/items/91bcdd687357ea5b58eb

Linuxでディスクが一杯になった時の対処法
https://qiita.com/masarufuruya/items/0c794083fc4ea03cd9f9

シェルスクリプトは文字通りの逐次実行
https://sleepy-yoshi.hatenablog.com/entry/20090917/p1

実行中のシェルスクリプト自体を修正した場合の動作
https://oshiete.goo.ne.jp/qa/3573184.html

Linux とWindows では「ファイルを消す」際の挙動が異なるんだ。気をつけろ
https://lukesilvia.hatenablog.com/entry/20081001/p1

削除したファイルをlsofで復元する
https://mag.osdn.jp/06/11/23/0451251

inode番号からファイルのパスを得る方法を教えてください。
http://q.hatena.ne.jp/1281884424

inode番号でmvを使う
https://hacknote.jp/archives/30128/

【 stat 】コマンド――ファイルの属性や日付などを表示する:Linux基本コマンドTips(122) - @IT
https://www.atmarkit.co.jp/ait/articles/1706/29/news027.html

【 cmp 】コマンド――ファイルを1バイトずつ比較する:LinuxコマンドTips(106) - @IT
https://www.atmarkit.co.jp/ait/articles/1704/27/news027.html

Linux におけるファイル I/O の基礎
https://itkq.jp/blog/2017/05/10/linux-file-and-io/

発展:テセウスの船?

ファイル末尾で自分自身を source してやるとどうなるか。
間に sleep を入れないと CPU 使用率が酷いことになりそう。

コード
ship_of_theseus.sh
#!/bin/sh
SHIP_OF_THESEUS=$(cat ./ship_of_theseus.sh)
if [ -z $GENERATION ]; then
        GENERATION=1
fi
echo $GENERATION
GENERATION=$(expr $GENERATION + 1)
stat --printf="%i\n" ./ship_of_theseus.sh
sleep 10
echo "$SHIP_OF_THESEUS" >./ship_of_theseus.sh
. ./ship_of_theseus.sh

実行結果

なんとなく不思議です。

スクリーンショット_2019-07-01_10-32-15.png

sleep 86400 とすると一日一回更新(60x60x24)。

sleep 31557600 とすると一年一回更新(86400x365.25)。

テセウス的ゾンビ

ファイルの inode が同一であり、かつ、echo >./p-zombie.sh しているにも関わらずプログラムが続いてしまうのはいささか不可解。

コード
p-zombie.sh
#!/bin/sh
PHILOSOPHICAL_ZOMBIE=$(cat ./p-zombie.sh)
if [ -z $GENERATION ]; then
        echo "PID $$"
        GENERATION=1
fi
echo $GENERATION
GENERATION=$(expr $GENERATION + 1)
stat --printf="%i\n" ./p-zombie.sh
echo >./p-zombie.sh
cat ./p-zombie.sh
sleep 10
echo "Hello, Everyone. I'm a zombie. Let's dance together!"
echo "$PHILOSOPHICAL_ZOMBIE" >./p-zombie.sh
. ./p-zombie.sh

実行結果

スクリーンショット_2019-07-01_11-48-43.png

スクリプトの構造上、kill プロセスID すると、大抵 p-zombie.sh の中身は空っぽです。

スクリプト名を to_a.sh とすると、ash で実行する場合 ash to_a.sh となり、灰は灰に。エクソシストっぽいかも。

番外:時間経過に比例して inode を消費するシェルスクリプト

コード途中で生成される dr_strangelove.sh と最初の dr_strangelove.sh の inode が異なり、最後の echo の行が到達不能コードである場合、このシェルスクリプトは時間の経過に比例して inode を消費するはず。

コード
dr_strangelove.sh
#!/bin/sh
DR_STRANGELOVE=$(cat ./dr_strangelove.sh)
stat --printf="%i\n" ./dr_strangelove.sh
rm ./dr_strangelove.sh
sleep 10
echo "$DR_STRANGELOVE" >./dr_strangelove.sh
stat --printf="%i\n" ./dr_strangelove.sh
. ./dr_strangelove.sh
echo 'or: How I Learned to Stop Worrying and Love the Fork Bomb.'

なお、コードの最終行で Fork Bomb に言及していますが、このコード自体は Fork Bomb ではありません。たぶん。

実行結果

思惑通りの挙動と結果を示してくれました。

スクリーンショット_2019-07-01_10-08-41.png

最終行は実行されません。

スクリーンショット_2019-07-01_10-09-23.png

dr_strangelove.sh を実行中のウィンドウを閉じると inode が開放されました。

自己同一性とクワインの比較

同一性に関する参考文献

哲学的な自己同一性のアレコレ

人格の同一性 - 心の哲学まとめWiki - アットウィキ
https://www21.atwiki.jp/p_mind/pages/128.html

私は本当に同じ私なのか? - Researchmap
https://researchmap.jp/mu2gwsdgy-1774112/?action=multidatabase_action_main_filedownload&download_flag=1&upload_id=91425&metadata_id=39541

【報告】バリー・デイントン教授連続講演
https://utcp.c.u-tokyo.ac.jp/blog/2015/08/post-780/

生成されたコードの同一性

クリーネの再帰定理 - Wikipedia
https://ja.wikipedia.org/wiki/%E3%82%AF%E3%83%AA%E3%83%BC%E3%83%8D%E3%81%AE%E5%86%8D%E5%B8%B0%E5%AE%9A%E7%90%86

ハッシュ関数 - Wikipedia
https://ja.wikipedia.org/wiki/%E3%83%8F%E3%83%83%E3%82%B7%E3%83%A5%E9%96%A2%E6%95%B0

情報 - Wikipedia
https://ja.wikipedia.org/wiki/%E6%83%85%E5%A0%B1

※ 難しく考えないなら diff をとって同じなら同一で良い気もします。

哲学的な同一性の問題と追記1のコードの比較

「昨日の私と今日の私は同じ私なのか?」は「同一のシェルで実行されているか否か」で類比出来るでしょうか。

記憶媒体の同一性を以って自己の同一性とみなすならば、「inode が同一であるか否か」が重要かも。

シェルスクリプトの在り様と人間の意識の在り様がなんとなく似ている気がしたので、あれこれつらつらと考えてみました。

追記2

拙作スクリプト「テセウス的ゾンビ」の挙動から推測するシェル毎のシェルスクリプトの扱いの違い

※ 実行時に自己改変・自己呼び出しする 変態 スクリプトを書く場合、注意する必要がありそうです。例えば Slackware 14.2 のように、sh が bash の POSIX 互換モードである場合、普通に実行権限を付与して実行するだけではテセウス君をゾンビ化出来ません。

bash

仮説:一行実行する毎に実体ファイルにアクセスする
予想:実体ファイルに改変があると即座に反映
※ 出典:シェルスクリプトは文字通りの逐次実行

コードと実行結果

街にゾンビがあふれても bash があれば安心です。
自身への変更を即座に反映されてしまう場合、「自己削除」は組み込みにくい。
p-zombie.sh
#!/bin/sh
PHILOSOPHICAL_ZOMBIE=$(cat ./p-zombie.sh)
if [ -z $GENERATION ]; then
        echo "PID $$"
        GENERATION=1
fi
tput setaf $GENERATION
echo "GEN $GENERATION"
GENERATION=$(expr $GENERATION + 1)
stat --printf="%i\n" ./p-zombie.sh
echo >./p-zombie.sh
cat ./p-zombie.sh
sleep 10
echo "Hello, Everyone. I'm a zombie. Let's dance together!"
echo "$PHILOSOPHICAL_ZOMBIE" >./p-zombie.sh
. ./p-zombie.sh

スクリーンショット_2019-07-01_18-50-56.png

本体ではなく source されるファイルを書き換えるようにすることで、類似する動作は実現可能でした。

コードと実行結果その2
p-zombie.bash
#!/bin/sh
PHILOSOPHICAL_ZOMBIE=$(cat ./p-zombie.sh)
if [ -z $GENERATION ]; then
        echo "PID $$"
        GENERATION=1
fi
tput setaf $GENERATION
echo "GEN $GENERATION"
GENERATION=$(expr $GENERATION + 1)
stat --printf="%i\n" ./p-zombie.sh
echo >./p-zombie.sh
cat ./p-zombie.sh
sleep 10
echo "Hello, Everyone. I'm a zombie. Let's dance together!"
echo "$PHILOSOPHICAL_ZOMBIE" >./p-zombie.sh
source ./p-zombie.sh

スクリーンショット_2019-07-01_19-46-35.png

ash dash ksh mksh zsh tcsh fish PowerShell

仮説:何らかのかたちで最初にキャッシュしてしまう
予想:読み込み後の実体ファイルの改変は source した場合を除き無視する

コードと実行結果

こちらの仕様の方が変なものを作りやすそう。

ash dash ksh mksh zsh

p-zombie.sh
#!/bin/sh
PHILOSOPHICAL_ZOMBIE=$(cat ./p-zombie.sh)
if [ -z $GENERATION ]; then
        echo "PID $$"
        GENERATION=1
fi
tput setaf $GENERATION
echo "GEN $GENERATION"
GENERATION=$(expr $GENERATION + 1)
stat --printf="%i\n" ./p-zombie.sh
echo >./p-zombie.sh
cat ./p-zombie.sh
sleep 10
echo "Hello, Everyone. I'm a zombie. Let's dance together!"
echo "$PHILOSOPHICAL_ZOMBIE" >./p-zombie.sh
. ./p-zombie.sh

スクリーンショット_2019-07-01_18-58-14.png

スクリーンショット_2019-07-01_19-00-33.png

スクリーンショット_2019-07-01_19-01-52.png

スクリーンショット_2019-07-01_19-04-10.png

スクリーンショット_2019-07-01_19-06-28.png

tcsh

参考文献

tcsh - コマンド (プログラム) の説明 - Linux コマンド集 一覧表
https://kazmax.zpp.jp/cmd/t/tcsh.1.html

csh cシェル 簡単なシェルプログラミングまとめ
https://uguisu.skr.jp/Windows/csh.html

cshスクリプトでよく使われる機能
http://www.coins.tsukuba.ac.jp/~yas/classes/syspro-2003/2003-06-23/csh.html

cshellで環境変数が設定されているか判断する方法がわかりません(><;)... - Yahoo!知恵袋
https://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q1039521865

variable of csh ( UNIX Operation II - AD99 )
http://www.not-enough.org/abe/manual/unix2-ad99/variable.html

array of csh ( UNIX Operation II - AD99 )
http://www.not-enough.org/abe/manual/unix2-ad99/array.html

text processing - Why is printf better than echo? - Unix & Linux Stack Exchange
https://unix.stackexchange.com/questions/65803/why-is-printf-better-than-echo

変数への改行の入れ方が分かりませんでした。

p-zombie.powder
set PHILOSOPHICAL_ZOMBIE = `cat ./p-zombie.tcsh`
if ! ( $?GENERATION ) echo "PID $$"
if ! ( $?GENERATION ) set GENERATION=1
tput setaf $GENERATION
echo "GEN $GENERATION"
@ GENERATION = $GENERATION + 1
stat ./p-zombie.tcsh
echo >./p-zombie.tcsh
cat ./p-zombie.tcsh
sleep 10
echo "Hello, Everyone. I'm a zombie. Let's dance together!"
printf "$PHILOSOPHICAL_ZOMBIE" >./p-zombie.tcsh
source ./p-zombie.tcsh
$ cat p-zombie.powder | tr "\n" ";" > p-zombie.tcsh

スクリーンショット_2019-07-02_20-09-38.png

fish

参考文献

環境変数について

https://fishshell.com/docs/current/tutorial.html#tut_variables

コマンド置換について

https://fishshell.com/docs/current/tutorial.html#tut_command_substitutions

改行を含む変数の登録や呼び出しについて

https://github.com/fish-shell/fish-shell/issues/159

https://stackoverflow.com/questions/34166077/multi-line-variables-remove-new-line-character-fish

POSIX との互換性について

https://github.com/fish-shell/fish-shell/wiki/POSIX-compatibility-thoughts

組み込みコマンドについて

https://fishshell.com/docs/current/commands.html

※ fish を対話的に実行している場合、help "組み込みコマンド名" でWebブラウザ上で詳細を閲覧可能。

fish 用のシェルスクリプトは色々と書き直す必要がありました。

・変数の設定には set コマンドを使い、= は使わない
・コマンド置換に () を使う
・改行を含む文字列の呼び出しには printf %s\n を使う

など、ずいぶん特徴的な書き方をする必要があります。実行時オプションで sh 互換に出来たりしないのかな?

学習コストを横に置くと、表記自体は見通しが良さそう。

p-zombie.fish
#!/bin/fish
set PHILOSOPHICAL_ZOMBIE (cat ./p-zombie.fish)
if test -z $GENERATION
        echo "PID $fish_pid"
        set GENERATION 1
end
tput setaf $GENERATION
echo "GEN $GENERATION"
set GENERATION (expr $GENERATION + 1)
stat --printf="%i\n" ./p-zombie.fish
echo >./p-zombie.fish
cat ./p-zombie.fish
sleep 10
echo "Hello, Everyone. I'm a zombie. Let's dance together!"
printf "%s\n" $PHILOSOPHICAL_ZOMBIE >./p-zombie.fish
. ./p-zombie.fish

スクリーンショット_2019-07-01_19-11-36.png

※ 最後に cat するファイルを間違えています。cat ./p-zombie.fish などとすべきでした。

PowerShell

参考文献

Slackware と Docker

SlackBuilds.org - docker
https://slackbuilds.org/repository/14.2/system/docker/

howtos:cloud:docker - SlackDocs
https://docs.slackware.com/howtos:cloud:docker

PowerShell

PowerShell スクリプト | Microsoft Docs
https://docs.microsoft.com/ja-jp/powershell/scripting/overview?view=powershell-6

PowerShell のコマンド ライン オプション | Microsoft Docs
https://docs.microsoft.com/ja-jp/powershell/scripting/components/console/powershell.exe-command-line-help?view=powershell-6

PowerShellの基本(前編) (1/5):Windows PowerShellコマンド&スクリプティング入門 - @IT
https://www.atmarkit.co.jp/ait/articles/0708/30/news137.html

PowerShellスクリプティングの第一歩(後編) (1/5):Windows PowerShellコマンド&スクリプティング入門 - @IT
https://www.atmarkit.co.jp/ait/articles/0709/20/news125.html

PowerShell/文法/コマンド置換(バッククオート) - yanor.net/wiki
http://yanor.net/wiki/?PowerShell/%E6%96%87%E6%B3%95/%E3%82%B3%E3%83%9E%E3%83%B3%E3%83%89%E7%BD%AE%E6%8F%9B%EF%BC%88%E3%83%90%E3%83%83%E3%82%AF%E3%82%AF%E3%82%AA%E3%83%BC%E3%83%88%EF%BC%89

What's the cmd/powershell equivalent of back tick on bash? - Stack Overflow
https://stackoverflow.com/questions/434038/whats-the-cmd-powershell-equivalent-of-back-tick-on-bash/434311#434311

Process コマンドレットによるプロセスの管理 | Microsoft Docs
https://docs.microsoft.com/ja-jp/powershell/scripting/samples/managing-processes-with-process-cmdlets?view=powershell-6

PowerShell/実行しているPowerShellの変数一覧を表示する・Get-Variable - Windowsと暮らす
https://win.just4fun.biz/?PowerShell/%E5%AE%9F%E8%A1%8C%E3%81%97%E3%81%A6%E3%81%84%E3%82%8BPowerShell%E3%81%AE%E5%A4%89%E6%95%B0%E4%B8%80%E8%A6%A7%E3%82%92%E8%A1%A8%E7%A4%BA%E3%81%99%E3%82%8B%E3%83%BBGet-Variable

WindowsでPowerShellスクリプトの実行セキュリティポリシーを変更する:Tech TIPS - @IT
https://www.atmarkit.co.jp/ait/articles/0805/16/news139.html

about_Execution_Policies | Microsoft Docs
https://docs.microsoft.com/ja-jp/powershell/module/microsoft.powershell.core/about/about_execution_policies?view=powershell-6

Powershellでファイルの文字コードを変換 - Qiita
https://qiita.com/tworks55/items/b7884c4bb6ec84e73e57

環境:
Windows 10 x86_64 上の PowerShell 5x

扱うデータが全て「文字列」というのも楽しいですが、扱うデータ全てが「オブジェクト」というのも色々新鮮でした。オブジェクトのパイプとかリダイレクトとか、とても面白い。

Docker を使うことで Slackware でも恐らくある程度簡単に PowerShell を試すことが出来ると予想。)
→runc のインストールに手間取りましたが(構築依存のインストール後に一度シェルを開き直す必要があった)、Docker をインストール出来ました。powershell も問題なくインストール出来ます

公式配布のバイナリが Slackware で普通に動きました

インストール - バイナリ アーカイブ | Linux への PowerShell Core のインストール | Microsoft Docs
https://docs.microsoft.com/ja-jp/powershell/scripting/install/installing-powershell-core-on-linux?view=powershell-6#installation---binary-archives

上記の Slackware への読み替え例

cd /tmp

curl -L -o /tmp/powershell-6.2.0-linux-x64.tar.gz https://github.com/PowerShell/PowerShell/releases/download/v6.2.0/powershell-6.2.0-linux-x64.tar.gz

mkdir -p /tmp/powershell-6.2.0/opt/microsoft/powershell/6.2.0

tar zxf /tmp/powershell-6.2.0-linux-x64.tar.gz -C /tmp/powershell-6.2.0/opt/microsoft/powershell/6.2.0

chmod +x /tmp/powershell-6.2.0/opt/microsoft/powershell/6.2.0/pwsh

mkdir -p /tmp/powershell-6.2.0/usr/bin

cd /tmp/powershell-6.2.0/usr/bin

ln -s /opt/microsoft/powershell/6.2.0/pwsh ./pwsh

cd /tmp

su -

cd /tmp/powershell-6.2.0/

makepkg --linkadd y --chown y /tmp/powershell-6.2.0-1_MYPKG.txz

installpkg /tmp/powershell-6.2.0-1_MYPKG.txz

cd /root

rm -r /tmp/powershell-6.2.0

mkdir /root/Downloads

cp /tmp/powershell-6.2.0-linux-x64.tar.gz /tmp/powershell-6.2.0-1_MYPKG.txz /root/Downloads/

※ /etc/slackpkg/blacklist の末尾に [0-9]+_MYPKG と追記し、パッケージ名で "1_MYPKG" とすることで、slackpkg clean-system 等の対象外に指定。

学習コストはかかりそうですが、興味深いシェルです。

p-zombie.ps1
$PHILOSOPHICAL_ZOMBIE=$(Get-Content -Encoding UTF8 ./p-zombie.ps1)
$GENERATION=$GENERATION+1
if($GENERATION -eq 1){
    Write-Output "PID $PID"
}
Write-Output "GEN $GENERATION"
Get-FileHash ./p-zombie.ps1
Write-Output "" >./p-zombie.ps1
Start-Sleep 10
Get-Content -Encoding UTF8 ./p-zombie.ps1
Write-Output "Hello, Everyone. I'm a zombie. Let's dance together!

"
Write-Output $PHILOSOPHICAL_ZOMBIE | Out-File -FilePath ./p-zombie.ps1 -Encoding utf8
.\/p-zombie.ps1



※ 末尾に改行が 3 行あると、最初のファイルと 2 番目以降のファイルのハッシュ値が一致しました。

※ Write-Output に改行を含める修正をした際にダブルクォーテーションマークを一つ消し忘れて実行不能になっていました。失礼しました。

powershellでzombie.png

シェル組み込みコマンドのみでスクリプトを構築した場合、移植性も高そう。

スクリーンショット_2019-07-02_11-49-19.png

スクリーンショット 2019-07-02 14.33.42.png

追記2:考察と展望

今回のスクリプトにより、無性生殖による世代交代の(ごく大雑把な)モデル化が出来ました。

次の機会には有性生殖により世代交代するスクリプトを書きたい。

exit コマンドによりループを開くことでアポトーシスも表現出来ますし、生命現象を比喩するスクリプトにはまだまだ開拓の余地がありそうです。

「シェル芸」の誤用に関するお詫び

ワンライナーへの指向を以ってシェル芸と定めるそうなので、今回のような単に非実用性を志向するだけのコードはシェル芸にはカテゴライズされないようです。

失礼しました。

(シェルアートと自称すれば問題ないかな?)

芸術 - Wikipedia
https://ja.wikipedia.org/wiki/%E8%8A%B8%E8%A1%93

アート - Wikipedia
https://ja.wikipedia.org/wiki/%E3%82%A2%E3%83%BC%E3%83%88

「シェル芸術」という概念が @yasuhiroki さんから提唱されています。
シェル芸術 <クワイン編> - Qiita
https://qiita.com/yasuhiroki/items/7e66ffdca67e834f55a1

9
9
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
9