diff --git a/docker-compose.yml b/docker-compose.yml index 4dabaa3..f7744f9 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -22,7 +22,7 @@ services: - homedir_main:/home/user - serverfiles:/serverfiles - mods:/serverfiles/steamapps/workshop/content - - mpmissions:/serverfiles/mpmissions + - mods:/mods - ./files:/files - ./web/bin/dz:/usr/local/bin/dz - ./web:/web @@ -34,9 +34,9 @@ services: build: server volumes: - homedir_server:/home/user - - serverfiles:/serverfiles:ro - - mods:/serverfiles/steamapps/workshop/content:ro - - mpmissions:/serverfiles/mpmissions:ro + - serverfiles:/serverfiles + - mods:/mods + - mpmissions:/serverfiles/mpmissions - profiles:/profiles - ./files:/files - ./server/bin/dz:/usr/local/bin/dz diff --git a/files/dz-common b/files/dz-common index 5d0e34d..81c5c25 100755 --- a/files/dz-common +++ b/files/dz-common @@ -81,9 +81,9 @@ check_mod_install(){ fi } -get_mod_id_by_index(){ +get_mod_id_by_index2(){ # If we were passed a valid mod id, just return it - if [[ -d "${WORKSHOP_DIR}/${1}" ]] + if [ -d "${WORKSHOP_DIR}/${1}" ] then echo -n ${1} return @@ -104,7 +104,7 @@ get_mod_id_by_index(){ # Get mod name by ID or index get_mod_name(){ - ID=$(get_mod_id_by_index ${1}) + ID=$(get_mod_id_by_index2 ${1}) if ! [ -d "${WORKSHOP_DIR}/${ID}" ] then echo "Mod ID ${1} doesn't exist" >&2 @@ -116,19 +116,19 @@ get_mod_name(){ # List mods list(){ - workshoplist="" X=1 C="${green}" spaces=" " - echo -e "\n ID Name URL Size" + echo "Installed mods:" + echo -e " ID Name URL Size" echo "------------------------------------------------------------------------------------------------------------------------" for dir in $(ls -tr ${WORKSHOP_DIR}) do ID=${dir} NAME=$(grep name "${WORKSHOP_DIR}/${dir}/meta.cpp" | cut -d '"' -f2 | sed -r 's/\s+//g') SIZE=$(du -sh "${WORKSHOP_DIR}/${dir}" | awk '{print $1}') - workshoplist+=" +workshop_download_item "${release_client_appid}" "${ID} printf "${C}%.3d %s %.23s %s https://steamcommunity.com/sharedfiles/filedetails/?id=%s %s${default}\n" ${X} ${ID} "${NAME}" "${spaces:${#NAME}+1}" ${ID} ${SIZE} X=$((X+1)) done + echo } diff --git a/files/mods/2692979668/types.env b/files/mods/2692979668/xml.env similarity index 80% rename from files/mods/2692979668/types.env rename to files/mods/2692979668/xml.env index 917f633..3a45bbe 100644 --- a/files/mods/2692979668/types.env +++ b/files/mods/2692979668/xml.env @@ -1,8 +1,9 @@ #!/usr/bin/env bash -CFGSPAWNABLETYPES=https://raw.githubusercontent.com/RedFalconKen/RedFalconFlightSystem-Heliz/main/Config%20Files/Event%20Spawn%20Config/RFFSHelis_cfgspawnabletypes.xml -CFGEVENTSPAWNS=https://raw.githubusercontent.com/RedFalconKen/RedFalconFlightSystem-Heliz/main/Config%20Files/Event%20Spawn%20Config/Chernarus/RFFSHelis_cfgeventspawns.xml +CFGSPAWNABLETYPES=https://raw.githubusercontent.com/RedFalconKen/RedFalconFlightSystem-Heliz/main/Config%20Files/Event%20Spawn%20Config/RFFSHelis_cfgspawnabletypes.xml #CFGEVENTSPAWNS=https://raw.githubusercontent.com/RedFalconKen/RedFalconFlightSystem-Heliz/main/Config%20Files/Event%20Spawn%20Config/Banov/RFFSHelis_cfgeventspawns.xml +CFGEVENTSPAWNS=https://raw.githubusercontent.com/RedFalconKen/RedFalconFlightSystem-Heliz/main/Config%20Files/Event%20Spawn%20Config/Chernarus/RFFSHelis_cfgeventspawns.xml +#CFGEVENTSPAWNS=https://raw.githubusercontent.com/RedFalconKen/RedFalconFlightSystem-Heliz/main/Config%20Files/Event%20Spawn%20Config/DeerIsle/RFFSHelis_cfgeventspawns.xml #CFGEVENTSPAWNS=https://raw.githubusercontent.com/RedFalconKen/RedFalconFlightSystem-Heliz/main/Config%20Files/Event%20Spawn%20Config/Namalsk/RFFSHelis_cfgeventspawns.xml -EVENTS=https://raw.githubusercontent.com/RedFalconKen/RedFalconFlightSystem-Heliz/main/Config%20Files/Event%20Spawn%20Config/RFFSHelis_events.xml +EVENTS=https://raw.githubusercontent.com/RedFalconKen/RedFalconFlightSystem-Heliz/main/Config%20Files/Event%20Spawn%20Config/RFFSHelis_events.xml TYPES=https://raw.githubusercontent.com/RedFalconKen/RedFalconFlightSystem-Heliz/main/Config%20Files/Types.XML/RFFSHelis_Types.xml diff --git a/files/mods/types.sh b/files/mods/types.sh deleted file mode 100755 index ac57196..0000000 --- a/files/mods/types.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!/usr/bin/env bash - -# A generic script to manage a mod's types.xml against all installed missions - -set -eE - -ID=${1} -MODE=${2} -TYPES_FILE="${WORKSHOP_DIR}/${ID}/extras/types.xml" - -if [[ ${3} != "" ]] -then - TYPES_FILE="${3}" -fi - -for file in $(find ${SERVER_FILES}/mpmissions -name types.xml -print -prune) -do - if [[ ${MODE} = "uninstall" ]] - then - # Remove the lines that were added by the mod's extras/types.xml from - # every db/types.xml in all mission directories - - # Chop the top tag from the source file - tail -n+2 ${TYPES_FILE} > /tmp/types-tmp.xml - - # Chop the bottom tag from the source file - head -n-1 /tmp/types-tmp.xml > /tmp/types-src.xml - - # Remove that content from the original file - grep -qvxFf /tmp/types-src.xml ${file} - else - # Add the contents of extras/types.xml to every db/types.xml in all - # mission directories - xmllint --noout ${TYPES_FILE} 2> /dev/null && { - echo -e "${green}${TYPES_FILE} passes XML lint test!" - echo -e "Merging to $file...${default}" - # Chop the bottom tag from the destination file - head -n-1 ${file} > /tmp/types-dst.xml - - # Chop the top 2 tags, xml and types, from the source file - tail -n+2 ${TYPES_FILE} > /tmp/types-src.xml - - # Concatenate the two files back into the source file - cat /tmp/types-dst.xml /tmp/types-src.xml > /tmp/types.xml - - xmllint --noout /tmp/types.xml 2> /dev/null && { - cp -v /tmp/types.xml ${file} - } || { - # Try again, but chop the top 3 tags, hopefully xml and types, from the source file... - echo "First merge attempt failed, trying again..." - tail -n+3 ${TYPES_FILE} > /tmp/types-src.xml - - # Concatenate the two files back into the source file - cat /tmp/types-dst.xml /tmp/types-src.xml > /tmp/types.xml - - # And lint again. This should probably be a recursive function... - xmllint --noout /tmp/types.xml && { - cp -v /tmp/types.xml ${file} - } || { - echo "XML lint check after merge failed! No files changed!" - } - } - } || { - echo -e "${red}${TYPES_FILE} fails XML lint test!" - echo -e "This will have to be merged by hand!${default}" - } - fi -done diff --git a/files/mods/xml.sh b/files/mods/xml.sh new file mode 100755 index 0000000..e4a6e57 --- /dev/null +++ b/files/mods/xml.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash + +# A generic script to manage merging mod XML files to mpmissions XML files + +set -eE + +ID=${1} + +source ${FILES}/mods/${ID}/xml.env + +# Iterate over the file names we can handle +for var in CFGEVENTSPAWNS CFGSPAWNABLETYPES EVENTS TYPES +do + if echo ${!var} | grep -q http + then + OUT="${WORKSHOP_DIR}/${ID}/${var,,}.xml" + echo "${var} is a URL, downloading to ${OUT}" + curl -so ${OUT} ${!var} + xmllint --noout ${OUT} 2> /dev/null || { + echo -e "${red}${var,,}.xml does not pass XML lint test!${default}" + } && { + echo -e "${green}${var,,}.xml passes XML lint test!${default}" + } + fi +done diff --git a/files/serverDZ.cfg b/files/serverDZ.cfg index d692701..3a19c82 100644 --- a/files/serverDZ.cfg +++ b/files/serverDZ.cfg @@ -85,8 +85,8 @@ class Missions { template="dayzOffline.chernarusplus"; // Chernarus // template="dayzOffline.enoch"; // Livonia -// template="empty.banov" // Banov -// template="empty.deerisle" // Deer Isle -// template="serverMission.Pripyat" // Pripyat +// template="empty.banov"; // Banov +// template="empty.deerisle"; // Deer Isle +// template="serverMission.Pripyat"; // Pripyat }; }; diff --git a/server/Dockerfile b/server/Dockerfile index 947ce97..426f7aa 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -23,6 +23,7 @@ RUN apt-get update && apt-get -y upgrade && apt-get -y install --no-install-reco nano \ procps \ python3-pip \ + strace \ wget RUN update-alternatives --install /usr/bin/python python /usr/bin/python3.9 1 @@ -43,8 +44,8 @@ RUN cd /usr/local && git clone https://github.com/indepth666/py3rcon.git # Setup a non-privileged user RUN groupadd user && \ useradd -l -g user user && \ - mkdir -p /home/user /serverfiles/mpmissions /serverfiles/steamapps/workshop/content /profiles /mods && \ - chown -R user:user /home/user /serverfiles /profiles /mods + mkdir -p /home/user /serverfiles/mpmissions /mods /mpmissions /profiles && \ + chown -R user:user /home/user /serverfiles /mods /mpmissions /profiles # Use our non-privileged user USER user diff --git a/server/bin/dz b/server/bin/dz index b50622b..7074a96 100755 --- a/server/bin/dz +++ b/server/bin/dz @@ -18,18 +18,10 @@ parameters="-config=${SERVER_CFG_DST} -port=${port} -freezecheck -BEpath=${SERVE # Used to check if dayZ is installed SERVER_INSTALL_FILE="${SERVER_FILES}/DayZServer" -# Workshop. This file will store metadata about what mods are in use in this server instance -WORKSHOP_CFG="${SERVER_PROFILE}/workshop.cfg" -if [ ! -f "${WORKSHOP_CFG}" ] -then - touch "${WORKSHOP_CFG}" -fi - -INSTALLED_MODS="${SERVER_FILES}/workshop.cfg" - # An array to store Workshop items. Each element contains the mod's ID, name, and state (active or not). -declare -a workshopID -WORKSHOP_DIR="${SERVER_FILES}/steamapps/workshop/content/${release_client_appid}" +#WORKSHOP_DIR="${SERVER_FILES}/steamapps/workshop/content/${release_client_appid}" +WORKSHOP_DIR="/mods/${release_client_appid}" +mod_command_line="" # Backups BACKUP_DIR="${HOME}/backup" @@ -56,7 +48,7 @@ Options and arguments: f|force - Forcibly kill the server. Use only as a last resort if the server won't shut down l|list - List Workshop items and their details n|rcon - Connect to the server using a python RCON client - restart - Restart the server without restarting the container + r|restart - Restart the server without restarting the container s|status - Shows the server's status: Running, uptime, mods, parameters, mod parameter, etc. stop - Stop the server ${default}" @@ -119,17 +111,19 @@ EOF > ~/py3rcon.config.json } -# Manage the mod symlink -symlink(){ - W=${1} - ID=${2} - NAME=${3} - if [ ! -L "${SERVER_FILES}/@${NAME}" ] && [[ ${W} = 1 ]] +get_mods(){ + workshoplist="" + mod_command_line="" + for link in $(ls -tdr ${SERVER_PROFILE}/@* 2> /dev/null) + do + ID=$(readlink ${link} | awk -F/ '{print $NF}') + MODNAME=$(get_mod_name ${ID}) + workshoplist+=" +workshop_download_item "${release_client_appid}" "${ID} + mod_command_line+="@${MODNAME};" + done + if [[ ${mod_command_line} != "" ]] then - ln -sv ${WORKSHOP_DIR}/${ID} "${SERVER_FILES}/@${NAME}" - elif [[ "${W}" = "0" ]] - then - rm -vf "${SERVER_FILES}/@${NAME}" + mod_command_line="-mod=${mod_command_line::-1}" fi } @@ -155,7 +149,7 @@ start(){ trap ' report ' EXIT - mod_cmd + get_mods cd ${SERVER_FILES} # Run the server. Allow docker to restart the container if the script exits with a code other than 0. This is so we can # safely shut the container down without killing the server within. @@ -164,7 +158,7 @@ start(){ # actual status with those echo ${mod_command_line} > /tmp/mod_command_line echo ${parameters} > /tmp/parameters - ./DayZServer ${mod_command_line} ${parameters} + ./DayZServer "${mod_command_line}" ${parameters} EXIT_CODE=$? if [ -f ${SERVER_FILES}/restart ] then @@ -213,42 +207,15 @@ config(){ fi } -# Assemble the workshop variables -get_installed_mods(){ - mapfile -t installedModsID < "${INSTALLED_MODS}" - workshoplist="" - for i in "${installedModsID[@]}" - do - ID=$(echo ${i} | cut -d: -f1) - workshoplist+=" +workshop_download_item "${release_client_appid}" "${ID} - done -} - -get_mods(){ - mapfile -t workshopID < "${WORKSHOP_CFG}" - workshoplist="" - for i in "${workshopID[@]}" - do - ID=$(echo ${i} | cut -d: -f1) - workshoplist+=" +workshop_download_item "${release_client_appid}" "${ID} - done -} - get_mod_id_by_index(){ - # If we were passed a valid mod id, just return it - if [[ -d "${workshopdir}/${1}" ]] - then - echo ${1} - return - fi X=1 - # Loop over mod list - for i in "${workshopID[@]}" + # Loop over mods + for link in $(ls -tdr ${SERVER_PROFILE}/@* 2> /dev/null) do - ID=$(echo ${i} | cut -d: -f1) + ID=$(readlink ${link} | awk -F/ '{print $NF}') if [[ ${X} = ${1} ]] then - echo ${ID} + echo -n ${ID} return fi X=$((X+1)) @@ -258,19 +225,13 @@ get_mod_id_by_index(){ # Get mod name by ID or index get_mod_name(){ # Check for an ID - if [ -d "${WORKSHOP_DIR}/${1}" ] - then - ID=${1} - else - ID=$(get_mod_id_by_index ${1}) - fi if ! [ -d "${WORKSHOP_DIR}/${ID}" ] then echo "Mod ID ${1} doesn't exist" >&2 exit 1 fi NAME=$(grep name ${WORKSHOP_DIR}/${ID}/meta.cpp | cut -d '"' -f2 | sed -r 's/\s+//g') - echo ${NAME} + echo -n ${NAME} } # Activate / Deactivate a mod @@ -285,35 +246,20 @@ activate(){ UU="un" COLOR="${red}" fi - ID=$(get_mod_id_by_index ${1}) - MODNAME=$(get_mod_name ${1}) + ID=$(get_mod_id_by_index2 ${1}) + MODNAME=$(get_mod_name ${ID}) # Toggle state or report nothing burger - if [[ "${ACTIVE}" != "${W}" ]] + pushd "${SERVER_PROFILE}" > /dev/null + if [ -L "${SERVER_PROFILE}/@${MODNAME}" ] then - sed -i "${WORKSHOP_CFG}" -e "s/${ID}:${MODNAME}:[0-1]/${ID}:${MODNAME}:${W}/" - echo -e "Mod id ${ID} - ${COLOR}${MODNAME}${default} ${WW}activated" + rm -vf "${SERVER_PROFILE}/@${MODNAME}" else - echo -e "Mod id ${ID} - ${COLOR}${MODNAME}${default} - is already ${WW}active" - fi - list -} - -# Assemble the mod command line -mod_cmd(){ - mod_command_line="" - for i in "${workshopID[@]}" - do - NAME=$(echo ${i} | cut -d: -f2) - ACTIVE=$(echo ${i} | cut -d: -f3) - if [[ ${ACTIVE} = "1" ]] - then - mod_command_line="${mod_command_line}@${NAME};" - fi - done - if [[ ${mod_command_line} != "" ]] - then - mod_command_line='-mod='${mod_command_line::-1} + ln -s "${WORKSHOP_DIR}/${ID}" "${SERVER_PROFILE}/@${MODNAME}" +# echo -e "Mod id ${ID} - ${COLOR}${MODNAME}${default} - is already ${WW}active" fi + echo -e "Mod id ${ID} - ${COLOR}${MODNAME}${default} ${WW}activated" + popd > /dev/null + status } checkXML(){ @@ -359,6 +305,29 @@ rcon(){ exec /usr/local/py3rcon/py3rcon.py --gui ~/py3rcon.config.json } +# List mods +activelist(){ + X=1 + C="${green}" + spaces=" " + have=no + for link in $(ls -tdr ${SERVER_PROFILE}/@* 2> /dev/null) + do + if [[ ${have} = "no" ]] + then + have="yes" + echo -e "\n ID Name URL Size" + echo "------------------------------------------------------------------------------------------------------------------------" + fi + ID=$(readlink ${link} | awk -F/ '{print $NF}') + MODNAME=$(get_mod_name ${ID}) + SIZE=$(du -sh "${WORKSHOP_DIR}/${ID}" | awk '{print $1}') + printf "${C}%.3d %s %.23s %s https://steamcommunity.com/sharedfiles/filedetails/?id=%s %s${default}\n" ${X} ${ID} "${MODNAME}" "${spaces:${#MODNAME}+1}" ${ID} ${SIZE} + X=$((X+1)) + done + echo +} + # Display the status of everything status(){ loadconfig @@ -385,8 +354,6 @@ status(){ RUNNING="${RUNNING}\nRunning Parameters: $(cat /tmp/parameters)\nRunning mod parameter: $(cat /tmp/mod_command_line)" fi - mod_cmd - MAP="none" # Map name if [[ -f ${SERVER_CFG_DST} ]] @@ -402,9 +369,10 @@ Server files installed: ${INSTALLED}" echo exit 0 fi + get_mods echo -ne " -Mods: " - MODS=$(list) +Active mods: " + activelist if [[ ${MODS} == "" ]] then echo -n "none" @@ -418,6 +386,7 @@ Working mod parameter: ${mod_command_line}" MAP=$(grep template ${SERVER_CFG_DST} | grep -v "^//" | cut -d= -f2 | cut -d\; -f1) echo "Map: ${MAP}" fi + echo } backup(){ @@ -438,8 +407,6 @@ shift || { usage } -get_mods - case "${C}" in a|activate) activate 1 "${@}" @@ -477,7 +444,7 @@ case "${C}" in r|remove) remove "${@}" ;; - restart) + r|restart) restart "${@}" ;; start) diff --git a/web/bin/dz b/web/bin/dz index adc3414..8d2235e 100755 --- a/web/bin/dz +++ b/web/bin/dz @@ -3,7 +3,7 @@ source /files/dz-common # An array to store Workshop items. Each element contains the mod's ID, name, and state (active or not). -WORKSHOP_DIR="${SERVER_FILES}/steamapps/workshop/content/${release_client_appid}" +WORKSHOP_DIR="/mods/${release_client_appid}" workshoplist="" @@ -18,18 +18,96 @@ Usage: ${green}$(basename $0)${yellow} option [ arg1 [ arg2 ] ] Options and arguments: - add id - Add a DayZ Workshop item by id. Added items become active by default + a|add id - Add a DayZ Workshop item by id. Added items become active by default i|install - Install the DayZ server files - l|list - List Workshop items and their details g|login - Login to Steam. m|modupdate - Update the mod files r|remove id - Remove all files and directories of a Workshop item by id - s|status - Shows the server's status: Running, uptime, mods, parameters, mod parameter, etc. + s|status - Shows Steam login status, if base files are installed, installed mods u|update - Update the server files ${default}" exit 1 } +# "Manage" XML files. +xml(){ + /files/mods/xml.sh ${1} +} + +# Copy mod keys +copy_keys(){ + if [[ ${1} = 1 ]] + then + echo "Copying key files..." + find ${WORKSHOP_DIR}/${2} -name "*.bikey" -exec cp -v {} "${SERVER_FILES}/keys/" \; + fi +} + +# Manage the mod symlink +symlink(){ + W=${1} + ID=${2} + NAME=${3} + if [ ! -L "${SERVER_FILES}/@${NAME}" ] && [[ ${W} = 1 ]] + then + ln -sv ${WORKSHOP_DIR}/${ID} "${SERVER_FILES}/@${NAME}" + elif [[ "${W}" = "0" ]] + then + rm -vf "${SERVER_FILES}/@${NAME}" + fi +} + +# Add a mod +add(){ + if [ -d "${WORKSHOP_DIR}/${1}" ] + then + echo -e "${yellow}Warning: The mod directory ${WORKSHOP_DIR}/${1} already exists!${default}" + MODNAME=$(get_mod_name ${1}) + fi + if [ -L "${SERVER_FILES}/@${MODNAME}" ] + then + echo -e "${yellow}Warning: The mod symlink ${SERVER_FILES}/@${MODNAME} already exists!${default}" + fi + echo "Adding mod id ${1}" + dologin + ${STEAMCMD} +force_install_dir ${SERVER_FILES} +login "${steamlogin}" +workshop_download_item "${release_client_appid}" "${1}" +quit + # Make sure the install succeeded + if [ ! -d "${WORKSHOP_DIR}/${1}" ] + then + echo -e "${red}Mod installation failed: The mod directory ${WORKSHOP_DIR}/${1} was not created!${default}" + echo "Installation failed! See above (You probably need to use a real Steam login)" + return + fi + # Get the name of the newly added mod + MODNAME=$(get_mod_name ${1}) + symlink 1 ${1} "${MODNAME}" + # Lower case all the files in mod directories. + find "${WORKSHOP_DIR}/${1}" -depth -exec rename -f 's/(.*)\/([^\/]*)/$1\/\L$2/' {} \; + # Copy the key files + copy_keys 1 ${1} + echo -e "Mod id ${1} - ${green}${MODNAME}${default} - added" +# checkTypesXML ${1} install +# checkInstall ${1} install +} + +# Remove a mod +remove(){ +# checkTypesXML ${1} uninstall +# checkInstall ${1} uninstall + if [ -d "${WORKSHOP_DIR}/${1}" ] + then + MODNAME=$(get_mod_name ${1}) + echo "Removing directory ${WORKSHOP_DIR}/${1}" + rm -rf "${WORKSHOP_DIR}/${1}" + fi + if [ -L "${SERVER_FILES}/@${MODNAME}" ] + then + echo "Removing symlink ${SERVER_FILES}/@${MODNAME}" + rm -f "${SERVER_FILES}/@${MODNAME}" + fi + echo -e "Mod id ${1} - ${red}${MODNAME}${default} - removed" +} + # Handle the Steam login information. login(){ if [ -f "${STEAM_LOGIN}" ] @@ -125,11 +203,28 @@ update(){ fi } +get_mods(){ + workshoplist="" + mod_command_line="" + for link in $(ls -tdr ${SERVER_FILES}/@* 2> /dev/null) + do + ID=$(readlink ${link} | cut -d/ -f7) + MODNAME=$(get_mod_name ${ID}) + workshoplist+=" +workshop_download_item "${release_client_appid}" "${ID} + mod_command_line+="@${MODNAME};" + done + if [[ ${mod_command_line} != "" ]] + then + mod_command_line='-mod='${mod_command_line::-1} + fi +} + + # Update mods modupdate(){ echo "Updating mods..." dologin -# echo ${STEAMCMD} +force_install_dir ${SERVER_FILES} +login "${steamlogin}" ${workshoplist} +quit + get_mods ${STEAMCMD} +force_install_dir ${SERVER_FILES} +login "${steamlogin}" ${workshoplist} +quit # Updated files come in with mixed cases. Fix that. echo -ne "\nFixing file names..." @@ -187,16 +282,13 @@ shift || { } case "${C}" in - add) + a|add) add "${@}" ;; i|install) install "${@}" ;; - l|list) - list "${@}" - ;; - login) + g|login) login "${@}" ;; m|modupdate) @@ -211,6 +303,9 @@ case "${C}" in u|update) update "${@}" ;; + x|xml) + xml "${@}" + ;; *) usage "$*" ;;