標準愚痴出力

個人的なIT作業ログです。もしかしたら一般的に参考になることが書いているかもしれません(弱気

PowerShell で、二つのフォルダーのファイル構成を比較するコマンドを作る

dirdiff.cmd

@set "arg1=%~1" & set "arg2=%~2"
@powershell "iex((@('','','')+(cat '%~f0' | select -skip 3))-join[char]10)"
@exit /b %ERRORLEVEL%

$md5 = New-Object System.Security.Cryptography.MD5CryptoServiceProvider

function Get-MD5s($dir){
    $md5s = @{}
    Get-ChildItem $dir |
    Where-Object { $_.Mode[0] -ne "d" } |
    ForEach-Object {
        $data = [System.IO.File]::ReadAllBytes($_.FullName)
        $bs = $md5.ComputeHash($data)
        $md5s[ $_.Name ] = `
            ([System.BitConverter]::ToString($bs).ToLower() -replace "-","")
    }
    return $md5s
}

$dir1 = $env:arg1
$dir2 = $env:arg2

$hash1 = (Get-MD5s $dir1)
$hash2 = (Get-MD5s $dir2)

foreach($p in $hash1.Keys){
    if( $hash2.ContainsKey($p) ){
        $private:val1 = $hash1[$p]
        $private:val2 = $hash2[$p]
        if( $val1 -ne $val2 ){
            Write-Output ("{0} differ" -f $p)
            Write-Output ("  {0} {1}" -f $val1,(Join-Path $dir1 $p))
            Write-Output ("  {0} {1}" -f $val2,(Join-Path $dir2 $p))
        }else{
            Write-Verbose ("{0} same" -f $p)
        }
    }else{
        Write-Output ("{0} not found" -f (Join-Path $dir2 $p))
    }
}

foreach($p in $hash2.Keys){
    if( -not $hash1.ContainsKey($p) ){
        Write-Output ("{0} not found" -f (Join-Path $dir1 $p))
    }
}

# vim:set ft=ps1:
  • 最初の3行は、PowerShell をバッチファイル化するためのまじない
  • 二つのディレクトリの直下にあるファイルの md5連想配列に格納して、比較するだけ

こういうのは別のバッチファイルから呼び出したいから、Goで作った方がいいな…と後から思いました。まる

追記

Goで作ってみました。https://github.com/zetamatta/experimental/blob/master/dirdiff/main.go

(補足)git で特定の2つのフォルダーを使ってる履歴のみ残して、後は消す

最初の commit だけは argf , seek フォルダーに関わってなくとも残ってしまう(あとで revert した)

git rebase -i -root で、最初の commit も削除できた。

git で特定の2つのフォルダーを使ってる履歴のみ残して、後は消す

フォルダー一つだけなら git filter-branch --subdirectory-filter が使えるが、複数の場合はそうもいかない。 複数のフォルダーだけを残すために、Lua スクリプトをさくっと書いた。

function getgitlog(files)
    local fd = io.popen("git log " .. table.concat(files," ") )
    local commit = {}
    if fd then
        for line in fd:lines() do
            if string.match(line,"^commit ") then
                commit[ #commit + 1 ] = string.sub(line,8)
                print( commit[ #commit ])
            end
        end
        fd:close()
    end
    return commit
end


local commit = getgitlog( {"argf","seek"} )
local all = getgitlog( {} )
local first = all[ #all ]

os.execute("git checkout " .. first)
os.execute("git branch tmp")
os.execute("git checkout tmp")
for i=#commit,1,-1 do
    os.execute("git cherry-pick "..commit[i])
end

自分の都合だけで作ったので、下記のような制限がある。

  • 1回限りの利用なので、残すフォルダー「argf」「seek」は決め打ち
  • argf , seek フォルダーに関わっている commit だけをcherry-pick するという方式なので、厳密には argf , seek だけを抽出するわけではない
  • 最初の commit だけは argf , seek フォルダーに関わってなくとも残ってしまう(あとで revert した)
  • git filter-branch --subdirectory-filter みたいに、フォルダーの解消まではしない(二つフォルダーがあるから実際無理)。
  • フォルダー名に空白が含まれると多分誤動作

これを使って公開したのが、先に公開した seek コマンドだったりする。

うっかり、オレオレ grep (seek)を書いてしまった。

先日、while( <> ){ } 的なものの Go 版ライブラリ argfを書いてみたわけだけれども、そのサンプルとして 簡単な grep (seek)を書いてみた。

最初は必要最小限にしていたんだけど、jvgrep みたいに「検索文字列の部分をハイライト表示」できないかなと思って頑張ってみたら、Go言語のライブラリが充実していることもあって、案外簡単に出来てしまった。

で、調子にのって、自分が grep に望む機能を実装してみた。

  • UTF8 と ANSI(現在のコードページマルチバイト文字列:日本なら SJIS~CP932)の行単位での簡易自動判定
    • UTF8 と解釈できなければ、ANSI と判断するという安直判定
    • 前は ShiftJIS の時は Windows 標準の findstr.exe、UTF8 なら jvgrep を使っていたが、いちいち使い分けるのは面倒くさかった。
    • jvgrep はテキストファイルチェックが厳しくて、すぐバイナリファイル扱いにしてしまう。
    • git で SJIS テキストの差分を見る時、git の UTF8 と、本文の SJIS が混ざってしまう。両方とも化けずに見たい場合があるので、行単位で別々の文字コード判定にしたかった。

本当に自分が必要とした機能しか入れていないので、以下のような制限がある

  • オプションは -i (大文字・小文字を区別しない)、-r再帰的にファイルを検索する)だけ
  • 速度はあまり重視しておらず、工夫なし
  • 常にカラー表示。「リダイレクトしていたらエスケープシーケンスを抑制」とか別にしない
  • 色使いは jvgrepパクリ 互換

貴殿も、オレオレ grep を作ってみませんか?

Lua 5.1 向け shebang for Windows

@lua5.1.exe -e "_,_,b=io.input([[%~f0]]):read('*l','*l','*a');assert(loadstring('\n'..b,[[%~f0]]))()"
@exit /b %ERRORLEVEL%

print('ahaha')
x

5行目にわざと Lua としては不適切な文字 x を入れている。これを実行すると

lua: (command line):1: [string "C:\Users\USERNAME\foo.cmd"]:5: syntax error near <
eof>
stack traceback:
        [C]: in function 'assert'
        (command line):1: in main chunk
        [C]: in ?
exit status 1

5行目がおかしいというエラーがちゃんと出る。x を消すと

$ foo
ahaha

とちゃんと実行される。

while( <> ){…} みたいなことを Go で

UNIX風のテキストファイルフィルターを書くのに便利なように Perl には

while( <> ){
   print $_
}

という構文がある。これと同じような手軽さで Go でもフィルターを書くためのライブラリ argf を書いてみた。

package main

import (
    "fmt"
    "os"

    "github.com/zetamatta/experimental/argf"
)

func main() {
    r := argf.New()
    for r.Scan() {
        fmt.Println(r.Text())
    }
    if err := r.Err(); err != nil {
        fmt.Fprintln(os.Stderr, err.Error())
    }
}
  • 基本インターフェイスbufio.Scanner に準拠した(=ほぼ同じメソッド構成)
  • len(os.Args) < 2 の時は、標準入力から読み取るが、そうでない時は os.Args[1:] のファイルを順に読み込んでゆく
  • argf.New()インスタンスには、AWK の組み込み変数と同じ名前のメソッドを実装している
    • NR() … 読み込みレコード数(行数)
    • Filename() … 現在読み込んでいるファイルの名前
    • FNR() … 現在のファイルで読み込んだレコード数(行数)

かんたんな grep もどきも書いてみた。

package main

import (
    "errors"
    "fmt"
    "github.com/zetamatta/experimental/argf"
    "os"
    "regexp"
)

func main1() error {
    if len(os.Args) < 2 {
        return errors.New("Usage: grep.exe REGEXP Files...")
    }
    rx, err := regexp.Compile(os.Args[1])
    if err != nil {
        return err
    }
    r := argf.NewFiles(os.Args[2:])
    for r.Scan() {
        if rx.MatchString(r.Text()) {
            fmt.Printf("%s(%d): %s\n", r.Filename(), r.FNR(), r.Text())
        }
    }
    return r.Err()
}

func main() {
    if err := main1(); err != nil {
        fmt.Fprintln(os.Stderr, err.Error())
        os.Exit(1)
    }
    os.Exit(0)
}

argf.New()os.Args[1:] を全て読み込み対象としてしまうが、grep もどきでは os.Args[1] は検索文字列となるので、読み込み対象から除きたい。そのために argf.NewFiles(args []string) という、読み込み対象のファイル名を、引数で与えるタイプのコンストラクタも用意した。

今週の zetamatta/experimental は以上です。

Go言語についての3つくらいの誤解

以下、自分の思い込みもあるとは思うが、あまり気にしないで書いてみる。

Goは短く書くための言語ではない

短く書けるようにした結果、落とし穴が発生したり、 読みにくくなったり誤解が生じる余地があるならば、 そのような書き方は排除されている。

貴方が書きたいコードは、誰にでも読みやすいコードなのか、 誤解の余地がまったくないコードなのか、そのあたりをよく考えてほしい。

サポートされていない機能は未来永劫サポートしないとは限らない

例外は将来的にもサポートされないだろうが、 ジェネリクスは優先度が低いとされているだけで、 別に未来永劫サポートしないわけではない。

ただ、複雑性が増すなど課題が多いのと、 ジェネリクスがなくても済ませられるケースが多いため、 後回しにされているだけである。

Goはあらゆる問題を解決する言語ではないし、誰にでも使ってもらおう等と譲歩した言語でもない

RubyJava のように、あらゆる課題を解決しようなどと思い上がった言語ではない。 不得意分野は不得意分野と割り切っている。

また、誰にでも使ってもらうなどと考えている言語でもない。 気に入らないのなら、選択肢は他に幾らでもあるだろう。

以上、ソースはないよ。コメントもらっても回答しないかも