🤦‍♂️

YAMLで複数行を書きたいときにインデントしたらハマった

2024/05/13に公開

ラブグラフでエンジニアをしております横江( @yokoe24 )です。
YAML を書いていて思わぬ罠にハマったので書いておきます。

何が起きたか

罠は、GitHub Actions を書いているときに発生しました。
スクリプトの実行に関して引数が見やすいよう、改行をおこなうように変更しました。

- name: Run gh-pull-requests-slack-reminder
  run: >
    gh-pull-requests-slack-reminder
      --token ${{ secrets.GITHUB_TOKEN }}
      --owner ${{ github.repository_owner }}
      --repo ${{ github.event.repository.name }}
      --label-name "レビュー依頼中"
      --webhook-url "${{ vars.REVIEW_REMINDER_SLACK_WEBHOOK_URL }}"
      --avoid-weekend --avoid-jp-holidays

しかし、このステップは失敗します。
「必要な引数 token が渡されていない」とのことでエラーになってしまいました。

なぜ起きたか

実行ログを見に行くと、

gh-pull-requests-slack-reminder
  --token ${{ secrets.GITHUB_TOKEN }}
  --owner ${{ github.repository_owner }}
// 以下略

のように実行されていたことがわかりました。

つまり、 gh-pull-requests-slack-reminder の時点で改行されているのです。
これではたしかに、引数がなにも指定されずに実行されている状況です。

YAML で複数行を書くときは |> を使うはずだが……?

YAML で複数行に分けて文章を書きたいとき、
|> を使えば可能だと、調べるとわかります。

- test1: |
    本日は
    ようこそ
    お越しくださいました
- test2: >
    本日は
    ようこそ
    お越しくださいました

は、JSON的には

[
  {
    "test1": "本日は\nようこそ\nお越しくださいました\n"
  },
  {
    "test2": "本日は ようこそ お越しくださいました\n"
  }
]

とレンダリングされます。

今回の

- name: Run gh-pull-requests-slack-reminder
  run: >
    gh-pull-requests-slack-reminder
      --token ${{ secrets.GITHUB_TOKEN }}
# 以下略

も、

[
  {
    "name": "Run gh-pull-requests-slack-reminder",
    "run": "gh-pull-requests-slack-reminder --token ${{ secrets.GITHUB_TOKEN }} ... 中略 ...\n"
  }
]

とレンダリングされる想定でしたが、そうはなりませんでした。

> を使うときにインデントはダメ

実は YAML には謎の仕様があって、
> を使っているブロック内で インデントがある場合 、改行が挿入されます。

どういう事情でそうしてるのかは、調べた限りでは謎でした……。

- test1: >
    本日は
      ようこそ
      お越しくださいました
- test2: >
    本日は
      ようこそ
        お越しくださいました
- test3: |
    本日は
      ようこそ
      お越しくださいました
- test4: |
    本日は
      ようこそ
        お越しくださいました

という YAML は、

[
  {
    "test1": "本日は\n  ようこそ\n  お越しくださいました\n"
  },
  {
    "test2": "本日は\n  ようこそ\n    お越しくださいました\n"
  },
  {
    "test3": "本日は\n  ようこそ\n  お越しくださいました\n"
  },
  {
    "test4": "本日は\n  ようこそ\n    お越しくださいました\n"
  }
]

と解釈されるのです。
>| が同じ解釈になってるじゃん!!」 とツッコみたくなります。

また、 空行がある場合 も必ず改行するよう扱われます。

- test1: >
    本日は

    ようこそ


    お越しくださいました
- test2: |
    本日は

    ようこそ


    お越しくださいました

は、JSON的には

[
  {
    "test1": "本日は\nようこそ\n\nお越しくださいました\n"
  },
  {
    "test2": "本日は\n\nようこそ\n\n\nお越しくださいました\n"
  }
]

と解釈されます。

それでもインデントしたい場合には

最後に、「それでもスクリプトが見やすいようインデントしたい!」 という場合は
どうすればいいでしょうか?

https://stackoverflow.com/questions/3790454/how-do-i-break-a-string-in-yaml-over-multiple-lines
の回答では、まだマシな方法として二重引用符 (") で囲う方法が紹介されています。

Plain や一重引用符 (') は避けるべき、と書いてあります。

どうしてか、見てみましょう。
YAML の > のインデント無しと同じ出力になるよう、以下の出力を比較します。

- test1: >
    本日は
    ようこそ
    お越しくださいました
- test2: "\
    本日は
      ようこそ
      お越しくださいました\n\
    "
- test3: # Plain
    本日は
      ようこそ
      お越しくださいました\n
- test4: '\
    本日は
      ようこそ
      お越しくださいました\n
    '

これはJSON的には、こう解釈されます。

[
  {
    "test1": "本日は ようこそ お越しくださいました\n"
  },
  {
    "test2": "本日は ようこそ お越しくださいました\n"
  },
  {
    "test3": "本日は ようこそ お越しくださいました\\n"
  },
  {
    "test4": "\\ 本日は ようこそ お越しくださいました\\n "
  }
]

二重引用符 (") を使う方法では上手く再現できましたが、
Plain 形式では \n の部分が \\n 、つまり改行記号でなく \n の文字になってしまいました。
一重引用符 (') の方法も、同様にバックスラッシュ (\) を文字列になるよう解釈しますから、エスケープ記号として機能させることができません。

以上のことから、
どうしてもインデントをさせたいのであれば二重引用符 (") で囲うのがもっとも適切な手段 ということがわかります!

おしまい

以上、YAML で複数行の文字列を書きたいときにハマるインデントの罠と、
どうしてもその罠を回避してインデントをしたい場合の方法でした!

お役に立ちましたら幸いです! 👍

ラブグラフのエンジニアブログ

Discussion