次ログ

次ログ

ゆるりと働いているSREの技術ブログのような何か。趣味の話も書く

テキストを左右中央寄せするalignコマンドをGoで作った

テキストを左右中央寄せするalignコマンドをGoで作りました。 なぜ作ったのか、と何ができるのか、について記載します。

作ったもの:

github.com

なぜ作ったのか

シェル芸bot環境で位置揃えを簡単にできるようにしたかったからです。 シェル芸bot環境では基本的にTwitterの140文字が文字数の限界で (引用リツイートを使うことで140文字以上入力することも可能ですが) 文字数を少しでも削りたい、という思いで作りました。

位置揃えの例

例えば以下のテキストがあります。

12345
abc
zzzzzzzzzzzzzzz

このテキストを右揃えするシェルは下記のようになります。

#!/bin/bash

align_right() {
  local p="$1"
  local max_line_width=0
  local lines=()
  while read -r line; do
    lines+=("$line")
    width=$(echo "$line" | wc -c)
    if [ "$max_line_width" -lt "$width" ]; then
      max_line_width=$width
    fi
  done

  for line in ${lines[@]}; do
    width=$(echo "$line" | wc -c)
    diff=$((max_line_width - width))
    pad=$(seq $diff | xargs -I@ echo -n "$p")
    echo "$pad$line"
  done
}

cat << EOS | align_right " "
12345
abc
zzzzzzzzzzzzzzz
EOS

無事実装できました。 中央寄せも同じように実装できます。

ただ毎回こんなのを実装するのも面倒ですし、 これだど日本語が混在するテキストの場合にきちんと位置を揃えられません。 位置揃えに使える文字も半角文字に限定されます。

入力のテキストに全角文字が混在していてもいい感じに処理できて、 位置揃えに全角文字も指定できるようにしたのが 今回作成したalignコマンドです。

alignコマンドの使い方

前述の例をalignを使うように書きかえると以下のようになります。

cat << EOS | align right -p " "
12345
abc
zzzzzzzzzzzzzzz

実行結果はこちら。

          12345
            abc
zzzzzzzzzzzzzzz

サブコマンドにはleft, center, rightが指定できます。 全角文字が混在する場合の例は下記。

cat << EOS | align right -p " "
あいうえお
abc
zzzzzzzzzzzzzzz
EOS

実行結果はこちら。

     あいうえお
            abc
zzzzzzzzzzzzzzz

無事、きちんと位置を揃えられています。 ※はてなブログ上ではフォントの関係でずれて表示されてますが・・・。

実装

位置を揃えるロジックについては前述のbashのコードと 同じようなことをやっています。

  1. 一番長い文字幅を取得する
  2. 差分を文字で埋める

重要なのは「文字幅をどう取得するか」です。

alignではgo-runewidthという外部ライブラリを使用することで 文字列の見た目上の文字幅を取得しています。

go-runewidthでは「全角文字なら文字幅2」「半角文字なら文字幅1」という具合に 見た目上のテキストの幅を返してくれます。 これを利用し、位置を揃えるようにしています。

Goで実装した箇所を抜粋します。

func MaxStringWidth(lines []string) (max int) {
    for _, v := range lines {
        l := runewidth.StringWidth(v)
        if max < l {
            max = l
        }
    }
    return max
}

// AlignRight は文字列を右寄せする。
// 右寄せは見た目上の文字列の長さで揃える。
// length = -1のときは、引数文字列の最長の長さに合わせる。
// padは埋める文字列を指定する。埋める文字が見た目上でマルチバイトの場合は
// たとえlengthが奇数でも+1して偶数になるように調整する。
func AlignRight(lines []string, length int, pad string) []string {
    if length == 0 || len(lines) < 1 {
        return lines
    }

    // 空白埋めする文字列がマルチバイト文字かどうか
    padWidtn := runewidth.StringWidth(pad)
    padIsMultiByteString := padWidtn == 2

    // -1のときは文字列の長さをalignの長さにする
    // パッディングの長さと、処理対象の文字列のより長い方を揃える数値に指定
    maxWidth := MaxStringWidth(lines)
    if length == -1 {
        length = maxWidth
    } else if length < maxWidth {
        length = maxWidth
    }

    // マルチバイト文字を使うときは長さを偶数に揃える
    if padIsMultiByteString && length%2 != 0 {
        length++
    }

    ret := []string{}
    for _, line := range lines {
        l := runewidth.StringWidth(line)
        diff := length - l
        if diff%2 != 0 {
            line = " " + line
            diff--
        }
        // Repeatするときにマルチバイト文字を使うときは2分の1にする
        if padIsMultiByteString {
            diff /= 2
        }
        s := strings.Repeat(pad, diff) + line
        ret = append(ret, s)
    }
    return ret
}

まとめ

自作のコマンドalignについてとその実装について一部紹介しました。

この程度ならシェルスクリプトだけでも実現できるようにも思いましたが Goの勉強もしたかったのでGoで実装しました。

余談

こういう自作のコマンドをはてなブログに書くかScrapboxに書くかQiitaに書くか悩みます。 前まではQiitaに書いてたけれど、自作のコマンドの紹介とかははてなブログに 書いたほうが良いみたい。

まぁQiitaは一般的な技術的TIPS、事実を述べる場所で 自作のコマンドは「作った人個人」に紐づくと考えると ブログに書くのが適当なのも納得がいきました。

Scrapboxに書くことも検討したんですが、Scrapboxは意図的に 外部に広く知ってもらう機能を実装していないそうです。 それだとせっかく作ったツールを知ってもらいたくても知ってもらえないように 感じたので書かないことにしました。

Scrapbox自体はすごく良いサービスなので、 社内WikiとかプロジェクトのWikiとしてはぜひ使ってみたいです。

物語におけるキャラ同士のやり取りをシーケンス図で整理する

はじめに

FF6みたいに複数のチームを切り替えながら操作するシーンであったり 世界崩壊後の仲間が散り散りになったシーンで、 キャラ同士のやり取りの整合性をキチンと取りながらストーリーを考えるにはどうすればいいだろう。

って考えた時に、シーケンス図を書くと割といい感じに整理できることに気づいた。 Twitterに数カ月前にちょろっとだけ載せたけれど、 せっかくだったのでキチンと整理して公開しておこうと思った次第。

目次

わかりにくいストーリーの例

あるところ日、青年田中は精霊祭の日に神のお告げを聞いた。 その日、田中は町をでた。100年に一度復活を遂げる魔王を倒すために。 田中は最初の町で魔王の手先を倒した。
田中は2つめの町で魔王の手先と対峙した、しかし敵は強かった。
その時、偶然居合わせた山田と鈴木が助けてくれて、協力して敵を倒した。
山田と鈴木の話を聞くに、彼らの故郷は魔王の手下に滅ぼされたそうだ。

なんやかんやあって、いい感じにストーリーが進んで海の神殿についた。 そこには封印された勇者が眠っていた。
田中はそのとき、なんかすごいパワーが覚醒して封印を解き放った。 おまけに敵も復活したけれど、いい感じに倒せた。
山田と鈴木は勇者が居眠りぶっこいていたせいで故郷を滅ぼされたことに激昂して問い詰めた。 勇者は魔王に単身挑んだものの返りうちにされたこととか諸々説明して無罪放免。

あとはなんかいい感じに魔王を倒してハッピーエンド。


適当に書いたからすごくわかりにくいけれど、わかりにくいことが伝わったなら十分。 ストーリーを思いついた順番に説明してもいい感じに伝わるわけがない。 そんな時こそシーケンス図。

たぶんわかりやすくまとまったストーリー図の例

シーケンス図

こうしてみると、キャラが裏でどういうことしてたのかがよくわかる。 プレイヤーが操作可能になるまえから、物語は始まっていた。 これがシーケンス図のパワー。

シーケンス図ってなんぞや

Wikipediaを読めばわかる

シーケンス図 - Wikipedia

UML図の一種なのでUML図のリンクも貼る。

統一モデリング言語 - Wikipedia

もともとはシステム設計をする時のデータのやり取りや オブジェクトの振る舞いを定義するためのモデリング言語であって ゲームのシナリオを整理するためのものじゃない。

こんな複雑な図を書けというのか?

PlantUMLを使うとすごく簡単に書ける。

Open-source tool that uses simple textual descriptions to draw beautiful UML diagrams.

手軽にWebブラウザ上で試したいなら以下のサイトがすごく便利。

sujoyu.github.io

テキストファイルとして管理できるのでGitHubとの相性も良い。 ちなみに前述の図は以下のテキストから生成されている。

@startuml img/chara_seq.png

title 仲間が全員合流するまでの行動

actor 田中 as tanaka
actor 山田 as yamada
actor 鈴木 as suzuki
actor 勇者山勇者太郎 as hogeyama
actor ラスボス as last_boss

activate last_boss

last_boss -> last_boss : 復活した
last_boss -> last_boss : 世界侵略を開始
activate hogeyama
hogeyama -> hogeyama : 100年の眠りから目覚める
note right : ほげ山は過去の勇者とかそのへん
activate yamada
hogeyama -> last_boss : 一人で戦いに挑む
yamada -> suzuki : 一緒に遠方の街に\n買い出しにでかける
deactivate yamada
activate suzuki

last_boss -> yamada : 手下だけ向かわせて故郷を焼き討ちにかける

suzuki -> yamada : 故郷に戻ってきた
activate yamada

yamada -> yamada : 壊滅した故郷を目の当たりにする
yamada -> yamada : last_bossの手先を撃退する
yamada -> yamada : 魔王討伐のたびにでる
last_boss -> hogeyama : 返り討ちにして、\n海底神殿に封印する
deactivate hogeyama
suzuki -> yamada : 同行する
deactivate suzuki

=== ゲーム開始 ===

activate tanaka
tanaka -> tanaka : 年に一度の精霊祭に参加する
hogeyama -> tanaka : 封印されながらも根性と神通力で語りかける
tanaka -> tanaka : 始まりの街をでる
tanaka -> tanaka : 最初のボスを倒す
tanaka -> tanaka : 第二の街に到着する
tanaka -> tanaka : 第二の街を襲っている魔物と対峙する
yamada -> tanaka : 戦いに参加する

deactivate yamada
deactivate suzuki
tanaka -> team : チーム結成
deactivate tanaka
activate team
team -> team : なんやかんやある
team -> team : 海底神殿につく

team -> hogeyama : 田中のなんかすごいフォースで\n封印を解く
activate hogeyama
team -> team : 一緒にボスも復活
hogeyama -> team : 共闘でボスを倒す
hogeyama -> team : チーム加入
deactivate hogeyama
team -> team : なんやかんや
team -> team : ラスボス城に到達
team -> last_boss : 決戦に挑む
team -> last_boss : 勝利
deactivate last_boss


deactivate team

@enduml

すごく簡単というわけではないけれど、 パワーポイントよりは簡単なはず。

以上

Scrapboxから戻ってきました

はじめに

お久しぶりです。次郎です。 はてなブログに復帰したという記事です。

GitHubPagesに移行したあと、また移行してScrapboxを使っていたのですが、結局戻ってきました。 戻ってきた理由と、これからについて書きます。

戻ってきた理由

主に集客的な側面からです。

ScrapboxWikiであってブログではなく、 集客の側面については意図的に排除しているとの記事を見かけまして戻ってきました。

別に集客には特にこだわりもなかったですし、今も余り興味ないのですが 絵の配布、ついては別だったので、それのためだけに戻ってきました。

使われるための絵

今まで、たまに絵を書いて、気に入ったものがあったら公開して配布できる形にしてきました。

Scrapboxでも同様のことを行っていたので すが、Scrapboxよりははてなブログのほうがより効果的に人の目につくのでは、と考え ました。

もともと僕は自分がゲームを作る時に自分が使うように絵を書いて、 ゲームが完成するまで潜ませとくのがもったいなくて公開するようになったのですが 最近はゲームを作る熱が冷めて、何かの実装か絵だけを書くような感じになりました。

書いた絵をより使ってもらえるように、よりひと目につきやすいとこに置いとこう、という感じです。

まぁ、Wikiサービスをブログとかの代わりに使っていたのが、 本来の用途とアンマッチしていた感じ...でしょうか。

とはいえ、Scrapboxを使っているから人が集まらない、ということは絶対になくて 実際Scrapboxのサービス製作者のプロジェクトは色んな人に閲覧されています。 単純に、僕の発信力、影響力が弱い、というだけです。

戻ってくる == Scrapboxを使わない というわけではない

Scrapboxは引き続き使います。 が、メインで告知する環境にはしないというだけです。

依然として思考するためのサービスとしてScrapboxは非常に素晴らしいので 引き続きScrapboxに技術的なメモは残していきます。

これから

じゃあこっちでは何を書くのか、というとイラストの配布やツール配布とかだけにしようと思ってます。

このブログも、以前は技術の記事とイラストの記事が混在していて それぞれカテゴリが全然違うものだったので、このブログの立ち位置がわからなく感じてました。

Scrapboxに移行してから技術用とイラスト用でプロジェクトを分けていて、 イラスト配布用のプロジェクトの機能だけをこのブログが引き継ぐ形になります。 よって、ゲームとイラストに関連した情報のみになる予定です。

引き続き技術系の情報はScrapboxに書きます。あちらのほうが居心地が良いので。

しっかりとした情報として公開したいものは、多分Qiitaに書くのかなぁと思います。 Qiitaだと情報が間違ってる場合に指摘してもらえるので。

GitHubPagesにブログ移転します。

GitHubPagesにブログを移転します。 移転先は下記。

jiro4989.github.io

配布イラスト一覧は下記 https://jiro4989.github.io/dist-illust/

はてなブログはエンジニアの利用率が結構高い印象です。 一番のメリットはMarkdown記法に対応していることだと思ってます。 僕もMarkdownが使えるから、ということでFC2から移ってきました。

他にもメール送信で記事の投稿ができるので スクリプトと連携して自動化したり Vimで記事を書いたりできる。 SEO対策もしっかりされてますし、 はてなブックマーク数やスター数が表示されるのも結構便利だったんですが 投稿後に記事を修正するための編集ページが重たかったり もろもろ不満もありました。

github.ioのドメインと柔軟性の高さ、 ブログ記事の更新履歴を残せる、 管理がやりやすい、 エディタが選べる、コマンドラインと相性が良い、自動化が簡単など いろんなメリットから移転を決意しました。

移行こちらのブログはおそらく更新しません。 イラストの配布についても新しいブログの方に告知していきます。

以上です。

自分が今まで書いたイラストの書いた時期と枚数を集計する

概要

集計の勉強がてらにbashawkで自分が今まで書いたイラストの枚数と傾向を集計して分析します。

目的

自分の絵師としてのモチベーションの傾向を分析し、未来の活動指針を決定するため。 というのは大嘘でbashawkで集計の練習がしたかったからです。

成果物は月単位、年単位で集計したグラフです。 本目的の達成は、上記のグラフを作成し可視化することをもって完了とします。

コード

成果物

csv形式で出力してLibreOfficeでグラフ化しました。 (コードの方ではcsv形式で出力するようにはしていないので、 スクリプト呼び出し時に| tr '[:blank:]' ','しました。

年単位での集計結果

月単位での集計結果

考察

2016年のころ、僕はまだ大学4年生でした。 大学4年は卒論の提出と就活が終わってしまえばあとはひたすら暇になるので その時間をイラストとプログラミングに最大に使っていたことがよく統計に現れているように思います。

2016年1月〜3月は冬休みでしたし、卒業研究も就活もまだ本腰でなかった時期なので その時間はひたすら絵を書いていたみたいですね。

しかし僕って2011年から絵を書いていたんですね...。 意外と結構期間が長いものです。

最近はめっきり絵を書く熱は冷めてしまって、プログラミングの熱ばかりあがっていますが、 それでもこれからも、年に1,2枚くらいは何かしら書くと思います。

Clojureでアクセスログの処理時間を集計

概要

最近Bashで集計をすることが多いです。集計作業楽しいです。 と同時に、Clojureという言語にも興味ができたので、せっかくなので 簡単な集計作業を両方の言語でやってみたというだけの話です。

目的

access_log というログファイルが存在します。 ログのデータはすべて行単位ですが、共通することは行の一番最後の数値は アクセス時の処理時間です。

このアクセスログの時間を集計して、合計秒と行数を出力したいです。

作成したコード

下記を参照。 ClojureBashのコードです。

まとめ

集計作業のコード量の短さや読みやすさ、手軽さはやっぱり BashAwkのほうが断然上だなぁ...と。 Awkとかはもろに集計のための言語なんでかなり便利。 Clojureに集計やらせるもんじゃないかな...。 でも大量の便利関数群をまだまだ把握できていないので、 それらを駆使したらもっと短くシンプルになるのかも?

個人的にはBashClojureってなんか似ているような気がします。 BashのパイプとClojure関数型言語の性質が似てて、 コードを書いていてパズルを解いているような感じがしてコーディングがすごく楽しい。 逆にGoだとClojureほどコーディングが楽しくなかったり... 実装自体は複雑な機能がないので、つまづくこともあまりなく高速で実装できますが 味気ない感じがどうもあります(気にするべきじゃないのかもですが)。

余談

集計作業を行うとき、今まではなんとなくPythonとかでやっていたのですが、 そこまで複雑ではない処理だったらむしろPythonよりもBashAwkのほうが 短く素早く実装できるなぁと実感しています。

シェルのコードをPythonに置き換えるような話をちらほら聞く中 時代を逆走するようにBashAwkの勉強を最近はしています。 でも使ってみるとかなり便利なので、これからも勉強しようと思います。

Windows環境でgoreleaserを使用してGitHubにデプロイしてみた

前書き

どうもです。

以前Ubuntu環境でgoreleaserを使用してGitHubにデプロイする作業について記事を作成しましたが、今回はそれのWindows環境版です。

前の記事については下記を参照してください。 http://usernameon666.hatenablog.com/entry/2017/10/08/125842

やってることはほとんど同じですが、デプロイのスクリプトbashでやってるかPowerShellでやってるかという違いだけですね。

開発環境

項目
OS Windows 10 Home
Go version 1.9.2
使用スクリプト PowerShell

成果物

game-manager
ゲームのプレイ時間を記録してCSVファイルとして保存するTUIアプリ

わざわざ作るほどのものでもないのですが、まぁ勉強用ということで...
同様のゲームのプレイ時間が把握できるもので僕が使用したことがあるのは SteamやMoeL2などがあったのですが、あれと同じようなもので ターミナル上で動作するものを作成してみたかったんです。

「みんなのGo言語」に登場したtermbox-goとHOME配下のconfigフォルダにデータを格納する手順を勉強するために作成しました。

リリース手順

GitHubリポジトリの作成

今回は下記の通り game-manager というリポジトリを作成しました。 https://github.com/jiro4989/game-manager

GoReleaserの設定ファイルの作成

.goreleaserという設定ファイルを$env:GOPATH/src/game-manager配下に作成します。

PS C:\Users\username\go\src\game-manager> pwd

Path
----
C:\Users\username\go\src\game-manager


PS C:\Users\username\go\src\game-manager> ls


    ディレクトリ: C:\Users\username\go\src\game-manager


Mode                LastWriteTime         Length Name
----                -------------         ------ ----
d-----       2017/12/24     20:05                data
d-----       2017/12/24     20:55                dist
d-----       2017/12/24     19:59                game
d-----       2017/12/24     20:51                res
d-----       2017/12/24     20:54                script
-a----       2017/12/24     20:48             19 .gitignore
-a----       2017/12/24     20:22            322 .goreleaser.yml
-a----       2017/12/24     20:04           5082 game-manager.go
-a----       2017/12/23      5:53            622 README.md

.goreleaserに設定を記入

.goreleaser.ymlの中身を下記の通りにする。

builds:
  - binary: game-manager
    goos:
      - windows
      - darwin
      - linux
    goarch:
      - amd64
      - 386
archive:
  format: tar.gz
  replacements:
    amd64: 64-bit
    darwin: mac
    linux: linux
  name_template: "{{ .ProjectName }}_{{ .Os }}_{{ .Arch }}"
  files:
    - README.md

デプロイスクリプトを作成

$env:GOPATH\src\game-manager\scriptに下記の内容のデプロイ用のPowerShellスクリプト"deploy.ps1"を作成します。

※別にスクリプトを作成しないといけないわけではないです。

git tag $args[0]
$env:GITHUB_TOKEN = cat ".\res\token.txt"
goreleaser --rm-dist
go install

GitHubのアクセストークンを配置

$env:GOPATH\src\game-manager\res\token.txtというテキストファイルを作成して、GitHubのアクセストークンを貼り付けます。 あんまりこういう運用方法って良くないと思いますけれど、まぁ個人で行う分には気にしないことにしています。

.gitignoreを作成する

デプロイするバイナリとtoken.txtがリポジトリ上に見えてしまうとまずいので それらを除外するように設定します。

.gitignoreの中身は下記のとおりです。 配置するパスは$env:GOPATH\src\game-managerです。

token.txt
/dist/

スクリプトを実行する

下記のパスがカレントディレクトリであることを確認します。

PS C:\Users\username\go\src\game-manager> pwd

Path
----
C:\Users\username\go\src\game-manager

下記のコマンドを実行します。
.\script\deploy.ps1 v1.0.0
※v1.0.0はタグ情報になるので、アップデートするたびに番号を更新する必要があります。

あとはひたすら待機...

PowerShell環境だと、制御コードがむき出しで表示されてパッと見バグったか失敗したように見えることがありますが、正常に動いているので、信じて待ちます。

リリースの確認

GitHubのリリースページを確認しにいきます。 https://github.com/jiro4989/game-manager/releases