✍️

ls -l はなぜ事故を起こすのか

3 min read 3

はじめに

この記事は以下の記事で事故がなぜ起きたかを技術的に理解し筆者のtipsを共有しようと思い書きました。あくまで筆者のtipsですので、これが正しいとは限りません。

https://zenn.dev/kobayashiyabako/articles/85902e6095ab0cdb7cf5

やはり、下記のコマンドが原因でした。

[root@foo script]# ls -l | sh

事故そのものは ls コマンドに -l を付けて実行してしまった事で、ls コマンドが出力したシンボリックリンクが以下の様になってしまったという話です。

link -> file

この出力が sh コマンドによりリダイレクトと判定され、余計なファイルを生成してしまった様です。

追記

ls | sh がやりたかった事ではなく ls | grep sh しようとしたら grep を忘れたというシナリオです。

なぜ事故は起きたのか

人により方法はいろいろだと思いますが、筆者の場合パイプで指定する場合は -l は使いません。ls は通常、標準出力がパイプの場合(isatty(3)が0)は、出力が1行1ファイルの出力になります。以下は GNU CoreUtils に含まれる ls のソースです。

https://github.com/coreutils/coreutils/blob/00ea4bacf6063ccc125209d5186f8f2382c6f0d4/src/ls.c#L1884-L1899
    case LS_LS:
      /* This is for the 'ls' program.  */
      if (isatty (STDOUT_FILENO))
        {
          format = many_per_line;
          set_quoting_style (NULL, shell_escape_quoting_style);
          /* See description of qmark_funny_chars, above.  */
          qmark_funny_chars = true;
        }
      else
        {
          format = one_per_line;
          qmark_funny_chars = false;
        }
      break;

この処理により出力が端末でない場合は one_per_line 形式(1行1ファイル)になります。しかし気を付けないといけないのは、これはデフォルト値という事です。-l の様なフラグを付けてしまうと、デフォルト値の one_per_linelong_format に変更されてしまいます。

https://github.com/coreutils/coreutils/blob/00ea4bacf6063ccc125209d5186f8f2382c6f0d4/src/ls.c#L2021
        case 'l':
          format = long_format;
          break;

-l だけではありません。-C によるマルチカラム表示、-g-o による詳細表示(ほぼ -l)、-m によるカンマ表示、-n による uid/gid の数値表示、-x による垂直表示、色々なフラグが出力形式を変更します。

ですので

ls | 

としていればシンボリックリンクに起因した事故は起きなかったという事になります。もちろん sh を書いてしまうと別の事故が起きた事にはなりますが。気になる人は -1 (-l ではない)を付ける事で、one_per_line の形式を強制できます。

どうやって防ぐか

これはあくまで筆者の考えですが、まず本番環境では幾ら慣れていてもカジュアルにシェル操作をしない事だと思っています。弊社の場合、本番で実行するシェル操作は事前に書き起こし、レビューが行われます。本番での作業はその作業手順書のコピペでしか行いません。もちろんカジュアルに本番環境を触って機動力を確保したい会社もあるので、方法はそれぞれです。

できれば本番環境に近い試験環境を用意し、そこで事前に実行するのが良いと思っています。しかし試験環境でさえ失敗してしまう事があります。そういった場合に筆者がよくやるのがスクリプトを書くことです。事前に試験環境で、想定通りのコマンドが実行される様なスクリプトを書いておき、レビューを受け、それを本番に持っていって実行します。

スクリプトにしておくことで、alias の影響も受けません。まさか本番環境で

alias ls="ls -l"

等が設定されているとは思えませんが、もし設定されていればいくら -l を付けていなかったとしても ls | で詳細表示が実行され、同じ事故が発生する事になります。

筆者の tips

筆者の場合、Vim というテキストエディタを使い、quickrun というプラグインを導入しています。

https://github.com/thinca/vim-quickrun

もちろん本番環境には導入しません。本番環境にエディタやシェルの拡張を持っていって動かしてしまい、本番環境に負荷を掛けてしまったという事例もあります。ですので筆者は本番環境にそういった物は導入しません。

さて話を戻して Vim と quickrun で編集する方法ですが、quickrun をインストールするとデフォルトで <leader>-r というキーにスクリプトの実行がアサインされます。何も設定していなければ \r のはずです。ファイルの一覧から何かを処理するのであれば、以下の様に書くでしょう。

#!/bin/sh
set -e
ls | while read line; do
  # 何か
  echo $line
done

ここでまず \r をタイプしてちゃんと1行1ファイルの出力がされている事を確認します。次に絞り込みを行います。

#!/bin/sh
set -e
ls | grep xxx | while read line; do
  # 何か
  echo $line
done

出力を確認して絞り込めている事を確認します。そして見つけたファイルを削除するコードを書くのですが、いきなり rm は実行しません。

#!/bin/sh
set -e
ls | grep xxx | while read line; do
  echo rm $line
done

この様にして、実際に実行されるコマンドを echo 表示します。出力を目視確認し、最後に echo を取ります。

#!/bin/sh
set -e
ls | grep xxx | while read line; do
  rm $line
done

「ここまで慎重にやる必要があるのか」と思う人もいるかもしれませんが、こういった小さい予防策を普段からやっておく事で「本番環境は怖い」という意識付けにもなると思っています。

まとめ

本番環境はカジュアルに触ってはならぬ。

この記事に贈られたバッジ

Discussion

お、ありがとうございます。後ではりかえておきます。

有用な記事ありがとうございます。

本番環境では、作業記録が残り再現性があることが望ましいと思いますので、スクリプトでの作業という点、納得しました。

手順書のみで済ませる例も見かけるのですが、誤操作、余計な手順、作業者の無意識な作業により上手く行っていたなどにらつながりかねないので、不確定要素を減らすためにスクリプトによる作業を心がけたいと思います。

また、qiitaの関連記事をリンクいたします(こちらの方が記述が更新されているようでしたので)

https://qiita.com/yabako_kobayashi/items/8113e682408c06066db4
ログインするとコメントできます