« ^ »

記事の一覧をhugoで取得する

所要時間: 約 5分

hugoで記事を書いていると書きかけのファイルが結構できるようになった。"書きかけの" の状態には、1つは書きかけだけれど公開してしまうもの、もう1つは公開しないものの2種類があった。公開しないものはページ属性のDRAFTをtrueにしてあり、書きかけだけれど公開してしまうものはDRAFTを設定しない代わりにWIP属性をtrueにしている。WIP属性はhugoとは関係なくて勝手にページ属性として設定している。この値はテンプレートで分岐し、WIPを表示をするかどうかを制御している。ファイルが多くなると、途中の状態で放置されたファイルが増えてきた。これらをきちんと管理するために、ページ属性によって絞り込みができるようにする事にした。

WIPやDRAFT状態の記事のパスとタイトルを取得し標準出力に出力できれば、jqによって探索できるのではと考えた。

{"draft": true, "wip": true, "title": "xxx"}
出力のイメージ

スクリプトを自分で実装する事もできるけれど、これはhugoで行いたい。RSSの生成と同じような仕組みで実現できると思い、調べはじめた。しかし実際にやろうとしてみると中々思うように行かない。こんな簡単な事なのにhugoを詳しく調べないというのは面倒だという感想を持った。

調べていくとhugoはコンテンツの一覧を出力するコマンド hugo list コマンドを提供している。例えば hugo list all では次のような出力になる。

path,slug,title,date,expiryDate,publishDate,draft,permalink
/opt/ng/symdon/pages/posts/1690684937.org,,,2023-07-30T11:34:48+09:00,0001-01-01T00:00:00Z,2023-07-30T11:34:48+09:00,true,http://localhost:1313/posts/1690684937/
/opt/ng/symdon/pages/posts/1690682046/index.org,,Jupyter Notebookを使う,2023-07-30T10:53:51+09:00,0001-01-01T00:00:00Z,2023-07-30T10:53:51+09:00,true,http://localhost:1313/posts/1690682046/
/opt/ng/symdon/pages/posts/1690679301.org,,株主総会に必要な要件,2023-07-30T09:45:25+09:00,0001-01-01T00:00:00Z,2023-07-30T09:45:25+09:00,true,http://localhost:1313/posts/1690679301/
hugo list allの結果を抜粋

もろもろの属性が出力されている。私は自分の要求からDRAFTというページ属性を付けているため、WIP属性も表示したいのだが、この属性は勝手に付与している属性だから hugo list all の出力に追加できない。

もしEmacs Lispだったら該当の関数をコピーしてきて、一部分を書き換える所だけれども、HugoはGoで実装されていてそういう事はできない。Goが割ともてはやされている場面を見るけれど、こういう部分で不便さを感じる事はある。同じ事はHaskellにも言えて、PandocはHaskellで記述されている為、同じ類いの不便さはある。ただしPandocの場合、Luaを使ってPandocの挙動を変更する事ができる為、多くの場面で問題を回避できるように思う。

Hugoはとても便利で愛用しているけれど、動作のカスタマイズについては今一つ物足りない。個人的にはSchemeを取り込んでくれたら良いのになと思う。また標準出力と標準エラー出力の扱いも雑に感じた。出力は標準出力に、エラーメッセージやワーニングは標準エラー出力にきちんと出力した方が良い。

幸い、先の一覧にはファイルのパスが記述されていた。WIP属性は正規表現で簡単に取得できる。そのため、それらの結果をマージする事で、やりたい機能は実現できるように思った。そこでそれらを実施する事にした。流れは次のような感じだ。

  1. hugo list all でコンテンツの一覧を取得する。

    cd /opt/ng/symdon/hugo
    hugo list all
  2. コンテンツディレクトリを再帰的にgrepし ^#+WIP: true を記述しているファイルの一覧を作成する。

    find /opt/ng/symdon/pages/posts -type f -name '*.org' | xargs grep '^#+WIP:\s*true*' | cut -d : -f 1

    org-modeのみが対象になってしまうが、コンテンツは全てorg-modeで記述しているので問題ない。

  3. 1の結果を1行ずつ読み込み、2の結果内に存在するかを確認し、あればwipをtrue、無ければwipをfalseにしてJSONを出力する。

3の処理に関してはbashでも出来そうだし、Lispでも出来そうだし、何でもできそうだったけれど、使い慣れているPythonで実装する事にした。

hugo-listj

#! /usr/bin/env python3
import csv
import json
import subprocess
import tempfile

HUGO_ROOT_DIR = "/opt/ng/symdon/hugo"
HUGO_CONTENT_DIR = "/opt/ng/symdon/pages/posts"

o1 = tempfile.NamedTemporaryFile("w+", encoding="utf8")

c1 = subprocess.Popen(["hugo", "list", "all"], stdout=o1, cwd=HUGO_ROOT_DIR)
c1.wait()

HEADER_LINE = "path,slug,title,date,expiryDate,publishDate,draft,permalink\n"

o1.seek(0)
for ii, line in enumerate(o1):
    if line.startswith("path,slug,title"):
        break
else:
    raise ValueError("no header")

csv_reader = csv.reader(o1)

data_list = [
    dict(
        path=row[0],
        slug=row[1],
        title=row[2],
        date=row[3],
        expiryDate=row[4],
        publishDate=row[5],
        draft=(row[6].lower() == "true"),
        permalink=row[7],
    )
    for row in csv_reader
]

o2 = tempfile.NamedTemporaryFile("w+", encoding="utf8")

c2 = subprocess.Popen(
    [
        "bash",
        "-c",
        f"find {HUGO_CONTENT_DIR} -type f -name '*.org' | xargs grep '^#+WIP:\\s*true*' | cut -d : -f 1",
    ],
    stdout=o2,
)
c2.wait()

o2.seek(0)
org_path_list = [line.strip() for line in o2]

for d in data_list:
    d["wip"] = d["path"] in org_path_list

for d in data_list:
    print(json.dumps(d, ensure_ascii=False))

実行結果の一部を抜粋する。

{"path": "/opt/ng/symdon/pages/posts/1612771283/index.org", "slug": "", "title": "macOSにditaaをインストールする", "date": "2021-02-08T16:55:26+09:00", "expiryDate": "0001-01-01T00:00:00Z", "publishDate": "2021-02-08T16:55:26+09:00", "draft": "false", "permalink": "http://localhost:1313/posts/1612771283/", "wip": false}
{"path": "/opt/ng/symdon/pages/posts/1612696782.org", "slug": "", "title": "", "date": "2021-02-07T19:52:12+09:00", "expiryDate": "0001-01-01T00:00:00Z", "publishDate": "2021-02-07T19:52:12+09:00", "draft": "true", "permalink": "http://localhost:1313/posts/1612696782/", "wip": false}{"path": "/opt/ng/symdon/pages/posts/1612342848/index.org", "slug": "", "title": "SMTPでメールを送信する", "date": "2021-02-03T17:59:39+09:00", "expiryDate": "0001-01-01T00:00:00Z", "publishDate": "2021-02-03T17:59:39+09:00", "draft": "false", "permalink": "http://localhost:1313/posts/1612342848/", "wip": false}
{"path": "/opt/ng/symdon/pages/posts/1612310870.org", "slug": "", "title": "ディレクトリを作成するEmacs Lisp", "date": "2021-02-03T09:01:15+09:00", "expiryDate": "0001-01-01T00:00:00Z", "publishDate": "2021-02-03T09:01:15+09:00", "draft": "false", "permalink": "http://localhost:1313/posts/1612310870/", "wip": false}

これである程度はjqで絞り込みができる状態になった。