git-flowでのフレキシブルなリリースについて

問題意識

git-flowでリポジトリを運用しているプロジェクトで、開発済み機能の、リリース時期、リリース対象機能をフレキシブルに選択できるようにしたい。

たとえば、機能Aと機能Bを開発中で、機能Aは来週リリース、機能Bは他社連携先機能のリリースに合わせたいとする。他社のスケジュールによって、機能Bが機能のリリースに前後したり、一緒になったりする、といったケースに対応したい。

いまの弊社プロジェクトのgit-flowの運用だと、すべての開発済みコミットをdevelopブランチに集約しているので、リリース時に対象機能のコミットを、人力でピックアップする羽目になっている。これはダサい。

注釈含め、以下と同じ気持ちだ。

追記 なぜdevelopブランチが複数あるのかという疑問が散見されたので,我々のケースについて書いておきます ざっくり言ってしまえばリリースサイクルの違いです.例えば大きめの新機能を実装しつつ既存コードの保守もするという時,その時点で稼働しているコード (master) から派生したdevelop/A (便宜上Aとします) の上で新機能の実装を入れてしまうと,保守のコードと新機能コードが混ざってしまい,保守のためのデプロイのタイミングで出て欲しくない新機能のコードまで露出してしまうことになるので,それを防ぐ (分離させる) という目的でdevelopを複数に分ける (例えばdevelop/Bで新機能の開発は行なう) というような形にしています.保守の為のdevelopブランチでも,新機能の為のdevelopでも,どちらも独立したステージング環境では見ておきたいので……

(中略)

*1:git-flowの解説ではdevelopブランチが複数存在している場合についてあまり言及されてない気がする……

moznion.hatenadiary.com

ググってみる

上の「問題意識」で引用した記事では、developを機能別に複数作ればいいという意見だ。以降、「複数developブランチパターン」と呼ぶことにする。

他にも、featureからfeatureを切ればいいという意見がある。「featureブランチネストパターン」と呼ぶことにする。

stackoverflow.com

比較検討

上の2つのやりかたに加え、弊社の現状の運用方法(「cherry-pickパターン」と呼ぶことにする)を加えた、3つの運用方法を比較検討する。

複数developブランチパターン

詳細

develop/x.x.x という命名規則でdevelopブランチを切る(x.x.xはバージョン番号)。リリース時は、対応するバージョンのdevelpブランチをベースに、featureブランチを切る。

感想

どのdevelopブランチになんの機能変更が入ったか、わからなくなりそう?

自社のプロジェクトの場合、リリースの順番が定まらないまま開発が始まったり、開発開始してから順番が変更になったりすることが多い。リリースごとに、バージョン番号が増加するとは限らない。わかりづらいので、バージョン番号を命名規約にすることは避けたい。追加内容別にブランチを切ればいいか。

複数のdevelopブランチをまとめてリリースしたいとき、とりまとめはどのブランチで行えばいいか?releaseブランチで行うのが適当か?

featureブランチネストパターン

詳細

大きな機能を開発開始するとき、featureブランチを切る。大きな機能のサブタスクを開発開始するとき、featureブランチからさらにfeatureブランチを切る。

感想

featureブランチはリリース直前までdevelopブランチにマージしない、ということになるのかな?大きな機能追加/変更時は、まずdevelopからfeatureブランチを切り、そのfeatureブランチからサブタスクごとにfeatureブランチをネストして切る。一段目のfeatureブランチが、従来のdevelopブランチになるイメージ。

一段目のfeatureブランチを切る粒度は、リリースの選択対象となるように。 二段目のfeatureブランチは、従来通りで良い。弊社ならRedmineのチケット単位。

リリース時、リリースに含まれる変更内容がわかりやすくなりそう。リリースノートが作りやすそう。 しかし、孫featureの親featureがどれか、わからなくなりそうか?命名規約で対処する?いや、切った本人は覚えているか。孫featureは基本的に開発担当者のローカルPCで完結するので、本人が覚えていれば十分。

hotfixするほどでないちょっとしたバグ修正はdevelopに入れてしまう?もしくは,feature/bugfixブランチを切る?後者のほうが、統一されてわかりやいかな。

cherry-pickパターン

現状。開発はRedmineのチケット単位(1日〜2,3日で終わるくらい)の粒度でdevelopからfeatureブランチを切り、都度developにマージする。 リリース時は、releaseブランチをmasterから切って、リリース対象機能と開発の順番がたまたま合っていれば、まだリリースしない開発の開始コミット直前までマージする。そうでなければ、リリース対象機能のコミットをdevelopブランチからcherry-pickする。

感想

masterとdevelopとの差分が把握しづらい。 マージした場合、リリース内容が把握しづらい。cherry-pickする場合、ピックするかどうかの判断が大変。

まとめ

機能を二段階の粒度で捉えることにする。 リリース時に取捨選択の対象になるような、大きな粒度の機能を「大きな機能」と呼ぶ。(例: ブログ機能の追加) 大きな機能のサブタスクを「小さな機能」と呼ぶ。(例: ブログへのいいね機能の追加)

ようするに、大きな機能単位でコミットを管理したい。 また、リリース時に含める大きな機能を、フレキシブルに選択したい。

cherry-pickパターンはどちらもできないのでボツ。 複数ブランチパターンは、リリース時の対象選択や、他developブランチへのバックポートの手順が煩雑そう。つまり、git-flowプラグインのコマンドひとつではできなさそう。 featureブランチネストパターンなら、コミット管理も、対象選択も簡単そうだ。採用。

結論

featureブランチネストパターンを採用する。

大きな機能を開発開始するときは、developブランチからfeatureブランチを切る。 今後、大きな機能をのfeatureブランチは、開発完了しても、リリース前までdevelopにはマージしないようにする。 リリース時に、大きな機能のfeatureブランチから含める機能をマージする。

git flow feature start <大きな機能>
git flow feature start <小さな機能> feature/<大きな機能>

でブランチを切れば、ネストしたfeatureブランチが切れる。

git flow feature finish <小さな機能>

で、うまく分岐元のfeature/<大きな機能>に取り込まれる。

ちょっとしたバグフィックスなども、直接developに入れるのではなく、feature/bugfix などのブランチを切ってい、そこにコミットしたほうが管理しやすいかと思う。

ネストしたfeatureブランチの動作確認

git-flowプラグインはnvie版とavh版がある。以下,avh版では期待通り動作することを確認した。

github.com

nvie版はプルリクエストはあるが、放置されている模様。

github.com

Welcome to fish, the friendly interactive shell
~ $ mkdir nested-features
~ $ cd nested-features/
~/nested-features $ git init
Initialized empty Git repository in /home/matsui/nested-features/.git/
~/nested-features (master|✔) $ git flow version
1.11.0 (AVH Edition)
~/nested-features (master|✔) $ git --version
git version 2.18.0
~/nested-features (master|✔) $ git flow init
No branches exist yet. Base branches must be created now.
Branch name for production releases: [master]
Branch name for "next release" development: [develop]

How to name your supporting branch prefixes?
Feature branches? [feature/]
Bugfix branches? [bugfix/]
Release branches? [release/]
Hotfix branches? [hotfix/]
Support branches? [support/]
Version tag prefix? []
Hooks and filters directory? [/home/matsui/nested-features/.git/hooks]
~/nested-features (develop|✔) $ echo base change > base.txt
~/nested-features (develop|…) $ git add .
~/nested-features (develop|●1) $ git commit -m "[add] base.txt"
[develop 16e1e67] [add] base.txt
 1 file changed, 1 insertion(+)
 create mode 100644 base.txt
~/nested-features (develop|✔) $ git flow feature start big-feature
Switched to a new branch 'feature/big-feature'

Summary of actions:
- A new branch 'feature/big-feature' was created, based on 'develop'
- You are now on branch 'feature/big-feature'

Now, start committing on your feature. When done, use:

     git flow feature finish big-feature

~/nested-features (feature/big-feature|✔) $
echo new small feature > small-feature.txt
~/nested-features (feature/big-feature|…) $ git add .
~/nested-features (feature/big-feature|●1) $
git commit -m "[add] small-feature.txt"
[feature/big-feature f1b0032] [add] small-feature.txt
 1 file changed, 1 insertion(+)
 create mode 100644 small-feature.txt
~/nested-features (feature/big-feature|✔) $
git flow feature start another-small-feature feature/big-feature
Switched to a new branch 'feature/another-small-feature'

Summary of actions:
- A new branch 'feature/another-small-feature' was created, based on 'feature/big-feature'
- You are now on branch 'feature/another-small-feature'

Now, start committing on your feature. When done, use:

     git flow feature finish another-small-feature

~/nested-features (feature/another-small-feature|✔) $
git log --graph --one-line
fatal: unrecognized argument: --one-line
~/nested-features (feature/another-small-feature|✔) $
git log --graph --all --oneline
* f1b0032 (HEAD -> feature/another-small-feature, feature/big-feature) [add] small-feature.txt
* 16e1e67 (develop) [add] base.txt
* 7527443 (master) Initial commit
~/nested-features (feature/another-small-feature|✔) $ git log --graph --all --oneline
* f1b0032 (HEAD -> feature/another-small-feature, feature/big-feature) [add] small-feature.txt
* 16e1e67 (develop) [add] base.txt
* 7527443 (master) Initial commit
~/nested-features (feature/another-small-feature|✔) $
echo another small feature > another-small-feature.txt
~/nested-features (feature/another-small-feature|…) $ git add .
~/nested-features (feature/another-small-feature|●1) $ git commit
Aborting commit due to empty commit message.
~/nested-features (feature/another-small-feature|●1) $ git commit -m "[add] another-small-feature.txt"
[feature/another-small-feature fe5c067] [add] another-small-feature.txt
 1 file changed, 1 insertion(+)
 create mode 100644 another-small-feature.txt
~/nested-features (feature/another-small-feature|✔) $ git log --graph --all --oneline
* fe5c067 (HEAD -> feature/another-small-feature) [add] another-small-feature.txt
* f1b0032 (feature/big-feature) [add] small-feature.txt
* 16e1e67 (develop) [add] base.txt
* 7527443 (master) Initial commit
~/nested-features (feature/another-small-feature|✔) $ git flow feature finish
Switched to branch 'feature/big-feature'
Updating f1b0032..fe5c067
Fast-forward
 another-small-feature.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 another-small-feature.txt
Deleted branch feature/another-small-feature (was fe5c067).

Summary of actions:
- The feature branch 'feature/another-small-feature' was merged into 'feature/big-feature'
- Feature branch 'feature/another-small-feature' has been locally deleted
- You are now on branch 'feature/big-feature'

~/nested-features (feature/big-feature|✔) $ git log --graph --all --oneline
* fe5c067 (HEAD -> feature/big-feature) [add] another-small-feature.txt
* f1b0032 [add] small-feature.txt
* 16e1e67 (develop) [add] base.txt
* 7527443 (master) Initial commit
~/nested-features (feature/big-feature|✔) $ # 期待通り
~/nested-features (feature/big-feature|✔) $ # developにはマージされていない。
~/nested-features (feature/big-feature|✔) $ git checkout develop
Switched to branch 'develop'
~/nested-features (develop|✔) $ git flow feature start anohter-big-feature
Switched to a new branch 'feature/anohter-big-feature'

Summary of actions:
- A new branch 'feature/anohter-big-feature' was created, based on 'develop'
- You are now on branch 'feature/anohter-big-feature'

Now, start committing on your feature. When done, use:

     git flow feature finish anohter-big-feature

~/nested-features (feature/anohter-big-feature|✔) $ echo another big feature > another-big-feature.txt
~/nested-features (feature/anohter-big-feature|…) $ git add .
~/nested-features (feature/anohter-big-feature|●1) $ git commit -m "[add] another-big-feature.txt"
[feature/anohter-big-feature b47cd48] [add] another-big-feature.txt
 1 file changed, 1 insertion(+)
 create mode 100644 another-big-feature.txt
~/nested-features (feature/anohter-big-feature|✔) $ git log --graph --all --oneline
* b47cd48 (HEAD -> feature/anohter-big-feature) [add] another-big-feature.txt
| * fe5c067 (feature/big-feature) [add] another-small-feature.txt
| * f1b0032 [add] small-feature.txt
|/
* 16e1e67 (develop) [add] base.txt
* 7527443 (master) Initial commit
~/nested-features (feature/anohter-big-feature|✔) $ git checkout feature/big-feature
Switched to branch 'feature/big-feature'
~/nested-features (feature/big-feature|✔) $ git branch
  develop
  feature/anohter-big-feature
* feature/big-feature
  master
~/nested-features (feature/big-feature|✔) $ git flow feature finish
Switched to branch 'develop'
Merge made by the 'recursive' strategy.
 another-small-feature.txt | 1 +
 small-feature.txt         | 1 +
 2 files changed, 2 insertions(+)
 create mode 100644 another-small-feature.txt
 create mode 100644 small-feature.txt
Deleted branch feature/big-feature (was fe5c067).

Summary of actions:
- The feature branch 'feature/big-feature' was merged into 'develop'
- Feature branch 'feature/big-feature' has been locally deleted
- You are now on branch 'develop'

~/nested-features (develop|✔) $ git log --graph --all --oneline
*   ef90ebe (HEAD -> develop) Merge branch 'feature/big-feature' into develop
|\
| * fe5c067 [add] another-small-feature.txt
| * f1b0032 [add] small-feature.txt
|/
| * b47cd48 (feature/anohter-big-feature) [add] another-big-feature.txt
|/
* 16e1e67 [add] base.txt
* 7527443 (master) Initial commit
~/nested-features (develop|✔) $ git checkout feature/anohter-big-feature
Switched to branch 'feature/anohter-big-feature'
~/nested-features (feature/anohter-big-feature|✔) $ git checkout develop
Switched to branch 'develop'
~/nested-features (develop|✔) $ ls
another-small-feature.txt  base.txt  small-feature.txt
~/nested-features (develop|✔) $ git checkout feature/anohter-big-feature
Switched to branch 'feature/anohter-big-feature'
~/nested-features (feature/anohter-big-feature|✔) $ git flow feature finish
Switched to branch 'develop'
Merge made by the 'recursive' strategy.
 another-big-feature.txt | 1 +
 1 file changed, 1 insertion(+)
 create mode 100644 another-big-feature.txt
Deleted branch feature/anohter-big-feature (was b47cd48).

Summary of actions:
- The feature branch 'feature/anohter-big-feature' was merged into 'develop'
- Feature branch 'feature/anohter-big-feature' has been locally deleted
- You are now on branch 'develop'

~/nested-features (develop|✔) $ git log --graph --all --oneline
*   d581427 (HEAD -> develop) Merge branch 'feature/anohter-big-feature' into develop
|\
| * b47cd48 [add] another-big-feature.txt
* |   ef90ebe Merge branch 'feature/big-feature' into develop
|\ \
| |/
|/|
| * fe5c067 [add] another-small-feature.txt
| * f1b0032 [add] small-feature.txt
|/
* 16e1e67 [add] base.txt
* 7527443 (master) Initial commit
~/nested-features (develop|✔) $

fish shell cdしたらlsしたい

確認環境

$ fish --version
fish, version 2.7.1

結論

設定ファイル(例えば~/.config/fish/config.fish)に、以下を追記する。

functions --copy cd standard_cd

function cd
  standard_cd $argv; and ls
end

説明

fish shellには2つのcdが存在する。ひとつはビルトインのcd。もうひとつは、ビルトインのcdをラップした、関数のcd。関数のcdは、おもに過去のディレクトリへの移動機能が加わっている。cd -など。

普通にcdすると、関数のほうが呼ばれる。

今回の変更では、関数のcdをさらにラップし、実行後にlsを行うようにした。

調査メモ

ググってみる。以下の2つの記事が、具体例付きで参考になりそう。

前者は、追記するコードが長い。長いということは、内容を把握するのに時間がかかる。そして自分の理解が間違っていないか不安になる。デフォルト設定に加え、lsを加えた関数を定義しているとのこと。元ネタのデフォルト設定はどこにあるんだろう?またこれだと、デフォルト設定のほうが改良されても、自動で反映されないのでは?(記事を書いてくれた方、ネガティブなことばかり書いてごめんなさい)

後者は短い。ぱっと見、自分のやりたいことにマッチしているように見える。これにしようかな? しかし、試してみると、cd - が機能しない。 というか、cd -のほかにもfish cdならではの便利機能があって、見落としてるとかはないか?その便利機能が、上記のデフォルト設定な気もする。

fishのいい感じの配慮を活かしつつ、lsを追加したい。 かつ、長い設定を書きたくない。だからfishを使ってるんです。

cd -が効かない件についてググってみる。

https://github.com/fish-shell/fish-shell/issues/4869 このisssueだ。

知りたいことは以下のコメントにすべて書いてあった。ありがとうkrader1961さん。

dirh, prevd, and nextd do not work as usual after `builtin cd` · Issue #4869 · fish-shell/fish-shell · GitHub

このコメントの方法に従うと、cd -も行ける。

コメントによると、fishでは、builtin cd をラップするfunction cdを用意しているらしい。その関数のおかけでcd -が機能するとのこと。 以下、man cdからの抜粋。

Fish also ships a wrapper function around the builtin cd that understands cd - as changing to the previous directory. See also prevd. This wrapper function maintains a history of the 25 most recently visited directories in the $dirprev and $dirnext global variables. If you make those universal variables your cd history is shared among all fish instances.

上のGithubのコメントによると、type cdで関数のファイルの場所と処理内容が確認可能とのこと。 関数での処理内容はざっと見た感じ、移動履歴の管理関係。 引数の個数による例外処理、サブシェル時はビルトインcdを起動する、 cd -で前のディレクトリへ戻る(しかも戻って戻るともとにもどる)、 戻るじゃないときは履歴に追加(cdhなどで移動できる)、など。

上で参考にした、[fish-shell] ご注文はcdしたら自動でlsですか?で言うデフォルト設定とは、function cdの処理内容の模様。

調査まとめ

追加調査メモ

builtin cdだとcdhに記憶されない。cd - も効かない。確認してみる。

Welcome to fish, the friendly interactive shell
~ $ cdh
No previous directories to select. You have to cd at least once.
~ $ builtin cd ~/Documents/work/
~/D/work $ cd -
Hit end of history…
~/D/work $ cdh
No previous directories to select. You have to cd at least once.

function cdだと履歴が記録される。cd -でひとつ前にもどれる。確認してみる。

Welcome to fish, the friendly interactive shell
~ $ cdh
No previous directories to select. You have to cd at least once.
~ $ cd Documents/
~/Documents $ cd ~/Downloads/
~/Downloads $ cdh
 b  2)  ~
 a  1)  ~/Documents
Select directory by letter or number:
~/Downloads $ cd -
~/Documents $

以上

Rails4, Rails5の時間の足し算の挙動の違い

経緯

Rails4で、日付の足し算の挙動が自分の期待と異なっていたので調査した。 1/31の翌月の翌月も、1/31の二ヶ月後も、どちらも2018/3/31になるのを期待していたが、実際の結果は異なった。 前者は「1/31の翌月の翌月」 => 「2/28の翌月」 => 「3/28」と処理されているもよう。

Loading development environment (Rails 4.2.10)
irb(main):001:0> 1.month + 1.month == 2.month
=> true
irb(main):002:0> Date.new(2018, 1, 31) + (1.month + 1.month)
=> Wed, 28 Mar 2018
irb(main):003:0> Date.new(2018, 1, 31) + (2.month)
=> Sat, 31 Mar 2018

後に、Rails5でも確認したところ、挙動が変更され、自分の期待と同様に動くことを確認した。

結論

Rails5なら、自分の期待通りに動く。 Rails4のときは、 Date.new + x.month + y.momnthDate.new + (x + y).month は計算結果が異なることがあるので、足し方に注意。

調査環境

Rails4, Rails5での挙動は、下記の環境で確認した。

# Rails4環境
~/t/rails4-sandbox (master|✔) $ docker-compose exec web rails --version
Rails 4.2.10
~/t/rails4-sandbox (master|✔) $ docker-compose exec web ruby --version
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux]
~/t/rails4-sandbox (master|✔) $ docker-compose exec db  psql -U postgres -c "SELECT version()"
                                         version

--------------------------------------------------------------------------------
----------
 PostgreSQL 9.6.5 on x86_64-pc-linux-gnu, compiled by gcc (Debian 4.9.2-10) 4.9.
2, 64-bit
(1 row)
# Rails5環境
~/t/rails-sandbox (master|✔) $ docker-compose exec web rails --version
Rails 5.2.1
~/t/rails-sandbox (master|✔) $ docker-compose exec web ruby --version
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux]
~/t/rails-sandbox (master|✔) $ docker-compose exec db  psql -U postgres -c "SELECT version()"
                                         version

------------------------------------------------------------------------------------------
 PostgreSQL 9.6.5 on x86_64-pc-linux-gnu, compiled by gcc (Debian 4.9.2-10) 4.9.2, 64-bit
(1 row)

調査メモ

問題の挙動は下記の通り。 どちらも「二ヶ月後」の3/31になると期待していた。

Loading development environment (Rails 4.2.10)
irb(main):001:0> 1.month + 1.month == 2.month
=> true
irb(main):002:0> Date.new(2018, 1, 31) + (1.month + 1.month)
=> Wed, 28 Mar 2018
irb(main):003:0> Date.new(2018, 1, 31) + (2.month)
=> Sat, 31 Mar 2018

Rails5では自分の期待通りに動くようになっている。

Loading development environment (Rails 5.2.1)
irb(main):001:0> 1.month + 1.month == 2.month
=> true
irb(main):002:0> Date.new(2018, 1, 31) + (1.month + 1.month)
=> Sat, 31 Mar 2018
irb(main):003:0> Date.new(2018, 1, 31) + (2.month)
=> Sat, 31 Mar 2018
irb(main):004:0>

Rails4での、ActiveRecord::Durationどうしの足し算の処理方法は、下記の通り。

# https://github.com/rails/rails/blob/4-2-stable/activesupport/lib/active_support/duration.rb#L16-L24
    # Adds another Duration or a Numeric to this Duration. Numeric values
    # are treated as seconds.
    def +(other)
      if Duration === other
        Duration.new(value + other.value, @parts + other.parts)
      else
        Duration.new(value + other, @parts + [[:seconds, other]])
      end
    end

Rails4では、Durationどうしの足し算は、@parts部分は、じつは足していない。配列で足すべき要素を覚えているだけ。試しに実行してみると以下のようになる。

Loading development environment (Rails 4.2.10)
irb(main):001:0> (1.month + 1.month).parts
=> [[:months, 1], [:months, 1]]
irb(main):002:0> 2.month.parts
=> [[:months, 2]]

Rails5のDurationどうしの足し算処理の内容は下記の通り。@parts部分はHash。自分の期待通りに足している。

# https://github.com/rails/rails/blob/74e5205cc0e04a28db86fd3ec82124a8ebf4f549/activesupport/lib/active_support/duration.rb#L234-L247
    # Adds another Duration or a Numeric to this Duration. Numeric values
    # are treated as seconds.
    def +(other)
      if Duration === other
        parts = @parts.dup
        other.parts.each do |(key, value)|
          parts[key] += value
        end
        Duration.new(value + other.value, parts)
      else
        seconds = @parts[:seconds] + other
        Duration.new(value + other, @parts.merge(seconds: seconds))
      end
    end

実行結果。

Loading development environment (Rails 5.2.1)
irb(main):001:0> (1.month + 1.month).parts
=> {:months=>2}
irb(main):002:0> 2.month.parts
=> {:months=>2}

Date + Durationの処理は、Duration#sumが呼ばれる。

parts.injectで、足す単位(seconds, minutesなど)に応じてDate#advance || Date#sinceしている。

minutes, hoursの場合,Rails4ではadvanceだったのが、Rails5では秒換算でsinceされる。なぜだろう?

advenceされるのは、後のadvanceのコードを見ればわかるとおり、years, months, weeks, daysの場合だ。

# Rails4
# https://github.com/rails/rails/blob/3804d017333da16d76d9fc6633faf5635c7b03d7/activesupport/lib/active_support/duration.rb#L133-L145
      def sum(sign, time = ::Time.current) #:nodoc:
        parts.inject(time) do |t,(type,number)|
          if t.acts_like?(:time) || t.acts_like?(:date)
            if type == :seconds
              t.since(sign * number)
            else
              t.advance(type => sign * number)
            end
          else
            raise ::ArgumentError, "expected a time or date, got #{time.inspect}"
          end
        end
      end
# Rails5
# https://github.com/rails/rails/blob/74e5205cc0e04a28db86fd3ec82124a8ebf4f549/activesupport/lib/active_support/duration.rb#L402-L418
      def sum(sign, time = ::Time.current)
        parts.inject(time) do |t, (type, number)|
          if t.acts_like?(:time) || t.acts_like?(:date)
            if type == :seconds
              t.since(sign * number)
            elsif type == :minutes
              t.since(sign * number * 60)
            elsif type == :hours
              t.since(sign * number * 3600)
            else
              t.advance(type => sign * number)
            end
          else
            raise ::ArgumentError, "expected a time or date, got #{time.inspect}" 
          end
        end
      end

Date#sinceは以下の処理。Rails4もRails5も同じ。

# https://github.com/rails/rails/blob/3804d017333da16d76d9fc6633faf5635c7b03d7/activesupport/lib/active_support/core_ext/date/calculations.rb#L57-L62
# https://github.com/rails/rails/blob/74e5205cc0e04a28db86fd3ec82124a8ebf4f549/activesupport/lib/active_support/core_ext/date/calculations.rb#L59-L64
  # Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00)
  # and then adds the specified number of seconds
  def since(seconds)
    in_time_zone.since(seconds)
  end
  alias :in :since

。。 Date#advanceは以下の処理。Rails4もRails5も同じ。

# https://github.com/rails/rails/blob/3804d017333da16d76d9fc6633faf5635c7b03d7/activesupport/lib/active_support/core_ext/date/calculations.rb#L108-L118
# https://github.com/rails/rails/blob/74e5205cc0e04a28db86fd3ec82124a8ebf4f549/activesupport/lib/active_support/core_ext/date/calculations.rb#L110-L120
def advance(options)
  options = options.dup
  d = self
  d = d >> options.delete(:years) * 12 if options[:years]
  d = d >> options.delete(:months)     if options[:months]
  d = d +  options.delete(:weeks) * 7  if options[:weeks]
  d = d +  options.delete(:days)       if options[:days]
  d
end

秒数換算(1ヶ月30日)の@valuesと、seconds, months, weeks, daysなどの人間に優しい値?の@partsを持っている。

Loading development environment (Rails 4.2.10)
irb(main):001:0> 1.month.instance_variables
=> [:@value, :@parts]
irb(main):002:0> 1.month.instance_variable_get(:@value)
=> 2592000
irb(main):003:0> 1.month.instance_variable_get(:@parts)
=> [[:months, 1]]
irb(main):004:0> (1.month + 2.days + 3.month).instance_variable_get(:@value)
=> 10540800
irb(main):005:0> (1.month + 2.days + 3.month).instance_variable_get(:@parts)
=> [[:months, 1], [:days, 2], [:months, 3]]
irb(main):006:0> d = 1.month + 1.month.value
=> 1 month and 2592000 seconds
irb(main):007:0> 1.month.instance_variables
=> [:@value, :@parts]
irb(main):008:0> d.instance_variable_get(:@value)
=> 5184000
irb(main):009:0> d.instance_variable_get(:@parts)
=> [[:months, 1], [:seconds, 2592000]]
irb(main):010:0> d.value
=> 5184000
Loading development environment (Rails 5.2.1)
rb(main):001:0> 1.month.instance_variables
=> [:@parts, :@value]
irb(main):002:0> 1.month.instance_variable_get(:@value)
=> 2629746
irb(main):003:0> 1.month.instance_variable_get(:@parts)
=> {:months=>1}
irb(main):004:0> (1.month + 2.days + 3.month).instance_variable_get(:@value)
=> 10691784
irb(main):005:0> (1.month + 2.days + 3.month).instance_variable_get(:@parts)
=> {:months=>4, :days=>2}
irb(main):006:0> d = 1.month + 1.month.value
=> 1 month and 2629746 seconds
irb(main):007:0> 1.month.instance_variables
=> [:@parts, :@value]
irb(main):008:0> d.instance_variable_get(:@value)
=> 5259492
irb(main):009:0> d.instance_variable_get(:@parts)
=> {:months=>1, :seconds=>2629746}
irb(main):010:0> d.value
=> 5259492

1.yearは360日でも365日でもない。365.25日。1.yearと1.monthのvalue換算の値の大きさが、Rails4とRails5で異なっている。

Loading development environment (Rails 4.2.10)
irb(main):001:0> 1.year.value
=> 31557600.0
irb(main):002:0> 1.month.value * 12
=> 31104000
irb(main):003:0> 1.day.value * 365
=> 31536000

Rails4での各単位の秒数については下記の通り。

# Rails4
# https://github.com/rails/rails/blob/3804d017333da16d76d9fc6633faf5635c7b03d7/activesupport/lib/active_support/duration.rb#L59-L81
   # Returns the number of seconds that this Duration represents.
    #
    #   1.minute.to_i   # => 60
    #   1.hour.to_i     # => 3600
    #   1.day.to_i      # => 86400
    #
    # Note that this conversion makes some assumptions about the
    # duration of some periods, e.g. months are always 30 days
    # and years are 365.25 days:
    #
    #   # equivalent to 30.days.to_i
    #   1.month.to_i    # => 2592000
    #
    #   # equivalent to 365.25.days.to_i
    #   1.year.to_i     # => 31557600
    #
    # In such cases, Ruby's core
    # Date[http://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html] and
    # Time[http://ruby-doc.org/stdlib/libdoc/time/rdoc/Time.html] should be used for precision
    # date and time arithmetic.
    def to_i
      @value.to_i
    end

Rails5での各単位の秒数については下記の通り。 1ヶ月は30日でなく、1年の12分の1に変更されている。

# Rails5.2
# https://github.com/rails/rails/blob/74e5205cc0e04a28db86fd3ec82124a8ebf4f549/activesupport/lib/active_support/duration.rb#L322-L344
    # Returns the number of seconds that this Duration represents.
    #
    #   1.minute.to_i   # => 60
    #   1.hour.to_i     # => 3600
    #   1.day.to_i      # => 86400
    #
    # Note that this conversion makes some assumptions about the
    # duration of some periods, e.g. months are always 1/12 of year
    # and years are 365.2425 days:
    #
    #   # equivalent to (1.year / 12).to_i
    #   1.month.to_i    # => 2629746
    #
    #   # equivalent to 365.2425.days.to_i
    #   1.year.to_i     # => 31556952
    #
    # In such cases, Ruby's core
    # Date[http://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html] and
    # Time[http://ruby-doc.org/stdlib/libdoc/time/rdoc/Time.html] should be used for precision
    # date and time arithmetic.
    def to_i
      @value.to_i
    end
Loading development environment (Rails 5.2.1)
irb(main):001:0> 1.year.value
=> 31556952
irb(main):002:0> 1.month.value * 12
=> 31556952
irb(main):003:0> 1.day.value * 365
=> 31536000
irb(main):004:0>

余談:SQL

SQLでも、「二ヶ月間 + 一ヶ月間」と「一ヶ月+二ヶ月間」では、結果が違うことがある。

psql (9.6.5)
Type "help" for help.

postgres=# select date('2017-01-31'), (date('2017-01-31') +  make_interval(months := 2) + interval '1 month') AS expire;
    date    |       expire
------------+---------------------
 2017-01-31 | 2017-04-30 00:00:00
(1 row)

postgres=# select date('2017-01-31'), (date('2017-01-31') +  make_interval(months := 1) + interval '2 month') AS expire;
    date    |       expire
------------+---------------------
 2017-01-31 | 2017-04-28 00:00:00
(1 row)

postgres=# select date('2017-01-31'), (date('2017-01-31') +  make_interval(months := 1) + make_interval(months := 2)) AS expire;
    date    |       expire
------------+---------------------
 2017-01-31 | 2017-04-28 00:00:00
(1 row)

postgres=# select date('2017-01-31'), (date('2017-01-31') +  interval '1 month' + interval '2 month') AS expire;
    date    |       expire
------------+---------------------
 2017-01-31 | 2017-04-28 00:00:00
(1 row)

以下の様に書くと、つねに(私の)期待どおり動作する。

psql (9.6.5)
Type "help" for help.

postgres=# select date('2017-01-31'), (date('2017-01-31') +  make_interval(months := 1 + 2));
    date    |      ?column?
------------+---------------------
 2017-01-31 | 2017-04-30 00:00:00
(1 row)

docker-composeを環境別に使い分けたい

経緯

もともと、docker-compose.ymlで開発環境を用意していた。 プレビュー環境構築にあたり、諸々の事情により、プレビュー環境では別途新規に書き起こされたdocker-compose.yml、Dockerfileを使うことになった。 既存の設定は開発環境で使いつつ、プレビュー環境では新規の設定を使いたい。

やりたいこと

以下のような配慮をしつつ、Docker環境を使い分けたい。

  • 開発用PCでは、開発用docker-composeとプレビュー用docker-composeを適宜使い分けられるようにしたい。
  • プレビュー用ホストではプレビュー用docker-composeのみを使いたい。
  • プレビュー用ホストでうっかり開発用docker-composeで立ち上げることは絶対にしたくない。フールプルーフにしたい。
  • 開発PCでうっかりプレビュー用docker-composeを立ち上げてしまうのも避けたい。逆よりはマシだが。

実現案

  1. docker-composeのファイル名を分ける(例: docker-compose.development.yml)
    1. メリット
      1. フールプルーフ
      2. ファイル名を見れば用途がわかりやすい
    2. デメリット
      1. docker-compose -fオプションでいちいち指定するのが面倒
        1. だからといってよくつかう環境用の設定ファイルの名前をdocker-compose.ymlにすると、他の環境で使用してしまうというオペミスが発生する
        2. 環境変数COMPOSE_FILEで使用するdocker-compose.ymlを指定すれば対処できる 参考: suin.io
          1. メリット
            1. 一度設定すれば-fオプションが不要
          2. デメリット
            1. 環境構築時、知らない/忘れたひとは-fオプション無しで起動してハマる
              1. README.mdに書いておけばいいのでは
            2. 特定ディレクトリ下で環境変数を設定するいいかんじの方法が、自分はまだわからない => dotenvがよさそう。他の開発者と共有しやすそうなので。
              1. dotenv
              2. bashrcに記述 https://stackoverflow.com/a/14463040
              3. tmuxinator
  2. ブランチを分ける。追加開発分は適宜プレビュー用ブランチにマージする
    1. メリット
      1. -fオプションが不要
      2. 知らないひとがとりあえずdocker-compose upしてイライラすることもない
    2. デメリット
      1. フールプルーフでもフェイルセーフでもない。知らないで適当にdevelopブランチをプルしてきてdocker-compose upするとまずい
        1. previewで動かすとき、developブランチで合ってるのかな?というのは普通に気にするよね?
      2. docker-compose.ymlを変更したとき、おそらく変更がマージされてしまう
  3. 差分の設定を上書く
    1. メリット
      1. 公式の安心感 Use Compose in production
      2. ブランチを分ける必要がない
    2. デメリット
      1. フールプルーフでもフェイルセーフでもない。うっかりするとdevelopmentモードで起動してしまう
      2. docker-compose設定の全体像を把握しづらそう
      3. docker-compose設定を変更しづらそう
      4. 今回のケースだと、docker-compose設定が違いすぎるので、向かなさそう
  4. 環境ごとにわけない
    1. メリット
      1. 環境が分かれていることによるミスが発生しない
      2. じつは公式にも似たようなことが書いてある (The easiest way to deploy an application is to run it on a single server, similar to how you would run your development environment. If you want to scale up your application, you can run Compose apps on a Swarm cluster.)
    2. デメリット
      1. 当然、環境別の変更設定ができない

とりあえずの結論

1つ目の案でやってみる。フールプルーフで、かつ環境ごとに変更できるため。環境変数はdotenvを使って設定する。

Arch Linuxでvim8でdeoplete.nvimがすんなり動かなかった

やりたいこと

vim8でdeoplete.nvimを動かす

やったこと

期待してたこと

  • vim起動時にエラーが出ない
  • neocomplete.vimを使ってたときのように、適宜補完が効く

実際に起こったこと

  • vim起動時、以下のエラーが出る
[vim-hug-neovim-rpc] failed executing: pythonx import neovim
[vim-hug-neovim-rpc] Vim(pythonx):Traceback (most recent call last):
続けるにはENTERを押すかコマンドを入力してください`

解決法

pip install neovimのかわりに、 pacman -S python-neovim すればいいのかな? まったく自信ないです。

参考

調査メモ(読む価値ないです)

python全くわからんが、一応パッケージはインストールされてるっぽい?

~ $ python
Python 3.6.5 (default, Apr 14 2018, 13:17:30) 
[GCC 7.3.1 20180406] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import neovim
>>> import greenlet
>>>

vimでもpython認識してるっぽい

:echo('pythonx') #=> 1
:echo('python3') #=> 1
:echo('python') #=> 1
:echo('python2') #=> 0

vimでgreenletが読み込めない。 ファイル開いたらバイナリっぽかった

:pythonx import neovim
Traceback (most recent call last):
  File "<string>", line 1, in <module>
  File "/usr/lib/python3.6/site-packages/neovim/__init__.py", line 11, in <module>
    from .msgpack_rpc import (ErrorResponse, child_session, socket_session,
  File "/usr/lib/python3.6/site-packages/neovim/msgpack_rpc/__init__.py", line 10, in <module>
    from .session import ErrorResponse, Session
  File "/usr/lib/python3.6/site-packages/neovim/msgpack_rpc/session.py", line 6, in <module>
    import greenlet
ImportError: /usr/lib/python3.6/site-packages/greenlet.cpython-36m-x86_64-linux-gnu.so: undefined symbol:
 PyExc_ValueError

ほかの普通の?パッケージだとimportできるっぽい

 :pythonx import pycurl #=> エラー出ない

https://bbs.archlinux.org/viewtopic.php?id=232873を見ると、greenletはpacman経由でも入れることが可能で、そちらだと動くっぽい?

sudo pacman -S python-greenlet
:pythonx import greenlet #=> エラー出ない

neovimをpipで入れて、依存してるgreenletはpacmanで入れるって、大丈夫なのか?

なんかpacmanのリポジトリにpython-neovimってあるな。それ入れればいいか。

しかし、既存ディレクトリと衝突し、インストールできないとのこと。ログは以下の通り。

~ $ sudo pacman -S python-neovim
依存関係を解決しています...
衝突するパッケージがないか確認しています...

パッケージ (4) neovim-0.2.2-5  python-greenlet-0.4.13-1
               python-msgpack-0.5.6-1  python-neovim-0.2.6-1

合計インストール容量:  18.82 MiB

:: インストールを行いますか? [Y/n] Y
(4/4) キーリングのキーを確認                       [##########] 100%
(4/4) パッケージの整合性をチェック                 [##########] 100%
(4/4) パッケージファイルのロード                   [##########] 100%
(4/4) ファイルの衝突をチェック                     [##########] 100%
エラー: 処理を完了できませんでした (衝突しているファイル)
python-msgpack: /usr/lib/python3.6/site-packages/msgpack/__init__.py がファイルシステムに存在しています
python-msgpack: /usr/lib/python3.6/site-packages/msgpack/__pycache__/__init__.cpython-36.pyc がファイルシステムに存在しています
python-msgpack: /usr/lib/python3.6/site-packages/msgpack/__pycache__/_version.cpython-36.pyc がファイルシステムに存在しています
python-msgpack: /usr/lib/python3.6/site-packages/msgpack/__pycache__/exceptions.cpython-36.pyc がファイルシステムに存在しています
python-msgpack: /usr/lib/python3.6/site-packages/msgpack/__pycache__/fallback.cpython-36.pyc がファイルシステムに存在しています
python-msgpack: /usr/lib/python3.6/site-packages/msgpack/_packer.cpython-36m-x86_64-linux-gnu.so がファイルシステムに存在しています
python-msgpack: /usr/lib/python3.6/site-packages/msgpack/_unpacker.cpython-36m-x86_64-linux-gnu.so がファイルシステムに存在しています
python-msgpack: /usr/lib/python3.6/site-packages/msgpack/_version.py がファイルシステムに存在しています
python-msgpack: /usr/lib/python3.6/site-packages/msgpack/exceptions.py がファイルシステムに存在しています
python-msgpack: /usr/lib/python3.6/site-packages/msgpack/fallback.py がファイルシステムに存在しています
エラーが発生したため、パッケージは更新されませんでした。

メモにとってないが、いままでの調査中に、pipやらpacmanやらでneovimとgreenletを入れたり消したりしたような気がしている。衝突の原因はそれな気がする。

どういう副作用が出るかまったくわからないが、/usr/lib/python3.6/site-packages/msgpackディレクトリをmsgpack.bkにリネームした。おすすめしません。その後sudo pacman -S python-neovimしたらうまくいった。

差分は以下の通り。なんか違うね。

/u/l/p/site-packages $ diff msgpack msgpack.bk
共通のサブディレクトリー: msgpack/__pycache__ と msgpack.bk/__pycache__
バイナリーファイル msgpack/_packer.cpython-36m-x86_64-linux-gnu.so とmsgpack.bk/_packer.cpython-36m-x86_64-linux-gnu.so は異なります
バイナリーファイル msgpack/_unpacker.cpython-36m-x86_64-linux-gnu.so とmsgpack.bk/_unpacker.cpython-36m-x86_64-linux-gnu.so は異なります

最後に念の為入れ直す。

sudo pacman -R python-neovim
sudo pacman -R python-greenlet
sudo pacman -S python-neovim

(いま知ったが、pacman -Rには--recursiveオプションがある。sudo pacman -Rs <package>のほうがよかったか。)

以上で完了。vimは期待通り動く。なんかいろいろゴミを残している気がする。でももう面倒だからいいや。

rubocopでrubyコードのベストプラクティスを学ぶ(Rails + Visual Studio Code)

以下の文章はこんな方を想定しています

vscodeでrailsを書いている。 自分のコードをもっと「いいコード」にしたい。が、指導してくれる人間は周囲にいない。

rubocop

github.com

rubyコードの静的解析を行うGem. ベストプラクティスに従っていない箇所を指摘してくれる。 ベストプラクティスについてはデフォルトで設定されている。変更もできる。

インストール

公式のインストール方法は以下のとおり。

github.com

自分はbundlerを使いインストールした。

group :development, :test do
+  # コードの静的解析ツール
+  gem 'rubocop', require: false
end
bundle install

以下で解析が走る。

rubocop

いい感じの解析ルールを設定する

ネットを見る感じ、デフォルトのルールはそのまま使っているひとはあまりいなさそう。厳しすぎるらしい。

しかし、自分で設定する気は起きない… @onk さんの設定を真似させていただくことにした。

github.com

group :development, :test do
   # コードの静的解析ツール
   gem 'rubocop', require: false
+  # rubocopの解析ルール設定
+  gem 'onkcop', require: false
 end
bundle install
bundle exec onkcop init

自動生成されたファイルを編集する

 inherit_gem:
   onkcop:
     - "config/rubocop.yml"
     # uncomment if use rails cops
-    # - "config/rails.yml"
+    - "config/rails.yml"
     # uncomment if use rspec cops
-    # - "config/rspec.yml"
+    - "config/rspec.yml"

 AllCops:
   TargetRubyVersion: 2.5
   # uncomment if use rails cops
-  # TargetRailsVersion: 5.1
+  TargetRailsVersion: 5.1
rubocop

で走る。解析ルールがrubocopデフォルトから変更されていることを確認する。自分の場合、

- 59 files inspected, 284 offenses detected
+ 58 files inspected, 216 offenses detected

と変化した。(このあたり、間にほかの操作を入れたかもしれません。とりあえず、vimとかでシングルクオートで文字列リテラルを宣言したときに、『[Style/StringLiterals] Style/StringLiterals: Prefer double-quoted strings unless you need single quotes to avoid extra backslashes for escaping.』と怒られるようになっていれば、onkcopのルールが適用されているはずです。)

プロジェクトで動かしてみる

とりあえず試す

bundle exec rubocop で解析結果が出力される。自分の場合、たしか数百件のアラートが出た。アラートが大量に出るので面食らうが、ほとんどは自動修正できるものか、メトリクス関連(メソッドが長すぎる、など)だった。手動ですぐに直すべき箇所は少ないので、ビビらないでいいと思う。

自動修正する

bundle exec rubocop --auto-correct で、自動修正できるものはしてくれる。自分の場合、アラートは半分以下に減った。修正のほとんどは空白文字関連。自分が面白いと思ったのは以下の自動修正。

&:symbolによる圧縮
users.map{|user| user.id}
=>
users.map(&:id)
if文(というか返り値があるので式)の返り値を使って代入
if condition
  @user = foo
else
  @user = bar
end
=>
@user = if condition
          foo
        else
          bar
        end
ミュータブルな定数のフリーズ
SOME_CONSTS = %w[aaa bbb ccc]
=>
SOME_CONSTS = %w[aaa bbb ccc].freeze

(注: rubyの定数は再代入が可能)

【2018/02/20追記 Pocke様からご指摘をいただき修正】

freezeすることで、オブジェクトの変更ができなくなります。 (フリーズしても、定数への再代入は引き続き可能です。)

以下、freezeについて調べたメモ。

# 配列をフリーズ
NAMES = %w[yamada sato].freeze
=> ["yamada", "sato"]

# 配列への変更はできない
NAMES.sort!
#=> FrozenError (can't modify frozen Array)

# 配列をfreezeしても、配列の要素の変更はできる
NAMES.map!(&:upcase)
#=> FrozenError (can't modify frozen Array)
NAMES.map(&:upcase!)
=> ["YAMADA", "SATO"]
NAMES
=> ["YAMADA", "SATO"]

# 中身もフリーズすれば、上の操作も防げる
OTHER_NAMES = %w[takahasi akiyama].map(&:freeze).freeze
=> ["takahasi", "akiyama"]
OTHER_NAMES.map!(&:upcase)
FrozenError (can't modify frozen Array)
OTHER_NAMES.map(&:upcase!)
FrozenError (can't modify frozen String)

# freezeしても再代入はできる
OTHER_NAMES = ["bukkowasu"]
(irb):12: warning: already initialized constant OTHER_NAMES
(irb):8: warning: previous definition of OTHER_NAMES was here
=> ["bukkowasu"]
OTHER_NAMES
=> ["bukkowasu"]

【2018/02/20追記 Pocke様からご指摘をいただき修正 おわり】

残りを適宜手動で直す

bundle exec --auto-gen で、解析結果のまとめが.rubocop_todo.ymlに出力される。内容を見て、すぐ直せるものは修正していく。 メトリクス系(メソッドが長すぎる、等)はおいおい修正したい。

vscodeの設定

vscodeでもrubocop && onkcopが効くように設定する。

プラグインをインストール

以下の公式の記述に従い、Rubyプラグインをインストールする。

marketplace.visualstudio.com

設定ファイルを作成

vscodeの設定にはスコープがふたつある。ユーザーとワークスペースだ。 たとえばプロジェクトごとに設定を分けたい場合は、ワークスペース設定を行う。 わけなくていい場合はユーザー設定を行う。

以下に従い、ワークスペース設定を開く。

code.visualstudio.com

ruby language settingsを見つける。「設定の検索」欄に『ruby』と入れるとかんたんに絞りこめる。

ワークスペースを適宜設定する。設定は以下に記載がある。

marketplace.visualstudio.com

自分は以下のようにした。

{
    "ruby.lint": {
        "rubocop": true
    }
}

code.visualstudio.com

vscodeを再起動してrubyファイルを開くと、規約に従っていない箇所に緑の波線が入る。たしかにrubocopのチェックが走ることを確認した。指摘のでないかたは、完璧に従っているコードを書いている懸念があるので、おかしなコードをかいてみてください:)

自分はシステムには入れず、プロジェクトのvendor/bundle以下にrubocopを入れているが、特にそれを明示せずともうまく動いた。

bashの特殊ファイル(.bash_profile, .bashrcとか)のメモ

.bash_profileと.bashrcの違いについてまたグーグル検索。もう何回目だ…

備忘のためにメモしておく。 『入門bash』3章、「環境のカスタマイズ」を参考にしました。

以下3つのファイルを意識しておけばよさそう。ユーザーごとの設定。

  • .bash_profile …ログイン時に読み込まれる
  • .bash_logout …ログアウト時に読み込まれる
  • .bashrc …新しいシェルの起動時に読み込まれる

以下のファイルも特殊ファイルとして認識されるが、自分はあまり使う機会はなさそう。これもユーザーごとの設定。 ログイン時に.bash_profile, .bash_login, .loginの順に検索される。

  • .bash_login …Cシェル由来
  • .profile …Bourneシェル、Kornシェル由来

(.profileについては、他のシェルとの移行|共存がかんたんだよ、ということかな。.bash_loginはCシェル利用者に設計が理解しやすいというだけか?自分はこれらのシェルの設定ファイルを使わないので不要な知識…)

以下はシステム全体の設定。あまり変更することはないだろう。

  • /etc/profile …ログイン時に読み込まれる

理解が不安な、.bash_profile, .bashrc, /etc/profileの挙動を確認してみた。シャープイコール大なり(#=>)の行はコメント。

#=> まずは.bash_profile, .bashrcの確認
$ echo echo .bash_profile is loaded. >> ~/.bash_profile
$ echo echo .bashrc is loaded. >> ~/.bashrc
#=> bashrc, bash_profileにechoを仕込む。
$ echo $SHELL
/usr/bin/fish
#=> ふだんはfishがログインシェル
$ bash
#=> インタラクティブだがログインシェルではないbashを起動。.bashrcが読まれるはず。
.bashrc is loaded.
#=> 期待通り。
$ bash
#=> bash内でさらにbashを起動してみる。
.bashrc is loaded.
#=> もう一度読まれる。まあそうか。
$ ps --forest
#=> プロセスはこんな状態。
  PID TTY          TIME CMD
18565 pts/0    00:00:01 fish
21358 pts/0    00:00:00  \_ bash
21368 pts/0    00:00:00      \_ bash
21385 pts/0    00:00:00          \_ ps
$ exit
$ exit
$ bash --login
#=> ログインシェルとしてbashを起動。.bash_profileが読まれるはず。
.bash_profile is loaded.
#=> そのとおり。
$ ログアウト
$ echo source .bash_profile >> .bashrc
#=> .bashrcが読まれたら、.bash_profileも読まれるようにする。非ログインシェルでも、ログインシェル立ち上げ時に読まれるコードも読まれるようになる。
$ bash
.bashrc is loaded.
.bash_profile is loaded.
$ echo source .bashrc >> .bash_profile
#=> .bash_profileが読まれたら、.bashrcも読まれるようにする。無限ループになるはず
$ exit
$ bash --login
.bash_profile is loaded.
.bashrc is loaded.
.bash_profile is loaded.
.bashrc is loaded.
.bash_profile is loaded.
.bashrc is loaded.
.bash_profile is loaded.
.bashrc is loaded.
.bash_profile is loaded.
.bashrc is loaded.
.bash_profile is loaded.
.bashrc is loaded.
.bash_profile is loaded.
...
...
#=> 期待通り無限ループ。
^C$ ログアウト
$ vim .bash_profile
$ vim .bashrc
#=> sourceの行を削除して、無限ループにならないようにする。
#=> 最後に/etc/profileの挙動を確認する
$ echo 'echo etc/profile is loaded.' | sudo tee --append /etc/profile
#=> /etc/profile に書き込み。sudo権限が必要なため、このような書き方に
$ bash --login
etc/profile is loaded.
.bash_profile is loaded.
$ ログアウト
$ sudo vim /etc/profile
#=> 片付け。さっきのechoを消しておく
$ vim .bashrc .bash_profile
#=> 片付け。echoを消す

以上