【小ネタ】シェルスクリプトからLambdaを呼び出す際のJSONエスケープエラーとその解決策

2023.08.13

はじめに

データアナリティクス事業本部ビッグデータチームのkasamaです。
今回は、シェルスクリプトからlambdaを呼び出すpayloadでjsonのescape errorとなったのでその原因と解決策を記載したいと思います。

前提

作成したコードは以下のgithubに格納しました。

23_shell_script_with_json_escape

run_lambda.shでaws cliによりlambda(handler.py)を実行している処理になります。lambdaのdeployについてはserverless.ymlを使用しています。詳しくはgithubのREADME.mdをご確認ください。

run_lambda.sh

#!/bin/bash
# AWS CLIを使用してLambda関数を実行するシェルスクリプト


function run_lambda () {
  aws lambda invoke \
  --region "<lambda-region>" \
  --function-name shell-script-with-json-escape-handler \
  --payload '{"param_1": "'"$1"'", "params": {"param_2": "'"$2"'"}}' \
  --profile "<your-iam-role>" \
  --cli-binary-format raw-in-base64-out response.json
  if [ $? -ne 0 ]; then
    echo "shell-script-with-json-escape-handlerの実行に失敗しました。"
  fi
}

echo "**********START**********"

output="output: python Error: Expecting value: line 1 column 1 (char 0)

"

run_lambda "AAA" "$output"


# 実行結果の表示
cat response.json

# 一時ファイルの削除
rm response.json

※ payloadの'{"param_1": "'"$1"'", "params": {"param_2": "'"$2"'"}}'について

シングルクォートが最初に評価されてエスケープ処理を行い、次にダブルクォートが評価されて、バッククォートバックスラッシュ以外をエスケープ処理する流れとなります。

シェルスクリプトのクォーテーションについて理解をまとめる

評価順 評価後 説明
'{"param_1": "' {"param_1":\" シングルクォートの間を評価しスペースは\にエスケープされる
"$1" AAAA 変数の内容を取り出す
'", "params": {"param_2": "' ",\"params":{"param_2":\" シングルクォートの間を評価しスペースは\にエスケープされる
"$1" output: python Error: Expecting value: line 1 column 1 (char 0) 変数の内容を取り出す
'"}}' "}} シングルクォートの間を評価

組み合わせると{"param_1":\"AAAA",\"params":\{"param_2":\"output: python Error: Expecting value: line 1 column 1 (char 0)"}}となり、JSONの文字列として正しく形成されるようになります。

handler.py

import json
from aws_lambda_powertools.utilities.typing import LambdaContext


def handler(event, context: LambdaContext):
    # イベントデータをログに出力
    response = {"statusCode": 200, "body": event}

    return response

エラー内容と原因

エラー内容

シェルを実行したところ以下のエラーが発生しました。

(blog_env) @23_shell_script_with_json_escape % bash run_lambda.sh
**********START**********

An error occurred (InvalidRequestContentException) when calling the Invoke operation: Could not parse request body into json: Could not parse payload into json: Illegal unquoted character ((CTRL-CHAR, code 10)): has to be escaped using backslash to be included in string value
 at [Source: (byte[])"{"param_1": "AAA", "params": {"param_2": "output: python Error: Expecting value: line 1 column 1 (char 0)

"}}"; line: 1, column: 107]
shell-script-with-json-escape-handlerの実行に失敗しました。
cat: response.json: No such file or directory
rm: response.json: No such file or directory
(blog_env) @23_shell_script_with_json_escape %

エラーメッセージにも記載の通り、jsonのrequest body上でparse(変換)できない文字列があるので、バックスラッシュでescapeする必要があるようです。

原因

エスケープが必要な文字列ですが、以下の文字列があります。今回はoutput変数に改行が含まれていたことが原因だとわかります。

『"(ダブルコーテーション)』
『\(バックスラッシュ)』
『/(スラッシュ)』
『\b(バックスペース)』
『\f(改ページ)』
『\n(改行)』
『\r(キャリッジリターン)』
『\t(タブ)』

JSONでエスケープする必要のある特殊な文字

解決策1(特殊文字を削除する)

特殊文字が必要なければシェルスクリプト上で削除することで、エラーを無くします。

#!/bin/bash
# AWS CLIを使用してLambda関数を実行するシェルスクリプト

function run_lambda () {
  aws lambda invoke \
  --region "us-east-1" \
  --function-name shell-script-with-json-escape-handler \
  --payload '{"param_1": "'"$1"'", "params": {"param_2": "'"$2"'"}}' \
  --profile "infa-role" \
  --cli-binary-format raw-in-base64-out response.json
  if [ $? -ne 0 ]; then
    echo "shell-script-with-json-escape-handlerの実行に失敗しました。"
  fi
}

echo "**********START**********"

output="output: python Error: Expecting value: line 1 column 1 (char 0)

"

output_without_newline=$(echo "$output" | tr -d '\n' | sed 's/[^[:alnum:][:space:]]//g')
echo "$output_without_newline"

run_lambda "AAA" "$output_without_newline"


# 実行結果の表示
cat response.json

# 一時ファイルの削除
rm response.json
output_without_newline=$(echo "$output" | tr -d '\n' | sed 's/[^[:alnum:][:space:]]//g')

trコマンドとsedコマンドで以下の処理を行っています:

  • echoコマンドでoutputの内容を出力し、次の処理に渡します。
  • trコマンドは文字の変換や削除に使われるコマンドで、ここでは-dオプションを使って改行を削除します。結果として、一行で出力されます。
  • s/.../.../g: この部分は、sedに「検索して置き換える」という指示を出しています。具体的には...の最初の部分に一致するテキストを、二番目の...で指定されたテキストに置き換えるという処理です。gのフラグは、入力ラインの中で一致する全ての部分を置き換えるという意味です。
  • [^[:alnum:][:space:]]: この正規表現は、アルファベットと数字([:alnum:])および空白([:space:])を除く全ての文字に一致します。[^...]は、...に一致しない文字にマッチすることを意味しています。 要するに、このsedコマンドは、$outputの中のアルファベット、数字、空白以外の全ての文字を削除しています。そして、その結果がoutput_without_newlineという変数に保存されています。

sed コマンド【使い方 まとめ】
tr - 文字の変換や削除 - Linuxコマンド

実行結果

(blog_env)@23_shell_script_with_json_escape % bash run_lambda.sh
**********START**********
output python Error Expecting value line 1 column 1 char 0
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}
{"statusCode": 200, "body": {"param_1": "AAA", "params": {"param_2": "output python Error Expecting value line 1 column 1 char 0"}}}%               
(blog_env)@23_shell_script_with_json_escape %

成功しましたが、たとえばエラーメッセージに日本語があるなどは同様に削除されてしまうので、対応が必要となります。

解決策2(エスケープ処理を行う)

特殊文字が必要であれば、エスケープ処理をシェルスクリプト上で行うことで、エラーを無くします。

#!/bin/bash
# AWS CLIを使用してLambda関数を実行するシェルスクリプト


function run_lambda () {
  aws lambda invoke \
  --region "<lambda-region>" \
  --function-name shell-script-with-json-escape-handler \
  --payload '{"param_1": "'"$1"'", "params": {"param_2": "'"$2"'"}}' \
  --profile "<your_iam-role>" \
  --cli-binary-format raw-in-base64-out response.json
  if [ $? -ne 0 ]; then
    echo "shell-script-with-json-escape-handlerの実行に失敗しました。"
  fi
}

echo "**********START**********"

output="output: python Error: Expecting value: line 1 column 1 (char 0)

"

output_with_newline=$(echo "$output" | sed 's/$/\\n/' | tr -d '\n')

echo "$output_with_newline"

run_lambda "AAA" "$output_with_newline"


# 実行結果の表示
cat response.json

# 一時ファイルの削除
rm response.json
output_with_newline=$(echo "$output" | sed 's/$/\\n/' | tr -d '\n')

このシェルコマンドの処理は、入力された文字列($output)の各行の末尾に改行文字(\n)を追加し、その後で結果からすべての実際の改行を取り除く、というものです。

  • echo "$output": 変数$outputの内容を出力します。
  • sed 's/$/\\n/': sedは文字列の編集ツールで、このコマンドは各行の末尾に\n(改行のエスケープ文字)を追加します。$は行の末尾を示しています。
  • tr -d '\n': 先ほど同様で改行文字(\n)を取り除きます。

全体の結果として、元の$outputの内容が1行の文字列となり、その文字列の各元の行の末尾には\nという文字が追加されます。そして、その結果がoutput_with_newline変数に格納されます。

実行結果

(blog_env)@23_shell_script_with_json_escape % bash run_lambda.sh
**********START**********
output: python Error: Expecting value: line 1 column 1 (char 0)\n\n\n
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}
{"statusCode": 200, "body": {"param_1": "AAA", "params": {"param_2": "output: python Error: Expecting value: line 1 column 1 (char 0)\n\n\n"}}}%    
(blog_env) @23_shell_script_with_json_escape %

(追記)jqコマンドでのエスケープ処理

よりシンプルにescapeするためにjqコマンドを使ってJSON形式の文字列を安全に提案する方法があるので追記します。

#!/bin/bash
# AWS CLIを使用してLambda関数を実行するシェルスクリプト

function run_lambda () {
  payload=$(jq -n --arg p1 "$1" --arg p2 "$2" '{param_1: $p1, params: {param_2: $p2}}')
  aws lambda invoke \
  --region "<lambda-region>" \
  --function-name shell-script-with-json-escape-handler \
  --payload "$payload" \
  --profile "<your-iam-role>" \
  --cli-binary-format raw-in-base64-out response.json

  if [ $? -ne 0 ]; then
    echo "shell-script-with-json-escape-handlerの実行に失敗しました。"
    exit 1
  fi
}

echo "**********START**********"

output="output: python Error: Expecting value: line 1 column 1 (char 0)


"

run_lambda "AAA" "$output"

# 実行結果の表示
cat response.json

# 一時ファイルの削除
rm response.json
payload=$(jq -n --arg p1 "$1" --arg p2 "$2" '{param_1: $p1, params: {param_2: $p2}}')

jqはコマンドラインJSONプロセッサで、JSONの生成、変換、フィルタリングなどを行うのに非常に役立つツールです。上記のjqコマンドは以下の動作を行っています。

  • -n: このオプションは、jqに空の入力を使用させます。これは、外部からの入力を待たずに新しいJSONデータを生成する場合に使用されます。

  • --arg p1 "$1": これはp1という名前のjq内部変数に第1引数$1の値を代入します。--argオプションは文字列として変数をセットするため、特殊文字を含む文字列や改行を含む文字列も安全に扱えます。

  • --arg p2 "$2": 同様に、これはp2という名前のjq内部変数に第2引数$2の値を代入します。
  • '{param_1: $p1, params: {param_2: $p2}}': これは生成されるJSONの形式を指定しています。$p1$p2は先ほど設定したjqの変数で、それぞれの場所に代入されます。

シェル芸で使いたい jqイディオム

実行結果

(base) @23_shell_script_with_json_escape % bash run_lambda.sh
**********START**********
{
    "StatusCode": 200,
    "ExecutedVersion": "$LATEST"
}
{"statusCode": 200, "body": {"param_1": "AAA", "params": {"param_2": "output: python Error: Expecting value: line 1 column 1 (char 0)\n\n\n"}}}%                    
(base)@23_shell_script_with_json_escape %

最後に

細かい内容ですが、シェルスクリプト上での変換処理はあまり経験がなかったので記載しました。このブログがどなたかの助けになれば幸いです。