GoでCSVファイルを処理するCLIのツール(csvt)を作りました

CSVファイルを処理するcsvtというCLIのツールをGoで作りました。
作り始める時点で用意しようと思っていた機能が揃ったので紹介します。
(2021-12-07追記) その後欲しい機能が増えて、サブコマンドが倍増したので、現在の情報で更新します。

csvt

csvtはCSVファイルを処理するCLIのツールです。
Goで実装しています。

Goの勉強のために個人で作ったものですが、今所属するプロジェクト内でも利用してもらっています。

下記のようなサブコマンドを用意しており、CSVを編集するうえでたいていのことは、これらコマンドで事足りるようになりました。

  • add 列を追加します。
    固定値や他の列の値のコピー、テンプレートを使った値の生成(複数列の値を結合したものとか)も出来ます。
  • choose 列を選択して新しいCSVファイルを作成します。
  • concat 2つのCSVファイルを結合します。
  • count レコード数をカウントします。
    列を指定して値が存在するものをカウントすることもできます。
  • exclude 他のCSVファイルに存在する行を除外します。
  • filter 条件に一致する行に絞り込みます。
    条件には正規表現や他の列との比較も書くことができます。
  • head 先頭の数行を表示します。
  • header CSVファイルのヘッダを表示します。
  • include 他のCSVファイルに存在する行に絞り込みます。
  • join 指定した列をキーとして、CSVファイルのレコードを結合します。
    EXCELのVLOOKUPに似ています。
  • remove 列を削除します。
  • rename 列名(ヘッダ)を変更します。
  • replace CSVファイルの値を正規表現を使って置換します。
  • slice 指定した範囲の行を切り出します。
  • sort 指定した列の値でソートします。
    複数の列を使ってソートすることもできます。
  • split 指定された行数毎に分割します。
  • transform CSVファイルのフォーマットを変更します。
    デリミタ、セパレータ、クォート、文字コードなどを変更することが出来ます。
  • unique 指定した列の値を利用して重複するレコードを取り除きます。

一番大変だった気に入っているコマンドはjoinです。
ExcelのVOOKUP的なことができます。

$ csvt join -1 INPUT1 -2 INPUT2 -c COLUMN -o OUTPUT
Usage:
  csvt join [flags]

Flags:
  -1, --first string     First CSV file path.
  -2, --second string    Second CSV file path.
  -c, --column string    Name of the column to use for joining.
      --column2 string   (optional) Name of the column to use for joining in the second CSV file. Specify if different from the first CSV file.
  -o, --output string    Output CSV file path.
      --usingfile        (optional) Use temporary files for joining. Use this when joining large files that will not fit in memory.
      --norecord         (optional) No error even if there is no record corresponding to sencod CSV.
  -h, --help             help for join

例えば、input1.csvとして下記内容のCSVファイルと、

UserID,Name,Age,CompanyID
1,"Taro, Yamada",10,2
2,Hanako,21,1
3,Smith,30,2
4,Jun,22,4

input2.csvとして下記内容のCSVファイルを用意して、

CompanyID,CompanyName
1,CompanyA
2,CompanyB
3,CompanyC
4,"AAA Inc"

CompanyIDの値を使って結合します。

$ csvt join -1 input1.csv -2 input2.csv -c CompanyID -o output.csv

できあがったoutput.csvは下記のような内容になります。
input1.csvを基準として、input2.csvの内容を足していくようなイメージです。

UserID,Name,Age,CompanyID,CompanyName
1,"Taro, Yamada",10,2,CompanyB
2,Hanako,21,1,CompanyA
3,Smith,30,2,CompanyB
4,Jun,22,4,AAA Inc

--usingfile というオプションを利用すると、メモリにファイル全体を載せることなく結合するので、どんな大きなファイルでも問題なく処理できます。(数GBのCSVファイルでも使用メモリは数十MB)

他のコマンドも含め、詳しい利用方法はREADMEをご参照ください。

csvt でサポートするフォーマット

csvt の特徴的なところとして、柔軟にフォーマットが指定できるというところがあります。

CSV、TSVといった良くあるフォーマットだけでなく、マイナーなフォーマットにも対応するために、フォーマットとして下記が指定できるようになっています。
全てのサブコマンド共通のフラグです。

Global Flags:
      --delim string      (optional) CSV delimiter. The default is ','
      --quote string      (optional) CSV quote. The default is '"'
      --sep string        (optional) CSV record separator. The default is CRLF.
      --allquote          (optional) Always quote CSV fields. The default is to quote only the necessary fields.
      --encoding string   (optional) CSV encoding. The default is utf-8. Supported encodings: utf-8, shift_jis, euc-jp
      --bom               (optional) CSV with BOM. When reading, the BOM will be automatically removed without this flag.

技術的なところ

Cobra によるサブコマンドの実装

Cobra を利用することで、CLIでのサブコマンドの実装が簡単にできました。

cobra add で追加される雛形だと、サブコマンド毎のテストが書きずらかったので変えています。
root側にサブコマンドに関する処理を追加しなければならないのが煩雑ですが、テスト毎にサブコマンドを生成したかったのでやもえずです。 (ほんとはもっと良い方法があるのかもしれない、、)

フラグをソートしたくなかったり、エラーメッセージの表示タイミング変えたりとか、、いろいろ調べることもありましたが、Cobraに関する情報はたくさんあったので、やり方が見つからないといったようなことはありませんでした。

Bolt を利用することで大きなmapをメモリから追い出す

joinsortコマンドでは、CSVのレコードを全部読み込んだうえで処理しなければならず、それをメモリに載せてしまうと、巨大なCSVファイルを処理するときにメモリが足りなくなりかねません。

キーバリューストアのBoltを使うことで、mapをメモリの外(ファイル)に追い出すような実装も用意し、オプションで切り替えられるようにしました。

BoltはAPIがシンプルなのもあってか、迷うことなく簡単に利用することができました。

ASCII Table Writer

headコマンドでCSVの先頭数行を表形式で表示したかったので、何か良いライブラリが無いかなと調べたところ、ASCII Table Writerというライブラリを見つけました。

全角文字にもちゃんと対応しているので、日本語で崩れるといったようなこともなく、簡単に表形式での表示が実現できました。

go-customcsv

細かいフォーマットのカスタマイズが encoding/csv だと出来ないので、別途CSVパーサを書いてそれを利用するようにしました。

他のCSVライブラリもいくつか確認しましたが、要件満たしている&良く使われてそうでこれなら安心そう、、ってのが見当たらなかったので、Goの勉強も兼ねてということで、、

第20回 redmine.tokyo勉強会で『Redmine issue assign notice plugin の紹介』というタイトルで発表しました

第20回 redmine.tokyo にて、『Redmine issue assign notice plugin の紹介』というタイトルでLTさせていただきました。

資料はこちら。

Redmine issue assign notice pluginは、チケットの担当者を切り替えて進めていくようなプロジェクトでは、とても有用なプラグインだと思っていますので、今回このLTを見て試してみようという方がいたらうれしいです。

f:id:onozaty:20210523182619p:plain

今回の勉強会は、ところどころ抜けながらの参加(リモートだから家のことを途中で出来るので助かる)でしたが、聴いた発表はどれも良いものでした。
その中でも特に skys さんの「新型コロナウイルス感染者情報管理」は、Redmineをカスタマイズすることで、コロナウイルス感染者情報管理するという、とても面白い事例でした。

Redmineのカスタマイズ性の高さからか、開発以外の用途でも活用できる事例は多いですね。

2021年3月から4月にZennで書いた記事

2021年3月から4月までの記事

ShortcutKey2URLを使ってブラウザ操作を快適に(Chrome/Firefox拡張機能)

愛用しているShortcutKey2URLというChrome/Firefox用の拡張機能の紹介記事です。
個人的にはとても便利だと思っているので、もっといろんな人に使ってもらえたらなぁと思っています。

Gradle Shadow Pluginで作成したfat/uber JARで、複数のJDBCドライバがロードできない

Gradle Shadow Pluginで、デフォルトだとMETA-INF/services配下が上書きされてしまい、ハマった際の記事です。
忘れたころにまた嵌りそうです、、

社内勉強会で「最近作ったもの」というタイトルで発表しました

社内勉強会で最近作ったものってタイトルで、Goで作ったツールやRedmine issue assign notice pluginについて発表しました。

www.slideshare.net

在宅勤務で通勤時間&肉体的な負荷が無くなったことで、趣味でコード書ける時間が今までより増えたなーって思います。
あと、Goはちょっとしたツールを作って配布するためのものが全て揃っていて素晴らしいです。

Goでファイル一覧+αを出力するCLIのツール(filist)を作りました

Goの勉強も兼ねて、適当な課題見つけてCLIツールをちょこちょこ作っているのですが、他の人が使っても便利かもしれないものが出来たので紹介します。

指定したディレクトリ配下のファイル一覧を再帰的に表示するツールです。

$ filist -s -M .
a.txt   24  3d3a42d900823afcfdfeb6de338bcec1
b/1.txt 81  ae23e0b40e773ac132f477f661e89b86
b/2.txt 163 494ba81d0d828ff9a244da627b5ece47

ファイルパス以外にも、サイズ、更新日時、MD5、SHA1などといった情報も一緒に出力できます。

オプションは下記の通りです。

Usage: filist [options] directory ...
options
  -r, --rel      Print relative path (If neither 'rel' nor 'abs' is specified, 'rel' will be printed first column.)
  -a, --abs      Print absolute path
  -s, --size     Print file size
  -m, --mtime    Print modification time
  -M, --md5      Print MD5 hash
  -S, --sha1     Print SHA-1 hash
      --sha256   Print SHA-256 hash
  -h, --help     Help

オプションの指定順に情報が並ぶので、例えば先頭にMD5で、その後にファイル名、、といった指定もできます。

$ filist -M -r .
3d3a42d900823afcfdfeb6de338bcec1  a.txt
ae23e0b40e773ac132f477f661e89b86  b/1.txt
494ba81d0d828ff9a244da627b5ece47  b/2.txt

全てのファイルにmd5sumかけたい、、っていうようなときに、コマンド組み合わせてワンライナーで書けなくないですが、こういったツール用意しておくと便利かなーって思って作りました。
Goだと各OS用の実行ファイルをシングルバイナリで簡単に提供できるってのがほんと素晴らしいです。

Redmine issue assign notice plugin の v1.2.0 をリリースしました

チケットの担当者が変わったときに、SlackやRocket.Chat、Teamsなどに通知するプラグイン、Redmine issue assign notice pluginのv1.2.0をリリースしました。

Google Chatのメンションに対応しました。
Google ChatのIncoming Webhookの設定などは、下記をご参照ください。

また、下記の2つの問題を修正しています。

  • Rocket.Chat にて、"[]"がエスケープされることで \[KaTeX\] 表示となってしまう。
  • ドキュメントルート以外でRedmineがデプロイされている場合、カスタムフィールド作成へのリンクが正しく動作しない。

2021年1月から2月にZennで書いた記事

Zennで書くことについて

2021年から、技術的な記事(書くのに多少時間がかかるようなもの)は、Zennで書くことにしました。

Zennがとても良いと思ったのは、GitHubで記事が管理できるというところです。

使い慣れたエディタで、時間をかけて(必要に応じてコミットして)記事を書けるというのは、とても便利です。
また、GitHubで草が生えるのも、モチベーションの維持につながっています。

とはいえ、メインは今まで通り はてなBlog のままにするので、時々Zennで書いたことを、こちらのBlogでもリンクとして記載していこうと思います。

2021年1月から2月までの記事

PostgreSQLのtext型に対するINDEX

PostgreSQLのtext型に対してINDEXを張るといった記事です。あまりtext型に対してINDEXを張りたいって機会は無いとは思いますが、、そういった状況になった際に、参考になるかもしれません。
あとMySQLのプレフィックス長指定でINDEXを張れるのはとても便利なので、PostgreSQLにもぜひ入って欲しいです。

利用ライブラリをMavenリポジトリ形式でプロジェクト内のファイルとして保持する(JCenterシャットダウンに向けて)

ローカルにMavenリポジトリ形式でディレクトリ作っておいて、そこでライブラリを管理する方法について書いた記事です。
JCenterでしか公開されていないようなライブラリが、そのままインターネットから消えてしまうような場合になった際の回避策として書きました。

必要なライブラリをMavenリポジトリとして必要なディレクトリ構成でダウンロードするツールも作りました。

各チャットツールのIncoming Webhoookのまとめ

Redmine issue assign notice というRedmineのプラグインを公開しているのですが、こちらで各チャットツールのIncoming Webhookを使うにあたって、やり方を毎回調べる羽目になってしまっていたので、記事にまとめました。