Zshで長い処理をしている間に読込中を表示する


# 内容

- shellでのバックグラウンド処理
	- ジョブ制御フラグ
- メタ文字による標準出力の管理

[Android - GradleをZshで補完する - Qiita](http://qiita.com/petitviolet/items/11ae1c7791e6343a6c1e)という記事を書いた時に

> なお、初回のみタスクの読み込みに時間がかかるが、2回目以降はファイルキャッシュを読み込むため比較的速くなる

と書いたが、初回の読み込み時の長さが気になるので読み込み中であるindicatorを表示することにした

### 参考
[Pythonでローディングのぐるぐるを表示する - CAMPHOR- Tech Blog](http://tech.camph.net/python-guruguru/)


# 完成品

![loading_tasks.gif](https://qiita-image-store.s3.amazonaws.com/0/280/bf624efc-735a-8283-be56-3cda1a2f54c0.gif "loading_tasks.gif")

`./gradlew `のあとで`TAB`を押すと`tasks`を読み込む
読んでる間の時間が長いので、indicatorを出して読み込み中であることを示している

# コード

[gist](https://gist.github.com/petitviolet/910c0b54b067405d0aa3)も更新した

```zsh:_gradle
#!/usr/bin/env zsh

# refs: https://gist.github.com/nolanlawson/8694399 : bash-version

loading() {
  local count=30
  if [ $# -eq 1 ]; then
    count=$1
  fi
  # KILL -INT をtrapして最後の出力を消す
  trap "echo -en '\r                      '; exit 0" INT

  # 改行
  echo
  for i in `seq 1 1 $count`
  do
    echo -en '|\r' ; sleep 0.05;
    echo -en '/\r' ; sleep 0.05;
    echo -en '-\r' ; sleep 0.05;
    echo -en '\\\r'; sleep 0.05;
  done
  exit 0
}

_gradle() {
  local cur="$1"
  local gradle_cmd='gradle'
  if [[ -x ./gradlew ]]; then
    gradle_cmd='./gradlew'
  fi
  if [[ -x ../gradlew ]]; then
    gradle_cmd='../gradlew'
  fi

  local completions=''
  local cache_dir="$HOME/.gradle_tabcompletion"
  mkdir -p $cache_dir

  local gradle_files_checksum='hoge';
  if [[ -f build.gradle ]]; then # top-level gradle file
    if [[ -x `which md5 2> /dev/null` ]]; then # mac
      local all_gradle_files="$(find . -name build.gradle 2> /dev/null)"
      gradle_files_checksum="$(md5 -q -s "${all_gradle_files}")"
    else # linux
      gradle_files_checksum="$(find . -name build.gradle | xargs md5sum | md5sum)"
    fi
  else
    gradle_files_checksum='no_gradle_files'
  fi

  if [[ -f $cache_dir/$gradle_files_checksum ]]; then # cached! yay!
    completions=$(\cat $cache_dir/$gradle_files_checksum)
  else
    ################################
    # ジョブ制御を無効化
    set +m
    # バックグラウンドでindicatorを回す
    loading 1000 & >/dev/null 2>&1
    set -m
    # indicatorのprocess id
    local LOADING_PID=$!
    completions=$($gradle_cmd --no-color --quiet tasks | grep ' - ' | awk '{print $1}' | tr '\n' ' ')
    # indicatorを殺す
    kill -INT $LOADING_PID
    ################################
    if [[ ! -z $completions ]]; then
      echo $completions > $cache_dir/$gradle_files_checksum
    fi
  fi

  local -a commands
  commands=( "${(z)completions}" )
  compadd $commands
  return 0;
}

compdef _gradle gradle
compdef _gradle gradlew
compdef _gradle ./gradlew
```

## 読み込み中の表示

`loading`関数は引数を1つとってその引数回数だけindicatorを回転させるものとなっている
`while`を使って無限ループにしても良いと思う
`\r`は復帰文字と呼ばれるもので、標準出力で今いる行の先頭にカーソルを動かすイメージのもの
`\b`はバックスペース
参考：[シェル・スクリプト・リファレンス - 【 メタ文字の取り扱い 】：ITpro](http://itpro.nikkeibp.co.jp/article/COLUMN/20060228/231046/)

くるくる回る棒を`2>&1`して標準エラー出力にリダイレクトしても良いが、どうせ後で消すのでここではしなかった

この読み込み中のものだけ切りだすとこんな感じ

```zsh:loading.sh
#!/usr/bin/env zsh

loading() {
  local count=30
  if [ $# -eq 1 ]; then
    count=$1
  fi

  for i in `seq 1 1 $count`
  do
    echo -en '|\b'  1>&2; sleep 0.05;
    echo -en '/\b'  1>&2; sleep 0.05;
    echo -en '-\b'  1>&2; sleep 0.05;
    echo -en '\\\b' 1>&2; sleep 0.05;
  done
  # 最後の出力を消す
  echo -en ' \b' 1>&2;
  exit 0
}

loading $@
```

![loading.gif](https://qiita-image-store.s3.amazonaws.com/0/280/6b3e4ab2-0338-340b-4baf-c67a6f7e7f19.gif "loading.gif")

新しいバージョンだと`Bash`でも動きます

## 読み込み中を隠す

普通に`&`をつけてバックグラウンド処理をするとバックグラウンドに移った処理のプロセスIDが表示されてしまいます

![loading_background.gif](https://qiita-image-store.s3.amazonaws.com/0/280/3a97e0bd-c33f-5e85-d2d3-a8c8f98a45d8.gif "loading_background.gif")

鬱陶しいので`set +m`としてジョブ制御をしないフラグを立てることでこれを表示しないように出来る
参考: [Linuxコマンド集 - 【 set 】 シェルのオプションを設定する：ITpro](http://itpro.nikkeibp.co.jp/article/COLUMN/20060227/230881/)

![loading_no_job.gif](https://qiita-image-store.s3.amazonaws.com/0/280/4989dce0-3a1c-2abd-e720-1c443a704c3e.gif "loading_no_job.gif")

直前に実行したプロセスのIDは`$!`で取得出来るので、
これに対して読み込み中の表示を消したいタイミングで`kill`を送ってプロセスを殺す
`kill`で送られてきたシグナルを

```sh
trap "echo -en '\r                      '; exit 0" INT
```

のように`trap`で`INT`シグナルを受け取って標準出力を掃除してから`exit`するようにしている

## 心残り

どうしても
> [8]  + done       loading 1000

といった`kill`時に表示されるメッセージを消せなかった
これは`kill`した側のshellで表示されるらしく、`kill`する部分をサブプロセスにするなど、手を尽くしたがだめだった