Help us understand the problem. What is going on with this article?

Googleの肩に乗ってShellコーディングしちゃおう

More than 1 year has passed since last update.

はじめに

GoogleさんがShellスタイルガイドを共有していたので、いくつか気になった点をピックアップしました。
自分のShellスタイルはかなり我流なので、自省の意味も込めてコメントも併記します。

Googleスタイルガイドの元ネタ (Python/C++/Java/Rとかだけでなくdocumentガイドなど色々あります)
https://github.com/google/styleguide

Shellスタイルガイド (今回はこちら)
http://google.github.io/styleguide/shell.xml

本当は人間がチェックするのではなくcpplintのためXML定義なのかもですが、気にしない気にしない。
(見たところcpplintはc++だけだと思ってます)
commitフックでshell系のlint走らせろっていうのが今風なのかもしれませんが、キニシナイキニシナイ。

拾い読み

1. 利用可能なShellについて

Bashのみ利用可能。従って#!/bin/bashで始めましょう。
本来の機能を壊さないようShellのオプションを使うためのフラグを多用しないこと。
setを使いましょう。

2. 利用シーン

小さいツールやスクリプト実行時のラップ程度にShellの利用を抑えること。
100行超えるとPythonとか他の言語を使おうこと。

雑記
(ちょっとしたツール目的でも100行すぐ超えちゃいますね。)

3. SUID/SGIDについて

セキュリティ的な課題や他の実機への横展開が難しくなるのでSUID/SGIDの利用は禁止。
どうしても必要ならsudo使おう!

雑記
(そもそもSUID使ったことがなかったので問題なし!)

4. 標準出力・標準エラー出力

全てのエラーメッセージはSTDOUTではなくSTDERRに出力すること。
正常な状態と異常な状態を簡単に区別できるようにするためです。
下記のような関数を作っておくのがおススメ。

STDERRにメッセージを出力すべし
err() {
  echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')]: $@" >&2
}

if ! do_something; then
  err "Unable to do_something"
  exit "${E_DID_NOTHING}"
fi

雑記
(デバッグ用に出力するときは何も考えずにecho "hogehoge"と標準出力してました。反省。)

5. コメント

  • ファイル
    • Shellの機能を示す簡潔なトップレベルコメントを必ず書くこと。
  • 関数
    • ステップ数・複雑度に関係なく全ての関数にコメントを付けること。
    • グローバル変数、引数、戻り値が何か記載すること
コメント例
#!/bin/bash
#
# データベースのバックアップ用途

export PATH='/usr/xpg4/bin:/usr/bin:/opt/csw/bin:/opt/goog/bin'

#######################################
# Cleanup files from the backup dir
# Globals:
#   BACKUP_DIR
#   ORACLE_SID
# Arguments:
#   None
# Returns:
#   None
#######################################
cleanup() {
  ...
}

雑記
(Utilityぽい関数ではノーコメント派でした。心を入れ替えます。)

6. TODO

一時的にTODOを使う場合は、ユーザ名と補足内容を書くこと。
Issueの番号も書ければ望ましい。

TODO書き方
# TODO(user_name): Handle the unlikely edge cases (bug ####)

7. インデント

タブではなく、2スペース。
タブダメ絶対。

雑記
(タブ派なのでそのまま受け取ると消耗しますね)

8. パイプ

1パイプなら1行。
複数行なら改行して、2スペースを開けてパイプで繋げましょう。

パイプの書き方
# All fits on one line
command1 | command2

# Long commands
command1 \
  | command2 \
  | command3 \
  | command4

雑記
(あんまり行数増やしたくないんで、今までは長くなったら適当に改行してました。
ls -l | grep ^d | awk '{print $NF}' | sort | uniq -cとか1行で書きたいと思うんですけど、
 規約を厳格に適用すると4行になっちゃいますね)

9. ループ

;dothenは、forwhileifと同じ行に書くこと。

ループコーディング例
for dir in ${dirs_to_cleanup}; do  # ★セミコロンやdoはforと同じ行に書く
  if [[ -d "${dir}/${ORACLE_SID}" ]]; then
    log_date "Cleaning up old files in ${dir}/${ORACLE_SID}"
    rm "${dir}/${ORACLE_SID}/"*
    if [[ "$?" -ne 0 ]]; then
      error_message
    fi
  else
    mkdir -p "${dir}/${ORACLE_SID}"
    if [[ "$?" -ne 0 ]]; then
      error_message
    fi
  fi
done

雑記
(主観ですがネット上のサンプルでは、dothenの改行派が多そうなイメージです。
 でもこっちの方がすっきりして良いですね!)

10.変数

一般的に"$var"より"${var}"が推奨です。
ただ、詳細なルールは以下を確認してください。

変数の推奨例
# 特殊な書き方はそちらを優先
echo "引数インデックス: $1" "$5" "$3"
echo "特殊系: !=$!, -=$-, _=$_. ?=$?, #=$# *=$* @=$@ \$=$$ ..."

# 中括弧必須:
echo "many parameters: ${10}"

# 混乱を避けるために中括弧を付けること
# [出力例]a0b0c0
set -- a b c
echo "${1}0${2}0${3}0"

# 他の変数のため中括弧があると望ましい:
echo "PATH=${PATH}, PWD=${PWD}, mine=${some_var}"
while read f; do
  echo "file=${f}"
done < <(ls -l /tmp)
変数の非推奨例
# ダブルクォートや中括弧が無い変数 特殊変数なのに中括弧あり
echo a=$avar "b=$bvar" "PID=${$}" "${1}"

# "${1}0${2}0${3}0"と同じパターン
# ${10}${20}${30}という意味には当然ならないので注意
set -- a b c
echo "$10$20$30"

11. クオート

シングルクォートは置換が必要ないとき。
ダブルクォートは置換が必要なときに利用する。

12. $(command)

バッククォートではなく$(command)が推奨。

良い例
var="$(command "$(command1)")"
ダメな例
var="`command \`command1\``"

雑記
(意識せずバッククォート使っている時も多かったので、反省)

13. Test, [ and [[

[[ ... ]][testより推奨。
[[ ... ]]が引数なしや複数引数の場合でもエラーにならず、正規表現が使えるため。

良い例
# 正規表現使える
if [[ "filename" =~ ^[[:alnum:]]+]]; then
  echo "Match"
fi
悪い例
# f*がカレントディレクトリにの内容に展開され"too many arguments"が出るかも
if [ "filename" == f* ]; then
  echo "Match"
fi

14. ワイルドカード

*の代わりに./*を使おう。

命名:関数

関数名はlower_caseで命名すること。

良い例
# 関数名はlower_case
my_func() {
  ...
}
悪い例
# 関数名をcamelCaseにはしない
myFunc() {
  ...
}

15. 命名:変数

ループ変数は似たような名前にすること。

良い例
for zone in ${zones}; do
  something_with "${zone}"
done
ダメな例
for z in ${zones}; do    # ★変数名をzに。トレードオフだが非推奨
  something_with "${z}"
done

雑記
(他の言語でも同じですね)

16. 命名:定数、環境変数

定数や環境変数はSNAKE_CASEにする。

定数・環境変数
# 定数
readonly PATH_TO_FILES='/some/path'

# 環境変数
declare -xr ORACLE_SID='PROD'

雑記
(よく見かけますね。他の言語でもやりますね)

17. 命名:ファイル名

lower_case.shの形式でファイルの命名を行うこと

18. ローカル変数

関数内でしか用いない変数にはlocalで宣言すること。
グローバルネーム空間の汚染を防ぐことが出来ます。
代入する値がコマンド置換の場合は、localの宣言と分けること。

良い例
my_func2() {
  local name="$1"

  # local変数の宣言と代入はわけること!
  local my_var
  my_var="$(my_func)" || return
  ...
}
悪い例
  # コマンド結果を代入する場合は、ワンセンテンスで書かないこと。
  # $? の結果はmy_funcの結果ではなく、'local'の結果が格納されるため(!)
  local my_var="$(my_func)"
  [[ $? -eq 0 ]] || return

雑記
(これ、知らなかったです・・)

19. Conclusion

「常識的な、そして一貫性のあるコーディングをしましょう」
Use common sense and BE CONSISTENT.

まとめ

  • コーディング規約を読むと、ハマりどころな観点もまとまっているので勉強になりおすすめです
  • Javaとかもそうですが糖衣構文などで複数の書き方があると、規約が必要になってくるので大変ですね。
    • (Golangはそういう点がまだ少ない気するのでやはり後発有利)
  • みなさまも良いShellライフを!
ma91n
🗽Backend Engineer 🏢Future所属 📝会社の技術ブログ運営者  ⛏趣味でGo製ツール開発してます 📢エモ記事はMediumに書いてます✨
https://medium.com/@laqiiz
future
ITを武器とした課題解決型のコンサルティングサービスを提供します
http://future-architect.github.io/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away