Skip to content

Instantly share code, notes, and snippets.

@mickael9
Last active October 21, 2017 09:34
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mickael9/b2188bb49efc9c878e556c395cedb4c5 to your computer and use it in GitHub Desktop.
Save mickael9/b2188bb49efc9c878e556c395cedb4c5 to your computer and use it in GitHub Desktop.
A shell/awk wrapper around qstat for UrbanTerror, with filtering and sorting
#!/bin/bash
QSTAT_ARGS="-iourtm master.urbanterror.info:27900"
show_help () {
echo "Usage: $0 [[-]<keyword>...] [<var>=<val>...] [<var>=!<val>...]"
echo
echo "Examples:"
echo
echo " Show all public TS or CTF servers running either abbey or turnpike:"
echo " $0 -private ts ctf map=ut4_abbey map=turnpike"
echo
echo " Show all non-empty servers without bots excluding JUMP servers, lowest pings first:"
echo " $0 -empty -bots -jump sort=ping"
echo
echo "Keywords (negate by prefixing with -)"
echo " private private servers"
echo
echo " empty empty servers"
echo " full full servers"
echo " bots servers with bots"
echo
echo " ffa FFA servers"
echo " lms LMS servers"
echo " tdm TDM servers"
echo " ts TS servers"
echo " ftl FTL servers"
echo " cah CAH servers"
echo " ctf CTF servers"
echo " bomb BOMB servers"
echo " jump JUMP servers"
echo " freeze FREEZE servers"
echo " gungame GUNGAME servers"
echo
echo "Variable filters:"
echo " <var> can be one of the exported server cvars (g_gametype, sv_matchmode, etc.)"
echo " <var>=<val> includes servers that have the variable <var> equal to <val>"
echo " <var>=!<val> excludes servers in the same way"
echo " <var> can also be one of those special vars:"
echo " address server address and port (address:port)"
echo " name server name"
echo " map map name (eg ut4_turnpike)"
echo " players number of connected players"
echo " max_players max number of players"
echo " full 1 if the server is full, 0 otherwise"
echo " has_bots 1 if the server has bots, 0 otherwise"
echo " mode game mode name (TDM, TS, CTF, ...)"
echo
echo "Options:"
echo " sort=[-]<sort>: select the column number/name to use for sorting."
echo " Prefix with - to sort in descending order"
echo
echo " sort=1 sort by ascending address (using column number)"
echo " sort=-address sort by descending address"
echo " sort=address sort by ascending address"
echo " sort=name sort by ascending name"
echo " sort=map sort by ascending map name"
echo " sort=players sort by ascending player count"
echo " sort=ping sort by ascending ping"
echo " sort=mode sort by ascending game mode"
echo
echo " color=1 | 0: select wether to use colored output"
echo " color=1 convert game colors to terminal colors"
echo " color=0 don't convert colors"
echo
echo " The default is to use colors if the standard output is a terminal."
echo
echo " limit=<limit>: set the maximum number of results to return"
echo
exit
}
main() {
filters=()
sort_key=-4
color=0
[[ -t 1 ]] && color=1
while [[ $# > 0 ]]; do
arg=${1#-}
not=
[[ ${1:0:1} == '-' ]] && not='!'
shift
case ${arg,,*} in
private) filters+=("password=${not}1") ;;
empty) filters+=("cur_players=${not}0") ;;
full) filters+=("full=${not}1") ;;
bots) filters+=("has_bots=${not}1") ;;
[a-z_]*=*) filters+=("$arg") ;;
ffa|lms|tdm|ts|ftl|cah|ctf|bomb|jump|freeze|gungame)
filters+=("mode=${not}${arg^^*}")
;;
h|-help|help)
show_help
;;
*)
echo "Try $0 -h for help"
exit
;;
esac
done
show_servers "color=$color" "${filters[@]}"
}
show_servers() {
qstat -progress -carets -raw $'\t' -R -u $QSTAT_ARGS |
gawk '
BEGIN {
OFS = "\t"
RS = "\n\n"
FS = "[\t\n]"
limit = 0
sort[1] = "-cur_players"
# g_gametype string values
split("FFA LMS ? TDM TS FTL CAH CTF BOMB JUMP FREEZE GUNGAME", modes, " ")
# Quake -> ANSI color codes
split("30 31 32 33 34 36 35 0 33 33", colors, " ")
split("Address Name Map Players Ping Mode", columns, " ")
for (i = 1; i < ARGC; i++) {
arg = ARGV[i]
if (!parse_assignement(arg))
continue
name = tolower(name)
if (name == "sort") {
split_append(tolower(value), sort)
continue
} else if (name == "color") {
USE_COLORS = value
continue
} else if (name == "columns") {
split_append(value, columns)
continue
} else if (name == "limit") {
limit = strtonum(value)
continue
}
if (match(value, /^!(.*)$/, m)) {
# Hack to make sure arrays are correctly defined
exclude_data[name][0] = 0
delete exclude_data[name][0]
push(exclude_data[name], m[1])
} else {
include_data[name][0] = 0
delete include_data[name][0]
push(include_data[name], value)
}
}
for (c in columns) {
output[1][column_name(c)] = columns[c]
}
ARGC = 1
}
/^Q3S|IOURTS/ {
delete data
if (NF < 8)
next
data["raw_name"] = $3
colored_name = ansi_color($3)
for (i = 2; i <= 8; i++) {
$i = strip_colors($i)
}
for (i = 9; i <= NF; i++) {
if (parse_assignement($i)) {
data[tolower(name)] = value
}
}
mode = modes[data["g_gametype"] + 1]
data["mode"] = mode
data["address"] = $2
data["name"] = $3
data["map"] = $4
data["cur_players"] = $6
data["max_players"] = $5
data["players"] = $6 "/" $5
data["ping"] = $7
data["full"] = $6 > 0 && $6 == $5
data["has_bots"] = data["bots"] > 0
data["colored_name"] = colored_name
for (name in include_data) {
if (!(name in data))
next
matches = 0
for (i in include_data[name]) {
if (include_data[name][i] == data[name]) {
matches = 1
break
}
}
if (!matches) {
next
}
}
for (name in exclude_data) {
if (!(name in data)) {
break
}
for (i in exclude_data[name]) {
if (exclude_data[name][i] == data[name]) {
next
}
}
}
idx = length(output) + 1
for (i in data) {
output[idx][i] = data[i]
}
}
END {
asort(output, output, "compare")
for (line in output) {
line = strtonum(line)
if (limit > 0 && line > limit + 1)
break
for (col in columns) {
col_name = column_name(col)
col_len = length(output[line][col_name])
if (col_len > column_sizes[col])
column_sizes[col] = col_len
}
}
for (line in output) {
line = strtonum(line)
if (limit > 0 && line > limit + 1)
break
NF = 0
for (col in columns) {
col_name = column_name(col)
col_text = output[line][col_name]
padding = length(col_text) - column_sizes[col]
if (col_name == "name" && line > 1)
col_text = output[line]["colored_name"]
$col = sprintf("%s%*s", col_text, padding, "")
}
print
}
}
function strip_colors(str) {
gsub(/\^[0-9]/, "", str)
return str
}
function ansi_color(str) {
if (!USE_COLORS) {
return strip_colors(str)
}
if (str ~! /\^[0-9]/)
return strip_colors(str)
str = str "^7"
for (i in colors) {
pattern = "\\^" (i - 1)
gsub(pattern, "\x1B[" colors[i] "m", str)
}
return strip_colors(str)
}
function parse_assignement(a, result)
{
result = match(a, /^([^=+]+)(\+?)=(.*)$/, assign)
if (result) {
name = assign[1]
append = assign[2] == "+"
value = assign[3]
}
return result
}
function push(arr, elem) {
arr[length(arr) + 1] = elem
}
function split_append(text, arr, i)
{
if (append) {
split(text, append_split, /,/)
for (i in append_split) {
push(arr, append_split[i])
}
} else {
split(text, arr, /,/)
}
}
function column_name(col, name)
{
name = tolower(columns[col])
gsub(/ /, "_", name)
return name
}
function compare_strip(str)
{
str = tolower(str)
unstripped = str
gsub(/^[^a-zA-Z]+/, "", str)
if (str == "")
return unstripped
else
return str
}
function compare(i1, v1, i2, v2)
{
# Header line always stays at the top
if (i1 == 1) return -1
if (i2 == 1) return 1
for (sort_i in sort) {
sort_name = sort[sort_i]
if (substr(sort_name, 1, 1) == "-") {
sort_name = substr(sort_name, 2)
sort_direction = -1
} else {
sort_direction = 1
}
if (sort_name in columns)
sort_name = column_name(sort_name)
s1 = v1[sort_name]
s2 = v2[sort_name]
if (s1 ~ /^[0-9]/ && s2 ~ /^[0-9]/) {
s1 = strtonum(s1)
s2 = strtonum(s2)
} else {
s1 = compare_strip(s1)
s2 = compare_strip(s2)
}
if (s1 > s2)
return sort_direction
else if (s1 < s2)
return -sort_direction
}
return 0
}' "$@"
}
main "$@"
# vim: ts=4 sw=4 et
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment