ファイルを検索して文字列を置換するスクリプトをRubyとシェルスクリプトで書いてみた

この記事はESMアドベントカレンダー2023の15日目の記事です。

今回はディレクトリ以下のファイルを検索して指定した文字列を別の文字列に置換するスクリプトRubyシェルスクリプトでそれぞれ書いてみた事についてまとめたいと思います。

きっかけ

私が所属するアジャイル事業部ではEMの@koicさんと定期的に1on1を行なっています。 入社まもない頃、特定の文言修正を置換機能を使わず一つずつ修正してしまった・・・という話をkoicさんとしたことがあり、その流れで文字列置換スクリプトを自分で書いてみると練習になっていいですよというアドバイスをいただきました。

Rubyスクリプト

まずはRubyで書いたスクリプトから振り返っていきます。

要件は

  • Git管理しているファイルを対象とする
  • 引数にパスの指定はしない(カレントディレクトリ配下のみを対象とする)
  • 置換前後の文字列を引数指定する

にしました。

私が当初作成したスクリプトは以下です。

github.com

require "git"

def current_dirctory
  Dir.getwd
end

def repository
  Git.open(current_dirctory)
end

def replace_text(target_texts)
  target_text_before = target_texts[0]
  target_text_after = target_texts[1]

  git_repository = repository

  git_repository.grep(target_text_before).each do |results|
    results[0].scan(/\b:[^;]+/).each do |result|
      target_file = result.split(':').last
      target_file_text = File.open(target_file, "r") { |f| f.read }

      target_file_text.gsub!(target_text_before, target_text_after)
      File.open(target_file, "w") { |f| f.write(target_file_text) }
    end
  end
end

target_texts = ARGV
replace_text(target_texts)

このコードを載せるのは少し恥ずかしいのですが、入社直後の自分がダメなりに一生懸命書いたコードになります(笑)

入社直後は割と毎日必死でなぜGit管理しているファイルを対象にしたのかは、はっきりと覚えていないのですがRubyにはruby-gitというGemがあり、それを利用してスクリプトを作成しました。

やっていることとしては

  • ターミナルで置換前と後の文字列を指定してファイルを実行する
  • ARGVの配列で文字列を受け取る
  • Gitクライアントとして既存リポジトリを開く
  • 置換前の文字列でリポジトリ内をgrepする
  • 検索結果のファイルが得られるため、置換処理に渡すためファイルパス以外の不要な部分を除いている
  • gsub!で文字列を置換して保存する

という流れです。

ありがたいことにkoicさんがこのスクリプトをレビューしてくださり、とても勉強になったためどのような修正を加えたかをまとめていきます。

require 'git'

def replace_text(pattern, replace)
  git = Git.open(Dir.getwd)
  matches = git.grep(pattern).flat_map { |match| match.first.scan(/\b:[^;]+/) }
  file_paths = matches.map { |str| str.split(':').last }.uniq

  file_paths.each do |file_path|
    replaced_text = File.read(file_path).gsub(pattern, replace)

    File.write(file_path, replaced_text)
  end
end

replace_text(*ARGV)
  • current_dirctoryrepositoryは結果を返しているだけなため、メソッド定義ではなくスクリプト内で実行する
  • repositoryメソッドの名前適切・・・?
  • ARGVを可変長引数で受け取ればメソッド定義側で引数の意図が明確になり、メソッド内での代入が不要になる
  • scan がブロック引数を取るため、each は冗長
  • each入れ子で実行している箇所はflat_mapで同じ結果が得られる
  • File.open(target_file, "r") { |f| f.read }File.read(target_file) と書ける(Copもある)
  • File.open(target_file, "w") { |f| f.write(target_file_text) }File.write(target_file, target_file_text)と書ける(Copもある)
  • 全体的な命名の見直し

koicさんが自分ならこう書きますという例も示してくださり、フィードバックいただいた内容を踏まえて修正した結果ほぼkoicさんの例と同じものになりましたが、レビューを通じてもっとコードに対して深く考えて書けるようになるためにRuby自体やその周辺知識の強化を頑張ろうと誓いました。

シェルスクリプト

もう一つはシェルスクリプト版です。 Ruby版の方に書いた通り、Rubyの置換スクリプトはGit管理を前提にしていて管理配下にないファイルは対象になりません。 スクリプト作成後、これが結構不便な事に気づいたためGit管理していないファイルも検索して置換できるものを書いてみる事にしました。 シェルスクリプトにしたのはここ2ヶ月ほどLinuxの勉強をだいぶ頑張ったため、書いてみたかったという単純な理由です。

要件は

  • 引数にパスの指定はしない(カレントディレクトリ配下のみを対象とする)
  • 置換前後の文字列を引数指定する
  • 引数の数が不正な場合はメッセージを出す

にしました。 ちなみに引数にパスを指定していないのは、試作中にパスの指定を間違えて置換してはいけないディレクトリのファイルを一斉置換して冷や汗をかいたためです・・・。

github.com

case "$#" in
  2) target_path=`pwd` ;;
  *) echo "引数が不正です。"
esac

rg -l "$1" "$target_path" | xargs sed -i '' -e "s/$1/$2/g"

やっていることは

  • 引数の数を条件判定
  • 引数が正しい数の場合は変数にカレントディレクトリのパスを格納
  • 引数の数が不正な場合はメッセージを出す
  • ripgrep (rg)で指定したパスに対して再帰的に文字列を検索する
  • 検索結果をパイプで渡してxargssedの置換を実行

です。 Ruby版はパスの成形でだいぶコードが複雑になってしまったのですが、シェルスクリプト版はシンプルに書けました。

まとめ

今回、koicさんとの1on1を通じて自分で文字列置換のスクリプトを作成してみました。 文字列置換はVSCodeなどのテキストエディタに機能として備わっているため自作スクリプトを使うことはあまりないかもしれません。 ですが、すでにある機能を自分で要件を決めて実装していくことはエンジニアになりたての自分にとってはとても良い勉強の機会になりました。 これからも自分の身近にある便利な機能をRubyで書くなら?、シェルスクリプトでできるかな?と考えて実際に書いてみることを続けていきたいと思います。