#!/usr/bin/env bash # Copyright 2014 Vladimir Ivanov # Distributed under the terms of the GNU General Public License v2 REPO_DIR= # Specify the tournament here TOURNAMENT= # Version information VERSION="0.4" # Firefox User Agent FIREFOX_UA="Mozilla/5.0 (X11; Linux x86_64; rv:24.0) Gecko/20100101 Firefox/24.0" argv0=${0##*/} function usage { cat < ... $argv0 -h $argv0 -v Put the script under the root directory of your repository or set inner variable REPO_DIR to it. If the tournament is not the last one (default), store its sub-directory in inner variable TOURNAMENT. The first form fills the results of chess games and stores their PGN files, assuming that all the games were played by the same pair of players on tour of the tournament. The PGN files are sorted by their timestamps, unless '-u' is set. Each game should be available at the corresponding (lichess.org). The second form shows this help output. The third form shows version information. Options: -a Add current games to existing games -c Clean up existing games before adding -u Don't sort games by their timestamps EOF exit "${1:-0}" } function version { exec echo "${argv0}-${VERSION}" } function game_setup { date_re="[0-9?]{2}\.[0-9?]{2}\.[0-9?]{4}" : ${REPO_DIR:=`dirname "$0"`} # Synchronize the repository git --git-dir=${REPO_DIR}/.git pull # If no tournament given, set it to the last one if [[ -z $TOURNAMENT ]]; then local year_dir=$(ls -1 -d ${REPO_DIR}/[0-9][0-9][0-9][0-9]/ | tail -1) TOURNAMENT=$(ls -1 -d ${year_dir}[0-9]-*/ | tail -1 \ | sed -E "s|${REPO_DIR}/(.*)/|\1|") fi # Configuration file for players ply_ini=${REPO_DIR}/${TOURNAMENT}/players.ini [[ -f $ply_ini ]] || die "File ${ply_ini} not found." # Pairs of players on the tour tour_info=${REPO_DIR}/${TOURNAMENT}/tours/${TOUR}/tour_info [[ -f $tour_info ]] || die "File ${tour_info} not found." } function game_tmp_pgns { # Don't sort just one game [[ $# == 1 ]] && SORT_GAMES=false for url in $@; do local timestamp= [[ $url =~ ^(http://[^/]*)/([^/]*) ]] # Link to annotated game PGN local game_url=${BASH_REMATCH[1]}/game/export/${BASH_REMATCH[2]::8}.pgn # Get the timestamp of game for sorting if $SORT_GAMES; then local game_api=${BASH_REMATCH[1]}/api/game/${BASH_REMATCH[2]::8} local api_response=$(curl -q --fail --location --silent "$game_api") [[ -z $api_response ]] && die "Unreachable game API ${game_api}" timestamp=$(sed -En "s/.*\"timestamp\":([0-9]+).*/\1/p" <<< "$api_response") fi # Store PGN file in a temporal location local tmp_pgn=$(mktemp -t ${timestamp:-1}.pgn-XXXXXX) TMP_PGN_FILES+=($tmp_pgn) trap "rm ${TMP_PGN_FILES[*]}" EXIT wget -q -U "$FIREFOX_UA" -O $tmp_pgn "$game_url" \ || die "Unreachable game PGN ${game_url}" done $SORT_GAMES && TMP_PGN_FILES=( $(xargs -n1 <<< ${TMP_PGN_FILES[*]} | sort -n) ) } function game_get_info { # Get the names of players, whose games are in PGN files ply_names= game_get_names # Select the names of two players local players=( $(echo -en "$ply_names" | sed "s/ /\\"$'\n'"/" | sort -u) ) [[ ${#players[@]} == 2 ]] || die "Players of the games are not the same." # Find the white and black players local line0=$(grep " ${players[0]}" $tour_info) local line1=$(grep " ${players[1]}" $tour_info) [[ -z $line0 || -z $line1 || $line0 != $line1 ]] \ && die "No game between ${players[0]} and ${players[1]} found in ${tour_info}." [[ $line0 =~ ^((${date_re})\ +([^\ ]+)\ +-\ +([^\ ]+))(.*)$ ]] white=${BASH_REMATCH[3]} black=${BASH_REMATCH[4]} # Additional information about the game record rec_length=${#BASH_REMATCH[1]} game_date=${BASH_REMATCH[2]} res_old=$(xargs <<< ${BASH_REMATCH[5]}) game_validate if ! $ADD_GAMES || [[ $game_date =~ \? ]]; then # Assign the date of games to the date of the last game local pgn_last=${TMP_PGN_FILES[${#TMP_PGN_FILES[@]} - 1]} local date=$(sed -En "s/\[Date \"([^\"]*)\"\]/\1/p" $pgn_last) game_date=${date:8:2}.${date:5:2}.${date::4} fi } function game_get_names { # Make an associative array from Lichess nicks to players' names game_parse_config local sections=$(grep -o "config_section_player[0-9]*" $tmp_ini) declare -A NAMES=() for sect in $sections; do eval $sect NAMES+=( [$lichess]=$name ) done for pgn in ${TMP_PGN_FILES[@]}; do # Extract players on Lichess local wt_lichess=$(sed -En "s/\[White \"([^\"]*)\"\]/\1/p" $pgn) local bk_lichess=$(sed -En "s/\[Black \"([^\"]*)\"\]/\1/p" $pgn) game_add_player $wt_lichess ply_names+=" " game_add_player $bk_lichess ply_names+="\n" done } function game_parse_config { # Temporary files tmp_ini=$(mktemp -t `basename $ply_ini`.XXXXXX) TMP_INI_FILES="${tmp_ini} ${tmp_ini}.prev" trap "rm $TMP_INI_FILES ${TMP_PGN_FILES[*]}" EXIT # Copy player INI file to the temporary location # NOTE: an empty line is added to the file beginning in order to # match the only first occurrence for non-GNU sed echo > $tmp_ini cat "$ply_ini" >> $tmp_ini # Remove tabs or spaces around the `=' sed -E -i.prev "s/[[:blank:]]*=[[:blank:]]*/=/" $tmp_ini # Transform section labels into function declaration sed -E -i.prev "1,/^\[.*\]/s/^\[([^]]*)\]/config_section_\1() {/" $tmp_ini sed -E -i.prev "s/^\[([^]]*)\]/}\\"$'\n'"config_section_\1() {/" $tmp_ini echo -e "\n}" >> $tmp_ini # Source the file source $tmp_ini } function game_add_player { local lichess_ply=$1 local ply=${NAMES[$lichess_ply]} while [[ ! " ${NAMES[@]} " =~ \ $ply\ ]]; do echo -n "$(tput setaf 2)The list of players:$(tput sgr0)" sed "s/ /\\"$'\n'"$(tput setaf 6)*$(tput sgr0) /g" <<< " ${NAMES[@]}" echo -n "Type the name of ${lichess_ply}> " read ply done ply_names+=$ply } function game_validate { # By default, other games between the players are not allowed if ! $ADD_GAMES && ! $CLEANUP_GAMES; then if ! [[ $game_date =~ \? && -z $res_old ]]; then die "Some games between ${white} and ${black} already recorded." fi fi # Players' sides should interchange local length=$(echo -en "$ply_names" | wc -l) if $ADD_GAMES && [[ -n $res_old ]]; then local residue=$(( $(wc -w <<< "$res_old") % 2 )) else local residue=0 fi local ply_ordered= for ((i = 0; i < length; i++)); do if (( i % 2 == residue )); then ply_ordered+="${white} ${black}\n" else ply_ordered+="${black} ${white}\n" fi done # List found games echo "$(tput setaf 2)Found games:$(tput sgr0)" echo -en "$ply_names" \ | sed "s/ / - /;s/^/$(tput setaf 6)*$(tput sgr0) /" if [[ "$ply_names" != "$ply_ordered" ]]; then # List expected games echo "$(tput setaf 2)Expected games:$(tput sgr0)" echo -en "$ply_ordered" \ | sed "s/ / - /;s/^/$(tput setaf 6)*$(tput sgr0) /" local answer echo -n "Approve games with wrong players' sides? (Y/n)> " read answer [[ $answer =~ ^(Y|y|Yes|yes)$ ]] || exit 1 fi } function game_store_pgns { game_dir=$(ls -1 -d 2>/dev/null \ ${REPO_DIR}/${TOURNAMENT}/tours/${TOUR}/*-${white}-vs-${black}) local game_dir_old= if $CLEANUP_GAMES && [[ -n $game_dir ]]; then git --git-dir=${REPO_DIR}/.git rm --ignore-unmatch -q ${game_dir}/*.pgn [[ -d $game_dir ]] && game_dir_old=$game_dir game_dir= fi local pgn_index=0 if [[ -z $game_dir ]]; then local date=${game_date:6:4}-${game_date:3:2}-${game_date::2} game_dir=${REPO_DIR}/${TOURNAMENT}/tours/${TOUR}/${date}-${white}-vs-${black} if [[ $game_dir != $game_dir_old ]]; then echo "Creating directory ${game_dir}..." mkdir $game_dir if $CLEANUP_GAMES && [[ -n $game_dir_old ]]; then git --git-dir=${REPO_DIR}/.git mv ${game_dir_old}/* ${game_dir} rm -r $game_dir_old fi fi else $ADD_GAMES || die "Directory ${game_dir} already exist." local old_pgns=$(ls -1 -p 2>/dev/null ${game_dir}/*.pgn) [[ -n $old_pgns ]] && pgn_index=$(wc -l <<< "$old_pgns") fi for pgn in ${TMP_PGN_FILES[@]}; do (( pgn_index += 1 )) echo "Storing file ${game_dir}/${pgn_index}.pgn..." cp $pgn ${game_dir}/${pgn_index}.pgn done } function game_update_info { # The maximal length of game records, excepting results local length_max=$(grep -Eo "^${date_re} +[^ ]+ +- +[^ ]+" $tour_info \ | awk '{print length}' | sort -nr | head -1) local spaces=$(( length_max - rec_length )) sep= (( spaces == 0 )) || sep=$(printf "%${spaces}s" " ") local result= game_get_result echo "Updating file ${tour_info}..." sed -E -i.orig \ "s|^${date_re}( +${white} +- +${black}).*|${game_date}\1${sep}${result}|" \ $tour_info rm ${tour_info}.orig } function game_get_result { local res_index=0 while read ply_fst ply_snd; do local pgn=${TMP_PGN_FILES[$res_index]} local res=$(sed -En "s/\[Result \"([^\"]*)\"\]/\1/p" $pgn) if [[ $res == 1/2-1/2 ]]; then # Representation of draw res=1/2 elif [[ $ply_fst != $white ]]; then res=$(rev <<< $res) fi result+=" $res" (( res_index += 1 )) done <<< "$(echo -e "$ply_names")" $ADD_GAMES && [[ -n $res_old ]] && result=" ${res_old}${result}" } function game_git_commit { git --git-dir=${REPO_DIR}/.git add ${game_dir} $tour_info local title=$(awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}' \ <<< ${TOURNAMENT##*-}) git --git-dir=${REPO_DIR}/.git commit -m \ "Tournament ${title}, tour ${TOUR#0}: ${white} vs. ${black}." git --git-dir=${REPO_DIR}/.git push } function die { echo "$@" >&2 exit 1 } function checkargs { [[ $OPTARG =~ ^[0-9]+$ ]] || die "Incorrect tour number." TOUR=$(printf "%02g" $OPTARG) } ADD_GAMES=false CLEANUP_GAMES=false SORT_GAMES=true while getopts act:uhv opt; do case $opt in a) ADD_GAMES=true ;; c) CLEANUP_GAMES=true ;; t) checkargs ;; u) SORT_GAMES=false ;; h) usage ;; v) version ;; *) usage 1 ;; esac done shift $(($OPTIND - 1)) # For now, tour number should be given explicitly [[ -z $TOUR || $# == 0 ]] && usage 1 # Don't add and clean up games simultaneously $ADD_GAMES && $CLEANUP_GAMES && usage 1 game_setup declare -a TMP_PGN_FILES game_tmp_pgns $@ game_get_info game_store_pgns game_update_info game_git_commit exit 0