10
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Qiita CLIで取得した.mdファイルのファイル名をElixirで変更する

Last updated at Posted at 2023-08-31

はじめに

私はQiitaに投稿する記事をGitHubで管理しています。
Qiita CLIという「手元の環境で記事の執筆・プレビュー・投稿ができるツール」を使わせていただいております。

本記事では、Qiita CLIで取得した.mdファイルのファイル名を{id}.mdから作成年月日-タイトル.mdに変更します。

What is Qiita CLI ???

公式から発信されている記事を貼っておきます。

そうGitHubで管理できるわけですよ。
もちろん私もGitHubで管理しています。

私がQiita CLIを使い始めたころの記録(おもいで)は、次の記事に書いています。

ファイル名を変えたい

ここからはもうすでにQiita CLIを使っていらっしゃることを前提に書き進めます。

npx qiita previewnpx qiita pull -fをすると記事の一覧を取得できます。
この際ファイル名は、記事のidをもとに{id}.mdとなります。
記事のidとはここです。
例: 23c85293e673f537b5e4

スクリーンショット 2023-08-30 9.50.55.png

私は、作成年月日-タイトル.mdにファイル名を変えたいとおもいます。
このファイル名ですと、いつごろ書いたものなのか、どういった内容なのかが一目瞭然だからです。

スクリーンショット 2023-08-30 9.53.31.png

ファイル名勝手に変えていいの? という疑問は当然湧くとおもいますし、そこは私も気になるところです。理想はQiita CLIのソースコードのどこそこがこうなっているから変えていいんですと示すのが理想です。が、Qiita CLIの今後のアップデートでどう影響するのかわかりませんし、私とお友達のニシグチさんはファイル名を変更してもいまのところ不都合は生じていません。また記事を新規作成するときには、npx qiita new 記事のファイルのベース名と何でも指定してよいようですので、ファイル名は自由に付けてよい、自由には責任が伴うのだと読むことができます、と自分を納得させています。

この記事は上記リポジトリにあったbin/normalize-filenames.exsに強くインスパイアされて書きました。Elixirでプログラミングすることを楽しみました。

作ったもの

Elixirという素敵な世俗派関数型言語1でプログラミングを楽しみました。
その素敵具合は「Elixir Saves Pinterest $2 Million a Year In Server Costs」によく現れています。開発者も経営者もこの事実に瞠目することでしょう。$2 Million/年の節約ですってよ!、奥さん。

はい! プログラムです。

bin/normalize-filenames.exs
Mix.install([
  {:req, "~> 0.3.11"},
  {:timex, "~> 3.7"},
  {:yaml_front_matter, "~> 1.0"},
  {:zarex, "~> 1.0"}
])

defmodule Qiita do
  @token System.fetch_env!("QIITA_TOKEN")
  @headers [Authorization: "Bearer #{@token}", Accept: "Application/json; Charset=utf-8"]

  def get_item(item_id) do
    Req.get("https://qiita.com/api/v2/items/#{item_id}", headers: @headers)
  end

  def get_created_at(item_id) do
    {:ok, res} = get_item(item_id)
    Map.get(res, :body) |> Map.get("created_at")
  end
end

defmodule NormalizeFilename do
  def normalize(filename) do
    case Regex.match?(~r/^\d{8}-.+\.md$/, filename) do
      true -> maybe_do_normalize(filename)
      false -> do_normalize(filename)
    end
  end

  defp do_normalize(nil, _title, filename), do: filename

  defp do_normalize(id, title, _filename) do
    created_at =
      Qiita.get_created_at(id)
      |> Timex.parse!("{ISO:Extended}")
      |> Timex.format!("%Y%m%d", :strftime)

    normalized_title = normalized_title(title)

    build_filename(created_at, normalized_title)
  end

  defp do_normalize(filename) do
    {id, title} = filename |> path() |> parse_file()
    do_normalize(id, title, filename)
  end

  defp maybe_do_normalize(title, title, _date, filename), do: filename

  defp maybe_do_normalize(normalized_title, _filename_title, date, _filename),
    do: build_filename(date, normalized_title)

  defp maybe_do_normalize(filename) do
    {_id, raw_title} = filename |> path() |> parse_file()
    normalized_title = normalized_title(raw_title)

    %{"date" => date, "title" => filename_title} =
      Regex.named_captures(~r/^(?<date>\d{8})-(?<title>.+)\.md$/, filename)

    maybe_do_normalize(normalized_title, filename_title, date, filename)
  end

  defp path(filename), do: Path.join(File.cwd!(), "public") |> Path.join(filename)

  defp parse_file(path) do
    {%{"id" => id, "title" => title}, _body} = YamlFrontMatter.parse_file!(path)
    {id, title}
  end

  defp normalized_title(title) do
    Zarex.sanitize(title)
    |> then(&Regex.replace(~r/\[/, &1, "【"))
    |> then(&Regex.replace(~r/\]/, &1, "】"))
    |> String.slice(0, 80)
  end

  defp build_filename(created_at, normalized_title), do: "#{created_at}-#{normalized_title}.md"
end

defmodule Main do
  def main do
    File.ls!("public")
    |> Enum.reject(&File.dir?("public/#{&1}"))
    |> Enum.filter(&String.ends_with?(&1, ".md"))
    |> Enum.each(fn filename ->
      normalized = NormalizeFilename.normalize(filename)

      IO.puts(normalized)
      :ok = File.rename("public/#{filename}", "public/#{normalized}")
    end)
  end
end

Main.main()

ここにあります。

使い方

Elixirをインストールしていない方もいらっしゃるでしょうから、Dockerでイゴかせる2ようにコマンド例を書いておきます。

その前に配置の仕方を書いておきます。

treeの実行結果はこんな感じを想定しています。

.
├── README.md
├── bin
│   └── normalize-filenames.exs
├── public
│   ├── 009fc0559c70e5e69ca7.md
│   ├── 00b5b0a8b8e81ad0ae46.md
│   ├── newArticle007.md
│   └── newArticle008.md
└── qiita.config.json

Dockerのコマンド例です。
プロジェクトのルートで実行することを想定しています。

docker run \
       --rm \
       --mount type=bind,src=$(pwd),dst=/app \
       -w /app \
       -e QIITA_TOKEN="your token" \
       elixir:1.15.4-otp-25-slim \
       elixir bin/normalize-filenames.exs

ちなみに、--mountはよく-vで見かけることも多いとおもいます。

New users should use the --mount syntax. Experienced users may be more familiar with the -v or --volume syntax, but are encouraged to use --mount, because research has shown it to be easier to use.

(Dockerの)新しいユーザーは、--mount syntaxshould useですと、Dockerのドキュメントに書いてあります。

少しだけ解説

なぜ環境変数でQIITA_TOKENを指定する必要があるの? 盗もうとしているのじゃないの? が気になるとおもいます。

大丈夫です。安心してください。盗みません。

私は『スクール★ウォーズ』と『進撃の巨人』と、プロレス、オートレース、将棋、歴史小説が好きでして、それらを楽しんでいると、あなたのQIITA_TOKENをもらったところで使う時間がありません。
それは冗談として、話を続けます。

npx qiita pull等で記事を取得すると、.mdファイルの中身はこの写真のように、 created_atが無かとです!!!

スクリーンショット 2023-08-30 8.28.43.png

じゃあ、どっから持ってくるだ? ということでQiita APIを使うわけです。
GET /api/v2/items/:item_idの返戻JSONからcreated_atを得ます。

私ね、Qiitaに登録した記事が2023-08-30現在531記事あります。
@kaizen_nagoya さんの6341記事(2023-08-30現在)にはまだまだ遠く及びません。

Qiita API利用制限にこう書いてあるわけです。

認証している状態ではユーザーごとに1時間に1000回まで、認証していない状態ではIPアドレスごとに1時間に60回までリクエストを受け付けます。

この1時間1000回はマヂで弾かれます。体感です。なぜ知っているかというと、1時間に1000回以上リクエストしてエラーになった経験があるからです。Qiitaさん、たくさんリクエストしてしまってごめんなさい:pray:

記事の数が少ない人や1時間ごとに60件ずつ実行する気長にやれる人はQIITA_TOKENの指定は不要です。細かいことを念のため言っておくと、@headers [Accept: "Application/json; Charset=utf-8"]というふうにアクセストークンの指定は無しとするようプログラムを書き換えてから実行してください。

私は500記事以上ありましたし、一度の実行で終わらせたかったので、QIITA_TOKENを使用しました。

そして、この記事を書いていて気づきました。
GET /api/v2/authenticated_user/itemsを使えばもっと少ないAPIリクエストで実現できそうです。

さいごに

本記事では、Qiita CLIで取得した.mdファイルのファイル名を{id}.mdから作成年月日-タイトル.mdに変更することをElixirを使って楽しみました。
Qiita CLIを使って、Qiitaライフをエンジョイしてください。

私にInspireを与えてくれた @mnishiguchi さんに感謝です。Thank you so much.

それではごいっしょに!
「いやぁ、Qiitaって本当にいいもんですね~。それではまたご一緒に投稿を楽しみましょう」

  1. @kikuyuta 先生の「世俗派関数型言語 Elixir を関数型言語風に使ってみたらやっぱり関数型言語みたいだった」より。Elixirはコワくないですよ〜。

  2. 「動かせる」の意。おそらく西日本の方言、たぶん。NervesJPではおなじみ。2017年ころのオートレースの実況では、実況アナウンサーが「針3イゴきます」とはっきり言っていた。

  3. 大時計の針のこと。針がイゴいてある地点まで到達すると選手はスタートを切る。発走の合図。針がイゴきはじめると(おそらく)選手は緊張するし、スタートはその後のレース展開に大きく影響するので、車券を握りしめている観客たちがもっとも緊張する瞬間であるため、先の尖った鋭いものを連想させる針は緊張の暗喩としても言い得て妙。

10
1
10

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
10
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?