# shellcheck shell=bash # Bash completion for terractl (terraria-server-manager) # # Installed to /share/bash-completion/completions/terractl, where the # bash-completion package loads it automatically. Also works with 'sudo terractl'. # A setting from /etc/default/terractl, or its default: _terractl_setting . # The file is parsed rather than sourced, so nothing runs in your shell. _terractl_setting() { local result=$2 value if [[ -r /etc/default/terractl ]]; then value=$(sed -n "s/^[[:space:]]*$1=//p" /etc/default/terractl | tail -n 1) value=${value%\"}; value=${value#\"}; value=${value%\'}; value=${value#\'} if [[ -n $value ]]; then result=$value; fi fi printf '%s\n' "$result" } # Instances directory _terractl_base() { _terractl_setting TERRARIA_BASE /var/lib/terraria/instances; } # Server zips an instance can use, by its TYPE in instance.conf (all zips if # that can't be read). On a remote, all of its zips, from its daemon. _terractl_zips() { if [[ -n $2 ]]; then timeout 5 terractl --host "$2" info 2>/dev/null /dev/null | tail -n 1) case $type in vanilla) pattern='terraria-server-*.zip' ;; tmodloader) pattern='tModLoader*.zip' ;; esac while IFS= read -r f; do printf '%s\n' "${f##*/}"; done < <(compgen -G "$dist/$pattern") } # Instance names. Reading the directory needs root or the 'terraria' group; if # it isn't readable, fall back to the terraria@ services systemd knows about. _terractl_instances() { local base conf base=$(_terractl_base) if [[ -r $base && -x $base ]]; then while IFS= read -r conf; do conf=${conf%/instance.conf} printf '%s\n' "${conf##*/}" done < <(compgen -G "$base/*/instance.conf") elif command -v systemctl >/dev/null 2>&1; then systemctl list-units --all --plain --no-legend 'terraria@*.service' 2>/dev/null | sed -n 's/^terraria@\([^ ]*\)\.service.*/\1/p' fi } # Instance names here, or on remote if one is given _terractl_any_instances() { if [[ -n $1 ]]; then _terractl_remote_instances "$1"; else _terractl_instances; fi } # Remotes this user added with 'terractl remote add' _terractl_remotes() { local f=${TERRACTL_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/terractl}/remotes.json [[ -r $f ]] || return 0 python3 -c 'import json, sys; print("\n".join(json.load(open(sys.argv[1]))["remotes"]))' "$f" 2>/dev/null } # Instance names on a remote. This asks its daemon, so it can take a moment. _terractl_remote_instances() { timeout 5 terractl --host "$1" info 2>/dev/null :] for copy and move _terractl_places() { if [[ $cur == -* ]]; then _terractl_reply "--with-server" elif [[ $cur == *:* ]]; then mapfile -t COMPREPLY < <(compgen -P "${cur%:*}:" -W "$(_terractl_remote_instances "${cur%:*}")" -- "${cur##*:}") else mapfile -t COMPREPLY < <(compgen -W "$(_terractl_instances) $(_terractl_remotes | sed 's/$/:/')" -- "$cur") # After ":" comes an instance name, not a space if [[ ${#COMPREPLY[@]} -eq 1 && ${COMPREPLY[0]} == *: ]]; then compopt -o nospace 2>/dev/null; fi fi # Readline sees only the part after the last ':' if declare -F __ltrim_colon_completions >/dev/null 2>&1; then __ltrim_colon_completions "$cur"; fi } # Mods installed in an instance (*.tmod files), or enabled ones (enabled.json) _terractl_mods() { local mdir f mdir="$(_terractl_base)/$1/data/Mods" if [[ $2 == enabled ]]; then [[ -r $mdir/enabled.json ]] && grep -o '"[^"]*"' "$mdir/enabled.json" | tr -d '"' else while IFS= read -r f; do f=${f##*/} printf '%s\n' "${f%.tmod}" done < <(compgen -G "$mdir/*.tmod") fi } # File names, optionally only those ending in . (folders always offered) _terractl_files() { if declare -F _filedir >/dev/null 2>&1; then _filedir "$@" else compopt -o filenames 2>/dev/null if [[ -n ${1:-} ]]; then mapfile -t COMPREPLY < <(compgen -d -- "$cur"; compgen -f -X "!*.$1" -- "$cur") else mapfile -t COMPREPLY < <(compgen -f -- "$cur") fi fi } # Set COMPREPLY to the words matching what's typed so far _terractl_reply() { mapfile -t COMPREPLY < <(compgen -W "$1" -- "$cur") } _terractl() { local cur words cword if declare -F _init_completion >/dev/null 2>&1; then # shellcheck disable=SC2034 # prev is set by _init_completion but not needed here local prev # -n : keeps ":" (copy, move) in one word _init_completion -n : || return else cur=${COMP_WORDS[COMP_CWORD]} words=("${COMP_WORDS[@]}") cword=$COMP_CWORD fi local commands="new list info start stop restart enable disable attach watch cmd mods upgrade reinstall fix-perms fetch backup doctor delete rename export import" local console="save say playing kick ban motd password time dawn noon dusk midnight settle seed version maxplayers port clear help" # terractl --host ...: the same as on this host, except # that instance names come from the remote and mod names aren't offered local host="" if [[ ${words[1]:-} == --host || ${words[1]:-} == -H ]]; then if (( cword == 2 )); then _terractl_reply "$(_terractl_remotes)" return fi host=${words[2]} words=("${words[0]}" "${words[@]:3}") cword=$((cword - 2)) commands+=" --help --version" else commands+=" copy move remote daemon prepare run journal-access --host --help --version" fi if (( cword == 1 )); then _terractl_reply "$commands" return fi local command=${words[1]} prev=${words[cword-1]} case $command in list|journal-access|help|-h|--help|version|-V|--version) return ;; new) # new [vanilla|tmodloader] [port]: the name is new, so nothing to offer if (( cword == 3 )); then _terractl_reply "vanilla tmodloader"; fi return ;; import) # import : the name is new, so nothing to offer if (( cword == 3 )); then _terractl_files tar.gz; fi return ;; copy|move) _terractl_places return ;; remote) if (( cword == 2 )); then _terractl_reply "add list remove" elif (( cword == 3 )) && [[ ${words[2]} == remove ]]; then _terractl_reply "$(_terractl_remotes)" fi return ;; daemon) if (( cword == 2 )); then _terractl_reply "enable disable status token revoke" fi return ;; doctor) # doctor [] [--fix] if [[ $cur == -* ]]; then _terractl_reply "--fix"; else _terractl_reply "$(_terractl_any_instances "$host")"; fi return ;; fetch) # fetch vanilla|tmodloader [|latest] [--list] [--upgrade ] if (( cword == 2 )); then _terractl_reply "vanilla tmodloader" elif [[ $prev == --upgrade ]]; then _terractl_reply "$(_terractl_any_instances "$host")" elif [[ $cur == -* ]]; then _terractl_reply "--list --upgrade" else _terractl_reply "latest" fi return ;; backup) # backup enable|disable|now|list [--every ] [--keep ] if (( cword == 2 )); then _terractl_reply "enable disable now list" elif (( cword == 3 )); then _terractl_reply "$(_terractl_any_instances "$host")" elif [[ ${words[2]} == enable ]]; then case $prev in --every) _terractl_reply "daily weekly hourly monthly" ;; --keep) ;; *) _terractl_reply "--every --keep" ;; esac fi return ;; esac if (( cword == 2 )); then if [[ -n $host ]]; then _terractl_reply "$(_terractl_remote_instances "$host")" else _terractl_reply "$(_terractl_instances)" fi return fi local instance=${words[2]} case $command in cmd) if (( cword == 3 )); then _terractl_reply "$console"; fi ;; reinstall) if (( cword == 3 )); then _terractl_reply "--clean"; fi ;; upgrade) if [[ $cur == -* ]]; then _terractl_reply "--downgrade --no-backup" else _terractl_reply "latest $(_terractl_zips "$instance" "$host")" fi ;; enable|disable) if (( cword == 3 )); then _terractl_reply "--now"; fi ;; watch) # the number after -n is free-form if [[ $cur == -* ]]; then _terractl_reply "-n -q"; fi ;; delete) _terractl_reply "-y --yes --no-backup" ;; export) if [[ $cur == -* ]]; then _terractl_reply "--with-server"; else _terractl_files tar.gz; fi ;; mods) if (( cword == 3 )); then _terractl_reply "list add enable disable remove" return fi case ${words[3]} in add) # .tmod files and folders (e.g. the Windows export folder) _terractl_files tmod ;; enable|remove) if [[ -z $host ]]; then _terractl_reply "$(_terractl_mods "$instance" installed)"; fi ;; disable) if [[ -z $host ]]; then _terractl_reply "$(_terractl_mods "$instance" enabled)"; fi ;; esac ;; esac } complete -F _terractl terractl