RubyVM::AbstractSyntaxTree で Node を辿る処理をシンプルに実装する

RubyEnumerator を使うと簡単にRubyのコードを処理できて便利だったので、ブログに書いておく。

ast = RubyVM::AbstractSyntaxTree.parse(<<~RUBY)
  class User < ApplicationRecord
    def say(text)
      puts text
    end

    def sum(x, y)
      x + y
    end
  end
RUBY

enum = Enumerator.new { |y|
  nodes = ast.children
  until nodes.empty?
    nodes.select! { _1.is_a?(RubyVM::AbstractSyntaxTree::Node) }
    nodes.each { y << _1 }
    nodes = nodes.flat_map(&:children)
  end
}

method_names = enum.select { _1.type == :DEFN }.map { _1.children[0] }
pp method_names
#=> [:say, :sum]

【追記】Rails v7.1.0 で `can't be blank` が `can’t be blank` に変わる(リバートされました)

既存アプリやライブラリへの影響が大きく、この変更に対してネガティブなフィードバックも多かったためリバートされました。 github.com


概要

表題の通り、Rails v7.1.0 で APOSTROPHE (U+0027) が SINGLE QUOTATION MARK (U+2019) に変わります。

github.com

既存のRailsアプリをアップグレードする際に影響が大きそうなので、記事を書きました。

影響範囲

テストでエラーメッセージを検証していた場合、Rails v7.1.0 のアップグレードによって検証に失敗するようになります。

Expected: "can't be blank"
  Actual: "can’t be blank"

今回の変更を知らない場合、このテストのエラーメッセージだけで ' と ’ の違いを見分けるのは厳しそう。

SINGLE QUOTATION MARK (U+2019) の入力方法

日本語入力モードで「Shift + 7」を押すと入力できました。*1

変更が戻る可能性

マージされるときも「I'll merge this one and see how people will react to them during the pre-releases.」と書いてあり、マージ後にrails/rails#45463でいくつかコメントが挙がっているので、もしかすると変更がリバートされる可能性はあります。

気になる方やこの変更に対して懸念がある方は v7.1.0.rc が出る前にGitHubで絵文字リアクションやコメントを投稿しておくと良さそうです。

*1:私はJIS配列キーボード使ってるので、英字キーボードだと入力方法が異なるかもしれない

プルリクで更新されたgemの一覧を出力する

Gemfile.lock のdiffから更新されたgemを探す方法を調べたのでメモ

動作確認に使ったプルリク

github.com

GitHub APIでプルリクの情報を取得する

$ gh api '/repos/rails/rails/pulls/48955' --jq '.base.sha, .head.sha'
1e824aa8e0655b4717cf612e31010bdd808f4fdf
2d61a206f445ccd52645c8d47ce97de39a39ea9d

GitHub APIで変更前後のGemfileを取得する

$ gh api '/repos/rails/rails/contents/Gemfile.lock?ref=1e824aa8e0655b4717cf612e31010bdd808f4fdf' --jq '.content' | base64 --decode > Gemfile.lock.before
$ gh api '/repos/rails/rails/contents/Gemfile.lock?ref=2d61a206f445ccd52645c8d47ce97de39a39ea9d' --jq '.content' | base64 --decode > Gemfile.lock.after

Gemfile.lock の差分を調べる

$ BUNDLE_GEMFILE=Gemfile.lock.before ruby -rbundler -e \
  'puts ["Gemfile.lock.after", "Gemfile.lock.before"].map { Bundler::LockfileParser.new(File.read(_1)).specs }.inject(:-)'
nio4r (2.5.9)
puma (6.3.0)

Gemfileがないディレクトリだと Could not locate Gemfile or .bundle/ directory のエラーが出たので、 BUNDLE_GEMFILE を指定することでエラーを回避している。

Rubyやgit-gsubを使って正規表現で複数行を置換する part2

sinsoku.hatenablog.com

の続き。

回答 3

Ruby-0777 を指定するとファイル単位で読み込む方法を教えて頂きました。

$ cat foo.txt
foo

$ cat bar.txt
bar

$ cat buz.txt
foo
bar
buz

$ ruby -i -n0777 -e 'puts "hi, " + $_' foo.txt bar.txt buz.txt

$ cat foo.txt
hi, foo

$ cat bar.txt
hi, bar

$ cat buz.txt
hi, foo
bar
buz

詳細

-0777 のオプションは初めて知ったけど、 man ruby に記載されていました。

-0[octal]. (The digit “zero”.) Specifies the input record separator ($/) as an octal number. If no digit is given, the null character is taken as the separator. Other switches may follow the digits. -00 turns Ruby into paragraph mode. -0777 makes Ruby read whole file at once as a single string since there is no legal character with that value.

まとめ

複数のファイルを一括で編集するときに -0777 は覚えておくと便利そう。

Rubyやgit-gsubを使って正規表現で複数行を置換する

150個を超える *.tf をまとめて編集する方法を調べたので、備忘録としてまとめておく。

経緯

やりたかったこと

以下のようなコードが大量にあるとき、全てのファイルから指定のチームidを持つ team { ... } を削除したい。

resource "github_repository_collaborators" "foo" {
  repository = github_repository.foo.name
  team {
    permission = "push"
    team_id    = "rubyist"
  }
  team {
    permission = "admin"
    team_id    = "ruby-committers"
  }
}

ruby-jp に相談

Rubyが得意なのでRubyで置換する方法を調べていたが、うまくいかなかったので ruby-jp に相談した。

すると、何人かの方から回答を頂いた。

投稿した質問

Rubyでファイルの中身を置換する簡単な方法は何かあったりしないでしょうか?
ruby -e 'File.read("foo.txt").gsub(/def/, "!!").then { File.write("foo.txt", _1) }' みたいな処理を簡単に書けないものかなと。

回答1

Ruby-i -p を使う方法を教えて頂きました。

$ ruby -i -p -e '$_.gsub!(/def/, "!!")' foo.txt

これらのオプションは初めて知って勉強になった。

-i[extension] edit ARGV files in place (make backup if extension supplied)

-p assume loop like -n but print line also like sed

あと、 $_ も初めてみた。

docs.ruby-lang.org

ただ、これだと team { ... } のような複数行を処理できないため、別の方法が必要になった。

回答1-a

回答1 を参考に -i を使いつつ、複数行を取得して正規表現で置換することで目的は達成できた。

$ ruby -i -e 'puts readlines.join.gsub(/  team {[\s\w="\.\[\]]+(ruby-committers)[\s\w="\.\[\]]+}\n/, "")'

回答2

Ruby ではないですが、git-gsub を使う方法も教えて頂きました。

$ git gsub "(?s)team {[^{]+?ruby-committers.+?}" ""

正規表現もシンプルですし、 git-gsub は普段使いしやすそうなので、これも知ることができて勉強になった。

まとめ

ruby-jp に助けられたし、勉強になった。ありがとうございます。

手元にあるパッチの一覧を確認するシェルスクリプト

ブログに書いていなかったことに気づいたので、残しておく。

モチベーション

OSSや仕事のコードを修正してるときに「まだ動作確認ができていない」「コミットメッセージの説明が不十分」なコミットが手元に溜まることがある。

このパッチの一覧を確認するため、シェルスクリプトを使っている。

シェルスクリプト

このファイルを PATH の通ったディレクトリに git-patch というファイル名で置く。

$ cat << 'EOF' > ~/bin/git-patch
#!/bin/bash

ls ~/ghq/github.com/*/*/.git/refs/heads/patch/*
grep refs/heads/patch ~/ghq/github.com/*/*/.git/info/refs
EOF

$ chmod +x ~/bin/git-patch

Gitは git-*** という命名規則の実行可能ファイルをサブコマンドで実行できるので、以下のように使用できる。

$ git patch

このスクリプトを実行すると patch/ で始まるブランチの一覧を確認できる。

Railsの DATABASE_URL で指定できるパスワードの文字種

Railsアプリケーションを動かすAWSリソースをTerraformで作る場合、random_password を使ってDBパスワードを生成します。
しかし、 DBパスワードに一部の記号が含まれていると環境変数 DATABASE_URL で渡す際に URI::InvalidURIError が発生してしまうため、使える文字種を調べた。

調査

URI.parse を使ってるのは分かったので、実際に解析して調べた。

require 'uri'

specials = '!@#$%&*()-_=+[]{}<>:?'.chars
check = -> { URI.parse("postgres://myuser:#{_1}@localhost/somedatabase").password rescue nil }

specials.select(&check).join
#=> "!$&*()-_=+:"
specials.reject(&check).join
#=> "@#%[]{}<>?"

結果

使える文字種は "!$&*()-_=+:" なので、DBパスワードを生成するときの定義は以下の通り。

resource "random_password" "password" {
  special          = true
  override_special = "!$&*()-_=+:"
}

*1:他の箇所に記載あったかもしれないが、詳細は調べてない