Crystal書いてて気づいたこと

今日はuser agent parserのruby版をcrystalに移植していた。

github.com

crystalはv0.18.7を使っているのだけど書いてて気づいたことがあるのでメモ

not nilの推論はインスタンス変数には効かない

crystalは型に厳しいです。変数fooがStringあるいはNil型であるときはどちらの型にも存在するメソッドしか呼べません。
Stringにしかないメソッドを呼ぶときには条件分岐でnot nilであることを保証させます。

# foo : String?とする
if foo.nil?
  # ここではfooはNil型
else
  # ここではfooはnot nilなのでString型
end

ローカル変数ならこれでおkですがインスタンス変数はこれだとダメです。

class Klass
  @foo : String?
  
  def size
    if @foo.nil?
      0
    else
      @foo.size
    end
  end
end

puts Klass.new.size

# Error in line 13: instantiating 'Klass#size()'
# 
# in line 8: undefined method 'size' for Nil (compile-time type is (String | Nil))

いちどローカル変数にとれば通るみたいです。インスタンス変数だと関数スコープ外から変更可能だからかな、って推測しています。

class Klass
  @foo : String?
  
  def size
    bar = @foo
    if bar.nil?
      0
    else
      bar.size
    end
  end
end

puts Klass.new.size

#to_sと#inspectのoverride

Object#to_sObject#inspect はoverrideするべきではないらしい(usually MUST NOT)
ref: https://crystal-lang.org/api/0.18.7/Object.html#to_s-instance-method

代わりに #to_s(io : IO)#inspect(io : IO) に対してやれとのこと。

def Klass
  def to_s(io : IO)
    "this is Klass".to_s(io)
  end
end

puts Klass.new.to_s #=> "this is Klass"
puts "#{Klass.new}" #=> "this is Klass"

*argsはTuple型

調べればわかるんだけど調べたのでメモ。型違うのでちょっと注意。

def foo(*args)
  puts args.class
end

foo(1, 2, 3) #=> Tuple(Int32, Int32, Int32)
foo("bar", "foobar") #=> Tuple(String, String)

オフラインリアルタイムどう書くE06 Ruby で解く

問題はこちら:

qiita.com

コードは末尾に。

感想

第一感

ルールがしっかりしてるので適当にclass作って適当に殴ってればなんとかなりそうだなって印象。
テストケースの分量みてもナイーブな実装で特に問題なさげ。
というわけでホゲモンとトレーナーを作ってバトらせればいいかなって感じ。

実装

実際にその通りで問題なかった。 combination / each_sliceあたりがやっぱ便利だった

バグらせたのは2点。

  • コピペミスで対戦相手の勝利点が1じゃなくて2増えてしまった
  • sortするときにbreakするのを忘れた

心残り

  • テストコードが雑
  • RGBのあたりはmod3とかで回すと簡単になる気がした
  • battle_withが引数を変更するのでなかなかびっくりさせられる
    • battleをさせる別クラスが必要だった
  • ソートがもうちょっとエレガントにできるみたい
    • 勝数をマイナスにする
    • 勝数 * 100 - idとかにするとか

コード

require 'tapp'

class Monster
  attr_accessor :level
  attr_accessor :type
  attr_accessor :hp

  def initialize(l, t)
    @level = l.to_i
    @type = t
    @hp = @level
  end

  def recover
    @hp = @level
  end

  def dead?
    @hp <= 0
  end

  def battle_with(other)
    until dead? || other.dead?
      damage = damage_with(other)
      if level == other.level
        other.hp -= damage.first
        @hp -= damage.last
      elsif level > other.level
        other.hp -= damage.first
        break if other.dead?
        @hp -= damage.last
      else
        @hp -= damage.last
        break if dead?
        other.hp -= damage.first
      end
    end
  end

  def damage_with(other)
    return [2, 2] if type == other.type
    case type
    when "R"
      other.type == "G" ? [4, 1] : [1, 4]
    when "G"
      other.type == "B" ? [4, 1] : [1, 4]
    when "B"
      other.type == "R" ? [4, 1] : [1, 4]
    end
  end

  def to_s
    "#{hp}/#{level} #{type}"
  end
end

class Player
  attr_accessor :id
  attr_accessor :monsters
  attr_accessor :count

  def initialize(id, str)
    @id = id
    @count = 0
    @monsters = str.split('').each_slice(2).map { |a, b| Monster.new(a, b) }
  end

  def to_s
    puts "player#{id} #{count}win"
    @monsters.map(&:to_s).join("\n")
  end

  def defeated?
    @monsters.all?(&:dead?)
  end

  def head_monster
    @monsters.reject(&:dead?).first
  end

  def battle_with(other)
    monsters.each(&:recover)
    other.monsters.each(&:recover)
    until defeated? || other.defeated?
      head_monster.battle_with(other.head_monster)
    end

    # ひきわけ
    # raise "これどうすんの" if defeated? && other.defeated?
    return if defeated? && other.defeated?
    if defeated?
      other.count += 1
    else
      @count += 1
    end
  end
end

class Solver
  def solve(input)
    parse(input)
    @players.combination(2) { |a, b| a.battle_with(b) }
    sorted = @players.sort do |a, b|
      (b.count <=> a.count).tap { |e| break a.id <=> b.id if e == 0 }
    end
    sorted.map(&:id).join(",")
  end

  def parse(input)
    @players = input.split(',').each.with_index(1).map { |e, i| Player.new(i, e) }
  end
end


$no = 0
$fail = 0
def test(input, expected)
  $no += 1
  actual = Solver.new.solve(input)
  if actual == expected
    puts "#{$no} ok"
  else
    $fail += 1
    puts "#{$no} ng"
    puts "input   : #{input}"
    puts "actaul  : #{actual}"
    puts "expected: #{expected}"
  end
end

test("9B,3R2G,1R2B3G", "1,3,2")
test("1G", "1")
test("1G,1R,1B", "1,2,3")
test("8B,3R2G,1R2B3G", "3,1,2")
test("6G,9R7B7B", "2,1")
test("5B1B,1G1B2R6G,7B6G4B6B", "3,2,1")
test("7R,2R9G,6R4B1G6R,5G1G6G", "3,1,2,4")
test("2B9G8B3R,4R3G,2B,8B", "1,2,4,3")
test("1B,5G1R1B4R,5R,8B9B4G,7G5R8G", "5,4,3,2,1")
test("9G5B,6B6R1R5G,7G6G,8B5R,5G7G,2G5B7B", "2,1,3,4,5,6")
test("5B,1B8R2B,8G6R4B,4B1G6R8G,3B6G6G5R,7B", "3,4,5,6,1,2")
test("2G2G7G9B,6G5G5R,2G,4G,5R,3G8R,6G9R", "1,7,6,2,5,4,3")
test("6G7B4R6B,9R4G,6G5B5G3B,6R7G,9B,7G7B8R,5G8G2R,6B8G7B1B", "8,1,6,2,3,4,7,5")
test("9B8B2G4B,2B1R,7R6G8R,4R,1G7B7R,4B3B4R,4B3R2R4G,4G9R9R", "1,3,8,5,6,7,2,4")
test("5G,3G,9G7G8B,7B,8G6B1B5G,1G3B,5G8R,6G,7B", "3,5,7,8,1,2,4,9,6")
test("5B1R5B,6R,7R7R,8B1B,6R1G,7B3R2R,4R3B,6G1R8G,6B4R4R2B,9G5B", "10,1,4,6,9,3,8,5,7,2")
test("7R4G1G6R,9B3G3R4G,2G7G,5B,5R8R,9G7R9B,8R7R5G,7B9R1R8R,7R,9R1B", "6,8,7,2,1,5,4,10,3,9")
test("3G8B2B8G,7B7R5G,4B9G2R,4G,1G2R5R8R,1B,8R9G7G,7R6B,6B8B,3G3R,3R2R", "2,7,1,3,9,5,8,10,11,4,6")
test("5G3B,4B3G,7G8R2B7R,6G,1G,1B,1R9R2R7R,3R4G1R,4B3G2G8G,3B,2B1G,7R", "3,7,9,8,1,4,2,12,10,11,5,6")
test("4B2B5G1G,2G2B3R,7G4B9R9G,7R9G,5B,5G3G,7R5R,4B,6G3R4G3G,3R9G,8R9G4R,2R", "3,11,4,7,10,9,1,5,6,2,8,12")
test("8R,9R,5R,4G,3G2G1R,5G,4G5G,2G,6G6B1G,8R2G6B2G,1G5B8B,1G,7R", "10,9,2,1,11,13,3,7,6,4,5,8,12")
test("2R,4G7G,4R,1G1R7G,5B6G,2G,4B9R,7R2B7R4B,3B1G5G,8B9R,6B1G6R,1R2R,9G2B2R,4R9B", "10,11,14,8,13,5,7,2,3,4,9,1,12,6")
test("6R3R3B,1R,7R4B4G,7G9B,4G6G8B,4R7R4B,5R3R,3R,5B2G4R,1B,5B,9B2R,5G4R,6R3R3G", "4,3,5,12,9,14,6,11,1,13,7,8,2,10")
test("5B7B,8G,7G,6R9B3B,2B,3G3B8R7B,7R7G6R,4B6B5G,4R4G9R,4B7G6G5G,3B8B1B1G,5G7G2R,1B,2G6G5B3G,4R4B8B", "7,6,9,10,4,12,14,2,8,15,3,1,11,5,13")
test("8B9B,6R3B2G,5B,6R2R5R,3R1B,1R1R1B9B,4R4B9G9G,8R2B,6B,1B,2R,4B,6G7R,7G,3G2G7R,7B7G8G", "7,16,1,2,13,6,15,8,9,14,3,4,12,5,11,10")
test("8R,4G,8G5G,7G1R1R7R,6R,2G3B5B,7G3R1B,4B9G9G,5R4G5R7B,8B9B1B4G,5G9R1R,8B,7G1B,9B3R,2R9G,6G5G", "9,11,4,8,1,3,7,10,15,5,14,16,6,13,2,12")
test("1G3B8G,8R6B9B9B,7B,7R3B5G1B,3G,7B8G9B,2B2G6B6B,2B7G9R1B,2G8B6R8R,3R,9B3G5R2G,5B2R3R5B,8B4B,4G1R,2B,8R1B7B4R,9B", "6,2,9,11,8,1,4,7,16,12,17,13,14,3,5,10,15")
test("2B,6G1G6G4R,3B8B3B,9G,1R,3B,7R9G2R,6R1G4G6B,3B5G8G,8G1G3B4R,4G8G,6G2B5B,4G2G5R,1B,4G6G,3G1R9G,8B5R4B7R,4R3B", "7,8,17,10,2,9,13,16,18,4,12,11,15,3,6,1,5,14")
test("9G,6B3G1B4B,4B3R2R5G,2R1G,6B6R8B1R,4R3R1R,9R,8R8B,4G,3G9B,6G8B2R,5R8R6G,5B1B7B4B,2R3G1G3B,3R5B4R,8G5G,5G2B2R,8G", "5,11,12,3,8,15,10,16,1,7,2,13,6,18,14,17,4,9")
test("7B9R,2B3R1R2R,2G6B3G,6R,8G,6B7B6R,1R1G5B6G,9G,2R6G,7B6B9G5R,5G4G1B7B,4B9R2B5G,2G8B9G9G,8G3B5R,3G,2R,3R2B9B,8B3B,1R", "10,13,14,6,12,1,11,17,8,7,2,3,5,9,4,18,16,15,19")
test("8G6B9G,8G,7B4G2G6G,3B8R2R,4R1R3R8B,3G,2R1R,1R9G2B1G,4G8G,8B8B2R8R,2R1R1G,4B2B6R4B,6G9G3G6R,9B6B8R,9R7B,3G,5B4B,4B4G6B8B,4B5G,8B2R", "1,14,10,13,15,18,3,5,8,9,12,2,20,4,19,17,11,7,6,16")
puts $fail

ひさびさRubyでtapしてたら諸行無常を感じた

ここ2, 3ヶ月はGolangとjsとCrystalばっかりさわってて最近Ruby全然書いてなかったのですが一昨日からまた書いてます。

Rubyには『メソッド内の最後の評価値が戻り値になる」という言語仕様がありますね*1

そうするとFactoryっぽいものを書いてると、まあ例えばこうなるわけです。

module HogeFactory
  def create(opt)
    h = Hoge.new
    h.fuga = opt[:fuga]
    h.piyo = poyo(opt[:piyo])
    h
  end
end

んで、この最後のhがなんとなく落ち着かなさを演出するので、例えばこんな感じにできます。 参考: [初心者向け] RubyやRailsでリファクタリングに使えそうなイディオムとか便利メソッドとか - Qiita

module HogeFactory
  def create(opt)
    Hoge.new.tap do |e|
      e.fuga = opt[:fuga]
      e.piyo = poyo(opt[:piyo])
    end
  end
end

うん、最後の戻り値のhがなくなってすっきりしましたね。

いま書いてるのってyamlをパースして各クラスにマップするところなんでこういうコードが死ぬほど出てくるんですよね。
そんでずっとdefしてnewしてtapしてendするコードを書いてて思ったんだけど、これほんとにキレイなんすかね……。
賽の河原の地獄なんじゃないかと思ったけど……


というのが昨日の感想で、今朝ブログにまとめてるときに「どっちがマシかといったら下」なんだから別にtapに問題があるわけじゃないなって思い直しました。
我々に本当に必要なのはyaml to objectなmapperなのだなあ。

*1:これは正確な表現なんだろか。「最後の式の評価値が戻り値になる」と書きかけてなんとなくいまの表現にしている

Shippableでまた落とし穴にハマった話 PR作成編

この前こういう記事書いたらそこそこ伸びたようでありがとうございます。
ウチでも使ってるよ的な話は聞けてないので、使ってる方は是非なにか書いてもらえると嬉しいです。

hkdnet.hatenablog.com

さて、また落とし穴にハマったので日記です。今回のは僕はびっくりしたけどそうでもない人もいるかも。

Pull Request と 環境変数$BRANCH

PR作成したときにハマった落とし穴の話です。
コンボが決まると意図しないデプロイが走ったりすると思うんでお気をつけ下さい。

Pull Request

ShippableではpushをフックしてCIが走ります。まあいわゆるCIサービスとしては普通ですね

実はそれ以外にPull Requestの作成時にもCIが走ります。
なのでPR作成直後は必ずそのPRはCIステータスが黄色(Running)になります。CIが通るまで待ちましょう

僕はCircleCIとShippableくらいしか使ったことがないんですがPR作成時にCIが走るのって一般的なんでしょうか?

環境変数$BRANCH

ShippableにおいてShippable側が用意してくれている環境変数が多数あります。
前回の記事で紹介した $SHIPPABLE_CONTAINER_NAME$SHIPPABLE_BUILD_DIR などもそうです。
そういったCI環境の設定値以外に、メタなデータとしてブランチ名がはいっている $BRANCH やビルド番号がはいっている$BUILD_NUMBER などもあります。

topic-aブランチでCIをまわしたときは $BRANCH は何がはいるでしょうか。当然 "topic-a" ですよね。

では上述のPullRequest作成時を考えます。topic-aからmasterブランチにPRを作ったときは$BRANCHは何になりますか。
これは "master" になります。

ふーん……

なんでだよ!!!

いやまあ確かにPR作成時に走るCIなので最終コミットと全く同じ状態でやってもしょうがないんですけど……。

これを踏まえて、「masterの最新の状態をlatestタグをつけてdocker pushしたい」というのはこんな感じになります。*1
条件の前半を省略するとまだmasterにマージされてないPRの段階でlatestをpushしちゃうので気をつけてください。

if [ "$IS_PULL_REQUEST" = "false" ] && [ "$BRANCH" = "master" ]; then
  docker tag さっきビルドしたやつ hkdnet/awesome_image:master;
  docker push hkdnet/awesome_image:master;
fi;

最初は混乱したこの2つの仕様ですが、以下のように考えたらなんとなく納得できるようになりました。

PR作成 → ということは動作確認の機会がある → これからマージされるやつと同じ環境変数でビルドしたイメージを作りたい

逆に "$IS_PULL_REQUEST" = "true" なら動作確認環境へデプロイみたいな感じのことをするとステキかもしれませんね。

*1:そらで書いてるんで違ったらごめんなさい

Dockerを使っているプロジェクトのCI環境としてShippableを使ってる話

まえがき

みなさん突然ですけどDockerつかってます?
この前とある勉強会でそういう話題になったとき、ぶっちゃけ本番では全然使ってないって感じの反応が多くてそんなもんなのかなあと思ってしょんぼりしてます。

いま僕は開発環境も本番環境もDockerなプロジェクトをやってるんですけど、Docker化するとそれはそれでいろいろ面倒なことがあってまあ面倒です。はい。
この記事はそんな面倒なDockerの中でもCIとCDのところでShippableを使ってみているのでそれについて書きます。

本文

サービスについて

app.shippable.com

CIサービス業界での立ち位置

公式サイトにもおもいっきり with Docker って書いてありますね。CIサービスでDockerをウリにしてるのはShippableとCodeShipの2つという印象です。*1
CodeShipはサイトにホワイトペーパー*2があったり、Dockerの何か使うには別途申込が必要だったり、登録後のメールがちょっとめんどくさかったりという理由でいったん放置しています。

特に優れているなあと思う点は、Dockerのキャッシュが残るところでしょうか。
別プロジェクトで使っているCircleCIではbuild + pushで15分くらい待たされるのでなかなかつらいところがありますが、いまShippableを使っていると、pullしないで済んだなーとかbuildここまで使いまわせてるなーなどが確認できて嬉しいです。

あとマスコットがgopherくんの次にかわいいです。

価格

プライベートレポジトリでの利用を含め基本的に無料でいけます。
「並列実行をさせたい!」とか「複数箇所にデプロイしたい!」とかでお金がかかるように見えます。*3

Docker registryにpushするだけなら特にお金がかからないので今ところはタダで問題なく使えています。
複数プロジェクトでガンガンビルドするようになったら並列実行でお金が必要になるかも。

サポート + 情報量

公式のドキュメント(英語)とサポート用GitHubレポジトリ(英語)があります。
日本語情報はみた範囲ではなさそうでした。あってもDocker時代のではなかったので今は参考にならない気がしています。
だからいま書いてます。みんなももっと情報書いてくれー。ツッコミくれー。

GitHubのレポジトリは割と事例が多く、活発に動いているみたいです。参考になります。
いろいろ調べていく中でissue立て逃げの人が多いということも気付きました。解決したなら自分でクローズしようず……

ハマったところ

CircleCIを初めて使ったときとかはなんも気にしないでサンプルのymlをコピペしてそれっぽく直せば動いたんですけどShippableだと全然そんなことないです。

Shippableでは以下のビルドステップを順番にやっています。

  • pre_ci
  • ci
  • post_ci
  • on_success / on_failure
  • post

役割は名前通りなんですが実行されるコンテナが違います。
CIコンテナで実行されるコンテナは自分で選択できます。
そのコンテナはpost_ci, on_*ステップまで使われ続けます。

そのCIコンテナを準備するためにpre_ciステップがあります。
単なるdocker pullやoptionsだけでCIコンテナの設定が終わるなら不要ですが、ちょっと込み入ったことをしようとするとここでやることになります。 このpre_ci環境もci環境もdockerコンテナとして動いているのが特徴です。というかハマりどころです。

各ステップ内でのdockerコマンド

pre_ciステップでたちあがるコンテナもciステップで立ち上がるコンテナも /var/run/docker.sock をおおもとのマシン(以下shippable host)と共有しています。 これが何を意味するかというと、ciステップ内でdockerコマンドを実行した場合、実際にはコンテナは「ciステップのコンテナ上ではなく」「ciステップのコンテナを実行しているホスト上に」立ち上がります。

こんな感じのイメージです。

$ docker run --rm hkdnet/foo
誤
|   hkdnet/foo   |
| ---------------|
|  ci container  |
| ---------------|
| shippable host |

正
|  ci container  |   hkdnet/foo   |
| ---------------+----------------|
|          shippable host         |

ホストがci containerでないと何が困るのでしょうか。

volume問題

課題

volumeの指定方法は --volume host_path:container_path です。host_pathなので、ciコンテナ内のパスを指定してもダメです。

この仕様は、こういうストーリーのときに困ります(というか実際これで困った)

  1. CI対象のレポジトリをとってくる
  2. docker image化してあるツールとCI対象のレポジトリのファイルをつかって設定ファイルを生成する
  3. 設定ファイルをもとにdocker buildする
  4. buildしたimageでdocker runしてテストする

ここで2のときにdocker imageに対してレポジトリをマウントする必要がでてきます。
でも CI対象のレポジトリはci container内にしかなくshippable hostにない のでマウントできません。
こまった。

解決策

ci containerのrunオプションは pre_ci_boot で指定できます。
なのでci containerの特定のディレクトリをDataVolume化し、DataVolumeをマウントすればOKです。

build:
    pre_ci_boot:
        # ci containerの/dataDirをDataVolume化
        options: -v /dataDir 
    post_ci:
      - cp $SHIPPABLE_BUILD_DIR/settings.yml /dataDir/settings.yml
      # --volumes-fromでci containerを指定
      # ci containerは$SHIPPABLE_CONTAINER_NAMEという名前で動いている
      - docker run --volumes-from $SHIPPABLE_CONTAINER_NAME mycontainer

detachedで残る

CI環境なんだから毎回cleanになってるでしょーって思ったらshippable hostの部分は全然クリーンになってません。
まあキャッシュ残したいんだから仕方ない感じはあります。はい。
で、これでどういうときに困るかっていうとdetachedで実行したコンテナが次回も残り続けます。
そんでportとかのリソース競合起こしたりします。こまった。

on_successかon_failureでstopするか、実行前にstopするか、まあ何かしらの対応が必要でしょう。
テスト実行自体はdocker-composeしているので2つ目のプロジェクトを作った後にようやく気付きました。

プライベートレポジトリ

これよくわかってないのですが、ssh-keyのintegrationを通して/tmp/ssh/hogeに置いた鍵をssh-agent使いつつ bash -e 'git clone private-repogitory' 的なことをやっています。
正しいやりかたわかんないのでだれか助けて。

環境変数

globalとそうでないのがあるのでくせ者。
そもそもShippableでは環境変数毎に並列にテスト実行ができるのだけど、globalと明示していない環境変数はそれぞれのテスト実行時の値になってしまう。

env:
  - FOO=FOO
  - BAR=BAR 
  # -> FOO=FOOのときとBAR=BARのときの2回走る。FOO=FOOかつBAR=BARでは走らない

# globalならFOO=FOOかつBAR=BARにできる
env:
  global:
    - FOO=FOO
    - BAR=BAR
# あるいは1行に書けばよいみたい(試したことない
env:
  - FOO=FOO BAR=BAR

あとtokenとかを環境変数にいれるときはブラウザ側からencryptする必要があるのだけど、そのときVALUEを " でくくらないとエラーになるので注意。

# NG
FOO=FOO BAR=BAR
# OK
FOO="FOO" BAR="BAR"

参考


だらだらした記事になってしまった。
紹介部分とハマりどころ部分は別記事にしたほうがよかったのかもしれない。

*1:他のところでもやればできるんでしょうけど

*2:販促っぽいアレ

*3:Pricing | Shippable

pebble healthからデータを抜きたい(できてない

pebble time roundを使っているのだけど、歩数データとかが入ってるのでそれを抜きたい。
僕はWEBの人間なのでデータを抜いてなんかの形でサーバにHTTPリクエストを投げたい。

世の中そんなことを考える人はまあごまんといる……と思ってたのだけど1つしかヒットしなかった。なんでだよ……

github.com

さて、じゃあこれを使ってみるかと思ってサーバをちょっと書いたのだけどうまくいかない。

README.mdを読んでみると

  • 指定したURLにPOSTする
  • FormData形式
  • Keyは好きに決められる
  • ValueはCSV形式のがはいる

ふーんと思ってローカルでPOSTを受けるサーバを書いてみた。
そんでpebbleのhttpリクエストはたぶんスマホ経由で接続しにいくと思ったので、じゃあ家のLANに入っておけばOKかなと思って設定した。

でも実際にやってみるとこんな状態になっている。

  • iPhoneSafariからmacのport8080 ← GET/POST OK
  • pebbleのhttpリクエスト送るアプリからmacのport8080 ← GET/POST OK
  • pebble-health-exportの実行 ← そもそもリクエストがこない

なんでだろう……
というのでここ3日くらいずっと悩んでいる。

Homebrewでbrew updateしたら/usr/local/Library/ENV/scm/git: No such file or directoryと言われてしまう

事象

めっちゃ同じのが出る。

$ brew update
/usr/local/Library/brew.sh: line 32: /usr/local/Library/ENV/scm/git: No such file or directory
:

対応

brew prune でよい

$ brew prune
Pruned 0 symbolic links and 6 directories from /usr/local

$ brew update
:

これ同僚に教えてもらったんだけど、brew pruneってなんだろって思って調べたのでメモ

$ brew prune --help
brew prune [--dry-run]:
    Remove dead symlinks from the Homebrew prefix. This is generally not
    needed, but can be useful when doing DIY installations. Also remove broken
    app symlinks from /Applications and ~/Applications that were previously
    created by brew linkapps.

    If --dry-run or -n is passed, show what would be removed, but do not
    actually remove anything.

なんかまあ無効なシンボリックリンクどうにかしてくれるらしい。
普通はいらないと言われてるがなんでこれ出たのか不明。
DIY installationsとかかいてあるからtapしてるところとかになんか問題があったのかしら……。

prune自体はミキプルーンのプルーンと同じで、動詞だと「(不要な枝など)を切り取る」って感じっぽい。 gitでも、リモートレポジトリの削除されたブランチをローカル側のリモートブランチに反映させるコマンドがあるのでまあそんな感じなんだろう。

$ git fetch --prune