生涯未熟

生涯未熟

プログラミングをちょこちょこと。

ioutil.ReadDirでのシンボリックリンクの扱い

ioutil.ReadDirでディレクトリ内を走査した時にシンボリックリンクがあった場合、LStatとStatのどちらでFileInfoが返ってくるか分からなかったため試してみた。
testディレクトリ内に別のディレクトリに対してシンボリックリンクを張ったものを置いてあります。

package main

import (
    "fmt"
    "io/ioutil"
    "os"
    "path/filepath"
)

func main() {
    currentDir, err := os.Getwd()
    if err != nil {}
    dirPath := filepath.Join(currentDir, "test")

    infos, err := ioutil.ReadDir(dirPath)
    for _, info := range infos {
        fmt.Println("IsDir: ", info.IsDir())
    }
}

これで、実行結果が true ならリンク先を辿っているStatであり、 false ならリンク先を辿らないLstatになります。

ʕ ◔ϖ◔ʔ % go run main.go
IsDir:  false

おっ、これはLstatの動きですね。

一応 ioutil.ReadDir のコードも追っかけていきましょう。
Goの ioutil.ReadDir は内部的にはソート機能のついた os.File.Readdir で、そちらのコードを眺めてみると。

func (f *File) readdir(n int) (fi []FileInfo, err error) {
    dirname := f.name
    if dirname == "" {
        dirname = "."
    }
    names, err := f.Readdirnames(n)
    fi = make([]FileInfo, 0, len(names))
    for _, filename := range names {
        fip, lerr := lstat(dirname + "/" + filename)
        if IsNotExist(lerr) {
            // File disappeared between readdir + stat.
            // Just treat it as if it didn't exist.
            continue
        }
        if lerr != nil {
            return fi, lerr
        }
        fi = append(fi, fip)
    }
    if len(fi) == 0 && err == nil && n > 0 {
        // Per File.Readdir, the slice must be non-empty or err
        // must be non-nil if n > 0.
        err = io.EOF
    }
    return fi, err
}

真ん中くらいで lstat 使ってますね。
もし、「ディレクトリ内を走査しつつ、シンボリックリンクだった場合はリンク先を辿る」といった処理を実現したい場合には、以下のようにする感じですかね。

package main

import (
    "fmt"
    "io/ioutil"
    "os"
    "path/filepath"
)

func main() {
    currentDir, err := os.Getwd()
    if err != nil {}
    dirPath := filepath.Join(currentDir, "test")

    infos, err := ioutil.ReadDir(dirPath)
    for _, info := range infos {
        filePath := filepath.Join(dirPath, info.Name())
        i, err := os.Stat(filePath)
        if err != nil {}
        fmt.Println("IsDir: ", i.IsDir())
    }
}

これを実行すると

ʕ ◔ϖ◔ʔ % go run main.go
IsDir:  true

おっ、ちゃんとシンボリックリンクのリンク先を辿れてますね。
面倒なのでStatに対応した ioutil.ReadDiros.File.Readdir があればいいんですけどねぇ・・・

現場からは以上です。

os.Stat, os.Lstatのシンボリックリンク指定時の違い

いつまで経っても覚えられないので自戒のために書く。

os.Stat

指定されたファイルパスの情報(os.FileInfo)を返す。
シンボリックリンクを指定した場合には、リンクを辿った先の情報を返す。

os.Lstat

指定されたファイルパスの情報(os.FileInfo)を返す。 シンボリックリンクを指定された場合には、リンクを辿らずシンボリックリンク自体の情報を返す。

注意

os.FileInfoのNameはStat, Lstatのどちらも指定したシンボリックリンクの名前自体になる。

echoのmiddlewareで一度読んだRequestBodyをもう一度読む

Gopherの皆さんはecho使ってますでしょうか?僕は薄くて好きなのでよく使ってます。

さて、echoにはmiddlewareという機能がございます。
Middleware | Echo - High performance, minimalist Go web framework

ハンドラの実行前に処理差し込めるやつですが、このmiddlewareとして以下のような処理を差し込むとします。

func CheckJSON(next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
        jsonBlob, err := ioutil.ReadAll(c.Request().Body)
        if err != nil {
            return err
        }
        s := string(jsonBlob)
        if !validator.IsJSON(s) {
            return c.NoContent(http.StatusBadRequest)
        }

        return next(c)
    }
}

飛んできたリクエストがJSONかどうか?というのをチェックするmiddlewareになります。
さて、このmiddlewareを実行した後にハンドラでこのような処理を実行するとしましょう。

func Hoge(c echo.Context) error {
    var jsonBody map[string]interface{}
    jsonBlob, err := ioutil.ReadAll(c.Request().Body)
    if err != nil {
        return err
    }
    err := json.Unmarshal(jsonBlob, &jsonBody)
    if err != nil {
        return errors.New("failed unmarshal json")
    }

    return c.NoContent(200)
}

またもやリクエストから読み込んでる処理ですね。
さて、これを実行すると failed unmarshal json のエラーになります。
これは ioutil.ReadAll がc.Request().Bodyを全て読み込んでいるため、次回以降読み込もうとするとnilが返ってくるためです。

で、ここからどうやって2回目の読み込みを実現するかですが、答えは簡単で「Bodyの内容を上書く」という解決方法があります。

func CheckJSON(next echo.HandlerFunc) echo.HandlerFunc {
    return func(c echo.Context) error {
        jsonBlob, err := ioutil.ReadAll(c.Request().Body)
        if err != nil {
            return err
        }
        s := string(jsonBlob)
        if !validator.IsJSON(s) {
            return c.NoContent(http.StatusBadRequest)
        }

        c.Request().Body = ioutil.NopCloser(bytes.NewBuffer(jsonBlob))

        return next(c)
    }
}

こうすることで2回目にキチンと ioutil.ReadAll を実行しても内容を読み込んでくれます。

刺し身たんぽぽについて

刺し身たんぽぽについて調べてみました。

f:id:syossan:20190106180834p:plain

刺し身たんぽぽとは一体何なのか?

刺身の盛り合わせに添えられている食菊(食用菊)の通称。花の大きさからタンポポと誤認されやすい。菊には殺菌作用があり、かつ見た目の彩りや香りも添える。さらに刺身のツマとして食べられる。近年では形骸化しつつある。 - weblio辞書より引用 -

なるほど、実際にはたんぽぽでは無いのですね。勉強になります。
あと、食べれるものとは知りませんでした。むしろ、あんな花花しいものを食べる人はいるのだろうか?

いつから刺し身たんぽぽは誕生したのか?

奈良時代に、日本で現在でも食用菊として栽培されている「延命楽(もってのほか・カキノモト)」が中国から伝来した[2]。 - wikipediaより引用 -

そんな1000年以上前から刺し身たんぽぽはあったのですね・・・しゅごい。

食用としては、江戸時代から民間で食されるようになったとされており[4]、1695年に記された『本朝食鑑』に「甘菊」の記述が見られる[5]。 - wikipediaより引用 -

実際に食されるようになったのは結構最近なのですね。
刺し身のツマとして用いられるようになった経緯についても深掘りしたかったのですが、検索しても見つからず。インターネットの敗北です。

刺し身たんぽぽの効用とは?

刺し身にたんぽぽが入っている理由としては「殺菌作用・彩り」の2点のようですね。
このたんぽぽ自体には発ガン効果の抑制・コレステロールの低下・中性脂肪を低下させる効果があるそうです。めちゃくちゃ有能やんけ。

刺し身たんぽぽの危険性

ただ、こんな刺し身たんぽぽですがキク科のアレルギーを持つ方にとっては食すのに危険性が伴います。

togetter.com

食べる時には自分がアレルギーを持っていないか気を付けましょう。
また、刺し身たんぽぽという単語を真に受けて、その辺に生えてるたんぽぽを生で食べないようにしましょう。普通に汚いですからね。

刺し身たんぽぽについて完全に理解した

年始から刺し身たんぽぽの話題で溢れていたので、完全に理解するために色々と調べてみましたがどうだったでしょうか?
こういう単語はしっかり調べた上で、きちんと使える大人になりましょうね!

treeコマンド 〜Rオプションの謎〜

treeコマンド、皆さん知ってますか?
そうです、あのファイルやらディレクトリやらを良い感じに木構造で出力してくれるニクい奴です。

f:id:syossan:20190103044420p:plain

こんな感じ

さて、そんなtreeコマンドには多くのオプションがあります。
よく使われるのは -L だったり、 -a とかですかね?

そんな中、 -R オプションというものが存在するのをご存知でしょうか?

Rオプションとは何なのか?

まずは、 --help オプションに書かれている内容を見てみましょう。

-R            Rerun tree when max dir level reached.

直訳すると 最大ディレクトリレベルに達したらツリーを再実行します。 ってとこですかね。
あー、再帰的にツリーを辿るのねと試しに以下のようなコマンドを実行してみました。

$ tree -L 1 -R -o result

これで1階層ごとにtreeの結果が出力されるじゃね?と思ってみました。(あまりやっても意味ないですが・・・
で、やってみた結果、ワーキングディレクトリの下にしかresultが吐かれませんでした・・・

さてはて、予想が外れたのですが冷静に次はmanコマンドを参照してみます。

-R     Recursively cross down the tree each level directories (see -L option), and at each of them execute tree again adding `-o 00Tree.html' as a new option.

これを直訳すると

再帰的に各レベルのディレクトリにツリーをたどります(-Lを参照)
そして、それらのそれぞれで、新しいオプションとして '-o 00Tree.html'を追加して、それぞれtreeを実行します。

オッ、 --help の時と違って新たな -o 00Tree.html という概念が出てきましたね。
ただ、先の実行結果を見渡してみても 00Tree.html とやらが吐き出されている様子は無い模様。
ますます頭にはてなマークが生えてきました。

treeコマンドのソースコードを読む

このままでは埒が明かないので、treeコマンドのソースコードを読んじゃいましょう。
C言語で書かれているので、そこまで難しくはないはずです。

ソースコードは以下のリンク先からよしなに取得してきてください。
The Tree Command for Linux Homepage

さて、それでは読んでいきたいと思います。

まず、何はともあれ -R オプションを追っかけましょう。

case 'R':
  Rflag = TRUE;
  break;

-R オプションはこのようにコード内では Rflag という変数に置き換えられます。
これを追いかけようと、tree.cの中を見てみたのですが、まさかの以下のコードしか参照しているところがありませんでした。

if (Rflag && (Level == -1))
  Rflag = FALSE;

んん?意味自体は分かりますが、これが再帰処理に繋がる気配は無さそうですね・・・
一応他のコードも読んでみたのですが、これまたまさかの html.c というファイル内に以下の意味ありげなコードが書かれていました。

/* This is really hackish and should be done over. */
if (Rflag && (lev == Level) && (*dir)->isdir) {
  if (nolinks) fprintf(outfile,"%s",(*dir)->name);
  else {
    fprintf(outfile,"<a href=\"%s",host);
    url_encode(outfile,d+1);
    putc('/',outfile);
    url_encode(outfile,(*dir)->name);
    fprintf(outfile,"/00Tree.html\">");
    html_encode(outfile,(*dir)->name);
    fprintf(outfile,"</a><br>\n");
  }

  hdir = gnu_getcwd();
  if (sizeof(char) * (strlen(hdir)+strlen(d)+strlen((*dir)->name)+2) > pathsize)
    path = xrealloc(path, pathsize = sizeof(char) * (strlen(hdir)+strlen(d)+strlen((*dir)->name) + 1024));

  sprintf(path,"%s%s/%s",hdir,d+1,(*dir)->name);
  fprintf(stderr,"Entering directory %s\n",path);

  hcmd = xmalloc(sizeof(char) * (49 + strlen(host) + strlen(d) + strlen((*dir)->name)) + 10 + (2*strlen(path)));
  sprintf(hcmd,"tree -n -H \"%s%s/%s\" -L %d -R -o \"%s/00Tree.html\" \"%s\"\n", host,d+1,(*dir)->name,Level+1,path,path);
  system(hcmd);
  free(hdir);
  free(hcmd);
}

うーーーーーん、This is really hackish and should be done over.というコメントから察するに、なんか難易度高そうっすね・・・
一応この処理は -H オプションを付けて実行した際のHTMLを構築する処理の一部になります。

HTML構築のためにディレクトリを -L で指定した深さまで辿っている途中に、この処理が出てくるのですが、ざっくり読み解いてみると、どうやらRオプションがあり、かつLオプションで指定した深さのディレクトリの場合にHTMLを出力しつつ、再帰的に tree コマンドを実行していますね。
manの説明にあった、 00Tree.html の存在も伺えます。どうやら再帰的に実行する際に、各ディレクトリに 00Tree.html を生成しているようです。

というわけで、なんとなく分かったこととして -R オプションは -H オプションと共に使わないと意味が無さそうってことですね。
では実験してみましょう。

実験

それでは -H オプションを付加してやってみましょう。

$ tree -L 1 -HR .

<!DOCTYPE html>
<html>
<head>
 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
 <meta name="Author" content="Made by 'tree'">
 <meta name="GENERATOR" content="$Version: $ tree v1.8.0 (c) 1996 - 2018 by Steve Baker, Thomas Moore, Francesc Rocher, Florian Sesser, Kyosuke Tokoro $">
 <title>Directory Tree</title>
 <style type="text/css">
  <!--
  BODY { font-family : ariel, monospace, sans-serif; }
  P { font-weight: normal; font-family : ariel, monospace, sans-serif; color: black; background-color: transparent;}
  B { font-weight: normal; color: black; background-color: transparent;}
  A:visited { font-weight : normal; text-decoration : none; background-color : transparent; margin : 0px 0px 0px 0px; padding : 0px 0px 0px 0px; display: inline; }
  A:link    { font-weight : normal; text-decoration : none; margin : 0px 0px 0px 0px; padding : 0px 0px 0px 0px; display: inline; }
  A:hover   { color : #000000; font-weight : normal; text-decoration : underline; background-color : yellow; margin : 0px 0px 0px 0px; padding : 0px 0px 0px 0px; display: inline; }
  A:active  { color : #000000; font-weight: normal; background-color : transparent; margin : 0px 0px 0px 0px; padding : 0px 0px 0px 0px; display: inline; }
  .VERSION { font-size: small; font-family : arial, sans-serif; }
  .NORM  { color: black;  background-color: transparent;}
  .FIFO  { color: purple; background-color: transparent;}
  .CHAR  { color: yellow; background-color: transparent;}
  .DIR   { color: blue;   background-color: transparent;}
  .BLOCK { color: yellow; background-color: transparent;}
  .LINK  { color: aqua;   background-color: transparent;}
  .SOCK  { color: fuchsia;background-color: transparent;}
  .EXEC  { color: green;  background-color: transparent;}
  -->
 </style>
</head>
<body>
    <h1>Directory Tree</h1><p>
    <a href=".">.</a><br>
    ├── <a href="./LICENSE">LICENSE</a><br>
    ├── <a href="./Makefile">Makefile</a><br>
    ├── <a href="./README.md">README.md</a><br>
    ├── <a href="./cmd/00Tree.html">cmd</a><br>
Entering directory /Users/syossan27/go/src/go-tree/cmd
Entering directory /Users/syossan27/go/src/go-tree/cmd/go-tree
<br>
    ├── <a href="./dist/00Tree.html">dist</a><br>
Entering directory /Users/syossan27/go/src/go-tree/dist
<br>
    ├── <a href="./go.mod">go.mod</a><br>
    ├── <a href="./go.sum">go.sum</a><br>
    ├── <a href="./tree.go">tree.go</a><br>
    ├── <a href="./tree.html">tree.html</a><br>
    ├── <a href="./tree_windows.go">tree_windows.go</a><br>
    └── <a href="./validate.go">validate.go</a><br>
    <br><br>
    </p>
    <p>

2 directories, 9 files
    <br><br>
    </p>
    <hr>
    <p class="VERSION">
         tree v1.8.0 © 1996 - 2018 by Steve Baker and Thomas Moore <br>
         HTML output hacked and copyleft © 1998 by Francesc Rocher <br>
         JSON output hacked and copyleft © 2014 by Florian Sesser <br>
         Charsets / OS/2 support © 2001 by Kyosuke Tokoro
    </p>
</body>
</html>

おー、HTMLは無事出力されましたね。
この状態で、 00Tree.html とやらは各ディレクトリに出力されているのか確認します。

$ tree
.
├── LICENSE
├── Makefile
├── README.md
├── cmd
│   ├── 00Tree.html
│   └── go-tree
│       ├── 00Tree.html
│       └── main.go
├── dist
│   ├── 00Tree.html
│   ├── go_tree_darwin_386
│   ├── go_tree_darwin_amd64
│   ├── go_tree_freebsd_386
│   ├── go_tree_freebsd_amd64
│   ├── go_tree_freebsd_arm
│   ├── go_tree_linux_386
│   ├── go_tree_linux_amd64
│   ├── go_tree_linux_arm
│   ├── go_tree_netbsd_386
│   ├── go_tree_netbsd_amd64
│   ├── go_tree_netbsd_arm
│   ├── go_tree_openbsd_386
│   ├── go_tree_openbsd_amd64
│   ├── go_tree_windows_386.exe
│   └── go_tree_windows_amd64.exe
├── go.mod
├── go.sum
├── tree.go
├── tree.html
├── tree_windows.go
└── validate.go

オッ、たしかに出てますね。

で、出力されたHTMLを見てみると以下のような感じになります。

f:id:syossan:20190103053127p:plain

ほほー?一応cmdディレクトリのリンクを踏んでみましょうか。

f:id:syossan:20190103053158p:plain

なるほど、リンク先もtree構造のHTMLになってますね。
これが -R オプション無しだと、リンク先を踏んだ場合にはこんな感じになります。

f:id:syossan:20190103053251p:plain

普通にブラウザでローカルディレクトリを覗いた時の画面ですね。
という形の結果になりました。

まとめ

結局のところ -R オプションってなんぞ?というところなのですが、これはtreeコマンドの開発者が単に内部的に00Tree.htmlを吐く機構が欲しかったから作ったのでは・・・?という感想があります。
CHANGELOGとかも遡って見たり、インターネット上を彷徨ったりしたのですが特に有力な情報は見つからず・・・

あと、ちょっと気になるところとして 00Tree.html の親ディレクトリへのリンク先を押しても戻ることが出来ず、リンク先の出力ロジックがなんか間違えてるんじゃないかと。
こういった時のバグの報告とかどうやればいいのかちょっと謎なので、treeコマンドに詳しい方、情報お待ちしております。