ruby(正規表現)

文字列(String)の置き換えや取得方法

Regexp#matchでマッチした時に返ってきたMatchDataを変数名[ ]で取得する。
②String#scanで正規表現にマッチした部分を配列に入れて返す。
③String#[ ]で正規表現にマッチした部分を抜き出す。
④gsubで第1引数にマッチした文字列を第2引数の文字列で置き換える。
(どれも名前付きキャプチャを使える)

下記の記事も参考にした。
Rubyスタイルガイドを読む: 正規表現、%リテラル、メタプログラミング(最終回)|TechRacho by BPS株式会社

Macth

Regexp#match (Ruby 3.1 リファレンスマニュアル)

match(str, pos = 0) -> MatchData | nil
match(str, pos = 0) {|m| ... } -> object | nil

指定された文字列 str に対して位置 pos から自身が表す正規表現によるマッチングを行います。マッチした場合には結果を MatchData オブジェクトで返します。マッチしなかった場合 nil を返します。

irb(main):010:0> p bar = /foo(.*)baz/.match("foobarbaz").to_a[1]
"bar"
=> "bar"    

MatchData オブジェクトを配列にして、1個目の値を取り出している。
下記のやり方も出来るが配列にして取り出した方が、一度に代入できる。

irb(main):003:0> bar = /foo(.*)baz/.match("foobarbaz")
=> #<MatchData "foobarbaz" 1:"bar">
irb(main):004:0> p bar
#<MatchData "foobarbaz" 1:"bar">
=> #<MatchData "foobarbaz" 1:"bar">                
irb(main):005:0> p bar[1]
"bar"
=> "bar"             

String#matchもあり、それはレシーバと引数が逆になっていて挙動は同じ。

String#match? (Ruby 3.1 リファレンスマニュアル)

match?(regexp, pos = 0) -> bool
"Ruby".match?(/R.../)    #=> true

trueかfalseを返すもの

これも レシーバと引数が逆バージョンのRegexp#match?がある。

string['text']

・string['text']で単純な文字列を検索するのであれば正規表現にしない。

String#[] (Ruby 3.1 リファレンスマニュアル)

self[substr] -> String | nil
slice(substr) -> String | nil

self が substr を含む場合、一致した文字列を新しく作って返します。 substr を含まなければ nil を返します。

irb(main):004:0> p a = "foobar"["bar"]
"bar"
=> "bar"           
self[regexp, nth = 0] -> String
slice(regexp, nth = 0) -> String

正規表現 regexp の nth 番目の括弧にマッチする最初の部分文字列を返します。 nth を省略したときや 0 の場合は正規表現がマッチした部分文字列全体を返します。正規表現が self にマッチしなかった場合や nth に対応する括弧がないときは nil を返します。

p "def getcnt(line)"[ /def\s+(\w+)/, 1 ]   # => "getcnt"

下記の様にすれば置き換えも出来る

str[/^Hello, (.+?)$/, 1] = 'MRI'            # strのキャプチャグループ1を置き換える
puts str
#=> "Hello, MRI"
self[regexp, name] -> String
slice(regexp, name) -> String

正規表現 regexp の name で指定した名前付きキャプチャにマッチする最初の部分文字列を返します。正規表現が self にマッチしなかった場合は nil を返します。

s = "FooBar"
s[/(?<foo>[A-Z]..)(?<bar>[A-Z]..)/]        # => "FooBar"
s[/(?<foo>[A-Z]..)(?<bar>[A-Z]..)/, "foo"] # => "Foo"

String#gsub

String#gsub (Ruby 3.1 リファレンスマニュアル)

gsub(pattern, replace) -> String

文字列中で pattern にマッチする部分全てを文字列 replace で置き換えた文字列を生成して返します。

p 'xxbbxbb'.gsub(/x+(b+)/, 'X<<\1>>')  # => "X<<bb>>X<<bb>>"

置換文字列 replace 中の & と \0 はマッチした部分文字列に、 \1 ... \9 は n 番目の括弧の内容に置き換えられます。

p 'xbbb-xbbb'.gsub(/x(b+)/, "\\1")     # => "bbb-bbb"  # OK
p 'xbbb-xbbb'.gsub(/x(b+)/, '\1')      # => "bbb-bbb"  # OK
p 'xbbb-xbbb'.gsub(/x(b+)/, '\\1')     # => "bbb-bbb"  # OK

$1に値が入るのはこのメソッドの処理が終了してから。
また、「\」が部分文字列との置き換えという特別な意味を持つため、 replace に「\」自身を入れたいときは「\」を二重にエスケープしなければない。

このような間違いを確実に防止し、コードの可読性を上げるには、 & や \1 よりも下記のようにブロック付き形式の gsub を使うべきです。

p 'xbbb-xbbb'.gsub(/x(b+)/) { $1 }   # => "bbb-bbb"  # OK

puts '\n'.gsub(/\\/) { '\\\\' }      # => \\n        # OK

とリファレンスにはある。

rubocopでの警告「$1より Regexp.last_match(1)を優先する」について

リファレンスにはあるけど、 { $1 } という書き方をするとrubpcopの警告が出る。

最後にマッチしたグループの取り出しにPerl由来の$記法($1や$2など)を使わずにRegexp.last_match(n) を使う様にとのルールがある様。

ちなみにキャプチャグループは番号での指定ではなく名前付きグループでの指定が望ましいとのルールもある様。

Regexp.last_match(1)とは

Regexp.last_match (Ruby 3.1 リファレンスマニュアル)

カレントスコープで最後に行った正規表現マッチの MatchData オブジェクトを返します。このメソッドの呼び出しは $~ の参照と同じです

修正前のコード
# frozen_string_literal: true

require 'optparse'

class LSCommand
  def initialize
    opt = OptionParser.new
    params = {}
    opt.on('-a') { |v| params[:a] = v }
    opt.on('-r') { |v| params[:r] = v }
    opt.on('-l') { |v| params[:l] = v }
    opt.parse!(ARGV)

    @path = Dir.pwd if ARGV #== []
    @path = ARGV if ARGV[1]
    @path = ARGV[0] if ARGV[0] && ARGV[1].nil?
  end

  def output
    case @path
    when String
      file_date_in(@path)
      output_without_options
    when Array
      @path.each do |path|
        puts "#{path}:"
        file_date_in(path)
        output_without_options
      end
    end
  end

  def file_date_in(path)
    @file_date = {}
    @max_name_size = 0
    Dir.chdir(path) do
      Dir.glob('*').each do |n|
         filename = n.gsub(%r{.+/([^/]+$)}) { '\1' }
        @max_name_size = filename.size if @max_name_size < filename.size
        @file_date[:"#{filename}"] = File.stat(filename)
      end
    end
  end

  def output_without_options
    case @max_name_size
    when (35..)
      row = 1
    when (24..34)
      row = 2
      width = 36
    else
      row = 3
      width = 23
    end

    line = @file_date.size / row
    line += 1 if (@file_date.size % row).positive?
    line.times do |time|
      @file_date.select.with_index { |(_k, _v), i| i % line == time }.each_key { |k| print format("%-#{width}s", k) }
      print "\n"
      time += 1
    end
    print "\n"
  end
end

ls = LSCommand.new
ls.output

Rubocopの言っているのに従うとこうなりそうだが、このコードは読みづらい。

filename = "/Users/desuktop".gsub(%r{.+/([^/]+$)}) { Regexp.last_match(1) }
 p filename #=> “desuktop"

そもそもstring['text’]の書き方が一番分かりやすく書けそう。

filename = %r{.+/([^/]+$)}.match("/Users/desuktop").to_a[1]

と思ったが、このコードが動いていなかったことが発覚した。
Dir.glob(‘*’)で返ってくる値は絶対パスだと思っていたら、ファイル名だけだったので、置き換える必要がなくなった😵確認不足。コードも動いてしまっていたので気づかなかった。。
送ってしまっていたプルリクエストを修正した。

その他

'test.rb'.tr('.rb', '')     #=> "test"
'test.rb'.gsub('.rb', '')   # => "test"
  • 文字クラスを表す[ ]の中でエスケープが必要なのは、^、-、\、]だけ。
    これら以外の文字・記号は文字クラス内ではただのリテラルとして扱われます。

image.png

Rubyの書き方の参考になるもの
【保存版】Rubyスタイルガイド(日本語・解説付き)総もくじ|TechRacho by BPS株式会社

本スタイルガイドの元になっているbbatsov/ruby-style-guideは、同じ著者によるRuboCop gemで使われているスタイルです。

とのことなので参考になる。

・パターンの冒頭と末尾は^や$ではなく、\Aと\zで表すこと
・複雑な置換では、#subや#gsubにブロックやハッシュを与えてもよい
・%リテラルについて
など他にも参考になることがあった。