IDLookup CLI

Description:
Quickly lookup any player’s Username, EOSID and First & Last time seen
 as well as automatically copy the full EOSID to Clipboard (when run locally)
 Can search using Partial Playername OR Partial EOSID
Furthermore, provides capability to lookup a Player’s Tribe ID’s
across the entire cluster.

Commands:
idlookup <player>     ────  Search the database for a user’s EOSID  
idlookup -t <player>  ────  Search the database for a user’s TribeID  

idlookup -b           ────  Build EOSID database from ALL available logs  
 idlookup -bt          ────  Build Tribe database from ALL available logs

Examples:
   idlookup wolf
   idlookup -t wolf

Download idlookup.sh

#!/bin/bash
#
#
set -e
####################
# CONFIGURATION
####################

# DESIRED LOCAL DB PATHS
EOSID_DIR="/c/ark/eosid/"

DB_FILE="${EOSID_DIR}eosdb.csv"
TRIBE_DB="${EOSID_DIR}tribedb.csv"

EOSID_SCRAPED="${EOSID_DIR}eosid_scraped_logs.txt"
TRIBE_SCRAPED="${EOSID_DIR}tribe_scraped_logs.txt"

# LOCAL CLIPBOARD
CLIPBOARD="clip.exe"

# ARK SERVERS
#
# Format:
# "Map Name|Server Root Path"
#
SERVERS=(
    "Island|/C/ARK/Island"
    "Ragnarok|/C/ARK/Ragnarok"
    "Aberration|/C/ARK/Aberration"
    "Extinction|/C/ARK/Extinction"
    "Astraeos|/C/ARK/Astraeos"
    "Genesis|/C/ARK/Genesis"
)

####################
# END CONFIGURATION
####################

VERSION="EOSID Lookup v4.2.2"

####################
# DATA ARRAYS
####################

shopt -s nullglob
ALL_LOGS=()
for server in "${SERVERS[@]}"; do
    path="${server#*|}"
    ALL_LOGS+=("$path/ShooterGame/Saved/Logs/"*.log)
done
shopt -u nullglob

declare -A DB_SteamName
declare -A DB_SurvivorName
declare -A DB_FirstSeen
declare -A DB_LastSeen
declare -A DB_TimesSeen

####################
# COLOUR VARIABLES
####################

# Terminal Formatting
RESET="\033[0m"
BOLD="\033[1m"
# UI Colours
CLR_RED="\033[38;5;196m"    # Bright Red
CLR_GREEN="\033[38;5;46m"   # Bright Lime Green
CLR_GOLD="\033[38;5;226m"   # Gold
CLR_PURPLE="\033[38;5;141m" # Light Purple / Lavender
CLR_BLUE="\033[38;5;4m"     # Bright Sky Blue
CLR_DGREEN="\033[38;5;2m"   # Dark Forest Green
CLR_ORANGE="\033[38;5;208m" # Orange
CLR_AQUA="\033[38;5;51m"    # Aqua
CLR_PINK="\033[38;5;199m"   # PINK
CLR_PAQUA="\033[38;5;195m"  # Pale Aqua
CLR_TEXT="\033[38;5;252m"   # Light Grey
CLR_TITLE="\033[38;5;147m"  # Pale Blue
CLR_SAVE="\033[38;5;245m"   # Darker Grey

####################
# FUNCTIONS
####################

usage() {
    echo
    echo -e "${CLR_TITLE}${BOLD}${VERSION}${RESET}"
    echo
    echo -e "${BOLD}Description:${RESET}${CLR_TEXT}"
    echo -e "  Quickly lookup any player's Username, EOSID and First & Last time seen"
    echo -e "  as well as automatically copy the full EOSID to Clipboard"
    echo -e "  Can search using Partial Playername OR Partial EOSID"
    echo -e "  Furthermore, provides capability to lookup a Player's Tribe IDs across the entire cluster.${RESET}"
    echo
    echo -e "${BOLD}Usage:${RESET}${CLR_TEXT}"
    echo -e "  idlookup <Player>     ────  Search the database for a user's EOSID "
    echo -e "  idlookup -t <Player>  ────  Search the database for a user's TribeID "
    echo
    echo -e "  idlookup -b           ────  Build EOSID database from ALL available logs "
    echo -e "  idlookup -bt          ────  Build Tribe database from ALL available logs${RESET}"
    echo
    echo -e "${BOLD}Examples:${RESET}${CLR_TEXT}"
    echo -e "  idlookup wolf"
    echo -e "  idlookup -t wolf${RESET}"
    echo

    exit 1
}

#
# EOSID BUILD
#

cmd_build() {
    if [[ ! -f "$EOSID_SCRAPED" ]]; then
        echo
        echo -e "${CLR_GOLD}${BOLD}Building EOSID Database...${RESET}"
        echo "  Depending on the quantity of your archived logs, the first scan may take several minutes..."
        echo
        echo "  ${BOLD}Be Advised:${RESET} Once this first scan has completed, only new logs will be scanned in the future"
        echo "  this will ${BOLD}dramatically${RESET} reduce the processing time moving forward - from minutes to seconds."
    else
        echo -e "${CLR_GOLD}${BOLD} ▶ Scanning new logs & appending the EOSID Database...${RESET}"
    fi

    #
    # Load existing EOSID database
    #
    if [[ -f "$DB_FILE" ]]; then
        while IFS=',' read -r eosid steam survivor firstseen lastseen timesseen; do
            [[ "$eosid" == "EOSID" ]] && continue

            DB_SteamName["$eosid"]="$steam"
            DB_SurvivorName["$eosid"]="$survivor"
            DB_FirstSeen["$eosid"]="$firstseen"
            DB_LastSeen["$eosid"]="$lastseen"
            DB_TimesSeen["$eosid"]="$timesseen"
        done <"$DB_FILE"
    fi

    # cross-reference the all_logs array with the scaped_logs.txt - remove already scanned logs and add leftovers to
    # a new array NEW_LOGS
    touch "$EOSID_SCRAPED"

    declare -A SCRAPED_LOGS

    while IFS= read -r log; do
        SCRAPED_LOGS["$log"]=1
    done <"$EOSID_SCRAPED"

    NEW_LOGS=()

    for log in "${ALL_LOGS[@]}"; do
        if [[ -z ${SCRAPED_LOGS["$log"]+x} ]]; then
            NEW_LOGS+=("$log")
        fi
    done
    echo
    echo "───────────────────────────"
    echo "Total logs found: ${#ALL_LOGS[@]}"
    echo "New logs to scan:   ${#NEW_LOGS[@]}"
    echo "───────────────────────────"
    if [[ ${#NEW_LOGS[@]} -eq 0 ]]; then
        echo
        echo "No new logs to process."
        echo -e "${CLR_GREEN}All up to date!${RESET}"
        return
    fi

    #
    # PASS 1 - SteamName -> EOSID
    #
    while IFS= read -r line; do

        if [[ $line =~ ([0-9]{4}\.[0-9]{2}\.[0-9]{2}_[0-9]{2}\.[0-9]{2}\.[0-9]{2}):[[:space:]](.+)[[:space:]]\[UniqueNetId:([[:xdigit:]]+) ]]; then

            timestamp="${BASH_REMATCH[1]}"
            player="${BASH_REMATCH[2]}"
            eosid="${BASH_REMATCH[3]}"

            DB_SteamName["$eosid"]="$player"

            if [[ -z ${DB_FirstSeen[$eosid]} || "$timestamp" < "${DB_FirstSeen[$eosid]}" ]]; then
                DB_FirstSeen["$eosid"]="$timestamp"
            fi

            if [[ -z ${DB_LastSeen[$eosid]} || "$timestamp" > "${DB_LastSeen[$eosid]}" ]]; then
                DB_LastSeen["$eosid"]="$timestamp"
            fi

            ((++DB_TimesSeen["$eosid"]))
        fi

    done < <(
        grep -hEi "joined this ARK|left this ARK" "${NEW_LOGS[@]}"
    )

    #
    # PASS 2 - Survivor -> EOSID
    #
    local LastSurvivor=""

    while IFS= read -r line; do

        if [[ $line =~ :[[:space:]]([^:]+)[[:space:]]froze[[:space:]] ]]; then
            LastSurvivor="${BASH_REMATCH[1]}"
            continue
        fi

        if [[ $line =~ Frozen[[:space:]]by[[:space:]]ID:[[:space:]]([[:xdigit:]]+) ]]; then
            eosid="${BASH_REMATCH[1]}"

            if [[ -n "$LastSurvivor" ]]; then
                DB_SurvivorName["$eosid"]="$LastSurvivor"
            fi

            LastSurvivor=""
        fi

    done < <(
        grep -hEi "froze |Frozen by ID:" "${NEW_LOGS[@]}"
    )

    #
    # PASS 3 - Historical chat fallback
    #
    while IFS= read -r line; do

        if [[ $line =~ :[[:space:]](.+)[[:space:]]\(([^()]+)\):[[:space:]] ]]; then
            player="${BASH_REMATCH[1]}"
            survivor="${BASH_REMATCH[2]}"

            for eosid in "${!DB_SteamName[@]}"; do
                if [[ "${DB_SteamName[$eosid]}" == "$player" && -z "${DB_SurvivorName[$eosid]}" ]]; then
                    DB_SurvivorName["$eosid"]="$survivor"
                    break
                fi
            done
        fi

    done < <(
        grep -hE " \([^()]+\): " "${NEW_LOGS[@]}"
    )

    # Record logs that have been scraped
    for log in "${NEW_LOGS[@]}"; do
        if ! grep -Fxq "$log" "${EOSID_SCRAPED}"; then
            echo "$log" >>"${EOSID_SCRAPED}"
        fi
    done

    echo
    echo "────────────────────────────────────"
    echo -e "${CLR_GREEN} ▶ Finished Building EOSID Database!${RESET}"
    echo "────────────────────────────────────"

    cmd_export
}

cmd_export() {
    {
        echo "EOSID,SteamName,SurvivorName,FirstSeen,LastSeen,TimesSeen"

        for eosid in "${!DB_SteamName[@]}"; do
            echo "$eosid,${DB_SteamName[$eosid]},${DB_SurvivorName[$eosid]},${DB_FirstSeen[$eosid]},${DB_LastSeen[$eosid]},${DB_TimesSeen[$eosid]}"
        done

    } >"$DB_FILE"
}

#
# TRIBE
#

cmd_tribebuild() {

    if [[ ! -f "$TRIBE_SCRAPED" ]]; then
        echo
        echo -e "${CLR_GOLD}${BOLD}Building Tribe Database...${RESET}"
        echo "  Depending on the quantity of your archived logs, the first scan may take several minutes..."
        echo
        echo "  ${BOLD}Be Advised:${RESET} Once this first scan has completed, only new logs will be scanned in the future"
        echo "  this will ${BOLD}dramatically${RESET} reduce the processing time moving forward - from minutes to seconds."
    else
        echo -e "${CLR_GOLD}${BOLD} ▶ Scanning new logs & appending the Tribe Database...${RESET}"
    fi

    # cross-reference the all_logs array with the scaped_logs.txt - remove already scanned logs and add leftovers to
    # a new array NEW_LOGS
    touch "$TRIBE_SCRAPED"
    declare -A SCRAPED_LOGS

    while IFS= read -r log; do
        SCRAPED_LOGS["$log"]=1
    done <"$TRIBE_SCRAPED"

    NEW_LOGS=()

    for log in "${ALL_LOGS[@]}"; do
        if [[ -z ${SCRAPED_LOGS["$log"]+x} ]]; then
            NEW_LOGS+=("$log")
        fi
    done
    echo
    echo "───────────────────────────"
    echo "Total logs found: ${#ALL_LOGS[@]}"
    echo "New logs to scan:   ${#NEW_LOGS[@]}"
    echo "───────────────────────────"
    if [[ ${#NEW_LOGS[@]} -eq 0 ]]; then
        echo
        echo "No new logs to process"
        echo -e "${CLR_GREEN}All up to date!${RESET}"
        return
    fi

    declare -A TribeRecords
    declare -A JoinRecords

    #
    # Load existing Tribe database
    #
    if [[ -f "$TRIBE_DB" ]]; then
        while IFS=',' read -r map player playerid tribe tribeid; do
            [[ "$map" == "Map" ]] && continue

            key="$map|$player"

            TribeRecords["$key"]="$map,$player,$playerid,$tribe,$tribeid"

            if [[ "$playerid" != "Unknown" ]]; then
                JoinRecords["$key"]=1
            fi
        done <"$TRIBE_DB"
    fi

    #
    # PASS 1: Authoritative "joined tribe" records
    #
    while IFS= read -r line; do

        map=""

        for server in "${SERVERS[@]}"; do
            server_map="${server%%|*}"
            server_path="${server#*|}"

            if [[ "$line" == *"$server_path"* ]]; then
                map="$server_map"
                break
            fi
        done

        [[ -n "$map" ]] || continue

        joinline="${line%% Invited by Player *}"

        if [[ $joinline =~ Player[[:space:]](.+)[[:space:]]with[[:space:]]ID[[:space:]]([0-9]+)[[:space:]]joined[[:space:]]tribe[[:space:]](.+)[[:space:]]with[[:space:]]ID[[:space:]]([0-9]+)\. ]]; then

            player="${BASH_REMATCH[1]}"
            playerid="${BASH_REMATCH[2]}"
            tribe="${BASH_REMATCH[3]}"
            tribeid="${BASH_REMATCH[4]}"

            key="$map|$player"

            TribeRecords[$key]="$map,$player,$playerid,$tribe,$tribeid"
            JoinRecords[$key]=1
        fi

    done < <(grep -HE "joined tribe .* with ID [0-9]+" "${NEW_LOGS[@]}")

        #
    # PASS 2: Cryopod fallback
    #
    declare -A TribeLastSeen=()

    while IFS= read -r line; do

        map=""

        for server in "${SERVERS[@]}"; do
            server_map="${server%%|*}"
            server_path="${server#*|}"

            if [[ "$line" == *"$server_path"* ]]; then
                map="$server_map"
                break
            fi
        done

        [[ -n "$map" ]] || continue

        if [[ $line =~ \[([0-9]{4}\.[0-9]{2}\.[0-9]{2}-[0-9]{2}\.[0-9]{2}\.[0-9]{2}):[0-9]+\].*Tribe[[:space:]](.+),[[:space:]]ID[[:space:]]([0-9]+):.*:[[:space:]]([^:]+)[[:space:]]froze[[:space:]] ]]; then

            timestamp="${BASH_REMATCH[1]}"
            tribe="${BASH_REMATCH[2]}"
            tribeid="${BASH_REMATCH[3]}"
            player="${BASH_REMATCH[4]}"

            key="$map|$player"

            # Only accept the newest cryopod event for this player.
            if [[ -z ${TribeLastSeen[$key]:-} || "$timestamp" > "${TribeLastSeen[$key]}" ]]; then

                TribeLastSeen["$key"]="$timestamp"

                # Preserve authoritative PlayerID when we already have one.
                if [[ -n ${TribeRecords[$key]:-} ]]; then
                    IFS=',' read -r old_map old_player old_playerid old_tribe old_tribeid <<< "${TribeRecords[$key]}"
                    TribeRecords[$key]="$map,$player,$old_playerid,$tribe,$tribeid"
                else
                    TribeRecords[$key]="$map,$player,Unknown,$tribe,$tribeid"
                fi

            fi
        fi

    done < <(
        grep -HE "Tribe .* ID [0-9]+:.* froze " "${NEW_LOGS[@]}"
    )

    #
    # Record logs that have been scraped
    #
    for log in "${NEW_LOGS[@]}"; do
        if ! grep -Fxq "$log" "$TRIBE_SCRAPED"; then
            echo "$log" >>"$TRIBE_SCRAPED"
        fi
    done

    #
    # Export
    #
    {
        echo "Map,Player,PlayerID,TribeName,TribeID"

        for key in "${!TribeRecords[@]}"; do
            echo "${TribeRecords[$key]}"
        done | sort

    } >"$TRIBE_DB"

    echo
    echo "────────────────────────────────────"
    echo -e "${CLR_GREEN} ▶ Finished Building Tribes Database!${RESET}"
    echo "────────────────────────────────────"
}

#
# SEARCHING
#

cmd_search() {
    local query="$1"
    local found=0

    if [[ ! -f "$DB_FILE" ]]; then
        echo
        echo -e "${CLR_RED}${BOLD} Error:${RESET} EOSID database does not exist:${CLR_TEXT} ${DB_FILE} ${RESET}"
        echo
        echo -e " ▶ Run ${BOLD}idlookup -b${RESET} to scan logfiles and poopulate the database"
        echo
        exit 1
    fi

    echo
    echo "────────────────────────────────────────────────"

    while IFS=',' read -r eosid steam survivor firstseen lastseen timesseen; do

        # Skip the CSV header
        [[ $eosid == "EOSID" ]] && continue

        shopt -s nocasematch

        if [[ $eosid == *"$query"* || $steam == *"$query"* || $survivor == *"$query"* ]]; then

            printf "%s" "$eosid" | $CLIPBOARD

            echo -e "  ${BOLD}Steam${RESET}      : $steam"
            echo -e "  ${BOLD}Survivor${RESET}   : $survivor"
            echo -e "  ${BOLD}EOSID${RESET}      : $eosid"
            echo
            echo -e "  ${BOLD}First Seen${RESET} : $firstseen"
            echo -e "  ${BOLD}Last Seen${RESET}  : $lastseen"
            echo -e "  ${BOLD}Times Seen${RESET} : $timesseen"
            echo "────────────────────────────────────────────────"

            found=1
        fi

        shopt -u nocasematch

    done <"$DB_FILE"

    if [[ $found -eq 0 ]]; then
        echo "No matches found."
    else
        echo
        echo -e " ${CLR_GOLD}▶ EOSID copied to clipboard.${RESET}"
    fi

    echo
}

cmd_tribesearch() {
    local query="$1"
    local player=""
    local playerid=""
    local steam=""
    local survivor=""

    # Try to resolve query via EOS database
    if [[ -f "$DB_FILE" ]]; then

        # First: look for an exact Steam/Survivor/EOSID match
        while IFS=',' read -r eosid db_steam db_survivor firstseen lastseen timesseen; do
            [[ "$eosid" == "EOSID" ]] && continue

            shopt -s nocasematch
            if [[ "$db_steam" == "$query" || "$db_survivor" == "$query" || "$eosid" == "$query" ]]; then
                steam="$db_steam"
                survivor="$db_survivor"
                break
            fi
            shopt -u nocasematch
        done < "$DB_FILE"

        # If no exact match, fall back to partial matching
        if [[ -z "$steam" ]]; then
            while IFS=',' read -r eosid db_steam db_survivor firstseen lastseen timesseen; do
                [[ "$eosid" == "EOSID" ]] && continue

                shopt -s nocasematch
                if [[ "$db_survivor" == *"$query"* || "$db_steam" == *"$query"* || "$eosid" == *"$query"* ]]; then
                    steam="$db_steam"
                    survivor="$db_survivor"
                    break
                fi
                shopt -u nocasematch
            done < "$DB_FILE"
        fi
    fi

    if [[ ! -f "$TRIBE_DB" ]]; then
        echo
        echo -e "${CLR_RED}${BOLD} Error:${RESET} Tribe database does not exist:${CLR_TEXT} ${TRIBE_DB} ${RESET}"
        echo
        echo -e " ▶ Run ${BOLD}idlookup -bt${RESET} to scan logfiles and poopulate the Tribes database"
        echo
        exit 1
    fi

    #
    # If EOS lookup found a player, use their Steam name.
    # Otherwise use the original query.
    #
    [[ -n "$steam" ]] || steam="$query"

    #
    # Find matching player in Tribe DB
    #
    while IFS=',' read -r map db_player db_playerid db_tribe tribeid; do
        [[ "$map" == "Map" ]] && continue

        shopt -s nocasematch

        if [[ "$db_player" == "$steam" ]]; then
            player="$db_player"
            playerid="$db_playerid"
            break
        fi

        shopt -u nocasematch

    done <"$TRIBE_DB"

    shopt -u nocasematch

    if [[ -z "$player" ]]; then
        echo
        echo "No player matches found."
        echo
        return
    fi

    #
    # Display player
    #
    echo
    echo "──────────────────────────────"
    echo -e "${CLR_GOLD}Steam       ${RESET}: $steam"
    echo -e "${CLR_GOLD}Survivor    ${RESET}: $survivor"
    echo -e "${CLR_GOLD}Player ID   ${RESET}: $playerid"
    echo "──────────────────────────────"
    echo -e "${CLR_AQUA}Tribe: ${RESET}"

    #
    # Display every map's tribe + tribe ID
    #
    while IFS=',' read -r map db_player db_playerid db_tribe tribeid; do
        [[ "$map" == "Map" ]] && continue

        if [[ "$db_player" == "$player" ]]; then
            printf "  %-12s : %-18s : %s\n" "$map" "$db_tribe" "$tribeid"
        fi

    done <"$TRIBE_DB"

    echo "──────────────────────────────"
}

####################
# COMMANDS
####################

case "${1:-}" in
    -b)
        cmd_build
        ;;
    -bt)
        cmd_tribebuild
        ;;
    -t)
        [[ -n ${2:-} ]] || usage
        cmd_tribesearch "$2"
        ;;
    "")
        usage
        ;;
    -*)
        usage
        ;;
    *)
        cmd_search "$1"
        ;;
esac