Homebrewの自作コマンドを正しく作る with ShellScript

Homebrewの自作コマンドの作成は様々な言語で実装された記事がありました。ここではShellScriptでHomebrewのコマンドを作成します。

- Go
http://girigiribauer.com/archives/20161004/

- Python
https://rcmdnk.com/blog/2013/09/17/computer-mac-homebrew/

- C
http://qiita.com/masawada/items/484bbf83ef39cad7af74


今回は普段管理するライブラリの数が増えて大変になってきていたために必要に感じて`git-pulls`というコマンドを作成しました。これは指定したディレクトリ以下のレポジトリを一括で`git pull`してくれるHomebrewのコマンドを作成しました。
https://github.com/hayashier/git-pulls


# 作成コマンドの利用方法
以下の2つのコマンドを入力するだけで利用できるようになります。

```
$ brew tap hayashier/git-pulls
$ brew install git-pulls
```

利用方法は以下のように一括で`git pull`するパスを指定します。除外するパスも`-e`オプションでカンマ区切りで複数指定できます。

```
$ git-pulls -h
Usage: git-pulls [-h] [-e /exclusivePath1/to,/exclusivePath2/to,/exclusivePath3/to ] /path/to
```

 例えば、`git-pulls ./`のようにコマンドを打つと以下のように一括で`git pull`してくれます。

```
$ git-pulls ./
Start pulling all git project in /Users/hayashier/repository/.
Exclude path: []
/Users/hayashier/repository/me/sample-repository1
Already up-to-date.
/Users/hayashier/repository/me/sample-repository2
Already up-to-date.
/Users/hayashier/repository/me/sample-repository3
Already up-to-date.
```

`-e`オプションで除外するパスも指定できます。またカンマ区切りで除外するパスを複数指定もできます。

```
$ git-pulls -e me ./
Start pulling all git project in /Users/hayashier/repository/
Exclude path: me
/Users/hayashier/repository/me/sample-repository1
Passing...
/Users/hayashier/repository/me/sample-repository2
Passing...
/Users/hayashier/repository/me/sample-repository3
Passing...
/Users/hayashier/repository/team1/sample-repository1
Already up-to-date.
/Users/hayashier/repository/team2/sample-repository2
Already up-to-date.
```


# Homebrewコマンドの作成
## コマンドのプロジェクト作成
1.ディレクトリを作成します。

```
$ mkdir -p git-pulls/bin
$ cd git-pulls
```

必要なファイルを編集します。

```
$ vim bin/git-pulls
```

```bash
#!/bin/bash

GIT_PULLS_VESION="0.0.1"

function contains() {
  target=$1
  shift
  paths=$(echo $@ | tr -s ' ' ' ')
  if [ "${paths[1]}" == " " ]; then
    flag=$(echo $target | grep ${paths[0]})
    if [[ -n $flag ]]; then
      return 0
    fi
  else
    for ((i=0;i < ${#paths[@]};i++)) {
      if [ ${paths[$i]} == [] ]; then
        continue
      fi
      flag=$(echo $target | grep ${paths[$i]})
      if [[ -n $flag  ]]; then
        return  0
      fi
    }
  fi

  return 1
}

function formArrayPaths() {
  exclude_paths=$(echo $1 | tr -s ',' ' ')
  for ((i=0;i < ${#exclude_paths[@]};i++)) {
    path=${exclude_paths[$i]}
    flag=$(echo $path | grep $EXECUTING_PLACE)
    if [ -n $flag ] && [ $i -ne 0 ] && ([ ${path:0:1} == "/" ] || [ ${path:0:1} == "~" ]); then
      num=$(( ${#path} - 1))
      exclude_paths[$i]="$EXECUTING_PLACE${path:1:num}"
    fi
  }
  echo ${exclude_paths[@]}
}

if [ -z $1 ]; then
  echo 'Usage: git-pulls [-h] [-e /exclusivePath1/to,/exclusivePath2/to,/exclusivePath3/to ] /path/to'
  exit
fi

EXECUTING_PLACE=$PWD
trap "cd $EXECUTING_PLACE && exit" {1,2,3,15}

FLAG=true
origin=$HOME
exclude_paths=[]

if [ ${1:0:1} != "-" ]; then
  origin=$1
  shift 1
  FLAG=false
fi

while getopts ":e:hv" OPT ;
do
  case $OPT in
  e)
    result=$(formArrayPaths $OPTARG)
    exclude_paths=${result[@]}
    ;;
  h)
    echo 'Usage: git-pulls [-h] [-e /exclusivePath1/to,/exclusivePath2/to,/exclusivePath3/to ] /path/to'
    exit
    ;;
  v)
    echo "git-pulls version $GIT_PULLS_VESION"
    exit
    ;;
  esac
done

SHIFT_NUM=$((OPTIND - 2))
if [ $SHIFT_NUM -gt 0 ]; then
  shift $SHIFT_NUM
fi

if $FLAG; then
  shift 1
  origin=$1
fi

if [ ${origin:0:1} != "/" ] && [ ${origin:0:1} != "~" ]; then
  origin="$PWD/$origin"
fi

echo "Start pulling all git project in $origin."
if [ -n $exclusive_path ]; then
  echo "Exclude path: ${exclude_paths[@]}"
fi

find $origin -type d -name .git | xargs -n 1 dirname | sort | while read line; do echo $line ; contains $line ${exclude_paths[@]}; if [ $? -eq 0 ]; then echo "Passing..."; continue; fi  && cd $line && git pull && cd $origin; done
```


```
$ vim README.md
```

````README.md
# git-pulls
`git-pulls` command pulls all git repositories under specified path.

# Install (OSX)

$ brew tap hayashier/git-pulls
$ brew install git-pulls

# Usage

$ git-pulls [-v] [-h] [-e /exclusivePath1/to,/exclusivePath2/to,/exclusivePath3/to ] /path/to

## Options

-v : Display version
-h : Display usage
-e : Specify path which exclude paths. If you specify multiple paths, enumerate paths separated by comma
````

予めGitHubに`git-pulls`というレポジトリを作成しておき、これにpushします。

```
$ git init
$ git add ./
$ git commit -m "first commit"
$ git remote add origin https://github.com/hayashier/git-pulls.git
$ git push -u origin master
```

tag付けをしてpushします。

```
$ git tag 0.0.1
$ git push origin 0.0.1
```


## Formulaの作成

Formulaを作成します。ファイル名は[アプリケーション名].rbです。ここではアプリケーション名はgit-pullsとなります。

1.ディレクトリを作成して移動します。

```
$ mkdir homebrew-git-pulls && cd $_
```

2.Formulaを作成して編集します。

```
$ brew create https://github.com/hayashier/git-pulls.git
$ cp /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/Formula/git-pulls.rb git-pulls.rb
$ vim git-pulls.rb
```

Formulaは以下の通りとなります。

```git-pulls.rb
REPOSITORY_URL="https://github.com/hayashier/git-pulls".freeze
HOMEBREW_GITPULLS_VERSION="0.0.1".freeze

class GitPulls < Formula
  desc "Pulls all git repository under specified paths."
  homepage REPOSITORY_URL
  url "#{REPOSITORY_URL}/archive/#{HOMEBREW_GITPULLS_VERSION}.tar.gz"
  sha256 "ed3b6451a05567505bd718db8fdf343f47e97d70fc7d4d92e7f5cb3c9532b753"
  head "#{REPOSITORY_URL}.git", :tag => HOMEBREW_GITPULLS_VERSION

  def install
    bin.install "bin/git-pulls"
  end

  test do
    system "false"
  end
end
```

なお、sha256の値はtag付したバージョンの`tar.gz`の形式のファイルを以下のようなURLからダウンロードしてきます。
https://github.com/＜ユーザ名＞/git-pulls/releases

そして、`openssl`コマンドを用いて以下のようにファイルのハッシュ値を調べます。ハッシュ値は`xxxxxx`に当たる部分となります。

```
$ openssl dgst -sha256 ~/Downloads/git-pulls-0.0.1.tar.gz
SHA256(homebrew-git-pulls-0.0.1.zip)= xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

予めGitHubに`homebrew-git-pulls`というレポジトリを作成しておき、これにpushします。
レポジトリ名を`homebrew-`から始まるものでないといけないので注意。

```
$ git init
$ git add ./
$ git commit -m "first commit"
$ git remote add origin https://github.com/hayashier/homebrew-git-pulls.git
$ git push -u origin master
```

## 利用してみる

作成したコマンドは他のユーザも以下のコマンドでインストールできます。

```
$ brew tap hayashier/git-pulls
$ brew install hayashier/git-pulls
```

<!--
```
$ brew tap hayashier/git-pulls
Updating Homebrew...
==> Auto-updated Homebrew!
Updated Homebrew from 9105aca to c4c3588.
No changes to formulae.

==> Tapping hayashier/git-pulls
Cloning into '/usr/local/Homebrew/Library/Taps/hayashier/homebrew-git-pulls'...
remote: Counting objects: 6, done.
remote: Compressing objects: 100% (5/5), done.
remote: Total 6 (delta 0), reused 2 (delta 0), pack-reused 0
Unpacking objects: 100% (6/6), done.
Checking connectivity... done.
Tapped 1 formula (31 files, 23KB)

$ brew install git-pulls
==> Installing git-pulls from hayashier/git-pulls
==> Downloading https://github.com/hayashier/homebrew-git-pulls/archive/0.0.0.zip
==> Downloading from https://codeload.github.com/hayashier/homebrew-git-pulls/zip/0.0.0
######################################################################## 100.0%
🍺  /usr/local/Cellar/git-pulls/0.0.1: 4 files, 3.0KB, built in 3 seconds
```
-->

## 注意点
- 同じバージョンでタグを切り直したとき場合のインストール方法
同じバージョンでタグを切り直したときはアンインストールした上でキャッシュを消しておく。

```
$ brew uninstall git-pulls
$ rm /Users/hayashier/Library/Caches/Homebrew/git-pulls-0.0.1.zip
```

改めてインストールします。

```
$ brew install git-pulls
```

- 古いバージョンをインストールしていて新しいバージョンをインストールする場合のインストール方法

```
$ brew uninstall git-pulls
$ brew unlink git-pulls
```

改めてインストールします。

```
$ brew install git-pulls
```



# Formulaの構文チェック

`brew audit`コマンドでFormulaの構文チェックを行うことができます。


```
$ brew audit --strict git-pulls
hayashier/git-pulls/git-pulls:
  * `version` (line 10) should be put before `checksum` (line 8)
  * Stable: version 0.0.1 is redundant with version scanned from URL
  * Stable: Use GitHub tarballs rather than zipballs (url is https://github.com/hayashier/homebrew-git-pulls/archive/0.0.2.zip).
  * Description shouldn't include the formula name
  * C: 1: col 16: Freeze mutable objects assigned to constants.
  * C: 1: col 16: Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.
  * C: 2: col 27: Freeze mutable objects assigned to constants.
  * C: 2: col 27: Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.
  * C: 9: col 41: Prefer `to_s` over string interpolation.
  * C: 10: col 11: Prefer `to_s` over string interpolation.
Error: 10 problems in 1 formula
```

コマンド実行時のFormulaが以下の状態のときの修正方法について説明します。

```git-pulls.rb
REPOSITORY_URL='https://github.com/hayashier/git-pulls'
HOMEBREW_GITPULLS_VERSION='0.0.1'

class GitPulls < Formula
  desc "git-pulls command pulls all git repository under specified paths."
  homepage "#{REPOSITORY_URL}/README.md"
  url "#{REPOSITORY_URL}/archive/#{HOMEBREW_GITPULLS_VERSION}.zip"
  sha256 "04777124c8e1ef8b3ba2d884f91ad47378437611b2b1349e730ef85913c2fa30"
  head "#{REPOSITORY_URL}.git", :tag => "#{HOMEBREW_GITPULLS_VERSION}"
  version "#{HOMEBREW_GITPULLS_VERSION}"

  def install
    bin.install "bin/git-pulls"
  end

  test do
    system "false"
  end
end
```

1.

```
Stable: version 0.0.1 is redundant with version scanned from URL
```

URLにバージョンが含まれているのでversionは冗長だからいらない。

2.

```
`version` (line 10) should be put before `checksum` (line 8)
```

バージョンは消すからこれは無視。

3.

```
Stable: Use GitHub tarballs rather than zipballs (url is https://github.com/hayashier/homebrew-git-pulls/archive/0.0.1.zip).
```

`.zip`じゃなくて`.tar.gz`で圧縮したものを指定する。

4.

```
Freeze mutable objects assigned to constants.
```

文字列に`.freeze`をつけて可変でないようにする

5.

```
Prefer double-quoted strings unless you need single quotes
```

定数はシングルクオテーションじゃなくてダブルクオテーション

6.

```
Prefer `to_s` over string interpolation.
```

`"#{CONSTANT}"`のようにしているところを`CONSTANT`とする。

以上となります。

## `--new-formula`オプションでさらに厳格に

```
$ brew audit --strict --new-formula git-pulls
hayashier/git-pulls/git-pulls:
  * The URL https://github.com/hayashier/homebrew-git-pulls/README.md is not reachable (HTTP status code 404)
  * GitHub repository not notable enough (<20 forks, <20 watchers and <50 stars)
  * GitHub repository too new (<30 days old)
Error: 3 problems in 1 formula
```

1.

```
The URL https://github.com/hayashier/homebrew-git-pulls/README.md is not reachable (HTTP status code 404)
```

アクセス可能なURLを指定する。

2.

```
GitHub repository not notable enough (<20 forks, <20 watchers and <50 stars)
```

20フォーク以上、または20watcher以上、もしくは50スター以上

3.

```
GitHub repository too new (<30 days old)
```

レポジトリ作成から30日以上

# 参考
http://tbpgr.hatenablog.com/entry/2016/07/08/015327
http://girigiribauer.com/archives/20161004/
http://qiita.com/masawada/items/484bbf83ef39cad7af74
http://qiita.com/sakajunquality/items/48b5138986056eb4b49c