Practice of Programming

プログラム とか Linuxとかの話題

Nanocで全文検索を実装する

いくつかNanocの記事を書きましたが、検索がないと、実際問題使えないですよね。

というわけで、Googleで検索してみましたが、下記が見つかるくらいでした。

groups.google.com

上記からたどったところ、下記のようなものがあれば、なんとかなりそうというところですが...... 元にしているNanocのバージョンが古いのと、英語のみを対象としているようなので、これを元に自作してみました。検索の実装自体はあまり変えていません。

github.com

github.com

lib/search.rb

検索用のindexを作るRubyスクリプトです。

# coding: utf-8
require 'json'
require 'nokogiri'

# 下記は適当に変えてください
$ROOT_PATH = ENV.fetch("SITE_ROOT"){Dir.getwd + '/output'}
# search-data.js.erbのMAX_WORD_LENと合わせる
$MAX_WORD_LEN = 3;

# item が一つのドキュメント
def search_terms_for(item)
  # md 以外は除外
  if item.identifier != nil && item.identifier.ext != nil && item.identifier.ext == 'md'
    # HTMLになった内容を取得
    content = item.reps.fetch(:default).compiled_content
    doc = Nokogiri::HTML(content)

    # 対象とするタグが足りなければ増やしてください
    full_text = doc.css("td, pre, div, p, h1, h2, h3, h4, h5, h6").map{|el| el.inner_text}.join("").gsub(/ /, '')
    # $MAX_WORD_LEN文字ずつに区切った組み合わせの文字をindexとしています
    "#{item.identifier.without_ext}#{item[:title]}#{item[:meta_description]}#{full_text}".each_char.each_cons($MAX_WORD_LEN).map{|chars| chars.join }
   end
end

def search_index
  # 後で、idとページ情報を紐付けるのに使います
  id = 0;
  idx = {
    "approximate" => {},
    "terms" => {},
    "items" => {}
  }
  if @items == nil
    return idx
  end
  @items.each do |item|
    # 上で定義した、search_terms_forを使って、$MAX_WORD_LEN ごとに区切られた単語の配列を取得
    result = search_terms_for(item)
    if result == nil
      next
    end
    result.each do |term|
      # termにマッチする id を入れておく入れ物
      idx["terms"][term] ||= []
      idx["terms"][term] << id
      # wor がtermの場合、 w, wo も検索対象とするための処理
      (0...term.length - 1).each do |c|
        # term の 0番目からc番目までを、subtermとします
        subterm = term[0...c]
        idx["approximate"][subterm] ||= []
        unless idx["approximate"][subterm].include?(id)
          idx["approximate"][subterm] << id
        end
      end
    end
    url = $ROOT_PATH + "#{item.identifier.without_ext}"+".html"
    # items に id と title, url等の情報を紐付けて持っておきます
    idx["items"][id] = {
      "url" => url,
      "title" => item[:title] || item.identifier.without_ext,
      "crumb" => item[:crumb]
    }
    id += 1
  end
  idx
end

content/search-data.js.erb

このJavaScriptの一番最後にある、<%= search_index.to_json %>; で、lib/search.rbsearch_index から返されたものをJSONに変えて、埋め込んでいます。

埋め込まれるのは、具体的には、下記のようなJSONですね。

{
  "approximate": {"w": [0,1,2], "wo": [0,1,2]}, 
  "terms":{"wor": [0,1,2]},
  "items": {
    "0": {"url":"/path/to/page1", "title1": "title1",
    "1": {"url":"/path/to/page2", "title2": "title2",
    "2": {"url":"/path/to/page3", "title3": "title3"
  }
}

実際に検索を行うJavaScriptの実装です。

// lib/search.rb の $MAX_WORD_LEN と合わせる
const MAX_WORD_LEN = 3

function unique(arrayName)
 {
    let newArray = new Array();
    label: for (let i = 0; i < arrayName.length; i++)
    {
        for (let j = 0; j < newArray.length; j++)
        {
            if (newArray[j] == arrayName[i])
            continue label;
        }
        newArray[newArray.length] = arrayName[i];
    }
    return newArray;
}

function search(query, callback) {
  let terms = $.trim(query).split(/\s+/);
  let matching_ids = null;
  let l = terms.length;
  for (let i = 0; i < l; i++) {
    let j = i
    for (;terms[j].length > MAX_WORD_LEN;) {
       // MAX_WORD_LEN文字までのindexなので、その文字数を超えた場合は、雑に別にする
       let s = terms[j]
       terms[j] = s.slice(0, MAX_WORD_LEN);
       terms.push(s.slice(MAX_WORD_LEN));
       j = terms.length - 1;
    }
  }
  for (let i = 0; i < terms.length; i++) {
    let term = terms[i];
    let exactmatch = index.terms[term] || [];
    let approxmatch = index.approximate[term] || [];
    let ids = unique(exactmatch.concat(approxmatch));
    if (matching_ids) {
      matching_ids = $.grep(matching_ids, function(id) {
        return ids.indexOf(id) != -1;
      });
    } else {
      matching_ids = ids;
    }
  }
  callback($.map(matching_ids, function(id){ return index.items[id]; }))
}

let index = <%= search_index.to_json %>;

content/search.haml

下記が、検索ページになります。

- content_for(:javascripts) do
  %script(type="text/javascript" src="/javascripts/search-data.js")
  %script(type="text/javascript" src="/javascripts/jquery.url.packed.js")
  :javascript
    $(function(){
      let getQuery = decodeURIComponent($.url.param("query"));
      let q = $.url.param("q") || getQuery;
      if ( q != '' ) {
        let query = q.replace("+"," ");
        $('input#q').attr('value', query);
        search(query, displayResults);
      }
      $('input#q').keyup(function(){
        search(this.value, displayResults);
      });
    })
    function displayResults(items) {
      if (items.length > 0) {
        let html = ""
        for (let i = 0; i < items.length; i++) {
          html += '<li><a href="'+items[i].url+'?h=' + $("#q").val().replace(' ', '%20') + '">'+items[i].title+'</a></li>';
        }
        $('ol#results').html(html)
      } else {
        $('ol#results').html("<li class='none'>Nothing found.</li>");
      }
    }
%input#q{:type => "text", :placeholder=>"Search"}

%h2 Results
%ol#results
  %li.none Please enter a search term.

実際の検索

自前のものが用意できれば良かったのですが、最初に貼っていたコードのサイトを貼っておきます。ほぼほぼ似たような感じで動きます。

compass-style.org

欠点

やむなしですが、search-data.js.erb からできあがる search-data.js は、ページ数が多いとサイズが大きくなってしまいます。 553ページほどあるところで試しましたが、18MBくらいにはなりました。

ですが、読み込んでしまえば、速いので、そこはやむなしとします。

ページが少なければ、そこまで大きくもなりませんし、問題にはならないかと思います。

Nanocで記事ごとにUUIDを付けてリンク作ってPermalinkとする

静的ファイルジェネレータの弱点(と僕が勝手に思っている)は、URL=ファイルのパスというところです。 何が困るかと言うと、移動した場合にURLが変わってしまう。

これを解消するために、記事ごとにUUIDをつけて、実際のパスへのシンボリックリンクを作り、それをPermalinkとして使えば良いということです。

メタデータを利用する

Nanocのドキュメントの上部には、メタデータを入れることができます。

---
KEY: VALUE
---

のような感じです。ここに、UUID用に

---
NANOC_UUID: "....."
---

というのを、挿入します。こうすると、コンパイル時に attributeとして使えるようになります。layoutをhamlで書いていたとすると、

@item[:NANOC_UUID]

で、参照できます。filter内であれば、下記のように参照できます。

@item.attributes[:NANOC_UUID]

こうしておけば、permalinkようのURLとして、上記のUUIDを使ったURLを作るとか、 index tree を作るような処理のなかで、通常のパスの代わりに、UUIDのリンクの方を使うようにすればよいかと思います。

コード

最初 filterで作っていたのですが、こういうのはRules内のpreprocessでやるべきでした。filter内だと、attributeの変更はできませんが、preprocessであれば、attributeの変更ができます。

preprocess do
  @items.each do |item|
    path = item.raw_filename.sub(/^.+\/content\//, "content/")
    # markdownのみ対象
    next if path != nil && !path.match(/\.md$/);

    content = ''

    File.open(path, 'r') do |f|
      content += f.read
    end

    if content.match(/^NANOC_UUID: (.+)$/)
      # すでにある場合それを作る(とり方がちょっと雑ですが)
      uuid = $1
    else
      # 新しく作成して先頭に書き込む
      uuid = SecureRandom.uuid
      if (content.match(/\A---/))
        content = content.sub(/\A---/, "---\nNANOC_UUID: " + uuid + "\n")
      else
        content = "---\nNANOC_UUID: " + uuid + "\n---\n" + content
      end

      File.open(path, "w") do |f|
        f.puts(content)
      end
      # attribute に追加する
      item.attributes[:NANOC_UUID] = uuid
    end

    if (uuid)
      link = 'output/' + uuid + '.html'
      if !File.exist?(link)
        # output のルートに、実際のパスへのリンクを作る
        File.symlink(path.sub(/^content\/(.+)\.md$/, "\\1.html"), link)
      end
    end
  end
end

結果

こんな感じのリンクが出来上がるので、permlink として、b247771c-a07a-4cb6-835d-ee8b2d461e49.html の方を使えばいい感じです。

lrwxrwxrwx 1 ktat ktat    10 Apr 29 16:01 b247771c-a07a-4cb6-835d-ee8b2d461e49.html -> index.html
-rw-r--r-- 1 ktat ktat 12169 Apr 29 16:02 index.html

元のファイルにメタデータを追加しているので、nanoc コマンドのラッパーのshell scriptかなんかをかいて、git commit するようにしとくのが良いのではないかなと思います。

元のパスをURL欄からコピーされるのを防ぎたければ、パスにUUIDが含まれていない場合は、JavaScriptで強制的に移動させれば良いんじゃないかなと思います。

注意

filter :relativize_path を使っていると、相対パスになってしまうので、元ファイルの場所がルートに存在しない場合、シンボリックリンクをルートに置いている関係上、相性が悪いです。 cssやjsは、絶対パスで指定するようにする必要があります。

Nanocで絵文字を表示する

前回の続きでNanocの話です。

honkitでは、:name: という表記で、絵文字を表記できたりしますが、Nanocではできません。 これも、自分でフィルターを簡単に書くことができます。

gemojione を使って、絵文字を表示するフィルターを作る

lib/nanoc/filters/gemojione.rb とか適当にファイルを作ればOKです。

module Nanoc::Filters
  class ChangeCodeBlock < Nanoc::Filter
    identifier :gemojione

    require 'gemojione'

    def run(content, params = {})
      index = Gemojione::Index.new
      content = content.gsub(/:([a-z0-9 _-]+?):/) {|s|
        emoji = index.find_by_name($1) || index.find_by_shortname(":" + $1 + ":")
        moji = s
        if (emoji != nil)
          moji = emoji['moji']
        end
        moji
      }

      content
    end
  end
end

Rulesで、下記のように使うだけです。

filter :gemojione

余談

しかし、gemojioneなんですが、こんな感じになっていて、

        emoji_hash["description"] = emoji_hash["name"]
        emoji_hash["name"] = key
        @emoji_by_name[key] = emoji_hash if key

なぜか、@emoji_by_name なのに、key をキーとしていて、定義上、実質、keyshortnameが同じなので、

  • find_by_name
  • find_by_shortname

にほぼ違いがないんですよね。謎い。絵文字はこんな感じで定義されています。

※ masterは調べてませんが、v3.3.0に関しては、key と shortnameは一致してそうでした。

{
  "100": {
    "unicode": "1F4AF",
    "unicode_alternates": [

    ],
    "name": "hundred points symbol",
    "shortname": ":100:",
    "category": "symbols",
    "aliases": [

    ],
    "aliases_ascii": [

    ],
    "keywords": [
      "numbers",
      "perfect",
      "score",
      "100",
      "percent",
      "a",
      "plus",
      "school",
      "quiz",
      "test",
      "exam",
      "symbol",
      "wow",
      "win",
      "parties"
    ],
    "moji": "💯"
  },
  // ...
}

Nanoc で PlantUMLを表示する

Nanoc とは?

静的サイトジェネレーターです。jekyllryhonkitとかの仲間です。

GitLabで静的ページを作るのに、honkitを使っていたのですが、最近、Nanocを試していました。

nanoc.app

Nanocの利点

  • ビルドが速い(honkitと比較すると爆速といえる)
  • filterを書くのが簡単

デメリットは、honkitだと、色々プラグインが存在して楽なんですが、あんまりなさそう?というのと、僕がRubyに慣れてないくらいですね。

お題の件の通り、honkitだと、PlantUMLもプラグインで表示ができるのですが、nanoc ではできません。

Nanocのフィルターを書いてPlantUMLに対応する

ですが、Nanocは、自分で簡単にフィルターを書くことができます。

フィルターは、lib/nanoc/filters/ の下に、.rb なファイルを作り、identifier に名前をつけて、それを、Rules ファイルの中で使うだけです。

もともと、markdownをhtmlに変えるのは、kramdownというフィルターがありますが、このソースコードをコピーしてきて、下記のパッチを当てて、適当なファイル名で保存すればOKです。

@@ -2,10 +2,10 @@
 
 module Nanoc::Filters
   # @api private
-  class Kramdown < Nanoc::Filter
-    identifier :kramdown
+  class KramdownPlantUML < Nanoc::Filter
+    identifier :kramdownplantuml
 
-    requires 'kramdown'
+    requires 'kramdown-plantuml'
 
     # Runs the content through [Kramdown](https://kramdown.gettalong.org/).
     # Parameters passed to this filter will be passed on to Kramdown.

単に、名前を変えているのと、require しているものをkramdownからkramdown-plantumlに変えるだけですね。

そして、Rules のほうで、

  filter :kramdownplantuml, :input => "GFM"

のようにすれば、PlantUMLに対応することができます。

GraphViz::DBI で ER図を吐き出す

この記事は, Perl Advent Calendar 201920日目の記事です。 19日は、doikojiさんの、「WINI & cal: perlベースの新しい簡易マークアップ言語WINIで来年のカレンダーを作りましょう! 」でした。

この記事は、もともとWanoグループのAdvent Calendar に書くつもりでしたが、Perl の Advent Calendarに空きがあったので、 会社の方はRustの記事を載せました。よろしければ、そちらもどうぞ。

qiita.com

閑話休題

最近、DocBaseGrowiGitlabWiki (設定すれば)などPlantUMLでの表示をサポートするものが増えてきています。 自分の手でGraphVizのER図を書くのも可能ですが、正直、だいぶ面倒ですし、テーブル数が多いとやる気が出ません。

PerlGraphViz::DBIでさくっと書いてみましょう。

ですが、僕の担当している、とあるシステムはテーブル数が1000近くある関係で、はきだされたものをそのままPlantUMLに渡すと、長すぎて壊れてしまいました。 そらそうですね。

これは同種のテーブルが負荷対策の関係で分けたりしているのが原因で非常に多くなっちゃってるのですが、もし同様なケースでしたら、似たようなテーブルはグルーピングするなりの処理をしたり、 ER図をいくつかのグループに分割するなどしたほうが良いですね(例えば、カテゴリだけをグルーピングするとか、特定機能に関わるテーブルだけをグルーピングするなど)。

下記のようにグルーピングの定義をしてやると良いと思います。 この時に、似たようなテーブルがいくつかある場合は、1つのテーブルのみをグループの中に入れてやれば良いでしょう。

tie my %GROUP_TABLES, 'Tie::IxHash';
%GROUP_TABLES = (
    # グルーピング => 表示したいテーブルの正規表現を書く(※正規表現は、`schema`.`table_name` にマッチするようにする)
    'グループ1'       => qr{},
    'グループ2'       => qr{},
  );

出力時に下記のようにすれば、各グループ毎にER図を出力できます。

foreach my $group (keys %GROUP_TABLES) {
    my $filter = $GROUP_TABLES{$group};
    my $g = __PACKAGE__->new($dbh);
    $g->{tables} = [ grep {$_ =~ $filter} $g->get_dbh->tables];
    my $txt = $g->graph_tables->as_text;

 # これは、表示を簡略化するためなので、グルーピングとは関係ないです。 
   # `schema`.`table_name` => table_name だけにしています
    $txt =~ s{`$SCHEMA`\.`(.+?)`}{$1}g;
    print "## $group\n\n";
    print "```plantuml\n\@startuml\n", $txt, "\n\@enduml\n```\n\n";
}

他にも、削除フラグとか更新日時とか、ほとんどのテーブルにあるけど、特に表示したくないものもあるでしょうから、

my %IGNORE_COLUMNS = (
  updated_at => 1,
  delete_flag => 1,
  created_at => 1,
);

のように定義しておいて、graph_tables をオーバーライドして、調整するとよいかもしれません(後のコードを参照)。

また、GraphViz::DBI の外部キーの実装は、下記のようになっています。

sub is_foreign_key {
    # if the field name is of the form "<table>_id" and
    # "<table>" is an actual table in the database, treat
    # this as a foreign key.
    # This is my convention; override it to suit your needs.

    my ($self, $table, $field) = @_;
    return if $field =~ /$table[_-]id/i;
    return unless $field =~ /^(.*)[_-]id$/i;
    my $candidate = $1;
    return unless $self->is_table($candidate);
    return $candidate;
}

コメントを訳すと、

フィールド名が、"<table>_id"で、"<table>"が実際にDBに存在する
テーブルの場合、これを外部キーとします。
これは、自分の慣例なので、必要に応じてオーバーライドしてください。

とした、ざっくりな感じなので、コメントの通り継承してオーバーライドしました。

my %SPECIAL_FOREIGN_KEY = (
   '特殊な命名の外部キー1' => "`$SCHEMA`.`テーブル`", # 外部キーの参照しているテーブル
   '特殊な命名の外部キー2' => "`$SCHEMA`.`テーブル`",
   # ...
);

sub is_foreign_key {
    my ($self, $table, $field) = @_;

    my $candidate;
    if (not $candidate = $SPECIAL_FOREIGN_KEY{$field}) {
    # TABLE_NAME_ID は TABLE_NAME の外部キーとみなす
        return unless $field =~ /^(.*)[_-]id$/i;
        $candidate = "`$SCHEMA`.`$1`";
    }
    return unless $self->is_table($candidate);
    return $candidate;
}

僕も同じような慣例なので、例外的なものを追加するくらいでOKでした。

全体像は、こんな感じになります。

use strict;
use DBI;
use parent 'GraphViz::DBI';

my $SCHEMA = "スキーマ";
my $dbh = DBI->connect("dbi:mysql:$SCHEMA;host=DB_HOST", 'user', 'password');

use Tie::IxHash;

tie my %GROUP_TABLES, 'Tie::IxHash';
%GROUP_TABLES = (
    # グルーピング => 表示したいテーブルの正規表現を書く
    'グループ1'       => qr{},
    'グループ2'       => qr{},
  );

my %SPECIAL_FOREIGN_KEYS = (
   '特殊な命名の外部キー1' => "`$SCHEMA`.`テーブル`", # 外部キーの参照しているテーブル
   '特殊な命名の外部キー2' => "`$SCHEMA`.`テーブル`",
   # ...
);

my %IGNORE_COLUMNS = (
  created_at => 1,
  delete_flag => 1,
  updated_at => 1,
);


sub is_foreign_key {
    my ($self, $table, $field) = @_;

    my $candidate;
    if (not $candidate = $SPECIAL_FOREIGN_KEYS{$field}) {
    # TABLE_NAME_ID は TABLE_NAME の外部キーとみなす
        return unless $field =~ /^(.*)[_-]id$/i;
        $candidate = "`$SCHEMA`.`$1`";
    }
    return unless $self->is_table($candidate);
    return $candidate;
}
sub graph_tables {
    my $self = shift;

    my %table = map { $_ => 1 } $self->get_tables;

    for my $table ($self->get_tables) {
        my $sth = $self->get_dbh->prepare(
            "select * from $table where 1 = 0");
        $sth->execute;
        my @fields = @{ $sth->{NAME} };
        $sth->finish;

        my $label = "{$table|";

        for my $field (@fields) {
            next if $IGNORE_COLUMNS{$field};
            $label .= $field.'\l';
            if (my $dep = $self->is_foreign_key($table, $field)) {
                $self->{g}->add_edge({ from => $table, to => $dep });
            }
        }

        $self->{g}->add_node({ name => $table,
                               shape => 'record',
                               label => "$label}",
                           });

    }
    return $self->{g};
}

foreach my $group (keys %GROUP_TABLES) {
    my $filter = $GROUP_TABLES{$group};
    my $g = __PACKAGE__->new($dbh);
    $g->{tables} = [ grep {$_ =~ $filter} $g->get_dbh->tables];
    my $txt = $g->graph_tables->as_text;
    $txt =~ s{`$SCHEMA`\.`(.+?)`}{$1}g;
    print "## $group\n\n";
    print "```plantuml\n\@startuml\n", $txt, "\n\@enduml\n```\n\n";
    # Growiならこっち
    # print "\@startuml\n", $txt, "\n\@enduml\n\n";
}

こんな感じでやると、下記のようなER図になります(自分で作ってたサービスのDBです)。

f:id:ktat:20191220111301p:plain PlantUMLで表示

ちなみに、GraphViz::DBIは、13年前の更新が最後ですが、最新のPerlでも動くと思いますよ(5.8.9 でも、5.25.1でも動いたので)。

これを定期的に回して、Wikiのページも自動的に更新とかしてやれば、良いかもしれません。 ER図をDBの情報を読み取って吐き出すツールは普通にありますが、テーブル数が多いと、使い物になりませんので、こんな感じで特定部分をフィルタリングして出すのも割と有用だと思いますよ。

21日のPerl Advent Calendar 2019はMorichanさんによる「PerlだけでWebサイトを作る - Qiita」です。

tcat という行頭に日時を付加するコマンドを作った

cat のように標準入力かファイルの中身を出力するものですが、先頭行に日時がつきます。
https://github.com/ktat/tcat にあります。

なんで、こんなものが必要かというと、例えば、サーバの様子を top -b を記録して後からみたいなーとかいう時とかに...

% top -b >> top.log

みたいにしたすると、ファイルの中身はこんな感じ。

top - 00:44:33 up 16:59,  7 users,  load average: 1.76, 1.75, 1.62
Tasks: 345 total,   1 running, 344 sleeping,   0 stopped,   0 zombie
%Cpu(s): 30.3 us, 11.1 sy,  0.0 ni, 57.9 id,  0.4 wa,  0.0 hi,  0.2 si,  0.0 st
KiB Mem : 16312908 total,   409000 free,  9663728 used,  6240180 buff/cache
KiB Swap: 16657404 total, 16655344 free,     2060 used.  4595576 avail Mem 

  PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
 9618 ktat      20   0   42952   3800   3140 R  12.5  0.0   0:00.03 top
 5253 ktat      20   0 1745952 424904  69868 S   6.2  2.6   6:14.04 slack
 5306 ktat      20   0 1497416 376924 181836 S   6.2  2.3  17:12.10 slack
    1 root      20   0  185412   6012   3944 S   0.0  0.0   0:04.64 systemd
    2 root      20   0       0      0      0 S   0.0  0.0   0:00.04 kthreadd

後から見ようとした時に、どの時間のものだったのか調べようとすると....

top - 00:44:33 up 16:59,  7 users,  load average: 1.76, 1.75, 1.62

ここにあるけど、とっても使いにくいし、日またいだら、どうするんだという感じなので...

% top -b | tcat >> top.log

とすることで、

2017-01-25 00:48:25: top - 00:48:25 up 17:03,  7 users,  load average: 1.29, 1.63, 1.62
2017-01-25 00:48:25: Tasks: 341 total,   1 running, 340 sleeping,   0 stopped,   0 zombie
2017-01-25 00:48:25: %Cpu(s): 30.1 us, 11.0 sy,  0.0 ni, 58.3 id,  0.4 wa,  0.0 hi,  0.2 si,  0.0 st
2017-01-25 00:48:25: KiB Mem : 16312908 total,   582792 free,  9512076 used,  6218040 buff/cache
2017-01-25 00:48:25: KiB Swap: 16657404 total, 16655352 free,     2052 used.  4772668 avail Mem 
2017-01-25 00:48:25: 
2017-01-25 00:48:25:   PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
2017-01-25 00:48:25:  9850 ktat      20   0   42952   3732   3076 R  12.5  0.0   0:00.03 top
2017-01-25 00:48:25:     1 root      20   0  185412   6012   3944 S   0.0  0.0   0:04.64 systemd
2017-01-25 00:48:25:     2 root      20   0       0      0      0 S   0.0  0.0   0:00.04 kthreadd
2017-01-25 00:48:25:     3 root      20   0       0      0      0 S   0.0  0.0   0:08.62 ksoftirqd/0
2017-01-25 00:48:25:     5 root       0 -20       0      0      0 S   0.0  0.0   0:00.00 kworker/0:0H

のように記録される(日付のフォーマットは自由に変えられます)ので、

% grep '2017-01-25 00:48' top.log

のようにして、検索しやすくなります。

で、作ってから気づいたのですが、まったく同じ名前の同様のコマンドが C で書かれていました。
https://github.com/marcomorain/tcat

orz

とりあえず、違いを出すために、cat のオプションを全て実装してみました("-n" 以外使ったことないけど)。
"-v", "-E", "-T" あたりの実装は、cat のソースコードからロジックをそのまま移した感じになります。

Go言語さわってみた

一年前(2014年5月末の作成日の.goなファイルがあった)くらいにGo Tourを途中までやったけど、ほとんど覚えてなかったです。
ディレクトリとか環境変数とかは、前若干コード書いたので、それっぽく残ってました。emacsのgo-modeも入ってた。
という状況から、ちょうど一週間くらいたった感じです。

作ったもの

https://github.com/ktat/go-coloring テキストを正規表現で色付けするもの。そういうツールってあるっけ?
https://github.com/ktat/go-pager ↑のやつにlessっぽくしようとしたpagerを組み込んでいたけど、分離した(go-termboxを利用)。

コードは、まぁ、まだまだアレというか間違ってる/分かってない可能性が高いです。

参考にしたところ/したいところ

golang.org には書いてるけど、golang.jp には書いてないこともあったので、
golang.org 見たほうが良いんじゃないかなとか思う。

作法

変数/関数の命名規則などが慣習も含め決まっているので先に読んだほうが良いと思う。

https://golang.org/doc/effective_go.html#names

例えば、セッターは

 SetParamName

だが、ゲッターは

 GetParamName

ではなく、単純に

 ParamName

とする。

パブリックなものは大文字から、プライベートなものは小文字の名前をつける(メソッドも構造体の変数も)

gofmt というコマンドがあるので、それで整形を行う(必ず)。インデントはタブです。

コンパイル時のチェック

使ってないパッケージが import されてたり、宣言されただけで使ってない変数とかがあると、コンパイル通らない。

ファイル構成

詳しくはこちらをご覧ください。
https://golang.org/doc/code.html

 go/src/github.com/ユーザー名/プロジェクト名/ソースコード.go

にしておいて、

 GOPATH=/home/USER_NAME/go

としておくと、$GOPATH/src/ $GOPATH/pkg/ 以下にソースコードコンパイル済みのライブラリがあると想定する。

 import (
   "github.com/mattn/go-getopt"
 )

のように import すると、

 /home/USER_NAME/go/src/github.com/*/
 /home/USER_NAME/go/pkg/ARCHITECTURE/github.com/*/

以下の *.go, *.a ファイルが読まれるもよう。

コンパイル

  % go build 名前.go

  名前

の実行ファイルができあがる。

即時実行したければ、

 % go run ファイル名 引数

とすればよい。今のところ go run ばっか使ってます。

クォート

シングルクォートはルーン(rune)というもの(int32のalias)で、ユニコードのコードポイント

 fmt.Println('a') // 97 == 0x61

ダブルクォートはstring 。byte(uint8 の alias)の配列

 "12345"
 "12345"[1] == '2'

以下のような比較はエラー

 "あ"[0] == 'あ' // overflows uint8

"あ"[0] の0番目のバイト(uint8)と 'あ'(rune(int32のalias))の比較になるため

バッククォートは複数行にまたがって書ける。

  fmt.Println(`...
 ...
 `)

println

プリントデバッグで使ったりしますが、長い文字列を渡すと

 [string too long]

と文句言われる。

 fmt.Println(`...
 ...
 `)

は問題ない。

main

実行したいというときは、main package に main 関数をつくればよい。

// test.go
package main
    
func main () {
    // なんか書く   
}

そして、以下で実行できる。

go run test.go

変数宣言

 var hoge string = "abcd"
 hoge := "abcd"

:= は型推論してくれる。
最初の代入で使うものなので、同一スコープで := を再度使うとエラー。var hoge の後に := 使ってもエラー。

スコープはブロックスコープ(https://golang.org/ref/spec#Declarations_and_scope)なので、以下はセーフ。

	var str string = "abcde"
	println(str[3:4])
	{
		str := "eee"
		println(str)
	}

以下のように複数の宣言もできる。

 var (
   hoge string
   foo bool
 )

トップレベルで宣言するときは、:= は使えない。

 var hoge string
 var hoge string = "aaa"

blank idenitifier

"_"はblank idenitifier(https://golang.org/ref/spec#Blank_identifier)であり、無視される。

  i, _ :=  strconv.Atoi("12")

配列

	array := make([]string, 0)

追加するときは

	array = append(array, "hogehoge")

map

ハッシュ的なもの。
https://blog.golang.org/go-maps-in-action

  dict := map[string]string {
      "key": "value"
  }

  result := make(map[string]string)
  result["a"] = "b"
キーの存在チェック
 if _, ok := dict["key"]; ok {
    println("key exists")
 }

http://stackoverflow.com/questions/2050391/how-to-check-if-a-map-contains-a-key-in-go

キーの削除
 delete(mapVar, "a")
map の値に関数

はもたせられない(nested func not allowed と言われる)

 map[string]func

みたいな定義はできるけど、代入したらエラーになる。
定義できる意味ないんじゃ...?
牧さんからコメントいただいて、出来るということです。
http://play.golang.org/p/voWrwbQ2oe

 test := map[staing]func() {}
 test["a"] = func () {println(1)}
 test["a"]()

みたいに、引数とかを書いてなかったからだそうです。言われてみれば、そらそうだーという感じがしました。
funcの定義を書くわけなので、別のタイプの関数は取れません。

	test := map[string] func() {}
	test["aiueo"] = func() {println(123)} // これはいいけど
	test["akasa"] = func(x string) {println(x)} // これはダメ

最初にググってでてきた、interface を使う方法は、どんなタイプの関数でも入れれるといえば入れれる(使いドコロがあるのかは知らないけど)

	test := map[string] interface{} {}
	test["aiueo"] = func() {println(123)}
	test["akasa"] = func(x string) {println(x)}
	test["aiueo"].(func () )()
	test["akasa"].(func (x string))("a")

追記終わり

interaface とやらを使うらしい(http://stackoverflow.com/questions/6769020/go-map-of-functions)。

	colorFunc := map[string]interface{}{
		"red":    func(s coloring.String) string { return s.Red() },
		"green":  func(s coloring.String) string { return s.Green() },
		"blue":   func(s coloring.String) string { return s.Blue() },
		"yellow": func(s coloring.String) string { return s.Yellow() },
		"white":  func(s coloring.String) string { return s.White() },
		"cyan":   func(s coloring.String) string { return s.Cyan() },
		"black":  func(s coloring.String) string { return s.Black() },
		"purple": func(s coloring.String) string { return s.Magenta() },
	}

これを呼ぶときは、

	colorFunc["red"].(func (colorling.string) string)(val)

のように呼ぶ。

メソッドリストのない空のinterface は何が来てもOKだから、func も入るということかと思う。

型変換

	[]byte(str)
	string(someByte)

数値 <-> 文字列

  str := strconv.Itoa(12)
  i, err :=  strconv.Atoi("12")

文字列連結

 "hoge" + "fuga"

関数

  func 名前 (型) 戻り型 {
       return ...
  }

戻り値が複数の時は

  func 名前 (型) (戻り型,戻り型) {
       
       return ...
  }

戻り値にも名前をつけられる

  func 名前 (型) (名前 戻り型, 名前 戻り型) {
       
       return // 変数を渡さなくても良い
  }

パッケージの関数

 packageName.FunctionName()

で呼べる。

パッケージの型とメソッド

 package hogehoge

 type String string

 func (i String) Name String{
      // ...
 }

とかすると

 hogehoge.String("hoge").Name()

とかって使える。

ループ

for しかない

ノーマル
 for i := 0; i < len(str); i++ {

 }
無限ループ
 for {

 }

break で止める
continue で次のループへ
goto ラベルへ

break, continue も ラベルを取れる。
ラベルは以下のように記述。

 Loop: 
配列の取り出し

indexのみを取る

 for i := range array { }

indexと値

 for i, v := range array { }
map取り出し

キーのみ

 for k := range array { }

キー、バリュー

 for k, v := range map { }

if

 if len(str) == 0 {
 
 } else if ... {
 
 } else {
  
 }

条件は bool でないとダメ

switch

case のところは同一タイプじゃないとダメらしい。

        switch c {
        case 'f':
                fileName = OptArg
        case 'h':
                usage()
                os.Exit(1)
        default:
		if color, ok := colorMap[string(c)]; ok {
			replace = append(replace, fmt.Sprintf("(?P<%s>%s)",  color, OptArg))
		} else {
			os.Exit(1)
		}
        }

三項演算子

ありません。

substring

スライスで取る。

	var str string
	str = "abcde"
	println(str[0:3]) // abc
	println(str[3:4]) // d
	println(str[4:5]) // e

文字列分割

    strings.SplitN(str, sep, -1)

最後の引数はsplitする数。-1だと以下と同じ。

    strings.Split(str, sep)

http://golang.org/pkg/strings/#SplitN

文字列連結(strings.Join)

    strings.Join(array, sep)

http://golang.org/pkg/strings/#Join

正規表現

関数がいっぱいあるなー。

http://tip.golang.org/pkg/regexp/
http://tip.golang.org/pkg/regexp/syntax/
https://gobyexample.com/regular-expressions

regexp.Compile

変な正規表現渡しても死なないので、自分でハンドリング。

        re,regexpErr := regexp.Compile(pattern)

        if regexpErr != nil {
                log.Fatal(regexpErr)
		// しかし、log.Fatal や os.Exit は使わないほうがいいらしい
        }
regexp.MustCompile

変な正規表現渡すとエラーで死ぬ。
どっかからもらったんじゃなくて、自分で書いた正規表現はこっちで良いでしょう。

        re := regexp.MustCompile(pattern)
置換

以下、全て同じ

   regexp.MustCompile("^(a)(b)").ReplaceAllString("abcdef", "$2$1")
   regexp.MustCompile("^(a)(b)").ReplaceAllString("abcdef", "${2}${1}")
   regexp.MustCompile("^(?P<first>a)(?P<second>b)").ReplaceAllString("abcdef", "$second$first")
 (?P<名前>パターン)

は、名前付きキャプチャ。

マッチしたものを後で使う
	re := regexp.MustCompile("^(?P<first>a)(?P<second>b)")
	match := re.FindSubmatchIndex([]byte("abcdefab"))

	var dst []byte
	dst = re.ExpandString(dst, "$second$first", "abcdef", match)
	fmt.Println(string(dst))

http://astaxie.gitbooks.io/build-web-application-with-golang/content/ja/07.3.html

regexp関係のFuncと付く関数でやっても意味ないと思われる。

フラグは先頭につける
 (?フラグ)パターン

以下のような感じ。

 (?im)^[a-z]+$

エラーハンドリング

error という型がある。log.Fatal に投げるとエラーメッセージを出してくれる

	log.Fatal(err)
	// 既述ですが、log.Fatal や os.Exit は使わないほうがいいらしい

以下を読むと良いのかもしれません。
http://blog.golang.org/error-handling-and-go

ファイルの読み取り

	whole, err =  ioutil.ReadFile(ファイル名)

http://golang.org/pkg/io/ioutil/

標準入力全部読み

	whole, err = ioutil.ReadAll(os.Stdin)

http://golang.org/pkg/io/ioutil/

一行ずつ

	scanner := bufio.NewScanner(os.Stdin)
	for scanner.Scan() {
		var l = scanner.Text()
	}

http://qiita.com/hnakamur/items/a53b701c8827fe4bfec7

コマンドライン引数

flag パッケージ

標準で flag というパッケージががある(使わなかったので見てないです)

https://golang.org/pkg/flag/

go-getopt

mattnさんの mattn/go-getopt が使える

https://github.com/mattn/go-getopt

example に使い方あり。

https://github.com/mattn/go-getopt/blob/master/example/example.go

 OptEror = 0 

にすると、エラーメッセージ(おかしなコマンドライン引数を渡した時とか)がでなくなる。
おかしなコマンドライン引数を渡した時を判別する場合は、switch の default でやれば良いかも。

ポインタ

以下のような感じで戻り値を受け取らずに、files にファイルを突っ込める。
ただ、こういうケースでポインタ使うのは間違いらしい。
http://qiita.com/ruiu/items/e60aa707e16f8f6dccd8

  seekDir(&files, "./")
  
  func seekDir (files *[]string, dirName string) {
  	fileInfo, ioerr := ioutil.ReadDir(dirName)
  	errCheck(ioerr)
  	for i := 0; i < len(fileInfo); i++ {
  		if fileInfo[i].IsDir() == false {
  			*files = append(*files, dirName + "/" + fileInfo[i].Name())
  		} else {
  			seekDir(files, dirName + "/" + fileInfo[i].Name())
  		}
  	}
  }
 &変数

が、ポインタ

 *変数

で、デリファレンス

メソッド

型に対してメソッドを作れる

 type S string
 
 func (s *S) aaaa () {
      fmt.Println("=== S aaaa ===")
      fmt.Println(s)
 }
 
 func main() {
      var str S = "Type S"
      S.aaaa()
 }

struct で名前なしで型を埋め込むことで、埋め込んだ型のメソッドを委譲できる。

 type S2 struct {
      str string
      S
 }
  
 func (s *S2) bbbb () {
      fmt.Println("=== S bbbb ===")
      fmt.Println(s.str)
 }
 
 func main() {
      var str S2
      S2.str = "Type S2"
      S2.aaaa() // 委譲したもの
      S2.bbbb() // S2のメソッド
 }

以下のように "*S2" じゃなくて、"S2" のようにもかけるけど、そうすると、メソッド内でメンバを書き換えたりしても、呼び出し元は変わらない。

 func (s S2) cccc() string{
   s.str = "cccc"
   println(s.str) // cccc
   return s.str
 }
 
 var s S2
 s.str = "s2"
 println(s.cccc()) // cccc
 println(s.str)    // s2

Go言語はオブジェクト指向かどうかという話は、以下。
http://golang.jp/go_faq#Is_Go_an_object-oriented_language

インターフェイス

インターフェースはメソッドの羅列。インターフェースに定義されているメソッドを実装すれば、
インターフェースを受け取れる関数を受け取れる。
※ちなみに以下は微妙な例になっておりますが、書き直すのがだるいのでそのままです。

Animalizer インターフェースを実装してみた

Moveメソッドがインターフェースのメソッドとして定義されている。
AsAnimal メソッドは引数に Animalizer インターフェースを受け取れる。

  package animal
  
  import "fmt"
  
  type Animalizer interface {
  	Move()
  }
  
  type Animal struct {}
  
  func (a Animal) Move (){
  	fmt.Printf("move as animal %T\n", a);
  }
  
  func AsAnimal (a Animalizer) {
  	fmt.Printf("animal is passed %T\n", a)
  }
Humanizer インターフェースを実装してみた

Talk と Move メソッドがインターフェースのメソッドとして定義されている。
AsHuman メソッドは引数に Humanizer インターフェースを受け取れる。

  package human
  
  import "fmt"
  
  type Humanizer interface {
  	Talk()
  	Move()
  }
  
  type Human struct {}
  
  func (h Human) Move (){
  	fmt.Printf("move as human %T\n", h);
  }
  
  func (h Human) Talk (){
  	fmt.Printf("I'm human %T\n", h)
  }
  
  func AsHuman (h Humanizer) {
  	fmt.Printf("human is passed %T\n", h)
  }
使ってみる
  package main
  
  import (
  	"github.com/ktat/test/animal"
  	"github.com/ktat/test/human"
  )
  
  var a animal.Animal;
  var h human.Human;
  
  func main () {
  	a.Move()
  	h.Move()
  	h.Talk()
  	animal.AsAnimal(a)
  	animal.AsAnimal(h)
  	human.AsHuman(h)
  }

結果

move as animal animal.Animal
move as human human.Human
I'm human human.Human
animal is passed animal.Animal
animal is passed human.Human
human is passed human.Human

仮に、

  	human.AsHuman(a)

なんてことをすると、

 ./test.go:18: cannot use a (type animal.Animal) as type human.Humanizer in function argument:
         animal.Animal does not implement human.Humanizer (missing Talk method)

と怒られる。

インターフェイスに定義されたメソッドを実装することで、同一のインターフェースのものということになる。
ロボット型に人のインターフェースをもたせれば人として扱える。
なんか面白いですね。

ちなみに、命名規則的に interface は、-er でつけて、- なメソッドを持つ(Stringer は、Stringを持つ)となってるので、上記は変な例だと思います。

例えば、Stringer インターフェースは、

 func String string {}

なメソッドを持っています。

ついでに説明すると、適当な型に

 type AnyType {
   str string
 }
 
 func (a AnyType) String string {
    return a.Str
 }

のようなメソッドを作ってやれば
fmt.Printf は、Stringer インターフェースを受け取ることができるため、

 var a AnyType
 a.Str = "1234"
 fmt.Printf("%s", a)

と渡すことが出来ます。

なお、以下のようなメソッド定義だと、なんでも受け取れるようになります。

 func (h Human) Any (i interface{}) {
 
 }

なお、インターフェースを満たすというのはメソッドを満たせばいいだけなので、単純に型を埋め込んでやるだけでも、インターフェースのメソッドを満たすことが出来ます。

type Human struct {
	animal.Animal
}

インターフェースを満たすだけであれば、これだけでも良いわけですが、結果はこうなります。

move as animal animal.Animal
move as animal animal.Animal   // さっきは、 move as human human.Human
I'm human human.Human
animal is passed animal.Animal
animal is passed human.Human
human is passed human.Human

メソッドは委譲されていますが、もともと、

func (a Animal) Move (){
	fmt.Printf("move as animal %T\n", a);
}

という定義なので、a は Animal なのです。


次は、gorutine とかチャンネルとか触ってみると思います(書くかは分かんないけど)。