[AWS CLI] s3 syncコマンドのexclude/includeオプションを使って上手くサブフォルダーの中身をアップロードする


2020/10/03

AWS S3のファイルの中身をCLIで管理している方も多いと思います。

特に、更新のあったファイルだけを自動で更新してくれる
syncコマンドはとても重宝しております。

ユーザー側できめ細かいファイル管理を利用する際に使う
--exclude--includeのオプションをシェルスクリプトの中で使う時の個人的なポイントを忘備録として残しておきたいと思います。


前知識編〜文字列からシェルコマンドとして実行するアレコレ

これはaws syncコマンドに限ったことではありません。

一度にたくさんの引数を要求されるコマンドをワンライナーで実行する場合、一旦コマンドを文字列として生成してから、その文字列を実行させると便利なときがあります。

文字列をコマンドとして実行させる方法はいくつかあり、例えば以下のように
1.shというスクリプトを実行してみます。

            
            #!/bin/bash

script='echo HELLO!'

#👇文字列をそのまま打ち出すと自動でコマンドと判別してくれるケース
${script}

#👇[`]文字で括ってechoコマンドで文字列を標準出力で打ち出すと文字列をコマンドと判別してくれるケース
`echo ${script}`

#👇理屈は上と同じで$()を使ってechoコマンドを実行しているケース
$(echo ${script})

#👇eval関数を利用するケース
eval ${script}
        

            
            $ chmod +x 1.sh
$ ./1.sh
HELLO!
HELLO!
HELLO!
HELLO!
        
一見上記の4通りの方法は簡単なコマンドの文字列だとほぼ違いがないように見えますが、実は細かな違いが存在しています。

この微妙な違いを理解するため、すこし実験してみます。

先ほどの
1.shを改造して、以下のような2.shを作って実行してみます。

            
            #!/bin/bash

script='hoge runs a function!'

hoge() {
    #👇$0以外のコマンドライン引数
    echo $*
    #👇コマンドラインの引数の数
    echo $#
}

${script}
`echo ${script}`
$(echo ${script})
eval ${script}
        

            
            $ ./2.sh
runs a function!
3
runs a function!
3
runs a function!
3
runs a function!
3
        
今回のスクリプトは一つの引数('runs a function!'の文字列)付きの関数hogeを実行するの文字列を走らせたつもりでも、3つの引数('runs' / 'a' / 'function!')があるように解釈されてしまいます。

どうやら空白文字があるために、hoge以降の文字列は全て個別の引数だと認識されているようです。

今度は、明示的に
"で括って文字列であることを教えてあげるとどうでしょうか。

            
            #!/bin/bash

script='hoge "runs a function!"'

hoge() {
    echo $*
    echo $#
}

${script}
`echo ${script}`
$(echo ${script})
eval ${script}
        

            
            $ ./2.sh
"runs a function!"
3
"runs a function!"
3
"runs a function!"
3
runs a function!
1
        

今度は、最初の3つのやり方はやはり引数が3つであると解釈されています。

evalを用いたときだけは正しく文字列を認識しているようです。

ちなみにダブルクォート
"とシングルクォート'を入れ替えても結果は一緒です。

            
            #!/bin/bash

script="hoge 'runs a function!'"
#👇のように\"を使っても一緒
#script="hoge \"runs a function!\""

hoge() {
    echo $*
    echo $#
}

${script}
`echo ${script}`
$(echo ${script})
eval ${script}
        

            
            $ ./2.sh
'runs a function!'
3
'runs a function!'
3
'runs a function!'
3
runs a function!
1
        
これらの結果を通して分かるように、eval以外のやり方は結局コマンド 引数1 引数2 ...というように空白文字を挟んで単純な解釈でしかコマンドを実行してくれないのに対し、evalは渡された文字列をコマンドの表現式だと捉え、よりスマートな評価を行いコマンドに翻訳してくれています。

以下はdate関数を利用した、
eval以外では思うように実行されないけれどevalなら実行されるケースを何パターンかやってみます。

            
            #!/bin/bash
echo '========= Execute a script ========='
a="date"
aarg="--date='TZ=\"America/Los_Angeles\" 09:00 next Fri'"
a="${a} ${aarg}"
${a}
`echo ${a}`
$(echo ${a})
eval ${a}

echo '========= Execute b script ========='
b="date '+%Y-%-m-%-d'"
${b}
`echo ${b}`
$(echo ${b})
eval ${b}

echo '========= Execute c script ========='
c="date \"+%Y-%-m-%-d\""
${c}
`echo ${c}`
$(echo ${c})
eval ${c}

echo '========= Execute d script ========='
darg='+%Y %-m %-d'
d="date '${darg}'"
${d}
`echo ${d}`
$(echo ${d})
eval ${d}

echo '========= Execute e script ========='
earg='+%Y %-m %-d'
e="date \"${earg}\""
${e}
`echo ${e}`
$(echo ${e})
eval ${e}
        

            
            $ ./3.sh
========= Execute a script =========
date: 余分な演算子 `next'
Try 'date --help' for more information.
date: 余分な演算子 `next'
Try 'date --help' for more information.
date: 余分な演算子 `next'
Try 'date --help' for more information.
2020年 10月 10日 土曜日 01:00:00 JST
========= Execute b script =========
date: `\'+%Y-%-m-%-d\'' は無効な日付です
date: `\'+%Y-%-m-%-d\'' は無効な日付です
date: `\'+%Y-%-m-%-d\'' は無効な日付です
2020-10-2
========= Execute c script =========
date: `"+%Y-%-m-%-d"' は無効な日付です
date: `"+%Y-%-m-%-d"' は無効な日付です
date: `"+%Y-%-m-%-d"' は無効な日付です
2020-10-2
========= Execute d script =========
date: 余分な演算子 `%-m'
Try 'date --help' for more information.
date: 余分な演算子 `%-m'
Try 'date --help' for more information.
date: 余分な演算子 `%-m'
Try 'date --help' for more information.
2020 10 2
========= Execute e script =========
date: 余分な演算子 `%-m'
Try 'date --help' for more information.
date: 余分な演算子 `%-m'
Try 'date --help' for more information.
date: 余分な演算子 `%-m'
Try 'date --help' for more information.
2020 10 2
        
ちょっと回りくどかったようですが、実験の結論としては文字列からのコマンド実行はeval関数を使いましょう。


aws syncコマンドを文字列から使う

前置きが長くなりましたが、ここからようやく本題です。

aws cliリファレンスの中で使い方に言及されていますので参考にしてみます。

まず、アップロードする対象のファイルやフォルダを粒度細かくフィルター制御する場合には、
--exclude--includeのオプションをチェーンにして使う必要があります。

            
            $ aws sync --exclude "*" --include "*.txt" --include "hoge/*.txt" --include "piyo/*.txt"
        
上記のコマンドが推奨される使い方の一例です。

一旦全てのファイルを
--exclude "*"で除外しておき、--includeでアップロードの対象にしたいファイルを追加していくようなやり方です。

--excludeオプションと--includeオプションは何回も使えるものの、一度に複数の対象のファイル表現は出来ないので、このような数珠繋ぎな長い引数を取るようにしか使えないのが現状です。

場合によっては引数が長くなるので、一旦コマンドを文字列に落とし込んでから、その文字列をコマンドを実行するような場合に、上の節で説明したような
eval関数以外でのやり方に注意が必要になってきます。

たとえば以下の例では、ルートフォルダにある
index.htmlと、引数で指定したルート直下にあるサブフォルダの中身を選択的に同期アップロードしたい場合のスクリプトとなります。

            
            #!/bin/bash

#👇アップロード対象となるローカルのフォルダ
DIST_FOLDER_PATH=/path/to/dist/folder

#👇アップロード先のS3バケットのアドレス
S3_BUCKET_PATH=s3://your-bucket.online/

subfolders=(${1//,/ })
for subfolder in "${subfolders[@]}"; do
    include_option="${include_option} --include '${subfolder}/*'"
done
include_option="aws s3 sync ${DIST_FOLDER_PATH} ${S3_BUCKET_PATH} --exclude '*' --include 'index.html' ${include_option}"

eval ${include_option}
        
サブフォルダの指定には、フォルダの名前をコンマ刻みの文字列で与え、引数で渡しています。

この引数を元に
aws syncが実行できる形式の文字列に変換し、evalで実行するという流れです。

試しに、ルート直下にある
hoge/piyo/fugaというフォルダの中身を同期するようにしたい場合には、以下のように実行します。

            
            $ ./aws_sync.sh hoge,piyo,fuga
aws s3 sync /path/to/dist/folder s3://your-bucket.online/ --exclude '*' --include 'index.html' --include 'hoge/*' --include 'piyo/*' --include 'fuga/*'
#...同期アップロードが始まる
        
今回は簡単なスクリプトの例を示しました。

もっと複雑なアップロードが行いたければ、テキストファイルでホワイトリストを作成しておいて、
grepコマンドなどから読み取り、コマンド文字列を生成するなどの拡張が考えられます。

色々と応用の幅が広がりますが、今回はあまり深入りしないでおこうと思います。


まとめ

今回の金言: 何はともあれ、長い引数のコマンド文字列はeval関数を用いて実行しましょう。
記事を書いた人

記事の担当:taconocat

ナンデモ系エンジニア

主にAngularでフロントエンド開発することが多いです。 開発環境はLinuxメインで進めているので、シェルコマンドも多用しております。 コツコツとプログラミングするのが好きな人間です。