Skip to content

Instantly share code, notes, and snippets.

@844196
Last active August 29, 2015 14:13
Show Gist options
  • Save 844196/eba73ef6aa4026b7ac6e to your computer and use it in GitHub Desktop.
Save 844196/eba73ef6aa4026b7ac6e to your computer and use it in GitHub Desktop.
Minecraftのスキンアイコンを取得
#!/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
Copy link
Author

844196 commented Jan 12, 2015

@sasairc

指摘ですが、エラーコードを割り当てるのが面倒なのと例外処理がわかんないので、

  • ネットワーク周りはhttp status codeをそのままerror()に渡して終了コードとする
  • それ以外(オプションの不備とか)は1を終了コードとする

にしました(妥協)

一応動きますが、

  • 関数・変数の命名規則
  • 字下げ
  • 処理の仕方

について文句を言ってもらえたらウレシイ... ウレシイ...

@sasairc
Copy link

sasairc commented Jan 13, 2015

@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オプションと併用しても損はしないと思う。(多分ね)

1354967210938

@844196
Copy link
Author

844196 commented Jan 13, 2015

@sasairc

ここまでしっかりと書いてくれると思ってませんでした。アリガトナス!

オプションの不備(無効 OR 足りない)は、一般的なエラーとして-1を返すのが良いかと。
その他の内部で起こる、ユーザ操作に起因するエラー(getoptsを通過が、オプションが整数値でない)等はソース内のエラー出現箇所に応じて整数を加算する

素直に加算した方が作る方も使う方もわかりやすいですよね。参考になります。

例えば、function error()function do_error()になりますし
function checkHttpCode()function check_http_status()のようになります。

正直悩んでるところです。キャメルケースめいた書き方とスネークケースが混在してるので、どっちかに統一できればと思います。

変数やコマンドに対して実行結果を渡す際に、パイプが複数繋がるようであれば
一つの階層として目視できるよう、インデントをつけるように心がけてます。

「エディタ幅を越えたら行継続」みたいな曖昧なマイルールはあるんですが、一貫したルールは必要ですね... 何かしら考えます。

オプション解析は、getoptsを使うよりfor文で解析したほうが長いオプションも使える上にとてもパワフルでかつ汎用性があるのでオススメ。

はぇ^〜 すっごい参考になる... 試しにテスト書いてみます。


唐突なレビュー依頼でしたが回答してくれてありがとうございます(ありがとうなさま)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment