#!/usr/bin/env bash # Steam game compatibility checker for Linux # # Features: # - Gets your games either via Steam Web API (owned library) or by scanning installed appmanifests. # - Fetches system requirements from Steam Store API (no key required). # - Compares against your system (CPU, RAM, GPU vendor, OS/arch) with simple heuristics. # - Ranks games from most to least likely to run. # # Optional env vars (for full library): # STEAM_API_KEY - Your Steam Web API key # STEAM_ID64 - Your 64-bit Steam ID # # Dependencies: curl, jq, awk, sed, grep, sort, lspci, free, uname # Recommended: timeout (coreutils) to guard slow network calls set -euo pipefail SCRIPT_NAME=${0##*/} ABORT=0 on_abort() { ABORT=1 log "Aborted by user" exit 130 } trap on_abort INT TERM # --------------------------- CLI args --------------------------- usage() { cat << USAGE Usage: $SCRIPT_NAME [--refresh] [--clear-cache] [--verbose] [--help] Options: --refresh Re-analyze all games and overwrite cache (ignore previous results). --clear-cache Delete cached results before running (implies --refresh). -v, --verbose Print detailed progress and HTTP/parse steps. -h, --help Show this help message and exit. USAGE } FORCE_REFRESH=0 CLEAR_CACHE=0 VERBOSE=0 parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --refresh) FORCE_REFRESH=1 shift ;; --clear-cache) CLEAR_CACHE=1 FORCE_REFRESH=1 shift ;; -v | --verbose) VERBOSE=1 shift ;; -h | --help) usage exit 0 ;; *) die "Unknown option: $1 (use --help)" ;; esac done } log() { printf "[%s] %s\n" "$SCRIPT_NAME" "$*" >&2; } die() { printf "[%s] ERROR: %s\n" "$SCRIPT_NAME" "$*" >&2 exit 1 } vlog() { if [[ $VERBOSE -eq 1 ]]; then printf "[%s][verbose] %s\n" "$SCRIPT_NAME" "$*" >&2; fi; } require_cmd() { command -v "$1" > /dev/null 2>&1 || die "Missing dependency: $1" } for cmd in curl jq awk sed grep sort lspci free uname; do require_cmd "$cmd" done HAS_TIMEOUT=0 if command -v timeout > /dev/null 2>&1; then HAS_TIMEOUT=1 fi # Safe HTTP GET with optional timeout if available http_get() { local url="$1" shift || true local ua="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124 Safari/537.36" if [[ $HAS_TIMEOUT -eq 1 ]]; then timeout 12s curl -sSL --compressed --retry 2 --retry-delay 0.5 --retry-connrefused \ -H "User-Agent: $ua" -H 'Accept: application/json, text/plain, */*' "$url" "$@" 2> /dev/null else curl -sSL --compressed --retry 2 --retry-delay 0.5 --retry-connrefused \ -H "User-Agent: $ua" -H 'Accept: application/json, text/plain, */*' "$url" "$@" 2> /dev/null fi } # --------------------------- System detection --------------------------- SYSTEM_CPU_MODEL="" SYSTEM_CPU_CLASS="unknown" SYSTEM_GPU_VENDOR="unknown" SYSTEM_RAM_GB=0 SYSTEM_ARCH="$(uname -m || echo unknown)" to_int() { awk '{gsub(/[^0-9]/,""); if($0=="") print 0; else print $0}' <<< "$1"; } detect_system() { # CPU model if command -v lscpu > /dev/null 2>&1; then SYSTEM_CPU_MODEL=$(lscpu | awk -F': *' '/Model name/ {print $2; exit}') fi if [[ -z $SYSTEM_CPU_MODEL && -r /proc/cpuinfo ]]; then SYSTEM_CPU_MODEL=$(awk -F': *' '/model name/ {print $2; exit}' /proc/cpuinfo) fi SYSTEM_CPU_MODEL=${SYSTEM_CPU_MODEL:-unknown} # CPU class (very rough) lc_model=$(tr '[:upper:]' '[:lower:]' <<< "$SYSTEM_CPU_MODEL") if grep -qiE 'i9-|core\(tm\) i9| ryzen 9' <<< "$lc_model"; then SYSTEM_CPU_CLASS="tier4" elif grep -qiE 'i7-|core\(tm\) i7| ryzen 7' <<< "$lc_model"; then SYSTEM_CPU_CLASS="tier3" elif grep -qiE 'i5-|core\(tm\) i5| ryzen 5' <<< "$lc_model"; then SYSTEM_CPU_CLASS="tier2" elif grep -qiE 'i3-|core\(tm\) i3| ryzen 3| pentium|celeron|atom' <<< "$lc_model"; then SYSTEM_CPU_CLASS="tier1" else SYSTEM_CPU_CLASS="tier2"; fi # GPU vendor local vga vga=$(lspci 2> /dev/null | grep -iE 'vga|3d|display' | head -n1 || true) lc_vga=$(tr '[:upper:]' '[:lower:]' <<< "$vga") if grep -q 'nvidia' <<< "$lc_vga"; then SYSTEM_GPU_VENDOR="nvidia" elif grep -q -E 'amd|ati|radeon' <<< "$lc_vga"; then SYSTEM_GPU_VENDOR="amd" elif grep -q 'intel' <<< "$lc_vga"; then SYSTEM_GPU_VENDOR="intel" else SYSTEM_GPU_VENDOR="unknown"; fi # RAM GB local mem_kb mem_kb=$(awk '/MemTotal/ {print $2; exit}' /proc/meminfo 2> /dev/null || echo 0) if [[ $mem_kb -gt 0 ]]; then SYSTEM_RAM_GB=$(((mem_kb + 1023 * 1024) / (1024 * 1024))) else local mem_mb mem_mb=$(free -m | awk '/Mem:/ {print $2; exit}') SYSTEM_RAM_GB=$(((mem_mb + 1023) / 1024)) fi } cpu_class_rank() { case "$1" in tier1) echo 1 ;; tier2) echo 2 ;; tier3) echo 3 ;; tier4) echo 4 ;; *) echo 2 ;; esac } required_cpu_rank_from_text() { local t t=$(tr '[:upper:]' '[:lower:]' <<< "$1") if grep -qE 'i9|ryzen 9' <<< "$t"; then echo 4 return fi if grep -qE 'i7|ryzen 7' <<< "$t"; then echo 3 return fi if grep -qE 'i5|ryzen 5' <<< "$t"; then echo 2 return fi if grep -qE 'i3|ryzen 3|pentium|celeron|atom' <<< "$t"; then echo 1 return fi echo 2 } gpu_vendor_required_from_text() { local t t=$(tr '[:upper:]' '[:lower:]' <<< "$1") if grep -qE 'nvidia|geforce|gtx|rtx' <<< "$t"; then echo nvidia return fi if grep -qE 'amd|radeon|rx[ -]?[0-9]' <<< "$t"; then echo amd return fi if grep -qE 'intel( graphics| arc| iris| hd)' <<< "$t"; then echo intel return fi echo unknown } strip_html() { sed -E 's/<[^>]+>//g; s/ / /g; s/&/\&/g; s/\r//g' <<< "$1" } parse_ram_gb() { # Extract first RAM mention and convert to GB integer local text="$1" local num val # Prefer GB num=$(grep -oiE '([0-9]+)\s*(gb|gib)' <<< "$text" | head -n1 | grep -oiE '^[0-9]+' || true) if [[ -n $num ]]; then echo "$num" return fi # Try MB num=$(grep -oiE '([0-9]+)\s*(mb|mib)' <<< "$text" | head -n1 | grep -oiE '^[0-9]+' || true) if [[ -n $num ]]; then val=$(((num + 1023) / 1024)) echo "$val" return fi echo 0 } # --------------------------- Steam data --------------------------- CONFIG_DIR="${XDG_CONFIG_HOME:-$HOME/.config}/steam-compat-check" CONFIG_FILE="$CONFIG_DIR/credentials.conf" # Cache for analyzed results CACHE_DIR="${XDG_CACHE_HOME:-$HOME/.cache}/steam-compat-check" RESULTS_CACHE="$CACHE_DIR/results.tsv" load_credentials() { # Prefer environment, else config file if [[ -n ${STEAM_API_KEY:-} && -n ${STEAM_ID64:-} ]]; then return 0 fi if [[ -r $CONFIG_FILE ]]; then # shellcheck disable=SC1090 . "$CONFIG_FILE" || true fi if [[ -z ${STEAM_API_KEY:-} || -z ${STEAM_ID64:-} ]]; then return 1 fi return 0 } save_credentials() { local key="$1" id="$2" mkdir -p "$CONFIG_DIR" chmod 700 "$CONFIG_DIR" 2> /dev/null || true umask 177 cat > "$CONFIG_FILE" << EOF # Saved by $SCRIPT_NAME STEAM_API_KEY="$key" STEAM_ID64="$id" EOF } prompt_for_credentials() { if [[ ! -t 0 ]]; then die "STEAM_API_KEY/STEAM_ID64 not set and input is non-interactive. Export them or create $CONFIG_FILE." fi echo "Steam Web API credentials are required to scan your full library." echo echo "Where to get them:" echo "- Steam Web API Key: https://steamcommunity.com/dev/apikey" echo " Log in, set any domain (e.g., 127.0.0.1), then copy the key." echo "- SteamID64 (17-digit ID starting with 765):" echo " * Easiest: https://steamid.io/ (paste your profile URL to get the 64-bit ID)" echo " * Or enable URL bar in Steam (Settings > Interface), open your profile; the URL contains the ID." echo local key id read -r -p "Enter Steam Web API Key: " key read -r -p "Enter Steam 64-bit ID (begins with 765…): " id if [[ -z $key || -z $id ]]; then die "Credentials not provided. Exiting." fi # Light validation for ID64 if ! grep -qE '^765[0-9]{14}$' <<< "$id"; then log "Warning: Steam ID64 format unexpected; continuing anyway." fi STEAM_API_KEY="$key" STEAM_ID64="$id" export STEAM_API_KEY STEAM_ID64 save_credentials "$STEAM_API_KEY" "$STEAM_ID64" log "Saved credentials to $CONFIG_FILE" } STEAM_DIRS=("$HOME/.steam/steam" "$HOME/.local/share/Steam") find_steamapps_dirs() { local dirs=() for base in "${STEAM_DIRS[@]}"; do [[ -d $base ]] || continue if [[ -f "$base/steamapps/libraryfolders.vdf" ]]; then # Newer format includes nested objects with paths local paths paths=$(grep -oE '"path"\s+"[^"]+"' "$base/steamapps/libraryfolders.vdf" | sed -E 's/.*"([^"]+)"/\1/' || true) if [[ -n $paths ]]; then while IFS= read -r p; do [[ -d "$p/steamapps" ]] && dirs+=("$p/steamapps") done <<< "$paths" fi fi [[ -d "$base/steamapps" ]] && dirs+=("$base/steamapps") done # de-dupe printf "%s\n" "${dirs[@]}" 2> /dev/null | awk '!seen[$0]++' } list_installed_games() { local d appid name while IFS= read -r d; do [[ -d $d ]] || continue for mf in "$d"/appmanifest_*.acf; do [[ -f $mf ]] || continue appid=$(grep -oE '"appid"\s+"[0-9]+"' "$mf" | sed -E 's/.*"([0-9]+)"/\1/' | head -n1) name=$(grep -oE '"name"\s+"[^"]+"' "$mf" | sed -E 's/.*"([^"]+)"/\1/' | head -n1) if [[ -n $appid ]]; then printf "%s\t%s\n" "$appid" "${name:-Unknown}" fi done done < <(find_steamapps_dirs) } list_owned_games_via_api() { local key="${STEAM_API_KEY:-}" sid="${STEAM_ID64:-}" if [[ -z $key || -z $sid ]]; then return 1 fi local url="https://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/?key=${key}&steamid=${sid}&include_appinfo=1&include_played_free_games=1&format=json" http_get "$url" | jq -r '.response.games[]? | "\(.appid)\t\(.name)"' || return 1 } fetch_appdetails_json() { local appid="$1" # Using store API (no key) local url="https://store.steampowered.com/api/appdetails?appids=${appid}&l=en&cc=us" http_get "$url" || true } extract_requirements_and_platforms() { # Input: JSON from appdetails; Output: TSV of fields # Fields: success, linux, windows, mac, min_text, rec_text, type local appid="$1" json="$2" # Some apps return {"APPID": {"success":true, "data":{...}}} local out out=$(jq -r --arg APP "$appid" ' .[$APP] as $root | if ($root.success==true and ($root.data|type)=="object") then ($root.data.platforms.linux // false) as $linux | ($root.data.platforms.windows // false) as $windows | ($root.data.platforms.mac // false) as $mac | ($root.data.type // "") as $type | # Prefer Linux reqs when present ($root.data.linux_requirements.minimum // $root.data.pc_requirements.minimum // "") as $min | ($root.data.linux_requirements.recommended // $root.data.pc_requirements.recommended // "") as $rec | ["ok", ($linux|tostring), ($windows|tostring), ($mac|tostring), ($min|tostring), ($rec|tostring), ($type|tostring)] | @tsv else ["fail", "false", "false", "false", "", "", ""] | @tsv end' 2> /dev/null <<< "$json") || true if [[ -z $out ]]; then out=$'fail false false false ' fi printf '%s\n' "$out" } # Read JSON from stdin (avoids storing large/binary data in variables) extract_requirements_and_platforms_stdin() { local appid="$1" local out out=$(jq -r --arg APP "$appid" ' .[$APP] as $root | if ($root.success==true and ($root.data|type)=="object") then ($root.data.platforms.linux // false) as $linux | ($root.data.platforms.windows // false) as $windows | ($root.data.platforms.mac // false) as $mac | ($root.data.type // "") as $type | ($root.data.linux_requirements.minimum // $root.data.pc_requirements.minimum // "") as $min | ($root.data.linux_requirements.recommended // $root.data.pc_requirements.recommended // "") as $rec | ["ok", ($linux|tostring), ($windows|tostring), ($mac|tostring), ($min|tostring), ($rec|tostring), ($type|tostring)] | @tsv else ["fail", "false", "false", "false", "", "", ""] | @tsv end' 2> /dev/null) || true if [[ -z $out ]]; then out=$'fail\tfalse\tfalse\tfalse\t\t\t' fi printf '%s\n' "$out" } score_game() { local linux_support="$1" min_txt="$2" rec_txt="$3" local score=0 # Linux platform support if [[ $linux_support == "true" ]]; then score=$((score + 50)) else score=$((score + 20)) # Assume Proton potential fi local min_plain rec_plain min_plain=$(strip_html "$min_txt") rec_plain=$(strip_html "$rec_txt") local min_ram rec_ram min_ram=$(parse_ram_gb "$min_plain") rec_ram=$(parse_ram_gb "$rec_plain") # RAM checks if [[ $min_ram -gt 0 ]]; then if [[ $SYSTEM_RAM_GB -ge $min_ram ]]; then score=$((score + 15)); else score=$((score - 30)); fi fi if [[ $rec_ram -gt 0 ]]; then if [[ $SYSTEM_RAM_GB -ge $rec_ram ]]; then score=$((score + 10)); else score=$((score - 10)); fi fi # CPU checks (very rough tiers) local req_rank sys_rank req_rank=$(required_cpu_rank_from_text "$min_plain $rec_plain") sys_rank=$(cpu_class_rank "$SYSTEM_CPU_CLASS") if [[ $sys_rank -ge $req_rank ]]; then score=$((score + 10)); else score=$((score - 10)); fi # GPU vendor hints local req_gpu vendor req_gpu=$(gpu_vendor_required_from_text "$min_plain $rec_plain") vendor="$SYSTEM_GPU_VENDOR" if [[ $req_gpu == "unknown" ]]; then score=$((score + 5)) elif [[ $req_gpu == "$vendor" ]]; then score=$((score + 10)) else score=$((score - 10)) fi # 64-bit OS requirement if grep -qi '64-?bit' <<< "$min_plain $rec_plain"; then if [[ $SYSTEM_ARCH == "x86_64" || $SYSTEM_ARCH == "aarch64" ]]; then score=$((score + 5)) else score=$((score - 20)) fi fi printf "%s\t%s\t%s\n" "$score" "$min_ram" "$rec_ram" } print_header() { printf "%-5s %-8s %-6s %-6s %-8s %-9s %s\n" "Rank" "Score" "MinRAM" "RecRAM" "Linux" "ProtonDB" "Title" } check_network_or_exit() { # Quick probe to Steam Store API; exit early if not reachable local probe_url="https://store.steampowered.com/api/appdetails?appids=10&l=en&cc=us" if ! http_get "$probe_url" | jq -e '."10".success == true' > /dev/null 2>&1; then log "Warning: store.steampowered.com probe failed (network or rate-limit). Continuing and handling per-app." fi } is_known_tool_name() { local name_lc name_lc=$(tr '[:upper:]' '[:lower:]' <<< "$1") if grep -qE 'steam linux runtime|proton|compatibility tool' <<< "$name_lc"; then return 0 fi return 1 } ensure_cache_dir() { mkdir -p "$CACHE_DIR" 2> /dev/null || true } declare -A CACHE_MAP load_cache_map() { CACHE_MAP=() if [[ -r $RESULTS_CACHE ]]; then while IFS= read -r raw_line; do # Normalize historical caches that contain literal "\t" instead of real tabs local norm_line norm_line=$(printf "%s" "$raw_line" | sed -E $'s/\\t/\t/g; s/\r$//') IFS=$'\t' read -r c_score c_appid c_linux c_min c_rec c_name c_pdb <<< "$norm_line" [[ -z ${c_appid:-} ]] && continue c_pdb=${c_pdb:-unknown} CACHE_MAP["$c_appid"]="$c_score\t$c_appid\t$c_linux\t$c_min\t$c_rec\t$c_name\t$c_pdb" done < "$RESULTS_CACHE" fi } # --------------------------- ProtonDB integration --------------------------- fetch_protondb_tier() { local appid="$1" local url="https://www.protondb.com/api/v1/reports/summaries/${appid}.json" # Returns minimal JSON including .tier, .confidence; we only need .tier local tier tier=$(http_get "$url" | jq -r 'try .tier // "unknown"' 2> /dev/null || true) if [[ -z $tier || $tier == "null" ]]; then echo "unknown" else tr '[:upper:]' '[:lower:]' <<< "$tier" | tr -d '\r\n' fi } protondb_allowed() { local tier tier=$(tr '[:upper:]' '[:lower:]' <<< "${1:-}") case "$tier" in platinum | native | gold | silver | unknown | "") return 0 ;; bronze | pending | borked | unsupported | broken) return 1 ;; *) return 0 ;; esac } main() { parse_args "$@" detect_system log "System: CPU=[$SYSTEM_CPU_MODEL] class=$SYSTEM_CPU_CLASS | GPU=$SYSTEM_GPU_VENDOR | RAM=${SYSTEM_RAM_GB}GB | Arch=$SYSTEM_ARCH" local tmpdir tmpdir=$(mktemp -d) trap '[[ -n "${tmpdir:-}" ]] && rm -rf "$tmpdir"' EXIT local games_tsv="$tmpdir/games.tsv" : > "$games_tsv" # Ensure credentials exist: load from env/config or prompt, else exit if ! load_credentials; then prompt_for_credentials fi # Fail fast if we cannot reach the store API to avoid noisy per-app errors check_network_or_exit if list_owned_games_via_api > "$games_tsv" 2> /dev/null; then log "Fetched owned games via Steam Web API" fi if [[ ! -s $games_tsv ]]; then die "No games found from Steam Web API. Check STEAM_API_KEY/STEAM_ID64 and network connectivity." fi # Fail fast if we cannot reach the store API to avoid noisy per-app errors check_network_or_exit ensure_cache_dir if [[ $CLEAR_CACHE -eq 1 ]] && [[ -f $RESULTS_CACHE ]]; then rm -f "$RESULTS_CACHE" || true log "Cleared cache: $RESULTS_CACHE" fi if [[ $FORCE_REFRESH -eq 0 ]]; then load_cache_map else CACHE_MAP=() fi local results_combined="$tmpdir/results.tsv" : > "$results_combined" local count=0 local total total=$(wc -l < "$games_tsv" | tr -d ' ') [[ -z $total ]] && total=0 while IFS=$'\t' read -r appid name; do [[ $ABORT -eq 1 ]] && break [[ -n $appid ]] || continue if is_known_tool_name "$name"; then vlog "[$((count + 1))/$total] Skipping compatibility tool: $name ($appid)" continue fi # If cached, reuse; else analyze and cache if [[ $FORCE_REFRESH -eq 0 && -n ${CACHE_MAP[$appid]+isset} ]]; then # Normalize to include ProtonDB column if older cache lacked it IFS=$'\t' read -r c_score c_a c_linux c_min c_rec c_name c_pdb <<< "${CACHE_MAP[$appid]}" c_pdb=${c_pdb:-unknown} vlog "[$((count + 1))/$total] Cache hit: $name ($appid) | ProtonDB=$c_pdb" printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n" "$c_score" "$c_a" "$c_linux" "$c_min" "$c_rec" "$c_name" "$c_pdb" >> "$results_combined" continue fi count=$((count + 1)) log "Analyzing: $name ($appid) [$count/$total]" local row url url="https://store.steampowered.com/api/appdetails?appids=${appid}&l=en&cc=us" vlog "[$count/$total] Fetching store appdetails: $url" # Be gentle with the store API sleep 0.1 row=$(http_get "$url" | extract_requirements_and_platforms_stdin "$appid" || true) if [[ -z $row ]]; then continue; fi local status linux _windows _mac min_txt rec_txt type IFS=$'\t' read -r status linux _windows _mac min_txt rec_txt type <<< "$row" vlog "[$count/$total] Parsed store data: status=$status linux=$linux type=$type" # Occasionally Steam returns success=false spuriously; retry once if [[ $status != "ok" ]]; then vlog "[$count/$total] Store status=fail; retrying once..." sleep 0.3 row=$(http_get "$url" | extract_requirements_and_platforms_stdin "$appid" || true) IFS=$'\t' read -r status linux _windows _mac min_txt rec_txt type <<< "$row" vlog "[$count/$total] After retry: status=$status" fi # Try filtered endpoint that often bypasses age/region gates if [[ $status != "ok" ]]; then local url2="https://store.steampowered.com/api/appdetails?appids=${appid}&filters=platforms,linux_requirements,pc_requirements,type&l=en&cc=us" vlog "[$count/$total] Retrying with filters: $url2" sleep 0.1 row=$(http_get "$url2" | extract_requirements_and_platforms_stdin "$appid" || true) IFS=$'\t' read -r status linux _windows _mac min_txt rec_txt type <<< "$row" vlog "[$count/$total] Filtered fetch status=$status" fi if [[ $status != "ok" ]]; then continue; fi if [[ $type != "game" && $type != "dlc" && $type != "" ]]; then continue; fi # ProtonDB tier [[ $ABORT -eq 1 ]] && break local pdb_tier vlog "[$count/$total] Fetching ProtonDB tier for appid=$appid" pdb_tier=$(fetch_protondb_tier "$appid") vlog "[$count/$total] ProtonDB tier=$pdb_tier" # Compute hardware-based score local score_line s_score s_min_ram s_rec_ram score_line=$(score_game "$linux" "$min_txt" "$rec_txt") IFS=$'\t' read -r s_score s_min_ram s_rec_ram <<< "$score_line" # Gate by ProtonDB: if bronze or below -> mark unplayable and force low score if ! protondb_allowed "$pdb_tier"; then s_score=-999 fi printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n" "$s_score" "$appid" "$linux" "$s_min_ram" "$s_rec_ram" "$name" "$pdb_tier" >> "$results_combined" vlog "[$count/$total] Scored and recorded: score=$s_score min=${s_min_ram}G rec=${s_rec_ram}G" done < "$games_tsv" if [[ ! -s $results_combined ]]; then die "No compatible entries parsed from store API." fi print_header local rank=0 sort -t $'\t' -k1,1nr -k6,6 "$results_combined" | while IFS=$'\t' read -r score appid linux min_ram rec_ram name pdb_tier; do rank=$((rank + 1)) local display_name="$name" if ! protondb_allowed "$pdb_tier"; then display_name="$name [UNPLAYABLE]" fi printf "%-5s %-8s %-6s %-6s %-8s %-9s %s\n" "$rank" "$score" "${min_ram}G" "${rec_ram}G" "$linux" "${pdb_tier:-unknown}" "$display_name" done # Persist updated results for future runs (only current library entries) cp -f "$results_combined" "$RESULTS_CACHE" 2> /dev/null || cat "$results_combined" > "$RESULTS_CACHE" } main "$@"