How to spend the terminal

技術ブログでさえない

SSDに換装した

成果物がある程度完成させてから書くというスタイルなので何も書いていませんでした。

WindowsのノートPCが遅くてMacBook Airをずっと使っていたのですが、最近SSDが安いという話を聞いてSSDに換装しようと思いました。
ちょうどプライムデーでWD BLUE SSDが安かったので購入してEaseUS Todo Backup(https://jp.easeus.com/backup-software/free.html)
でクローニングして換装しました。
換装作業はドライバーで蓋を開けて抜き差しするだけでした。今までHDDの交換はPATAしかやったことがなかったのですが、SATAは非常に楽で時代の進歩を感じました PATAは15年前のPCだからね

交換後に起動するとすぐに立ち上がり、アプリもすぐに起動しました。
そもそもWindows PCの遅さの原因はディスク由来(タスクマネージャで常に100%になっていた)だったので、それが解決したことによってサクサク動くようになりました。

まとめ

SSDを交換することによって延命できるという話をよく目にするのですが、それは正しいと思いました。
SSDは1TBで従来のHDDよりも容量がある上に1万ちょっとで購入できて、時代は進んでいると思いました。
もうHDDには戻れないと感じましたので、みなさんもSSDにしてみてはいかが、どうでしょうか。

Goで作ったprintfでHello world

この記事は、SLP KBIT Advent Calendar 2017 12日目の記事です。

はじめに

予告していた通りGo言語でHello worldを書きます。 なぜHello worldを書くのかというと、アドベントカレンダーの記事の水準が上がってきていて、初心者が書きにくくなってきているからです。Hello worldを書いた程度の微笑ましい記事でもいいと私は思っています。

package main
import "fmt"
func main() {
    fmt.Printf("Hello world\n")
}

はい、Hello worldを書けました。これで終わりです・・・と本当に言ったら怒られるので本題に入ります。

printfを作る

*nixのシェルにはprintfコマンドが組み込まれています。printfコマンドはC言語のprintfのように引数を書式にしたがって変換し、出力します。

これを作ろうと思った理由は二つあって、Windowsで*nixシェルっぽいコマンドを作っていたのと、楽に作れそうだからです。 楽に作れそうだったのですが、なかなか大変でした。

fmt.Printfに投げて終わり?

はじめに考えていたのは次のようなプログラムでした。

package main

import (
    "fmt"
    "os"
)

func main() {
      if len(os.Args) < 2 {
        fmt.Fprintf(os.Stderr, "%s: not enough arguments\n", os.Args[0])
    } else if len(os.Args) == 2 {
        f := os.Args[1]
        fmt.Printf(f)
    } else {
        f := os.Args[1]
        s := os.Args[2:]
        fmt.Printf(f, s...)  // スライスを可変引数にして渡す
    }
}

os.Args[1](1番目の引数)がフォーマット(書式)で、os.Args[2:](2番目以降の引数)が引数となっています。 このまま実行できればそれで終わりだったのですが、数値の変換が行われません。 なぜそうなるかというと、os.Argsがstringの配列で、stringとして渡されるからです。

引数を変換する

fmt.Printf()の引数をGoDocで見てみましょう。

func Printf(format string, a ...interface{}) (n int, err error)

1番目の引数は書式で文字列、2番目以降の引数はinterface{}(任意の型)の可変引数です。つまり、2番目以降の引数を変換したものを[]interface{}(interface{}の配列)に入れておけばいいわけです。

では、どうやって変換すればいいでしょうか。例えば、99が整数であるか文字列であるか判断することはできません。そこでformatを参照して文字列であるか整数であるか実数であるかを判断します。

判断する関数を作る

使える書式

今回のprintfで使える書式は以下の書式です。

  • 文字列
    • %s
    • %q(ダブルクォーテーションをつける)
  • 整数
    • %d
    • %o
    • %x
    • %X
  • 実数
    • %f
    • %F
    • %e
    • %E
  • その他
    • %%(%を出力)

      どう解析するか

      1文字ずつ調べて、'%'を見つけたら書式フラグを立てます。 書式であることがわかれば文字列であれば文字列、整数であれば整数、実数であれば実数を配列に加えます。整数か実数が指定されているのにそうでない場合は0もしくは0.0を配列に加えます。 書式でない文字が出てきた場合か書式フラグが立ったまま解析が終わった場合、エラーを返します。

      コード

func StrToIF(f string, s []string) ([]interface{}, error) {
    a := []interface{}{}
    runes := []rune(f)
    argNum := 0
    percent := false
    verb := ""
    for _, r := range runes {
        if percent {
            verb += string(r)
            switch r {
            case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
            case '+', '-', '.', '#':
            case '%':
                if verb != "%%" {
                    return a, FormatError{verb}
                } else { // %%
                    percent = false
                    continue
                }
            case 's', 'q':
                if argNum < len(s) {
                    a = append(a, s[argNum])
                    argNum++
                } else {
                    a = append(a, "")
                }
                percent = false
            case 'd', 'o', 'x', 'X':
                var i int64
                var err error
                if argNum < len(s) {
                    i, err = strconv.ParseInt(s[argNum], 0, 64)
                    if err != nil {
                        i = 0
                    }
                    argNum++
                } else {
                    i = 0
                }
                a = append(a, i)
                percent = false
            case 'f', 'F', 'e', 'E':
                var fl float64
                var err error
                if argNum < len(s) {
                    fl, err = strconv.ParseFloat(s[argNum], 64)
                    if err != nil {
                        fl = 0.0
                    }
                    argNum++
                } else {
                    fl = 0.0
                }
                a = append(a, fl)
                percent = false
            default:
                return a, FormatError{verb}
            }
        }
        if r == '%' {
            if percent == false {
                percent = true
                verb = "%"
            }
        }
    }
    if percent {
        return a, FormatError{verb}
    }
    return a, nil
}

エスケープシーケンスの処理

書式に合わせた変換はできるようになりましたが、"Hello world\n"を入力すると改行されずにそのまま出力されます。これは"[\n]"が"[\][n]"といったようになるからです。 そのため、エスケープシーケンスに変換します。stringsパッケージを用いて泥臭く実装しました。

func formatReplace(f string) string {
    f = strings.Replace(f, "\\a", "\a", -1)
    f = strings.Replace(f, "\\b", "\b", -1)
    f = strings.Replace(f, "\\f", "\f", -1)
    f = strings.Replace(f, "\\n", "\n", -1)
    f = strings.Replace(f, "\\r", "\r", -1)
    f = strings.Replace(f, "\\t", "\t", -1)
    f = strings.Replace(f, "\\v", "\v", -1)
    f = strings.Replace(f, "\\'", "'", -1)
    f = strings.Replace(f, "\\\"", "\"", -1)
    return f
}

最後に加工した書式と[]interface{}を可変引数に展開してfmt.Printf()に投げて完成です。

まとめ

以上よりprintfを作ることができました。ソースコードGitHubに上げています。

github.com

標準ライブラリのみで使ったのでWindowsでも簡単に使うことができます。 これでprintfを行うことで誰でもシェル上でHello worldを行うことができます。 シェルならWindowsでもechoコマンドでできますが。

Go言語でnkfみたいなやつ

nkfという今年で30周年になる古いソフトがあります。nkfでは文字コードを変換することができます。 nkfには様々な機能がありますが、次の3点を満たすものをGo言語で作ってみました。

文字コードを変換する

文字コードの変換は以下の記事を参考にしました。というか丸コピ

qiita.com

文字コードを判定する

文字コードの判定はchardetを使用しました。chardetは文字コードを判定するためのライブラリです。 判定に失敗した時にはエラーを返します。

改行コードを変換する

改行コードの変換は正規表現を使用しました。

コード

コードは以下のようになりました。

main.go

package main

import (
    "flag"
    "fmt"
    "log"
    "os"
)

var (
    inJIS     = flag.Bool("J", false, "Input JIS(ISO2022JP)")
    inEUC     = flag.Bool("E", false, "Input EUCJP")
    inSJIS    = flag.Bool("S", false, "Input Shift_JIS")
    inUTF8    = flag.Bool("W", false, "Input UTF-8")
    outJIS    = flag.Bool("j", false, "Output JIS(ISO2022JP)")
    outEUC    = flag.Bool("e", false, "Output EUCJP")
    outSJIS   = flag.Bool("s", false, "Output Shift_JIS")
    outUTF8   = flag.Bool("w", false, "Output UTF-8")
    unix      = flag.Bool("Lu", false, "LF(UNIX) Newline")
    windows   = flag.Bool("Lw", false, "CRLF(Windows) Newline")
    macintosh = flag.Bool("Lm", false, "CR(Macintosh) Newline")
    override  = flag.Bool("override", false, "Override")
    guess     = flag.Bool("g", false, "Detect Charset")
)

func setIn() string {
    switch {
    case *inJIS:
        return "ISO2022JP"
    case *inEUC:
        return "EUCJP"
    case *inSJIS:
        return "ShiftJIS"
    case *inUTF8:
        return "UTF8"
    default:
        return ""
    }
}

func setOut() string {
    switch {
    case *outJIS:
        return "ISO2022JP"
    case *outEUC:
        return "EUCJP"
    case *outSJIS:
        return "ShiftJIS"
    case *outUTF8:
        return "UTF8"
    default:
        return "ISO2022JP"
    }
}

func setNl() string {
    switch {
    case *unix:
        return "UNIX"
    case *windows:
        return "WINDOWS"
    case *macintosh:
        return "MACINTOSH"
    default:
        return ""
    }
}

func main() {
    var fp *os.File
    var err error
    flag.Parse()
    if len(flag.Args()) < 1 {
        fp = os.Stdin
    } else {
        fp, err = os.Open(flag.Args()[0])
        if err != nil {
            log.Fatal(err)
        }
        defer fp.Close()
    }

    in := setIn()
    out := setOut()
    nl := setNl()
    if *guess {
        charset, _ := Guess(fp)
        fmt.Println(charset)
    } else {
        str, err := Convert(fp, in, out, nl)
        if err != nil {
            log.Fatal(err)
        }
        if *override && !(len(flag.Args()) < 1) {
            fp.Close()
            fp, err = os.OpenFile(flag.Args()[0], os.O_WRONLY, 0666)
            if err != nil {
                log.Fatal(err)
            }
            defer fp.Close()
            fmt.Fprint(fp, str)
        } else {
            fmt.Fprint(os.Stdout, str)
        }
    }
}

convert.go

package main

import (
    "errors"
    "io/ioutil"
    "os"
    "regexp"
    "strings"

    "github.com/saintfish/chardet"
    "golang.org/x/text/encoding/japanese"
    "golang.org/x/text/transform"
)

var errDet = errors.New("Couldn't detect")

func charDet(b []byte) (string, error) {
    d := chardet.NewTextDetector()
    res, err := d.DetectBest(b)
    if err != nil {
        return "", err
    }
    switch res.Charset {
    case "Shift_JIS":
        return "ShiftJIS", nil
    case "UTF-8":
        return "UTF8", nil
    case "EUC-JP":
        return "EUCJP", nil
    case "ISO-2022-JP":
        return "ISO2022JP", nil
    default:
        return res.Charset, errDet
    }
}

func toUtf8(str string, in string) (string, error) {
    var u8 []byte
    var err error
    switch in {
    case "ISO2022JP":
        u8, err = ioutil.ReadAll(transform.NewReader(strings.NewReader(str), japanese.ISO2022JP.NewDecoder()))
    case "EUCJP":
        u8, err = ioutil.ReadAll(transform.NewReader(strings.NewReader(str), japanese.EUCJP.NewDecoder()))
    case "ShiftJIS":
        u8, err = ioutil.ReadAll(transform.NewReader(strings.NewReader(str), japanese.ShiftJIS.NewDecoder()))
    case "UTF8":
        u8, err = []byte(str), nil
    default:
        u8, err = ioutil.ReadAll(transform.NewReader(strings.NewReader(str), japanese.ShiftJIS.NewDecoder()))
    }
    if err != nil {
        return "", err
    }
    return string(u8), err
}

func toSjis(str string) (string, error) {
    sjis, err := ioutil.ReadAll(transform.NewReader(strings.NewReader(str), japanese.ShiftJIS.NewEncoder()))
    if err != nil {
        return "", err
    }
    return string(sjis), err
}

func toEuc(str string) (string, error) {
    euc, err := ioutil.ReadAll(transform.NewReader(strings.NewReader(str), japanese.EUCJP.NewEncoder()))
    if err != nil {
        return "", err
    }
    return string(euc), err
}

func toJis(str string) (string, error) {
    jis, err := ioutil.ReadAll(transform.NewReader(strings.NewReader(str), japanese.ISO2022JP.NewEncoder()))
    if err != nil {
        return "", err
    }
    return string(jis), err
}

func nlRep(str string, nl string) string {
    rep := regexp.MustCompile(`\r\n|\r|\n`)
    switch nl {
    case "UNIX":
        return rep.ReplaceAllString(str, "\n")
    case "WINDOWS":
        return rep.ReplaceAllString(str, "\r\n")
    case "MACINTOSH":
        return rep.ReplaceAllString(str, "\r")
    }
    return str
}

/*
Only detect Character encoding
*/
func Guess(file *os.File) (string, error) {
    input, err := ioutil.ReadAll(file)
    if err != nil {
        return "", err
    }
    det, err := charDet(input)
    return det, err
}

/*
Convert Character encoding
*/
func Convert(file *os.File, in string, out string, nl string) (string, error) {
    input, err := ioutil.ReadAll(file)
    if err != nil {
        return "", err
    }
    if in == "" {
        in, err = charDet(input)
        if err != nil {
            return "", err
        }
    }
    u8, err := toUtf8(string(input), in)
    if err != nil {
        return "", err
    }
    if nl != "" {
        u8 = nlRep(u8, nl)
    }
    var output string
    switch out {
    case "ISO2022JP":
        output, err = toJis(u8)
    case "ShiftJIS":
        output, err = toSjis(u8)
    case "EUCJP":
        output, err = toEuc(u8)
    case "UTF8":
        output = u8
    }
    return output, err
}

まとめ

EUC-JPの判定はあまりうまくいかないのでEオプションをつけるといいと思います。 chardetはShift_JISEUC-JPで誤判定が発生する可能性があるため、overrideオプションを使う時は入力オプションを使うことをおすすめします。 ここまで長くなるとコードをまるまる載せるのではなく、GitHubで公開したほうがいいと思いました。 GitHubに公開しました(2017/07/14)

github.com

Windowsでuptimeコマンドもどき(Go言語)

Windowsで稼働時間を確認する方法としてコマンドプロンプトでの"net statistics server"やタスクマネージャなどがあります。
しかし、"net statistics server"で得られる情報は開始日時で稼働時間を得るには計算がいりますし、タスクマネージャは起動しなければいけません。
そのためにコマンドラインで稼働時間を取得するためにGo言語でuptimeもどきを作りました。ロードアベレージを取得するには前もって用意していないと駄目で、ユーザ数を取得するWin32 APIは知らないので現在時刻と稼働時間のみ出力します。

package main

import (
	"fmt"
	"log"
	"os"
	"syscall"
	"time"
)

func main() {
	dll, err := syscall.LoadDLL("Kernel32.dll")
	if err != nil {
		log.Fatal(err)
	}
	defer dll.Release()

	proc, err := dll.FindProc("GetTickCount64")
	if err != nil {
		log.Fatal(err)
	}

	tcP, _, lastErr := proc.Call()
	lastErr = syscall.GetLastError()
	if lastErr != nil {
		log.Fatal(lastErr)
	}
	tc := (uint64)(tcP)
	sec := tc / 1000
	min := sec / 60
	hour := min / 60
	day := hour / 24
	hour %= 24
	min %= 60
	var up string
	if day > 0 {
		if day > 1 {
			up = fmt.Sprintf("up %d days, %02d:%02d", day, hour, min)
		} else {
			up = fmt.Sprintf("up %d day, %02d:%02d", day, hour, min)
		}
	} else if hour > 0 {
		up = fmt.Sprintf("up  %02d:%02d", hour, min)
	} else {
		up = fmt.Sprintf("up  %d min", min)
	}

	t := time.Now()
	now := fmt.Sprintf("%02d:%02d:%02d", t.Hour(), t.Minute(), t.Second())

	fmt.Fprintf(os.Stdout, " %s %s\n", now, up)
}

実行結果は次のようになります。XPの場合、GetTickCount64がないのでGetTickCountを使ってtcのキャストをuint32にしてください。
f:id:moxtsuan:20170630134706p:plain

長い時間稼働させると何が起こるかわからないので定期的に再起動は行いましょう。

Go言語でriver.go.jpからダムの貯水率を取得する

最近雨が降らないな、と思っていたらニュースで早明浦ダム取水制限が始まることを知り驚きました。 取水制限が始まるくらい貯水率が減っていたのなら早明浦ダムbotが毎時間ツイートしているだろう、 と思っていたら止まっていました。(2017/06/18現在動いています。)

作者の 103yen (@103yen) | Twitter さんによると、 river.go.jpの仕様が変わって貯水率を取得できなかったようです。

なんでbotが止まっていたかどうかわからなかったので、自分で貯水率を取得してつぶやこうと思いました。 作者さんのWebページ( ダムbotシリーズについて )とブログ( Pythonでriver.go.jpからダムの貯水率を取得する - 99円のへたれ日記! )を参考にしてGo言語で実装しました。(ほぼgoquery)

package main

import (
    "fmt"
    "log"

    "github.com/PuerkitoBio/goquery"
)

type Storage struct {
    date string
    time string
    pos  string
}

var (
    BASE_URL           = "http://www1.river.go.jp"
    OUTSIDE_URL_BEFORE = "/cgi-bin/DspDamData.exe?ID="
    OUTSIDE_URL_AFTER  = "&KIND=3&PAGE=0"
    ID                 = "1368080700010"
)

func GetPerOfStorage() (s Storage) {
    found := false
    outside_url := BASE_URL + OUTSIDE_URL_BEFORE + ID + OUTSIDE_URL_AFTER
    outside, err := goquery.NewDocument(outside_url)
    if err != nil {
        log.Fatal(err)
    }
    iframe := outside.Find("iframe")
    iframe_url, _ := iframe.Attr("src")
    contents_url := BASE_URL + iframe_url
    contents, _ := goquery.NewDocument(contents_url)
    contents.Find("tr").EachWithBreak(func(i int, tr *goquery.Selection) bool {
        if found == true {
            return false
        }
        tr.Find("td").EachWithBreak(func(j int, td *goquery.Selection) bool {
            if j == 0 {
                s.date = td.Text()
            } else if j == 1 {
                s.time = td.Text()
            } else if j == 6 {
                s.pos = td.Text()
            }
            if s.pos != "" && s.pos != "-" {
                found = true
                return false
            }
            return true
        })
        return true
    })
    return
}

func main() {
    s := GetPerOfStorage()
    fmt.Printf("%s %s %s\n", s.date, s.time, s.pos)
}

これで取得したデータをRubyを用いてツイートしました。

2時間ごとにツイートしていたのですが、早明浦ダムbotが復活したのでtwitterでのツイートはやめました。 (GNU socialでは毎時間クイップしてます)

Raspberry Pi 2 Model BでGNU social

この記事は、SLP KBIT Advent Calendar 2016 10日目の記事です。

はじめに

Twitterはことあるごとにダウンします。 自分はツイートをよくするのでじれったく思います。 Twitterは最近身売りを検討していると聞きます。 買収されるならともかく、もしTwitterがなくなったらどうすればいいでしょうか。

Twitterが使えない時にどのSNSを使えばいいでしょうか、FB、Instagram、LINEなど様々なSNSがありますが、Twitterの完全な代替となるようなSNSはほとんどないと思います。 しかし、その中でもGNU socialはTwitterの代わりになりうると思いました。

GNU social

GNU socialはTwitterのようなマイクロブログを提供するソフトウェアです。 名前の通りフリーソフトです。 GNU socialの目的はマイクロブログの連合を作ることによる中央集権化された資本主義なサービスからの離脱、とのことです。 ソースコードgithubで公開されているため、簡単に導入することができます。PHPで書かれているのでビルドの必要はありません。

導入した理由

以前より、 https://Quitter.no でアカウントを作ってクイップを行っていたのですが、GNU socialの思想が(フリーソフトウェアという思想の他に)自分のサーバに掲示板を建てたりするパソコン通信と似ているのではと思い、導入しようと思いました。導入の際にApacheとnginxの使い方を学ぶこともできました。

Raspberry Piを使った理由

Raspberry Piで建てられるなら安価でGNU socialを普及することができると思ったからです。RPiより安いマシンがあるかも

今回使ったRaspberry Piは「Raspberry Pi 2 Model B」です。

(2017/04/16 追記)OSはRaspbian Jessieです。

参考サイト

以下のサイトを参考にしました。

GNU socialのインストール - Akionux-wiki

Instalar GNUSocial en una Raspberry Pi - la Enredadera

GNU Social · Mesh network Practical Guide

導入まで

下準備

GNU socialには - PHP - MariaDB - サーバ

の3つが必要なので、 GNU socialのインストール - Akionux-wiki を参考にしてインストールします。

nginxを用いる場合、

sudo apt-get install git nginx php5 mariadb-server mariadb-client php5-curl php5-gd php5-intl php5-gmp php5-json php5-mysqli openssl exif gettext

で揃います。

MariaDBの設定

MariaDBにログインして、データベースを作ります(ここではsocialとします)。

CREATE DATABASE social;
GRANT ALL on social.* TO 'social'@'localhost' IDENTIFIED BY 'agoodpassword'

GNU socialのダウンロード

GNU Socialを適当なディレクトリにダウンロードします。

git clone https://git.gnu.io/gnu/gnu-social.git

ダウンロードしたら公開するディレクトリ(ここでは/var/wwwとします)にgnu-socialを移動して、書き込み権限を与えます。

sudo chmod a+w /var/www/gnu-social/

gnu-socialに - avatar - background - file

というディレクトリを作って書き込み権限を与えます。

nginxの設定

nginxの設定を GNU Social · Mesh network Practical Guide の"Confiugre Nginx Web Server to Server GNU Social"を参考にして行います。

基本的には参考元の設定(server_nameの名前は変える、httpsを使う場合はポート番号を443にしてSSL設定を追加する)を使えばいいのですが、httpでアクセスされた時にhttpsに変換するために次の設定を追加しました。

server {
  listen 80;
  server_name example.com;
  rewrite ^ https://$server_name$request_uri? permanent;
}

アクセス

アクセスしてうまく設定ができていれば初期設定ができます。初期設定ができれば導入完了です。

最後に

2ヶ月ほどの試行錯誤の末、GNU socialを導入することができました。

自分で立てたインスタンスなので、何をやっても自由です。 みなさんも自由な自宅SNSを立ててみてください。

グローバルIPの変更をメールで通知するシェルスクリプト

グローバルIPアドレスは固定サービスを使わない限り変わります。 IPアドレスが変わってしまうといろいろ困るため、メールで通知するようなシェルスクリプトを書きました。 DiCE使えばこんな面倒なことしなくていい

#!/bin/sh -eu

MAIL_TO="TO"
SUBJECT="SUBJECT"

globalip="/var/tmp/globalip" #"XXX.XXX.XXX.XXX"だけ書かれたファイル
tmp=`mktemp`
curl -sS inet-ip.info > ${tmp}
set +e
cmp ${globalip} ${tmp} > /dev/null
res=$?
set -e

ip=`cat ${tmp}`

mail_send () {
echo ${ip} | mail -s "$SUBJECT" "$MAIL_TO"
}

if [ $res -eq 1 ]; then
        mail_send
        cat ${tmp} > ${globalip}
fi

exit 0

このスクリプトの問題点はcurlを使うことで、この手のグローバルIPを教えてくれるサイトはよく落ちます。
そのため、複数の候補から応答するものを使うようにしようと思っています。 また、以前のIPが書いてあるファイルがなければ作るなど改良したい所はいろいろあります。