-
-
Save 844196/eba73ef6aa4026b7ac6e to your computer and use it in GitHub Desktop.
| #!/bin/bash | |
| # | |
| # @(#) Minecraftのスキンアイコンを取得 | |
| # | |
| # Author: | |
| # 844196 (@84____) | |
| # | |
| # License: | |
| # MIT | |
| # | |
| # initialize | |
| ## option | |
| set -u | |
| set -e | |
| ## about me | |
| readonly ME="${0##*/}" | |
| readonly VERSION="0.3" | |
| ## usage | |
| function usage() { | |
| cat <<-EOF 1>&2 | |
| Usage: | |
| ${ME} [optinos] username | |
| Required: | |
| curl, jq, base64, ImageMagick | |
| Argument: | |
| username Minecraft Username | |
| Options: | |
| -v, --version Print version and exit successfully. | |
| -h, --help Print this help and exit successfully. | |
| -s, --size Specify the output image size. (default: 800) | |
| -o, --output Specify the output path. (default: ./username.png) | |
| EOF | |
| return 0 | |
| } | |
| ## error | |
| function error() { | |
| echo "${ME}: ${1}" 1>&2 | |
| exit "${2:-1}" | |
| } | |
| ## temporary | |
| readonly TMPDIR="${TMP:-/tmp}/${ME}.$$" | |
| mkdir -p "${TMPDIR}" | |
| trap 'rm -r ${TMPDIR}' 0 | |
| function makeTmpFile() { | |
| local filename="${TMPDIR}/${1:-$RANDOM.tmp}" | |
| mktemp "${filename}" | |
| return 0 | |
| } | |
| ## check require command | |
| required_command=('curl' 'jq' 'base64' 'convert') | |
| for command in "${required_command[@]}" | |
| do | |
| type "${command}" >/dev/null 2>&1 || error "requires command -- ${command}" "2" | |
| done | |
| # get and check argument, options | |
| ## get option | |
| for OPTIONS in "${@-}" | |
| do | |
| case "${OPTIONS}" in | |
| '-h'|'--help' ) | |
| usage | |
| exit 0 | |
| ;; | |
| '-v'|'--version' ) | |
| echo "${VERSION}" 1>&2 | |
| exit 0 | |
| ;; | |
| '-s'|'--size' ) | |
| if [[ -z "${2-}" ]] || [[ "${2-}" =~ ^-+ ]]; then | |
| error "option requires an argument -- '${1}'" "-1" | |
| fi | |
| output_size="${2}" | |
| shift 2 | |
| ;; | |
| '-o'|'--output' ) | |
| if [[ -z "${2-}" ]] || [[ "${2-}" =~ ^-+ ]]; then | |
| error "option requires an argument -- '${1}'" "-1" | |
| fi | |
| output_path="${2}" | |
| shift 2 | |
| ;; | |
| '--debug' ) | |
| echo "${ME} ${VERSION} debug mode" | |
| set -x | |
| shift 1 | |
| ;; | |
| -* ) | |
| error "illegal option -- '${1}'" "-1" | |
| ;; | |
| * ) | |
| if [[ -n "${1-}" ]] && [[ ! "${1-}" =~ ^-+ ]]; then | |
| args+=( "${1}" ) | |
| shift 1 | |
| fi | |
| ;; | |
| esac | |
| done | |
| ## get username | |
| while read -t 1 stdin | |
| do | |
| : "${args[0]:=${stdin}}" | |
| done | |
| if [[ -n "${args[0]-}" ]]; then | |
| readonly username="${args[0]}" | |
| else | |
| error "invaild argument" "-1" | |
| fi | |
| ## check options | |
| ### output size | |
| readonly output_size="${output_size:-800}" | |
| if ! [[ "${output_size}" =~ [1-9][0-9]*$ ]]; then | |
| error "invaild option -- 'output_size'" "3" | |
| fi | |
| ### output path | |
| output_path=${output_path:-./${username}.png} | |
| output_filename="${output_path##*/}" | |
| output_directory="$(dirname "${output_path}")" | |
| if [[ -e "${output_directory}" ]]; then | |
| output_directory="$( | |
| cd "${output_directory}" | |
| pwd | |
| )" | |
| readonly output_path="${output_directory%/}/${output_filename}" | |
| else | |
| error "invaild option -- 'output_path'" "4" | |
| fi | |
| # get skin | |
| ## check http status code | |
| function checkHttpStatusCode() { | |
| local http_status_code="$( | |
| echo "${1}" | | |
| jq -r 'select(has("http_status_code")) | .http_status_code' | |
| )" | |
| case "${http_status_code}" in | |
| '200' ) | |
| return 0 | |
| ;; | |
| '000' ) | |
| return 5 | |
| ;; | |
| * ) | |
| return "${http_status_code}" | |
| ;; | |
| esac | |
| } | |
| ## get UUID | |
| function getUuid() { | |
| local uuid_json="$( | |
| curl -s https://api.mojang.com/users/profiles/minecraft/"${1}" \ | |
| -w '{"http_status_code":"%{http_code}"}' | |
| )" | |
| if checkHttpStatusCode "${uuid_json}"; then | |
| uuid="$( | |
| echo "${uuid_json}" | | |
| jq -r 'select(has("id")) | .id' | |
| )" | |
| else | |
| error "internal error -- 'getUuid()'" "${?}" | |
| fi | |
| return 0 | |
| }; getUuid "${username}"; | |
| ## get skin URI and download | |
| function getSkinUri() { | |
| local skin_json="$( | |
| curl -s https://sessionserver.mojang.com/session/minecraft/profile/"${1}" \ | |
| -w '{"http_status_code":"%{http_code}"}' | |
| )" | |
| if checkHttpStatusCode "${skin_json}"; then | |
| local skin_uri="$( | |
| echo "${skin_json}" | | |
| jq -r 'select(has("properties")) | .properties[].value' | | |
| base64 -D | | |
| jq -r '.textures.SKIN.url' | |
| )" | |
| curl -s -o "$(makeTmpFile skin.png)" "${skin_uri}" | |
| else | |
| error "internal error -- 'getSkinUri()'" "${?}" | |
| fi | |
| return 0 | |
| }; getSkinUri "${uuid}"; | |
| ## convert | |
| function skinConvert() { | |
| convert -crop 8x8+8+8 "${TMPDIR}/skin.png" "$(makeTmpFile face.png)" && : | |
| [[ "${?}" -ne "0" ]] && error "internal error -- 'skinConvert()'" "6" | |
| convert -crop 8x8+40+8 "${TMPDIR}/skin.png" "$(makeTmpFile hair.png)" && : | |
| [[ "${?}" -ne "0" ]] && error "internal error -- 'skinConvert()'" "6" | |
| convert "${TMPDIR}/face.png" "${TMPDIR}/hair.png" -composite "$(makeTmpFile head.png)" && : | |
| [[ "${?}" -ne "0" ]] && error "internal error -- 'skinConvert()'" "6" | |
| convert -scale x"${output_size}" "${TMPDIR}/head.png" "${output_path}" && : | |
| [[ "${?}" -ne "0" ]] && error "internal error -- 'skinConvert()'" "7" | |
| return 0 | |
| }; skinConvert && echo "${output_path}" && exit 0; |
| testEchoUsage | |
| testEchoVersion | |
| testFalseExitNoUsername | |
| testFalseExitInvaildOptionSize | |
| testFalseExitInvaildOptionOutputPath | |
| testTrueExitPipe | |
| 30.. 20.. 10.. 5 4 3 2 1 | |
| testTrueExitArgs | |
| 30.. 20.. 10.. 5 4 3 2 1 | |
| Ran 7 tests. | |
| OK |
| #!/bin/bash | |
| # common function | |
| function countDown() { | |
| for i in $(seq "${1}" -1 1) | |
| do | |
| if [[ $(( i % 10 )) = "0" ]]; then | |
| printf "%d.. " "${i}" 1>&2 | |
| fi | |
| if [[ "${i}" -le "5" ]]; then | |
| printf "%d " "${i}" 1>&2 | |
| fi | |
| sleep 1 | |
| done | |
| printf "\n" 1>&2 | |
| return 0 | |
| } | |
| # test | |
| testEchoUsage() { | |
| local usage="$(~/getmcskin.sh -h 2>&1)" | |
| assertNotNull "${usage}" | |
| assertEquals 0 $? | |
| } | |
| testEchoVersion() { | |
| local version="$(~/getmcskin.sh -v 2>&1)" | |
| assertNotNull "${version}" | |
| assertEquals 0 $? | |
| } | |
| testFalseExitNoUsername() { | |
| ~/getmcskin.sh >/dev/null 2>&1 | |
| assertEquals 255 $? | |
| } | |
| testFalseExitInvaildOptionSize() { | |
| ~/getmcskin.sh -s foo 844196 >/dev/null 2>&1 | |
| assertEquals 3 $? | |
| } | |
| testFalseExitInvaildOptionOutputPath() { | |
| ~/getmcskin.sh -o /foo/save.png 844196 >/dev/null 2>&1 | |
| assertEquals 4 $? | |
| } | |
| testTrueExitPipe() { | |
| countDown '30' | |
| echo '844196' | ~/getmcskin.sh >/dev/null 2>&1 | |
| assertEquals 0 $? | |
| } | |
| testTrueExitArgs() { | |
| countDown '30' | |
| ~/getmcskin.sh 844196 >/dev/null 2>&1 | |
| assertEquals 0 $? | |
| } | |
| . shunit2 |
@844196
おかのした
(あくまで個人的に思うことなので)ま、多少はね?
適当に読んで「ええやん」と思うなら参考にして頂ければ嬉しいです。
指摘ですが、エラーコードを割り当てるのが面倒なのと例外処理がわかんないので
オプションの不備(無効 OR 足りない)は、一般的なエラーとして -1 を返すのが良いかと。
その他の内部で起こる、ユーザ操作に起因するエラー(getoptsを通過が、オプションが整数値でない)等は
ソース内のエラー出現箇所に応じて整数を加算する
例として
# check require command ブロックであれば return 1
# get username ブロックであれば return 2
のような方法が良いと思われます。
※ こんな適当でいいのかと思われそうですが、ベル研のUNIX標準コマンド群のエラーはこんな感じで決められてたり・・・(単純すぎて逆に読みやすい)
シェルスクリプトでのハンドリングは、他の言語に比べて極めて面倒なので
参考としてシェルスクリプトで * Try-Catch * 的な動作を実現する為の参考記事を載せておきます。
[Shell][Bash]BashでTry Catch Finally - http://zuqqhi2.com/?p=1148
関数・変数の命名規則
動詞+名詞を踏まえた上で個人の好みになってしまいますが、個人的には
- 全小文字
- 単語間含め、区切りはアンダーバー
- 関数名や変数名を短く保つ
- 一般に通じる略称は積極的に使う(string -> str等)
を心がけてかつ、自分が読みやすいと思ってます(他人が読みやすいとは言ってない)
例えば、function error() は function do_error() になりますし
function checkHttpCode() は ``function check_http_status()` のようになります。
字下げ
case文の条件内は処理の多少に関わらず、必ず改行しインデントを付けたほうが読みやすいと思います。
変数やコマンドに対して実行結果を渡す際に、パイプが複数繋がるようであれば
一つの階層として目視できるよう、インデントをつけるように心がけてます。
# local http_code="$(echo "${1}" | jq -r 'select(has("http_code")) | .http_code')"
local http_code="$(
echo "${1}" |\
jq -r 'select(has("http_code")) |\
.http_code'
)"
処理の仕方
オプション解析は、getoptsを使うよりfor文で解析したほうが長いオプションも使える上に
とてもパワフルでかつ汎用性があるのでオススメ。
・参考 http://qiita.com/b4b4r07/items/dcd6be0bb9c9185475bb
上記した * Try-Catch * による例外処理は、bashで-eオプションと併用しても損はしないと思う。(多分ね)
ここまでしっかりと書いてくれると思ってませんでした。アリガトナス!
オプションの不備(無効 OR 足りない)は、一般的なエラーとして
-1を返すのが良いかと。
その他の内部で起こる、ユーザ操作に起因するエラー(getoptsを通過が、オプションが整数値でない)等はソース内のエラー出現箇所に応じて整数を加算する
素直に加算した方が作る方も使う方もわかりやすいですよね。参考になります。
例えば、
function error()はfunction do_error()になりますし
function checkHttpCode()はfunction check_http_status()のようになります。
正直悩んでるところです。キャメルケースめいた書き方とスネークケースが混在してるので、どっちかに統一できればと思います。
変数やコマンドに対して実行結果を渡す際に、パイプが複数繋がるようであれば
一つの階層として目視できるよう、インデントをつけるように心がけてます。
「エディタ幅を越えたら行継続」みたいな曖昧なマイルールはあるんですが、一貫したルールは必要ですね... 何かしら考えます。
オプション解析は、getoptsを使うよりfor文で解析したほうが長いオプションも使える上にとてもパワフルでかつ汎用性があるのでオススメ。
はぇ^〜 すっごい参考になる... 試しにテスト書いてみます。
唐突なレビュー依頼でしたが回答してくれてありがとうございます(ありがとうなさま)


@sasairc
指摘ですが、エラーコードを割り当てるのが面倒なのと例外処理がわかんないので、
error()に渡して終了コードとする1を終了コードとするにしました(妥協)
一応動きますが、
について文句を言ってもらえたらウレシイ... ウレシイ...