Hama Blog

Hama Blog

主にtech関連の記録

zsh の設定ファイルの説明

概要

macOSzsh を使っているが、.zshenv.zshrc などの違いをちゃんとわかっていなかったので(調べてもすぐに忘れてしまい、その都度ググって調べてしまうので)、簡単にまとめた内容をメモとして残しておく。

まとめ

  • zsh の設定ファイルは5種類ある。

    • .zshenv

    • .zprofile

    • .zshrc

    • .zlogin

    • .zlogout

  • ログインシェル、インタラクティブシェル、シェルスクリプト、ログアウト(ログインシェル終了時)で使うファイル、使わないファイルがある。

  • 読み込み順がある。

  • ホームディレクトリに置いておくものだと思ったが、$ZDOTDIR という環境変数とそれに対応するディレクトリを用意しておくと、そちらを読み込みにいってくれるらしい。

    $ZDOTDIR を用意するほうがホームディレクトリが多少スッキリしてよさそう。

    zsh: 5 Files

    If ZDOTDIR is unset, HOME is used instead.

  • global と local ファイルがあり、global は /etc~ に存在する。

    個人 PC だけなら、local ファイル(ホームディレクトリ、もしくは $ZDOTDIR に用意したファイル) だけを修正するのでよさそう。

    サーバーで使うときに、global のファイルを修正することがありそう。

    ログアウト以外では、global -> local の順に読み込まれるので、local が優先されることに注意。

  • 実際に各ファイルに echo を記述して確認するとわかりやすかった。

各ファイルの簡単な説明

.zshenv

ログアウト以外で読み込まれ、どれも最初に読み込まれる。

一番影響範囲が広いので、いきなりここに記述するのはよくない。

.zprofile

ログインシェルの場合だけ読み込まれる。

.zshrc

ログインシェルとインタラクティブシェルの場合だけ読み込まれる。

.zlogin

ログインシェルの場合だけ読み込まれる。

.zprofile -> .zshrc の後に読み込まれることに注意。

.zprofile.zshrc を使えばいいと思ったので、あまり使う機会はなさそう?

個人的には使ったことがない。

.zlogout

exit または logout でログインシェルを終了したときだけ読み込まれる。

個人的には使ったことがない。

zsh: 5 Files

When a login shell exits, the files $ZDOTDIR/.zlogout and then /etc/zlogout are read. This happens with either an explicit exit via the exit or logout commands, or an implicit exit by reading end-of-file from the terminal. However, if the shell terminates due to exec’ing another process, the logout files are not read.

(Google翻訳)

ログインシェルが終了すると、ファイル $ZDOTDIR/.zlogout と /etc/zlogout が読み取られます。これは、exit または logout コマンドによる明示的な終了、または端末からファイルの終わりを読み取ることによる暗黙的な終了のいずれかで発生します。ただし、別のプロセスを実行したためにシェルが終了した場合、ログアウト ファイルは読み取られません。

Railsでrenderの引数に渡したtemplateのパスの先頭に `/` があってもいける理由を調べてみた

概要

Rails の controller で別のパスにある template を呼び出したいことがたまにあり、その際は render template: "products/show" のような指定をするのだが、render template: "/products/show" のように先頭に / があってもエラーにならずにレンダリングできてしまったので、なぜいけるのか調べてみる。

記事執筆時の環境

結論

以下の path_prefix = path_prefix.from(1) if path_prefix.start_with?("/") のところで、先頭の / を消していることがわかった。

https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/actionview/lib/action_view/lookup_context.rb#L193-L199

def normalize_name(name, prefixes)
  name = name.to_s
  idx = name.rindex("/")
  return name, prefixes.presence || [""] unless idx

  path_prefix = name[0, idx]
  path_prefix = path_prefix.from(1) if path_prefix.start_with?("/")

詳しく見てみる

controller で render template: "/products/show" のような指定をするとどのように内部コードが呼び出されているのか見てみる。

まず、render メソッドは以下である。

https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/actionview/lib/action_view/renderer/template_renderer.rb#L5-L12

def render(context, options)
  @details = extract_details(options)
  template = determine_template(options)

  prepend_formats(template.format)

  render_template(context, template, options[:layout], options[:locals] || {})
end

引数の options には template: "/products/show" が入っている。

determine_template(options)#<ActionView::Template app/views/products/show.html.slim locals=[]> が返ってくることがわかった。

determine_template では、以下の部分が実行される。

https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/actionview/lib/action_view/renderer/template_renderer.rb#L45-L50

elsif options.key?(:template)
  if options[:template].respond_to?(:render)
    options[:template]
  else
    @lookup_context.find_template(options[:template], options[:prefixes], false, keys, @details)
  end

今回は template の中身が文字列なので else の @lookup_context.find_template(options[:template], options[:prefixes], false, keys, @details) が実行される。

@lookup_context.find_template は以下であるが、name, prefixes = normalize_name(name, prefixes) の左辺の prefixesproducts になっていたので、どうやら normalize_name で先頭の / が削除されているようだ。

(ちなみに、今回は render メソッドに prefix を指定していないので、右辺の prefixes にも何も入っていない)

https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/actionview/lib/action_view/lookup_context.rb#L124-L129

def find(name, prefixes = [], partial = false, keys = [], options = {})
  name, prefixes = normalize_name(name, prefixes)
  details, details_key = detail_args_for(options)
  @view_paths.find(name, prefixes, partial, details, details_key, keys)
end
alias :find_template :find

normalize_name の中身を見てみる。

上述したが、今回は prefixes は未指定であるため、name (/products/show) にだけ着目する。

https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/actionview/lib/action_view/lookup_context.rb#L193-L199

def normalize_name(name, prefixes)
  name = name.to_s
  idx = name.rindex("/")
  return name, prefixes.presence || [""] unless idx

  path_prefix = name[0, idx]
  path_prefix = path_prefix.from(1) if path_prefix.start_with?("/")

まず、idx = name.rindex("/")path_prefix = name[0, idx] により、path_prefix には /products が入る。

そして、path_prefix = path_prefix.from(1) if path_prefix.start_with?("/") により先頭の / が削除されることがわかった。

なので、render template: ~ では 先頭に / があるパスを渡しても問題ない。

DockerのRails開発環境でweb-consoleを表示する

概要

新しく Rails アプリケーションを Docker 環境で開発していたときに、web-console が表示されなくて困ったのでどうすれば表示されるか調べた。

記事執筆時の環境

  • Ruby on Rails: 7.0.4

  • Docker Desktop: 4.15.0

  • Docker Compose: 3.9

結論

config/environments/development.rb に以下を設定すればよい。

config.web_console.whitelisted_ips = '0.0.0.0/0'

詳しく見てみる

web-console は、デフォルトで 127.0.0.1 しか許可していないらしい。

https://github.com/rails/web-console/blob/167c2402aed90f4b4934b04a3ef2ed14034e9f1c/lib/web_console/permissions.rb#L7-L8

# IPv4 and IPv6 localhost should be always allowed.
ALWAYS_PERMITTED_NETWORKS = %w( 127.0.0.0/8 ::1 )

Docker 環境だと localhost でアクセスしても、Rails から見た IP アドレスは以下のように 192.168.80.1 などとなったりするみたい。

>> request.remote_ip
=> "192.168.80.1"

アプリケーション用のコンテナに入って Gateway の IP アドレスを確認。

$docker inspect --format='{{range .NetworkSettings.Networks}}{{.Gateway}}{{end}}' [CONTAINER ID]
192.168.80.1

上記の IP アドレスは コンテナを作り直すたびに変わるはずなので、開発環境では全ての IP アドレスを許可するように config/environments/development.rb に以下を設定すればよさそう。

config.web_console.whitelisted_ips = '0.0.0.0/0'

Railsのform_withのオプションでmethodを指定しない場合にHTTPメソッドがどのように決められるのか調べてみた

概要

Rails の form_with ヘルパーでは、method オプション(任意オプション)を指定すると form_with によって生成される html の form タグに対して HTTP メソッドを任意に決めることができる。

未指定の場合はデフォルトで POST になるのかと思っていた(ここに書いてあったのでそう思っていた)が、そうでもなさそうだったので未指定の場合の挙動について今回調べてみた。

追記

たしかに form タグ自体の method のデフォルトは POST になる。

記事執筆時の環境

結論

例えば、以下のように form_with を指定したとき( method オプションが未指定のとき)、@model が保存済み ( persisted? ) かどうかで HTTP メソッドが決まる。

/ erb ではなく slim で記述している
= form_with model: @model, local: true do |f|
~省略~
  • 保存済みの場合: POST

      <form action="/models/:id" accept-charset="UTF-8" method="post">
        <input type="hidden" name="authenticity_token" value="~省略~" autocomplete="off">
    
  • 未保存の場合: PATCH

    name="_method" の input タグが追加される。

      <form action="/models/:id" accept-charset="UTF-8" method="post">
        <input type="hidden" name="_method" value="patch" autocomplete="off">
        <input type="hidden" name="authenticity_token" value="~省略~" autocomplete="off">
    

詳しく見てみる

結論にも書いたが、以下のような記述で form_with を使ってみる。

(新規作成時 / 編集時 の両方の画面で使えるようなかたちにしてみた)

/ erb ではなく slim で記述している
= form_with model: @model, local: true do |f|
~省略~

ここからコードを探っていく。

まず、form_with ヘルパーは以下で定義されている。

https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/actionview/lib/action_view/helpers/form_helper.rb#L754-L777

def form_with(model: nil, scope: nil, url: nil, format: nil, **options, &block)
  options = { allow_method_names_outside_object: true, skip_default_ids: !form_with_generates_ids }.merge!(options)

  if model
    if url != false
      url ||= polymorphic_path(model, format: format)
    end

    model   = _object_for_form_builder(model)
    scope ||= model_name_from_record_or_class(model).param_key
  end

  if block_given?
    builder = instantiate_builder(scope, model, options)
    output  = capture(builder, &block)
    options[:multipart] ||= builder.multipart?

    html_options = html_options_for_form_with(url, model, **options)
    form_tag_with_body(html_options, output)
  else
    html_options = html_options_for_form_with(url, model, **options)
    form_tag_html(html_options)
  end
end

今回のサンプルコードの場合だと、ブロックを渡しているので if block_given? の条件分岐は true になる。

(ただ、今回の調査内容でいうと block_given? かどうかは調査結果に関係ない)

どちらにせよ、form_tag_with_body もしくは form_tag_html で html の form タグが作られそうである。

ひとまず、その前の html_options_for_form_with の中身を見てみると、html_options[:method] ||= :patch if model.respond_to?(:persisted?) && model.persisted? という記述があり、ここでとりあえず model が保存済みなら PATCH になることがわかる。

https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/actionview/lib/action_view/helpers/form_helper.rb#L1578-L1591

def html_options_for_form_with(url_for_options = nil, model = nil, html: {}, local: !form_with_generates_remote_forms,
  skip_enforcing_utf8: nil, **options)
  html_options = options.slice(:id, :class, :multipart, :method, :data, :authenticity_token).merge!(html)
  html_options[:remote] = html.delete(:remote) || !local
  html_options[:method] ||= :patch if model.respond_to?(:persisted?) && model.persisted?
  if skip_enforcing_utf8.nil?
    if options.key?(:enforce_utf8)
      html_options[:enforce_utf8] = options[:enforce_utf8]
    end
  else
    html_options[:enforce_utf8] = !skip_enforcing_utf8
  end
  html_options_for_form(url_for_options.nil? ? {} : url_for_options, html_options)
end

form_with メソッドに戻って、form_tag_with_bodyform_tag_html を見ると、form_tag_with_bodyform_tag_html を呼び出している。

どちらも途中で extra_tags_for_form というメソッドを呼んでおり、ここでいかにも method を決定していそうなことがわかった。

https://github.com/rails/rails/blob/8015c2c2cf5c8718449677570f372ceb01318a32/actionview/lib/action_view/helpers/form_tag_helper.rb#L950-L978

def extra_tags_for_form(html_options)
  authenticity_token = html_options.delete("authenticity_token")
  method = html_options.delete("method").to_s.downcase

  method_tag = \
    case method
    when "get"
      html_options["method"] = "get"
      ""
    when "post", ""
      html_options["method"] = "post"
      token_tag(authenticity_token, form_options: {
        action: html_options["action"],
        method: "post"
      })
    else
      html_options["method"] = "post"
      method_tag(method) + token_tag(authenticity_token, form_options: {
        action: html_options["action"],
        method: method
      })
    end

  if html_options.delete("enforce_utf8") { default_enforce_utf8 }
    utf8_enforcer_tag + method_tag
  else
    method_tag
  end
end

今回は model オプションに指定した @model が保存済みかどうかで html_options_for_form_with のところで html_options[:method]:patchnil が入るようになっていた。

なので、method_tag に代入している switch 文では elsewhen "post", "" 内の処理が実行されることになる。

( html_options[:method]nil の場合は extra_tags_for_formmethod = html_options.delete("method").to_s.downcase により "" になるので when "post", "" 内に入る)

まとめると、

  • まずは @model が保存済みかどうか関係なく、html_options["method"] = "post" が設定される。

    これは <form action="/models/:id" accept-charset="UTF-8" method="post"> の method の部分。

  • @model が保存済みなら、method_tag(method) ( method は :patch) が実行され、<input type="hidden" name="_method" value="patch" autocomplete="off"> の部分が作られる。

gem 'rails' のコードを Docker 環境で動かしながらコードリーディングする

概要

Rails で開発をしていて、たまに Rails 本体 (gem 'rails') のコードリーディングをしたいときがあるが、ただ読んでいるだけだと難しくて理解できないことが多い。

なので、Rails のコードをローカルにクローンし、開発中の Rails アプリからクローンした Railsディレクトリを直接参照するようにすることで、実際に処理を動かしながらコードを追えるようにしたい。

また、開発中のアプリは Docker 上で開発していたので、Docker 環境で確認できるようにする。

記事執筆時の環境

対応したこと

  1. 任意のバージョンの Rails コードをローカルにクローンする。(今回は 7.0.4)

    https://github.com/rails/rails/tree/v7.0.4

  2. Docker のアプリ用コンテナ(今回は web という名前)にクローンした Railsディレクトリをマウントする。

    docker-compose.yml に追記する。

    今回は開発中の Rails アプリと同階層にクローンしてきたので、以下のようにする。

    マウント先も任意なので、とりあえず /lib/rails としてみる。

       web:
         ~省略~
         volumes:
         ~省略~
           - ../rails:/lib/rails
    
  3. Gemfile で rails を記述している箇所を以下のように書き換える。

    path は Docker の volumes で指定したディレクトリ。

     - gem "rails", "~> 7.0.4"
     + gem "rails", path: '../lib/rails'
    
  4. docker-compose up し、web コンテナ内で bundle install する。

  5. あとは rails c で確認したい箇所のコードを実行し、binding.pry などで随時処理を止めながら確認したり、クローンした Rails のコード内で適当にログを出力する処理を書いたりして確認する。

    例えば、以下のような処理を任意の箇所に記述して、実際に該当箇所を通る処理を画面やコンソールで動かしてみると、開発中のアプリ内に debug.log が吐き出され、処理の動きを確認できる。(もっと賢いやり方はいろいろありそう)

     logger = Logger.new('debug.log')
     logger.debug('hoge')
    

    ※ クローンした Rails のコードを修正したら、開発中アプリで rails restart などで再起動が必要。