おでーぶでおでーぶ

いろいろ書く。いろいろ。

フィルターコマンドと組み合わせると幸せになれるコマンド集 その1

fzy, fzf, peco などなど様々なフィルターコマンドがこの世にはあるけれど、結局それで何をすれば DX があがるのか・・・と言われたので、よく使うやつを羅列しました。とりあえずその1ということで。

GitHub - jhawthorn/fzy: A better fuzzy finder

GitHub - junegunn/fzf: A command-line fuzzy finder

GitHub - peco/peco: Simplistic interactive filtering tool

動かしてる環境は zsh 、フィルターは fzf です。適宜変えてください。*1

コマンド履歴を見る

$ alias hist="fzf < ~/.zsh_history"

ghqディレクトリに移動する

$ alias cg='cd $(ghq root)/$(ghq list|fzf)'

git checkout <local> を簡単に

$ git config --global alias.co '!git checkout $(git branch | fzf)'
$ git co

git checkout -b <branch_name> <remote> を簡単に

$ git config --global alias.cor '!f() { local -r ref=$(git branch -r | fzf); git checkout -b "${1:-${ref#*/}}" $ref; }; f'
$ git cor
$ git cor <any_branch_name>

git <command> <file>... を簡単に

$ git config --global alias.staged 'diff --name-only --staged'
$ git config --global alias.modified 'diff --name-only'
$ git config --global alias.untracked 'ls-files --others --exclude-standard'
$ 
$ git config --global alias.add-m '!git add -- $(git modified | fzf -m)'
$ git config --global alias.add-u '!git add -- $(git untracked | fzf -m)'
$ git config --global alias.reset-s '!git reset -- $(git staged | fzf -m)'

特定の拡張子のファイルを探索したい

# ~/.zshrc

find_xxx_seed() {
  local -r ext="$1"
  echo "alias find_${ext}='find . -name \"*.${ext}\"|fzf -m'"
}

eval $(find_xxx_seed "apk")
eval $(find_xxx_seed "ipa")

# find_apk, find_ipa が有効になる

現在のワーキングディレクトリ化にある特定のディレクトリにいきたい

$ alias dive='cd $(find . -type d | fzf)'

他の用途には enhancd を使っています。また上の alias で「あ、この下にないんだった」と思ってキャンセルがてらCtrl+Cを押すと cd 無引数扱いになって履歴に移れるので重宝してます。(enhancd を使ってる場合に限る)

$ alias dive='f() { local -r go_to="$(find . -type d | fzf)"; if [[ -n "$go_to" ]]; then cd "$go_to"; fi; }; f'

Ctrl-C をしたら何もしてほしくないときは多分こう。

*1:ファイル数が多すぎるとパフォーマンスに支障が出ます。

git git status がつらいと聞いて

最近某社の人と、「そういうコマンドあったんだ」みたいな話ってあんまり外に出てこないし、無駄なことしてる可能性あるよね(意訳)みたいな話をした。

ということでそういうコマンドをどんどん書いていきたい。

1個目は git git 問題。

git まで打って、少し考えて git git status と走らせたことはないだろうか。僕はある。あれは悲しい。

drop-git() {
  if let "$# > 0"; then
    while [[ "$1" == "git" ]]; do
        shift 1
    done

    if type command >/dev/null 2>&1; then
      command git "$@"
    else
      git "$@"
    fi
  else
    if type command >/dev/null 2>&1; then
      command git
    else
      git
    fi
  fi
}

alias git="drop-git"

ということでこういう alias を張って凌いでいる。

コマンドのマニュアル・サンプルで使う記法について

よくコマンドの実行例で以下のような記法を見ることがあると思う。

$ git add <file>...

これは man で使ってる記法を元にしてることが多い。ただ「コマンドのサンプル(マニュアル)はこう書け」という厳密な構文が存在するわけではなく、MicrosoftIBM始め、ある程度の共通項持った様々な亜種を定義している。

表現記法

ぱっと目に着くものを試しに書き下してみると、

用途 記法 その他備考
foo は変数 foo or <foo> or foo or FOO 斜体にならず、_foo_ と表示される端末もある(ミスなのかは知らない)
foo は literal foo これは太字
foo または bar foo|bar
foo が optional [foo]
-f foo-b をグルーピング(1つも選ばなくてもよい) [-f foo|-b] 任意用の角括弧はグルーピングにも使える
-f foo-b をグルーピング(最低でも1つ選ぶ必要がある) (-f foo|-b) or {-f foo|-b}
foo を繰り返し指定できる foo...
最低でも1つの foo が必要 foo foo...
複数の変数を指定する foo bar baz 上記繰り返しでは表現できない場合のみ
空白を含めた foo bar を1つの引数とする "foo bar" シェルのオプションパース仕様に依存しているところが強い

これらの表現は合成・入れ子が可能とはいえ、1行で書くには限界がある。そういう場合は複数行に分けることが多い。例えば下記のコマンドのような表記の場合、

$ foo bar ([-a] <baz> | -b)

2行に分解し、排他であることを明確に示す。

$ foo bar [-a] <baz>
$ foo bar -b

共通表現

厳密な構文が存在しないとは言っても、広く用いられている共通の表現がある。

用途 記法
foo は変数 foo or <foo> or foo or FOO
foo または bar foo|bar
foo が optional [foo]
-f foo-b をグルーピング(1つも選ばなくてもよい) [-f foo|-b]
-f foo-b をグルーピング(最低でも1つ選ぶ必要がある) (-f foo|-b) or {-f foo|-b}
foo を繰り返し指定できる foo...

<foo> を optional の意味で使ったり、[foo] を変数の意味でとして使ったり、そういうことをしている明記されたルールは見たことがない。*1

正直ここらへんの規則すら破ってるサンプル表記は本当にやめて欲しいな、と思ったりする。*2

$ foo [bar]

で、実は bar が必須とか。それでいて bar は実は変数だとか。普通に実行出来る気がしない。*3

語弊が少なそうだなって思う記法の組み合わせ

僕の思う最強の(ry みたいな。

用途 記法
foo は変数 FOO or <foo>
foo は literal foo
foo または bar foo|bar
foo が optional [foo]
-f foo-b をグルーピング(1つも選ばなくてもよい) [-f foo|-b]
-f foo-b をグルーピング(最低でも1つ選ぶ必要がある) (-f foo|-b)
foo を繰り返し指定できる foo...
  • 斜体や太字 -> 見づらい*4
  • {} -> 正規表現のせいか () の方がグルーピングっぽく、{} にグルーピング感を感じない*5

RFCとかで制定されて欲しい今日この頃。

参考

*1:個人の感想です

*2:個人の感想です

*3:というか出来なかった。この記事を書いてる理由はまさにこれで、サンプルを見ても全く分からずソースコードを読むことになった。

*4:個人の感想です

*5:個人の感想です

Kotlin DSL を考慮した Gradle Plugin を記述するために必要だったこと

とある Gradle Plugin を 2.0.0 に移行する際、v1 から Kotlin DSL を使っていた人の環境でちょっと問題が発生したというツイートを見たので、Kotlin DSL がどうやって DSL Marker なしに lambda で書けるようにしてるのかちょっと調べてみた。ここで記述している問題は 2.0.1 では修正されていて、また Kotlin DSL での移行ステップも README に追記しておきました。

github.com

TL;DR

  • 外に見せる境界で def を使うのは避けておいた方が無難
  • Kotlin DSL は拡張関数で delegate してて、見るべきメソッドが違うかもしれないから気をつけよう
  • Kotlin と Gradle の言語仕様の違いに気をつけよう
  • kotlin-dsl を apply して開発しないと Groovy と Kotlin DSL で同じような Closure (Lambda) の記法に出来ない(多分) (2/25 追記: DomainObjectでない場合)

lambda と Closure の型変換

v1 では動いていた以下の記述が 2.0.0 だと動かず、 2.0.0 から導入された deployments であれば動くという問題が発生していた。

deploygate {
  apks { ... }
}

これは下記の通りに書き換えると apks 記述でも動く。

deploygate {
  apks(closureOf<NamedDomainObjectContainer<NamedDeployment>> { ... }
}

def apks(Closure) を呼び出そうとするが、渡している lambda と Closure の間で型変換が不可能であるので発生していた。これはclosureOf を使い、型情報も明示的に提供することで解決できる。が、普通に lambda で書きたいですよね。

lambda での Closure と見かけ上同等の構文の実現

じゃあそもそも v1 では def apks(Closure) と書いてなかったのか?というと、ちゃんと(?) def apks(Closure) としていた。実は Kotlin DSL で closureOf などを使わずに lambda で書けている場合、この Closure 関数が直接呼ばれているわけではない。

Groovy では apks { ... }def apks(Closure) で動くし、この Closure を引数に取る関数が実際に呼ばれているんだけど、Kotlin DSL において apks { ... }getApks().invoke(...) の糖衣構文となっており、これは Kotlin DSL が定義する拡張関数によって実現されていた。

github.com

ということで差異は getApks().invoke(...) の糖衣構文であるところに由来していた。v1 では getApks() に NamedDomainObjectContainer がちゃんと指定されていたんだけど、2.0.0 ではリファクタリングの過程で def を使って動的解決に変更してしまったことが原因だった。 そうすると Object 型として認識されてしまう。そして最終的に Kotlin は「def apks(Closure) を呼ぼうとしてるけど型が一致してないよ」とエラーを吐くという感じ*1。 ref: v1 / v2

ということで、

  • apks(Closure) は Groovy で apks { ... } と書く用途
  • getApks(): <concrete type> は Kotlin DSLapks { ... } と書く用途

として動かせることがわかったので、修正した *2

setter property アクセス

Kotlin でも Groovy でも Property アクセスがサポートされているので、configuration ではメソッド呼び出しではなく代入文の書き方で表現できる

propName = "..."

という感じなんだけど、 Kotlin と Groovy の言語仕様の関係で、以下の記述が Kotlin DSL だと v2 で動いてなかった。

deploygate {
  userName = "userName"
}

Kotlin では Setter の可視性は Getter よりも広くなければならない。つまり private getter + public setter のような property は存在せず、この場合は setFoo(...) という呼び出しを強いられる。

Groovy はそんな制約はなく、private getter + public setter のような property でも foo = ... と書ける。

今回 2.0.0 では deprecated とした設定値を全て public setter で定義し、新しい設定値の property に delegate している。その結果、v1 ではただの public property だったものが Kotlin 上では property 扱いとならなくなってしまった。なお Groovy では動くので、Groovy 用の後方互換テストケースでは気づけなかった。

これは getter を定義するだけで修正できる

lambda や Closure を受け取りたい block (not DomainObject)

2022/10/25 追記。実行側のGradle バージョン依存の動作差異(Kotlin 1.3 compatibility)でした

github.com

2.0.0 では配布ページ(英名: Distribution) の設定について、distribution ブロックを用意して切り出した。

deployments {
  create("debug") {
    distribution {
      ...
    }
  }
}

これは Kotlin DSL だと最初の項 lambda と Closure の型変換 と同じ理由で、そのままでは動かない。この distribution は NamedDomainObjectContainer ではないので、invoke による補助が使えない。下記のように closureOf + 型指定でも動くけど、型を明示的に書いてもらうと今後のアップデートに支障が出るので正直避けたい。

deployments {
   create("debug") {
    distribution(closureOf<Distribution> {
      ...
    })
  }
}

さて、Kotlin DSL で推奨されている org.gradle.api.Action にすれば動くんじゃない?って思ったけど今度は Groovy 側で落ちるようになった。caller (owner) が Distribution lambda の this になってしまって見つからないらしい。

  • Marks a SAM interface as a target for lambda expressions / closures
  • where the single parameter is passed as the implicit receiver of the
  • invocation ({@code this} in Kotlin, {@code delegate} in Groovy) as if
  • the lambda expression was an extension method of the parameter type.

ドキュメントを見た感じ、delgate に差し込んでくれるんじゃないの?と思って悩んでたんだけど、そもそも Gradle Plugin 開発時に kotlin-dsl plugin (というか Kotlin + SAML w/ receiver compiler plugin) を当てないとここらへんの機能提供ができないっぽい。なんとなく Kotlin で Gradle 設定を書く方をKotlin DSLと呼ぶ印象があって、Plugin 開発側では何も考えていなかった。

試そうと思って Kotlin DSL を入れたら Kotlin コードから Groovy のコードが見れず、凄い長い戦いになりそうだったのでやめました。詳しい人教えてください。

蛇足

ここに Kotlin DSLDSL として動くように拡張関数が定義されており、かなり楽しい。 うーん、Configurable#invoke 生やしてくれないかなー

https://github.com/gradle/kotlin-dsl/tree/master/subprojects/provider/src/main/kotlin/org/gradle/kotlin/dslgithub.com

Action がなんで this を解決できるかについては SAM-with-receiver Kotlin compiler plugin を調べればOK。 kotlin/build.gradle.kts at master · JetBrains/kotlin · GitHub

そういえばGroovy -> Kotlin DSL への書き換えについては DroidKaigi 2019 で tnj という人が書き換えについて発表をしています。

スライド中で触れられている「Gradle のファイルっていつの間にか増えるよね」で増やした張本人は僕です。

speakerdeck.com

www.youtube.com

*1:悲しい?嬉しい? ことに IDE では型推論が動いてくれる

*2:ちなみに deployments は型を指定していた。なんでか覚えてないけど・・・

DroidKaigi 2019 スタッフ業

今年もスタッフでした。昨年の致命傷はこちら。

jmatsu.hatenablog.com

今回は

  • 採択後のスピーカーとの各種やり取り
  • タイムテーブル周り
  • アプリなどで使うAPIの作成
  • 公式アプリのメンテナー(特にCI)
  • 前日設営配送周り
  • Day1 のホール周りの下拵え(圧倒的 keima さんと hidey さんによりホール設営自体は全てやってもらいました。感謝。)
  • パーティー前移動作戦本部
  • セッション部屋リーダーの補佐
  • ほんのちょっとだけセッション部屋の司会
  • 遊撃手
  • ネットワーク(全く稼働できず)

今年は例年の致命傷を避けようとして結果即死した感じです。めっちゃミスしました。本当にすみません。

本業は同僚全員凄く理解のある人たちで助かりました(去年もですよ)。本当にありがとうございます。ただレビューや問い合わせなど自分以外の人がロックされてしまうタスクを優先しつつも、甘えすぎてもいけないので普通に開発をしたりとかなり忙しい日々でした*1

副業も最高に理解があって、いきなり無理を言って 1月中の稼働は結局0時間とさせてもらいました。本当にありがとうございます。

でも飲み会の回数は減らしませんでした。許してください。

採択後のスピーカーとの各種やり取り

今年は問い合わせのうちのいくつかを他の担当の人にディスパッチしつつ、基本的にプログラム用問い合わせMLに流れてきたものを拾って返していました。

それでも単一障害点と化したのは間違いなくて、返信が遅くなったり二度手間を取らせてしまったこともありました。不快な気持ちになってしまった方がいらっしゃいましたら、本当に申し訳ありませんでした。プログラム用のMLでもいいのでフィードバックを頂けると嬉しいです。

タイムテーブル周り

今年はタイムテーブルを @Reyurnible@satsukies に手伝ってもらいました。とても雑なお願いでシュッとやってくれてありがとう。戻ってきたときにGASでverificationが書いてあってとても感動しました。

さて、今年も「XXXとYYYを聞きたいのに被ってる〜〜〜!」という声をいただきました

わかる。頑張ってこの世に存在できるインスタンス増やしてください。

これは本質情報なのですが、8インスタンスあれば全てのセッションを聞きつつ、codelabsやスポンサー巡りができます。

Day1 ホール直後の移動問題 + Day1 パーティー前の移動問題

さて、昨年来た方は Day1 ホールセッション後や Day1 パーティー前の移動の惨事を覚えていらっしゃるかと思います。あれはひどい事件でしたね。

今回はそれぞれに対応をしています。一応うまくいったと思います!

Day1 ホール直後の移動問題

最初のウェルカムトーク、ホールAのセッションを終えたときの開放部屋調整を行っています。具体的には去年と異なりホールBを使えるようにし、5F で開放する部屋も減らしました。

さらに急遽、ホールAの最初のセッションを同時通訳にしてもらいました*2。元々同時通訳は完全に Room 3 だけの予定だったのですが、やはり最初のセッションは日英どちらの方も楽しんでいただきたく、僕の独断と偏見で同時通訳対象セッションをバチコーン!しました*3。発表されたTakuma さん、本当にありがとうございました。

droidkaigi.jp

この午前の構成ですが、僕の独断と偏見をそれっぽく解説すると

  • 今年はAACやマルチモジュールの応募が多い
  • それに伴う DI やテストの偏移についての話題が多い
  • AAC に関する知識が熟成し始めている

という背景を感じてまして、

  • DI on multi-modules
  • Build optimization
  • Navigation Component from AAC
  • AAC with testing

を主題とするセッションで構成することで、テーマ感をある程度持たせることができたのかなと思います。多分。

Day1 パーティー前の移動問題

  • Day1 最後の方のセッションを変則にし、同時に終わる部屋数を減らす
  • ホールで Fireside Chat を開き、移動を促す
  • 先んじてホールでウェルカムドリンクを配れるように業者(ビヨンドさん)の方にお願いする
  • Fireside Chat とウェルカムドリンクを踏まえたホールレイアウトを考える
  • keima 神による誘導

ということを今回は計画していました。

とはいえ僕がやったのは「関係各所の人をざっと集めまして、こんな感じの戦略で1Fに人を流していきたい、宜しくお願いします!」というざっくりとした方向性の提案だけで、実際に Fireside Chat を企画したのはcodelabs班だし、ウェルカムドリンクやバリスタさんのコントロールは全て飲食班、keimaさんはkeimaさんだし、ズラした枠にうまくセッションを当ててくれたのは僕以外のタイムテーブル班、展示ルームのコントロールはスポンサー班って感じでした*4。本当にありがとうございます。もちろん来場者の皆さんのご協力があってこそでしたので、その点も深く感謝しています!

APIや公式アプリに関して

皆さんコントリビューションありがとうございます! @takahirom さんを始めとしたコアメンバーがコントリビューションしやすいような形で整えてくれて、本当にかっこよかったです。

CIについてはどっかで発表しますが、急ぎでないときは CI やプロジェクトに依存しないように書いていたので、大部分が皆さんのプロジェクトでも動くと思います。ぜひ感想を聞かせてください。CircleCI v2.1 にするPRは後で投げます。CircleCI Orb化や Bitrise Step化も視野に入れてます。

ちなみに bot名に DroidKaigi と入れなかったことにはちゃんと理由があって、もし token を使われて罵詈雑言などを書かれた場合などにヒッジョーに面倒臭いからです。その点、僕のbotであれば(全く良くはないんですけど)まだ被害が小さいので。でもアイコンはいらすとやの最高のアイコンですよ。 (cc しほちゃん)

遊撃手

ショートではない。僕はファーストが好きです。

英語

今年も Global 班(メンションは english) という班がありまして、翻訳や proofread をお願いしてました。駄文を投げるとキレイになって返ってきて最高でした。

各セッションの英語タイトルは僕が一通り訳して、Global班に推敲してもらいました。皆さんの思惑と違うものがあったら本当に申し訳ありません。

やはり難しかったのが「投稿者がタイトルに含めたモチベーションと意味」をどれだけ維持できるかで、例えば @yanzm さんの DDD の戦術的設計 という言葉は明らかに DDD 由来なので、エヴァンス本を引っ張り出して来て訳の参考にしたりしました。他にも @shiraj_i さんの How about reviewing your Android Studio settings? は「何回かしらじさんの英語発表聞いてるけど、How about ~ って結構言うし、この言い方しそうだなー」と思いながら訳しました。

嬉しかったこと

  • 問い合わせしてくれた人たちが現地で「回答(調整)ありがとう」って言ってくれた
  • 前職(Quipper)のフィリピンの元同僚や入れ違いで入ったエンジニアが来てくれて、話す機会が取れた
  • アプリのコントリビューターから声をかけてもらえた
  • コーヒーが美味しかった
  • こそこそしてたらウェルカムトークの司会から今年は逃げられた

P.S.

いま家にニトリのポールハンガーが25個あって威圧感が凄いです。(画像のダンボールの陰に同じ量のダンボールがいます)。少しの間、食費のかからない25個と1人暮らしです。

f:id:jmatsu:20190210132451j:plain

*1:スポンサー企業の1つであり、CEO はスタッフ&スピーカー

*2:Global班、通訳の方、本当にありがとうございます

*3:ここが日本語または英語のみだと、聞けない言語の方の「次セッションに早く行きたい欲」が増強されすぎるのではという懸念もあり、同時通訳にすることで解消を図りました

*4:書きながら、自分で何もしてないことがわかってウケる

buildSrc や Gradle Plugin の開発のために Debugger を当てる

buildSrc 内のソースや Gradle Plugin の開発中でも Debugger を当てたいときはあると思います。

./gradlew --stop
./gradlew <any task> -Dorg.gradle.debug=true --no-daemon

非デーモンのデバッグモードで Gradle を立ち上げると、Debugger Attach 待機状態になる。デフォルトのポートは 5005。

IntelliJ の Run Configuration から Remote を新規作成し、デフォルトの設定で繋がる最高。

f:id:jmatsu:20190129211948j:plain

Gradle Plugin 開発の場合は大体 Project Evaluated のタイミングで終わってしまうので、Breakpoint は Debugger を Attach する前につけておきましょう。

pre-Lolipop でも TextView に icon tint を書けたい

MaterialComponent 対応をしていて、menu iconなども全部 theme baseのカラーリングにリファクタリングしている。

その中に pre-Lolipop でも TextView のicondrawable にtintが効かないというものがあったので、以下の方法で対応した。

import android.content.Context
import android.os.Build
import android.util.AttributeSet
import androidx.appcompat.widget.AppCompatTextView
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.widget.TextViewCompat
import com.google.android.material.theme.MaterialComponentsViewInflater

class LayoutInflater : MaterialComponentsViewInflater() {

    // pre-L cannot apply tint color for text view drawable
    override fun createTextView(context: Context, attrs: AttributeSet?): AppCompatTextView {
        val textView = super.createTextView(context, attrs)

        // throwable をsuppress するだけのtry-catchを別で書いている
        trySafely {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
                val drawables = textView.compoundDrawables

                if (drawables.all { it == null }) {
                    return textView
                }

                val ta = context.theme.obtainStyledAttributes(R.styleable.AppTheme)

                val color = ta.getColor(R.styleable.AppTheme_drawableIconColor, -1)

                ta.recycle()

                if (color == -1) {
                    return textView
                }

                val newDrawables = drawables.map {
                    if (it != null) {
                        val wrapped = DrawableCompat.wrap(it)

                        DrawableCompat.setTint(wrapped, color)

                        wrapped.setBounds(0, 0, wrapped.intrinsicWidth, wrapped.intrinsicHeight)
                        wrapped
                    } else {
                        null
                    }
                }

                TextViewCompat.setCompoundDrawablesRelative(
                    textView,
                    newDrawables[0],
                    newDrawables[1],
                    newDrawables[2],
                    newDrawables[3]
                )
            }
        }

        return textView
    }
}

公式がやってない辺りにちゃんと理由がありそうで、とても気になる。なんでだろう。