[
  {
    "path": "Dockerfile",
    "content": "FROM ubuntu:bionic\n\nENV TERM xterm\n\nENV STEAM_DIR /home/steam\nENV STEAMCMD_DIR /home/steam/steamcmd\nENV CSGO_APP_ID 740\nENV CSGO_DIR /home/steam/csgo\n\nSHELL [\"/bin/bash\", \"-c\"]\n\nARG STEAMCMD_URL=https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz\n\nRUN set -xo pipefail \\\n      && apt-get update \\\n      && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends --no-install-suggests -y \\\n          lib32gcc1 \\\n          lib32stdc++6 \\\n          lib32z1 \\\n          ca-certificates \\\n          net-tools \\\n          locales \\\n          curl \\\n          unzip \\\n      && locale-gen en_US.UTF-8 \\\n      && adduser --disabled-password --gecos \"\" steam \\\n      && mkdir ${STEAMCMD_DIR} \\\n      && cd ${STEAMCMD_DIR} \\\n      && curl -sSL ${STEAMCMD_URL} | tar -zx -C ${STEAMCMD_DIR} \\\n      && mkdir -p ${STEAM_DIR}/.steam/sdk32 \\\n      && ln -s ${STEAMCMD_DIR}/linux32/steamclient.so ${STEAM_DIR}/.steam/sdk32/steamclient.so \\\n      && { \\\n            echo '@ShutdownOnFailedCommand 1'; \\\n            echo '@NoPromptForPassword 1'; \\\n            echo 'login anonymous'; \\\n            echo 'force_install_dir ${CSGO_DIR}'; \\\n            echo 'app_update ${CSGO_APP_ID}'; \\\n            echo 'quit'; \\\n        } > ${STEAM_DIR}/autoupdate_script.txt \\\n      && mkdir ${CSGO_DIR} \\\n      && chown -R steam:steam ${STEAM_DIR} \\\n      && rm -rf /var/lib/apt/lists/*\n\nENV LANG=en_US.UTF-8 \\\n    LANGUAGE=en_US:en \\\n    LC_ALL=en_US.UTF-8\n\nCOPY --chown=steam:steam containerfs ${STEAM_DIR}/\n\nUSER steam\nWORKDIR ${CSGO_DIR}\nVOLUME ${CSGO_DIR}\nENTRYPOINT exec ${STEAM_DIR}/start.sh\n"
  },
  {
    "path": "LICENSE",
    "content": "This is free and unencumbered software released into the public domain.\n\nAnyone is free to copy, modify, publish, use, compile, sell, or\ndistribute this software, either in source code form or as a compiled\nbinary, for any purpose, commercial or non-commercial, and by any\nmeans.\n\nIn jurisdictions that recognize copyright laws, the author or authors\nof this software dedicate any and all copyright interest in the\nsoftware to the public domain. We make this dedication for the benefit\nof the public at large and to the detriment of our heirs and\nsuccessors. We intend this dedication to be an overt act of\nrelinquishment in perpetuity of all present and future rights to this\nsoftware under copyright law.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND,\nEXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF\nMERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.\nIN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR\nOTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,\nARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\nOTHER DEALINGS IN THE SOFTWARE.\n\nFor more information, please refer to <http://unlicense.org>\n"
  },
  {
    "path": "Makefile",
    "content": "SHELL := /bin/bash\n\nCONTAINER_NAME ?= csgo-dedicated-server \nIMAGE_NAME ?= kmallea/csgo:latest\nSERVER_HOSTNAME ?= Counter-Strike: Global Offensive Dedicated Server\nSERVER_PASSWORD ?=\nRCON_PASSWORD ?= changeme\nSTEAM_ACCOUNT ?= changeme\nAUTHKEY ?= changeme\nIP ?= 0.0.0.0\nPORT ?= 27015\nTV_PORT ?= 27020\nTICKRATE ?= 128\nFPS_MAX ?= 400\nGAME_TYPE ?= 0\nGAME_MODE ?= 1\nMAP ?= de_dust2\nMAPGROUP ?= mg_active\nHOST_WORKSHOP_COLLECTION ?=\nWORKSHOP_START_MAP ?=\nMAXPLAYERS ?= 12\nTV_ENABLE ?= 1\nLAN ?= 1\nSOURCEMOD_ADMINS ?= STEAM_1:0:123456,STEAM_1:0:654321\nRETAKES ?= 0\nNOMASTER ?= 0\n\n.PHONY: all clean image test stop\n\nall: image\n\nclean:\n\tdocker rmi $(IMAGE_NAME)\n\nimage: Dockerfile\n\tdocker build -t $(IMAGE_NAME) \\\n\t--build-arg STEAMCMD_URL=https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz \\\n\t.\n\nserver:\n\tdocker run \\\n\t\t-i \\\n\t\t-t \\\n\t\t-d \\\n\t\t--net=host \\\n\t\t--mount source=csgo-data,target=/home/steam/csgo \\\n\t\t-e \"SERVER_HOSTNAME=$(SERVER_HOSTNAME)\" \\\n\t\t-e \"SERVER_PASSWORD=$(SERVER_PASSWORD)\" \\\n\t\t-e \"RCON_PASSWORD=$(RCON_PASSWORD)\" \\\n\t\t-e \"STEAM_ACCOUNT=$(STEAM_ACCOUNT)\" \\\n\t\t-e \"AUTHKEY=$(AUTHKEY)\" \\\n\t\t-e \"TICKRATE=$(TICKRATE)\" \\\n\t\t-e \"FPS_MAX=$(FPS_MAX)\" \\\n\t\t-e \"GAME_TYPE=$(GAME_TYPE)\" \\\n\t\t-e \"GAME_MODE=$(GAME_MODE)\" \\\n\t\t-e \"MAP=$(MAP)\" \\\n\t\t-e \"MAPGROUP=$(MAPGROUP)\" \\\n\t\t-e \"HOST_WORKSHOP_COLLECTION=$(HOST_WORKSHOP_COLLECTION)\" \\\n\t\t-e \"WORKSHOP_START_MAP=$(WORKSHOP_START_MAP)\" \\\n\t\t-e \"MAXPLAYERS=$(MAXPLAYERS)\" \\\n\t\t-e \"TV_ENABLE=$(TV_ENABLE)\" \\\n\t\t-e \"LAN=$(LAN)\" \\\n\t\t-e \"SOURCEMOD_ADMINS=$(SOURCEMOD_ADMINS)\" \\\n\t\t-e \"RETAKES=$(RETAKES)\" \\\n\t\t--name $(CONTAINER_NAME) \\\n\t\t$(IMAGE_NAME)\n\ntest:\n\tdocker run \\\n\t\t-i \\\n\t\t-t \\\n\t\t--rm \\\n\t\t--net=host \\\n\t\t--mount type=bind,source=\"$(PWD)/test\",target=/home/steam/csgo \\\n\t\t-e \"CI=true\" \\\n\t\t-e \"SERVER_HOSTNAME=$(SERVER_HOSTNAME)\" \\\n\t\t-e \"SERVER_PASSWORD=$(SERVER_PASSWORD)\" \\\n\t\t-e \"RCON_PASSWORD=$(RCON_PASSWORD)\" \\\n\t\t-e \"STEAM_ACCOUNT=$(STEAM_ACCOUNT)\" \\\n\t\t-e \"AUTHKEY=$(AUTHKEY)\" \\\n\t\t-e \"TICKRATE=$(TICKRATE)\" \\\n\t\t-e \"FPS_MAX=$(FPS_MAX)\" \\\n\t\t-e \"GAME_TYPE=$(GAME_TYPE)\" \\\n\t\t-e \"GAME_MODE=$(GAME_MODE)\" \\\n\t\t-e \"MAP=$(MAP)\" \\\n\t\t-e \"MAPGROUP=$(MAPGROUP)\" \\\n\t\t-e \"HOST_WORKSHOP_COLLECTION=$(HOST_WORKSHOP_COLLECTION)\" \\\n\t\t-e \"WORKSHOP_START_MAP=$(WORKSHOP_START_MAP)\" \\\n\t\t-e \"MAXPLAYERS=$(MAXPLAYERS)\" \\\n\t\t-e \"TV_ENABLE=$(TV_ENABLE)\" \\\n\t\t-e \"LAN=$(LAN)\" \\\n\t\t-e \"SOURCEMOD_ADMINS=$(SOURCEMOD_ADMINS)\" \\\n\t\t-e \"RETAKES=$(RETAKES)\" \\\n\t\t-e \"SM_PUGSETUP_SNAKE_CAPTAIN_PICKS=2\" \\\n\t\t--name $(CONTAINER_NAME) \\\n\t\t$(IMAGE_NAME)\n\nstop:\n\tdocker stop $(CONTAINER_NAME)\n\tdocker rm $(CONTAINER_NAME)\n\t"
  },
  {
    "path": "README.md",
    "content": "# CSGO containerized\n\nThe Dockerfile will build an image for running a Counter-Strike: Global Offensive dedicated server in a container.\n\nThe following addons and plugins are included by default:\n\n- [Metamod](https://www.sourcemm.net/)\n- [SourceMod](https://www.sourcemod.net/)\n- [SteamWorks](https://forums.alliedmods.net/showthread.php?t=229556)\n- [Updater](https://bitbucket.org/GoD_Tony/updater/downloads/updater.smx)\n- [PugSetup](https://github.com/splewis/csgo-pug-setup)\n- [Practice Mode](https://github.com/splewis/csgo-practice-mode)\n- [Retakes](https://github.com/splewis/csgo-retakes) (**disabled by default**)\n\nTo get a 10man/gather going, simply connect and type `.setup` in chat. Practice Mode should also be available from the menu.\n\nRetakes is disabled by default. To enable it, set the environment variable `RETAKES=1` and restart the container. Use can later use the cvar `sm_retakes_enabled 0` to turn if off on-demand.\n\n## How to Use\n\n```bash\ndocker pull kmallea/csgo:latest\n```\n\nTo use the image as-is, run it with a few useful environment variables to configure the server:\n\n```bash\ndocker run \\\n  --rm \\\n  --interactive \\\n  --tty \\\n  --detach \\\n  --mount source=csgo-data,target=/home/steam/csgo \\\n  --network=host \\\n  --env \"SERVER_HOSTNAME=hostname\" \\\n  --env \"SERVER_PASSWORD=password\" \\\n  --env \"RCON_PASSWORD=rconpassword\" \\\n  --env \"STEAM_ACCOUNT=gamelogintoken\" \\\n  --env \"AUTHKEY=webapikey\" \\\n  --env \"SOURCEMOD_ADMINS=STEAM_1:0:123456,STEAM_1:0:654321\" \\\n  kmallea/csgo\n```\n\nWould you rather use a bind volume so that you can access file contents directly? Use `--mount type=bind,source=$(pwd),target=/home/steam/csgo` instead of the one in the example above.\n\nIf you plan on managing plugins manually with a bind volume, you might want pass an empty or reduced `INSTALL_PLUGINS` environment variable to prevent conflicts (see below for default value of `INSTALL_PLUGINS`).\n\n### Required Game Login Token\n\nThe `STEAM_ACCOUNT` is a \"Game Login Token\" required by Valve to run public servers. Confusingly, this token is also referred to as a steam account (it's set via `sv_setsteamaccount`). To get one, visit https://steamcommunity.com/dev/managegameservers. You'll need one for each server.\n\nRemember that if you DO NOT give a valid Game Login Token, your server will be restricted to LAN only\n\n### Optional Steam Web API Key for Workshop Content\n\nTo access maps and collections from the Workshop, you need to provide a Steam Web API key. You can provide this via the evironment variable `AUTHKEY` and it will be passed to the command-line as `-authkey <key>`.\n\nIf you don't have a key you can generate one at http://steamcommunity.com/dev/apikey.\n\nWith a key set, you can also use the environment variables `HOST_WORKSHOP_COLLECTION` and `WORKSHOP_START_MAP` to specify a workshop collection and start the server with a workshop map, respectively.\n\nFor more information check out the [Valve developer wiki page](https://developer.valvesoftware.com/wiki/CSGO_Workshop_For_Server_Operators#How_to_host_Workshop_Maps_with_a_CS:GO_Dedicated_Server).\n\n### SourceMod admins\n\nThe optional `SOURCEMOD_ADMINS` environment variable is a comma-delimited list of Steam IDs. These will be added to SourceMod's admin list before the server is started.\n\n### Playing on LAN\n\nIf you're on a LAN, add the environment variable `LAN=1` (e.g., `--env \"LAN=1\"`) to have `sv_lan 1` set for you in the server.\n\n### Environment variable overrides\n\nBelow are the default values for environment variables that control the server configuration. To override, pass one or more of these to docker using the `-e` or `--env` argument (example above).\n\n```bash\nSERVER_HOSTNAME=Counter-Strike: Global Offensive Dedicated Server\nSERVER_PASSWORD=\nRCON_PASSWORD=changeme\nSTEAM_ACCOUNT=changeme\nAUTHKEY=changeme\nIP=0.0.0.0\nPORT=27015\nTV_PORT=27020\nTICKRATE=128\nFPS_MAX=400\nGAME_TYPE=0\nGAME_MODE=1\nMAP=de_dust2\nMAPGROUP=mg_active\nHOST_WORKSHOP_COLLECTION=\nWORKSHOP_START_MAP=\nMAXPLAYERS=12\nTV_ENABLE=1\nLAN=0\nSOURCEMOD_ADMINS=\nRETAKES=0\nNOMASTER=0\n```\n\nFor compatibility with the [Docker secrets](https://docs.docker.com/engine/swarm/secrets/) feature the following \nenvironment variables are also available as a '_FILE' variant.\n\n```bash\nSERVER_PASSWORD_FILE\nRCON_PASSWORD_FILE\nSTEAM_ACCOUNT_FILE\nAUTHKEY_FILE\nSOURCEMOD_ADMINS_FILE\n```\n\nIf one of these is set the content of the referred file is used as content for the non-'_FILE\" environment variable. If both\nenvironment variables are set, the content of the non-'_FILE' variable takes precedence.\n\nUsage of _FILE variables allows constructs like this in docker compose files:\n\n```yml\nversion: \"3.7\"\nservices:\n  app:\n    image: kmallea/csgo\n    secrets:\n      - csgo_rcon_password\n    environment:\n      - RCON_PASSWORD_FILE=/run/secrets/csgo_rcon_password\n\nsecrets:\n  csgo_rcon_password:\n    file: ${SECRETS_DIR}/csgo_rcon_password.txt\n```\n\n### PugSetup ConVars\n\nPugSetup's default configuration can also be controlled via environment variables. Any environment variables prefixed with `SM_PUGSETUP_` will have its corresponding cvar updated inside of `$CSGODIR/csgo/cfg/sourcemod/pugsetup.cfg`.\n\n**NOTE: `pugsetup.cfg` is automatically generated the first time the plugin is loaded. So you may have to restart the container after the first run so that the file exists.**\n\nFor example, if I wanted to enable set the cvars `sm_pugsetup_snake_captain_picks` and `sm_pugsetup_message_prefix`, I would set the following environment variables when starting the container:\n\n```bash\n  ...\n  --env \"SM_PUGSETUP_SNAKE_CAPTAIN_PICKS=2\" \\\n  --env \"SM_PUGSETUP_MESSAGE_PREFIX=[{YELLOW}Sesame Street{NORMAL}]\" \\\n  ...\n```\n\nThis would set these values in `$CSGODIR/csgo/cfg/sourcemod/pugsetup.cfg`:\n\n```bash\n...\nsm_pugsetup_snake_captain_picks \"2\"\nsm_pugsetup_message_prefix \"[{YELLOW}Sesame Street{NORMAL}]\"\n...\n```\n\n### Troubleshooting\n\nIf you're unable to use [`--network=host`](https://docs.docker.com/network/host/), you'll need to publsh the ports instead, e.g.:\n\n```bash\ndocker run \\\n  --rm \\\n  --interactive \\\n  --tty \\\n  --detach \\\n  --mount source=csgo-data,target=/home/steam/csgo \\\n  --publish 27015:27015/tcp \\\n  --publish 27015:27015/udp \\\n  --publish 27020:27020/tcp \\\n  --publish 27020:27020/udp \\\n  --env \"SERVER_HOSTNAME=hostname\" \\\n  --env \"SERVER_PASSWORD=password\" \\\n  --env \"RCON_PASSWORD=rconpassword\" \\\n  --env \"STEAM_ACCOUNT=gamelogintoken\" \\\n  --env \"AUTHKEY=webapikey\" \\\n  --env \"SOURCEMOD_ADMINS=STEAM_1:0:123456,STEAM_1:0:654321\" \\\n  kmallea/csgo\n```\n\n## Manually Building\n\n```bash\ndocker build -t csgo-dedicated-server .\n```\n\n_OR_\n\n```bash\nmake\n```\n\nThe game data is downloaded on first run (~26GB). Mount a volume to preserve game data if you need to recreate the container. The volume's target should be `/home/steam/csgo`. In these example I use a data volume, but you can use a bind volume as well since plugins are installed during container startup.\n\n### Overriding versions of SteamCMD, Metamod, SourceMod, and/or PugSetup\n\n#### SteamCMD\n\nSteamCMD is installed directly into the image at build time. To override the URL it installs from, pass in a build arg named `STEAMCMD_URL`:\n\n```bash\ndocker build \\\n  -t $(IMAGE_NAME) \\\n  --build-arg STEAMCMD_URL=https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz \\\n  .\n```\n\n#### Metamod, SourceMod, PugSetup, Retakes, etc\n\nAll plugins and extensions are installed during the startup of the container. This allows plugins can be managed via an environment variable.\n\nThe environment variable `INSTALL_PLUGINS` contains a space-delimited list of plugins to install. You can use newlines to delimit, they will be converted to spaces before processing. If you override this, make sure you include metamod and sourcemod or plugins that depend on them won't work.\n\n```bash\nINSTALL_PLUGINS=\"${INSTALL_PLUGINS:-https://mms.alliedmods.net/mmsdrop/1.10/mmsource-1.10.7-git971-linux.tar.gz\nhttps://sm.alliedmods.net/smdrop/1.10/sourcemod-1.10.0-git6478-linux.tar.gz\nhttps://github.com/splewis/csgo-pug-setup/releases/download/2.0.5/pugsetup_2.0.5.zip\nhttps://github.com/splewis/csgo-retakes/releases/download/v0.3.4/retakes_0.3.4.zip\nhttps://github.com/b3none/retakes-instadefuse/releases/download/1.4.0/retakes-instadefuse.smx\nhttps://github.com/b3none/retakes-autoplant/releases/download/2.3.0/retakes_autoplant.smx\nhttps://github.com/b3none/retakes-hud/releases/download/2.2.5/retakes-hud.smx\n}\"\n```\n\nLastly, a checksum is generated for each plugin's URL and is stored as `$CSGO_DIR/csgo/<checksum>.marker` to prevent re-downloading plugins that have already been installed.\n\n### Adding your own configs, other files etc.\n\n#### Build time\n\nThe directory `containerfs` (container filesystem) is the equivalent of the steam user's home directory (`/home/steam`). The `csgo` game data lives in here. This means that any files you want to add, simply put them in the correct paths under `containerfs`, and they will appear in the Docker image relative to the steam user's home directory.\n\nIt is recommended to use `INSTALL_PLUGINS` environment variable at run time to install plugins, so that they are decoupled from the image.\n\n#### Run time\n\nSee `INSTALL_PLUGINS` above in the section above to learn about installing plugins.\n\nIf you're using a data volume, you can use the `docker cp` command to copy files from your host machine into the data volume.\n\nIf you're using a bind volume, you can copy files in directly. You may want to clear the `INSTALL_PLUGINS` variable if you want to manage everything manually.\n\n### Test Locally\n\nAfter building:\n\n1. Edit the exported environment variables in the `Makefile` to your liking\n2. Run `make server` to start a local LAN server to test\n3. Run `make test` to run tests\n"
  },
  {
    "path": "cloud-config.yaml",
    "content": "#cloud-config\n\n# This config is an example of of a one-liner to getting a CSGO server up-and-running\n# in just a few minutes on Google Compute Engine with the following one-liner that consumes\n# the uncommented parts of this file:\n#\n# gcloud compute instances create csgo-server \\\n#     --project=$PROJECT \\\n#     --zone=$ZONE \\\n#     --image-family=cos-stable \\\n#     --image-project=cos-cloud \\\n#     --boot-disk-size=50GB \\\n#     --machine-type=c2-standard-4 \\\n#     --network=default \\\n#     --metadata-from-file user-data=$(PWD)/cloud-config.yaml\n\nwrite_files:\n- path: /etc/systemd/system/dynamic-dns.service\n  permissions: 0644\n  owner: root\n  content: |\n    # /etc/systemd/system/dynamic-dns.service\n    [Unit]\n    Description=Updates dynamic DNS record\n    Wants=dynamic-dns.timer\n\n    [Service]\n    ExecStart=/bin/sh -c '(\\\n                export PUBLIC_IP=$$(\\\n                  /usr/bin/curl \\\n                    -s \\\n                    -H \"Metadata-Flavor: Google\" \\\n                    https://domains.google.com/checkip \\\n                ) && \\\n                /usr/bin/curl \\\n                  -s \\\n                  --user <username>:<password> \\\n                  \"https://domains.google.com/nic/update?hostname=<hostname>&myip=$${PUBLIC_IP}\" \\\n              )'\n\n- path: /etc/systemd/system/dynamic-dns.timer\n  permissions: 0644\n  owner: root\n  content: |\n    # /etc/systemd/system/dynamic-dns.timer\n    [Unit]\n    Description=Runs dynamic-dns.service every 15 minutes\n    Requires=dynamic-dns.timer\n\n    [Timer]\n    Unit=dynamic-dns.service\n    OnUnitInactiveSec=15m\n\n- path: /etc/systemd/system/csgods.service\n  permissions: 0644\n  owner: root\n  content: |\n    [Unit]\n    Description=CSGO Dedicated Server Container\n    After=docker.service\n    Requires=docker.service\n\n    [Service]\n    StandardInput=tty-force\n    ExecStartPre=/usr/bin/docker pull kmallea/csgo\n    ExecStart=/usr/bin/docker run --name %n \\\n                                  --interactive \\\n                                  --tty \\\n                                  --rm \\\n                                  --network host \\\n                                  --cpuset-cpus 3 \\\n                                  --mount source=csgo-data,target=/home/steam/csgo \\\n                                  -e \"SERVER_HOSTNAME=Counter-Strike: Global Offensive Dedicated Server\" \\\n                                  -e \"SERVER_PASSWORD=\" \\\n                                  -e \"RCON_PASSWORD=changeme\" \\\n                                  -e \"STEAM_ACCOUNT=changeme\" \\\n                                  -e \"SOURCEMOD_ADMINS=STEAM_1:0:123456,STEAM_1:1:654321\" \\\n                                  -e \"AUTHKEY=changeme\" \\\n                                  -e \"FPS_MAX=1000\" \\\n                                  kmallea/csgo\n    ExecStop=-/usr/bin/docker stop %n\n    ExecStopPost=-/usr/bin/docker rm %n\n\nruncmd:\n- iptables -w -A INPUT -p tcp --dport 27015 -j ACCEPT\n- iptables -w -A INPUT -p udp --dport 27015 -j ACCEPT\n- iptables -w -A INPUT -p tcp --dport 27020 -j ACCEPT\n- iptables -w -A INPUT -p udp --dport 27020 -j ACCEPT\n- iptables -w -A INPUT -p udp --dport 27005 -j ACCEPT\n- iptables -w -A INPUT -p udp --dport 51840 -j ACCEPT\n- iptables -w -A INPUT -p tcp --dport 26900 -j ACCEPT\n- iptables -w -A INPUT -p tcp --dport 80 -j ACCEPT\n- iptables -w -A INPUT -p tcp --dport 443 -j ACCEPT\n- systemctl daemon-reload\n- systemctl enable dynamic-dns.timer\n- systemctl start dynamic-dns.service\n- systemctl start csgods.service\n"
  },
  {
    "path": "containerfs/README.md",
    "content": "## Adding your own files, plugins, etc.\n\nThe directory `containerfs` (container filesystem) is the equivalent of the root CSGO directory (`/home/steam/csgo`). Any files or plugins you want to add to the image, simply put them in the correct paths under `containerfs`, and they will appear in the Docker image relative to the CSGO directory.\n\nFor example, by default, CSGO is installed in the root path `/home/steam/csgo` within the docker image. If I want my `practice.cfg` file to live in the `cfg` directory, I would put that file in `containerfs/csgo/cfg/` and it will appear in the right place inside the docker image: `home/steam/csgo/csgo/cfg/practice.cfg` (Yes, `csgo` appears twice in the path because the CSGO installation has a sub-directory named `csgo`).\n"
  },
  {
    "path": "containerfs/manage_plugins.sh",
    "content": "#!/usr/bin/env bash\n\nset -ueo pipefail\n\n: \"${CSGO_DIR:?'ERROR: CSGO_DIR IS NOT SET!'}\"\n\nexport RETAKES=\"${RETAKES:-0}\"\n\nINSTALL_PLUGINS=\"${INSTALL_PLUGINS:-https://mms.alliedmods.net/mmsdrop/1.11/mmsource-1.11.0-git1148-linux.tar.gz\nhttps://sm.alliedmods.net/smdrop/1.11/sourcemod-1.11.0-git6934-linux.tar.gz\nhttp://users.alliedmods.net/~kyles/builds/SteamWorks/SteamWorks-git131-linux.tar.gz\nhttps://bitbucket.org/GoD_Tony/updater/downloads/updater.smx\nhttps://github.com/splewis/csgo-practice-mode/releases/download/1.3.4/practicemode_1.3.4.zip\nhttps://github.com/splewis/csgo-pug-setup/releases/download/2.0.7/pugsetup_2.0.7.zip\nhttps://github.com/splewis/csgo-retakes/releases/download/v0.3.4/retakes_0.3.4.zip\nhttps://github.com/B3none/retakes-instadefuse/releases/download/1.5.0/retakes-instadefuse.smx\nhttps://github.com/B3none/retakes-autoplant/releases/download/2.3.3/retakes-autoplant.smx\nhttps://github.com/b3none/retakes-hud/releases/download/2.2.5/retakes-hud.smx\n}\"\n\nget_checksum_from_string () {\n  local md5\n  md5=$(echo -n \"$1\" | md5sum | awk '{print $1}')\n  echo \"$md5\"\n}\n\nis_plugin_installed() {\n  local url_hash\n  url_hash=$(get_checksum_from_string \"$1\")\n  if [[ -f \"$CSGO_DIR/csgo/${url_hash}.marker\" ]]; then\n    return 0\n  else\n    return 1\n  fi\n}\n\ncreate_install_marker() {\n  echo \"$1\" > \"$CSGO_DIR/csgo/$(get_checksum_from_string \"$1\").marker\"\n}\n\nfile_url_exists() {\n  if curl --output /dev/null --silent --head --fail \"$1\"; then\n    return 0\n  fi\n  return 1\n}\n\ninstall_plugin() {\n  filename=${1##*/}\n  filename_ext=$(echo \"${1##*.}\" | awk '{print tolower($0)}')\n  if ! file_url_exists \"$1\"; then\n    echo \"Plugin download check FAILED for $filename\";\n    return 0\n  fi\n  if ! is_plugin_installed \"$1\"; then\n    echo \"Downloading $1...\"\n    case \"$filename_ext\" in\n      \"gz\")\n        curl -sSL \"$1\" | tar -zx -C \"$CSGO_DIR/csgo\"\n        echo \"Extracting $filename...\"\n        create_install_marker \"$1\"\n        ;;\n      \"zip\")\n        curl -sSL -o \"$filename\" \"$1\"\n        echo \"Extracting $filename...\"\n        unzip -oq \"$filename\" -d \"$CSGO_DIR/csgo\"\n        rm \"$filename\"\n        create_install_marker \"$1\"\n        ;;\n      \"smx\")\n        (cd \"$CSGO_DIR/csgo/addons/sourcemod/plugins/\" && curl -sSLO \"$1\")\n        create_install_marker \"$1\"\n        ;;\n      *)\n        echo \"Plugin $filename has an unknown file extension, skipping\"\n        ;;\n    esac\n  else\n    echo \"Plugin $filename is already installed, skipping\"\n  fi\n}\n\necho \"Installing plugins...\"\n\nmkdir -p \"$CSGO_DIR/csgo\"\nIFS=' ' read -ra PLUGIN_URLS <<< \"$(echo \"$INSTALL_PLUGINS\" | tr \"\\n\" \" \")\"\nfor URL in \"${PLUGIN_URLS[@]}\"; do\n  install_plugin \"$URL\"\ndone\n\necho \"Finished installing plugins.\"\n\n# Add steam ids to sourcemod admin file\nmkdir -p \"$CSGO_DIR/csgo/addons/sourcemod/configs\"\nIFS=',' read -ra STEAMIDS <<< \"$SOURCEMOD_ADMINS\"\nfor id in \"${STEAMIDS[@]}\"; do\n    echo \"\\\"$id\\\" \\\"99:z\\\"\" >> \"$CSGO_DIR/csgo/addons/sourcemod/configs/admins_simple.ini\"\ndone\n\nPLUGINS_ENABLED_DIR=\"$CSGO_DIR/csgo/addons/sourcemod/plugins\"\nPLUGINS_DISABLED_DIR=\"$CSGO_DIR/csgo/addons/sourcemod/plugins/disabled\"\nRETAKES_PLUGINS=\"retakes.smx retakes-instadefuse.smx retakes-autoplant.smx retakes-hud.smx retakes_standardallocator.smx\"\nPUGSETUP_PLUGINS=\"pugsetup.smx pugsetup_teamnames.smx pugsetup_damageprint.smx\"\n\n# Disable Retakes by default so that we have a working and predictable state without plugins conflict\nif [[ -f \"$PLUGINS_ENABLED_DIR\"/retakes.smx ]]; then\n  mv \"$PLUGINS_ENABLED_DIR\"/retakes*.smx \"$PLUGINS_DISABLED_DIR\"/\nfi\n\nif [ \"$RETAKES\" = \"1\" ]; then\n  if [[ -f \"$PLUGINS_ENABLED_DIR\"/pugsetup.smx ]]; then\n    (cd \"$PLUGINS_ENABLED_DIR\" && mv pugsetup*.smx \"$PLUGINS_DISABLED_DIR\")\n    echo \"Disabled PugSetup plugins\"\n  fi\n  # shellcheck disable=SC2086\n  (cd \"$PLUGINS_DISABLED_DIR\" && mv $RETAKES_PLUGINS \"$PLUGINS_ENABLED_DIR\")\n  echo \"Enabled Retakes plugins\"\nelse\n  if [[ -f \"$PLUGINS_DISABLED_DIR\"/pugsetup.smx ]]; then\n    # shellcheck disable=SC2086\n    (cd \"$PLUGINS_DISABLED_DIR\" && mv $PUGSETUP_PLUGINS \"$PLUGINS_ENABLED_DIR\")\n    echo \"Enabled PugSetup plugins\"\n  fi\nfi\n"
  },
  {
    "path": "containerfs/manage_pugsetup_configs.sh",
    "content": "#!/usr/bin/env bash\n\nset -ueo pipefail\n\n: \"${CSGO_DIR:?'ERROR: CSGO_DIR IS NOT SET!'}\"\n\nPUGSETUP_CONFIG=\"$CSGO_DIR/csgo/cfg/sourcemod/pugsetup/pugsetup.cfg\"\n\nif [[ -f \"$PUGSETUP_CONFIG\" ]]; then\n    # Update PugSetup cvars specified as envvars.\n    # e.g., `SM_PUGSETUP_SNAKE_CAPTAIN_PICKS=2` will set sm_pugsetup_snake_captain_picks \"2\" inside of $PUGSETUP_CONFIG\n    for var in \"${!SM_PUGSETUP_@}\"; do\n        cvar=$(echo \"$var\" | tr '[:upper:]' '[:lower:]')\n        value=${!var}\n        sed -i \"s/$cvar \\\"[^\\]*\\\"/$cvar \\\"$value\\\"/g\" \"$PUGSETUP_CONFIG\"\n    done\nfi\n"
  },
  {
    "path": "containerfs/start.sh",
    "content": "#!/usr/bin/env bash\n \n# These envvars should've been set by the Dockerfile\n# If they're not set then something went wrong during the build\n: \"${STEAM_DIR:?'ERROR: STEAM_DIR IS NOT SET!'}\"\n: \"${STEAMCMD_DIR:?'ERROR: STEAMCMD_DIR IS NOT SET!'}\"\n: \"${CSGO_APP_ID:?'ERROR: CSGO_APP_ID IS NOT SET!'}\"\n: \"${CSGO_DIR:?'ERROR: CSGO_DIR IS NOT SET!'}\"\n\n# set_env_from_file_or_def VAR [DEFAULT]\n# e.g. set_env_from_file_or_def 'RCON_PASSWORD' 'test'\n# Fills $VAR either with the content of the file with the name $VAR_FILE\n# or with DEFAULT. \n# If $VAR is already set nothing will be changed\n# If both $VAR and $VAR_FILE are set $VAR will keep its value and content\n# of $VAR_FILE will be ignored.\nfunction set_env_from_file_or_def() {\n\tlocal VAR=\"$1\"\n\tlocal FILEVAR=\"${VAR}_FILE\"\n\tlocal DEFAULTVAL=\"${2:-}\"\n\tlocal RETURNVAL=\"$DEFAULTVAL\"\n\n\tif [ \"${!VAR:-}\" ]; then\n\t\tRETURNVAL=\"${!VAR}\"\n\telif [ \"${!FILEVAR:-}\" ]; then\n\t\tRETURNVAL=\"$(< \"${!FILEVAR}\")\"\n\tfi\n\n\texport \"$VAR\"=\"$RETURNVAL\"\n\tunset \"$FILEVAR\"\n}\n\nexport SERVER_HOSTNAME=\"${SERVER_HOSTNAME:-Counter-Strike: Global Offensive Dedicated Server}\"\nset_env_from_file_or_def 'SERVER_PASSWORD'\nset_env_from_file_or_def 'RCON_PASSWORD' 'changeme'\nset_env_from_file_or_def 'STEAM_ACCOUNT' 'changeme'\nset_env_from_file_or_def 'AUTHKEY' 'changeme'\nset_env_from_file_or_def 'IP' '0.0.0.0'\nexport PORT=\"${PORT:-27015}\"\nexport TV_PORT=\"${TV_PORT:-27020}\"\nexport TICKRATE=\"${TICKRATE:-128}\"\nexport FPS_MAX=\"${FPS_MAX:-400}\"\nexport GAME_TYPE=\"${GAME_TYPE:-0}\"\nexport GAME_MODE=\"${GAME_MODE:-1}\"\nexport MAP=\"${MAP:-de_dust2}\"\nexport MAPGROUP=\"${MAPGROUP:-mg_active}\"\nexport HOST_WORKSHOP_COLLECTION=\"${HOST_WORKSHOP_COLLECTION:-}\"\nexport WORKSHOP_START_MAP=\"${WORKSHOP_START_MAP:-}\"\nexport MAXPLAYERS=\"${MAXPLAYERS:-12}\"\nexport TV_ENABLE=\"${TV_ENABLE:-1}\"\nexport LAN=\"${LAN:-0}\"\nset_env_from_file_or_def 'SOURCEMOD_ADMINS'\nexport RETAKES=\"${RETAKES:-0}\"\nexport ANNOUNCEMENT_IP=\"${ANNOUNCEMENT_IP:-}\"\nexport NOMASTER=\"${NOMASTER:-}\"\n\n\n# Create dynamic autoexec config\nmkdir -p \"$CSGO_DIR/csgo/cfg\"\n\nif [ ! -s \"$CSGO_DIR/csgo/cfg/autoexec.cfg\" ]; then\ncat << AUTOEXECCFG > \"$CSGO_DIR/csgo/cfg/autoexec.cfg\"\nlog on\nhostname \"$SERVER_HOSTNAME\"\nrcon_password \"$RCON_PASSWORD\"\nsv_password \"$SERVER_PASSWORD\"\nsv_cheats 0\nexec banned_user.cfg\nexec banned_ip.cfg\nAUTOEXECCFG\n\nelse\nsed -i \"s/^hostname.*/hostname \\\"$SERVER_HOSTNAME\\\"/\" $CSGO_DIR/csgo/cfg/autoexec.cfg\nsed -i \"s/^rcon_password.*/rcon_password \\\"$RCON_PASSWORD\\\"/\" $CSGO_DIR/csgo/cfg/autoexec.cfg\nsed -i \"s/^sv_password.*/sv_password \\\"$SERVER_PASSWORD\\\"/\" $CSGO_DIR/csgo/cfg/autoexec.cfg\n\nfi\n\n# Create dynamic server config\nif [ ! -s \"$CSGO_DIR/csgo/cfg/server.cfg\" ]; then\ncat << SERVERCFG > \"$CSGO_DIR/csgo/cfg/server.cfg\"\ntv_enable $TV_ENABLE\ntv_delaymapchange 1\ntv_delay 30\ntv_deltacache 2\ntv_dispatchmode 1\ntv_maxclients 10\ntv_maxrate 0\ntv_overridemaster 0\ntv_relayvoice 1\ntv_snapshotrate 64\ntv_timeout 60\ntv_transmitall 1\nwriteid\nwriteip\nsv_mincmdrate $TICKRATE\nsv_maxupdaterate $TICKRATE\nsv_minupdaterate $TICKRATE\nSERVERCFG\n\nelse\nsed -i \"s/^tv_enable.*/tv_enable $TV_ENABLE/\" $CSGO_DIR/csgo/cfg/server.cfg\n\nfi\n\n# Attempt to update CSGO before starting the server\n[[ -z ${CI+x} ]] && \"$STEAMCMD_DIR/steamcmd.sh\" +login anonymous +force_install_dir \"$CSGO_DIR\" +app_update \"$CSGO_APP_ID\" +quit\n\n# Install and configure plugins & extensions\n\"$BASH\" \"$STEAM_DIR/manage_plugins.sh\"\n\n# Update PugSetup configuration via environment variables\n\"$BASH\" \"$STEAM_DIR/manage_pugsetup_configs.sh\"\n\nSRCDS_ARGUMENTS=(\n  \"-console\"\n  \"-usercon\"\n  \"-game csgo\"\n  \"-autoupdate\"\n  \"-authkey $AUTHKEY\"\n  \"-steam_dir $STEAMCMD_DIR\"\n  \"-steamcmd_script $STEAM_DIR/autoupdate_script.txt\"\n  \"-tickrate $TICKRATE\"\n  \"-port $PORT\"\n  \"-net_port_try 1\"\n  \"-ip $IP\"\n  \"-maxplayers_override $MAXPLAYERS\"\n  \"+fps_max $FPS_MAX\"\n  \"+game_type $GAME_TYPE\"\n  \"+game_mode $GAME_MODE\"\n  \"+mapgroup $MAPGROUP\"\n  \"+map $MAP\"\n  \"+sv_setsteamaccount\" \"$STEAM_ACCOUNT\"\n  \"+sv_lan $LAN\"\n  \"+tv_port $TV_PORT\"\n)\n\nif [[ -n $HOST_WORKSHOP_COLLECTION ]]; then\n  SRCDS_ARGUMENTS+=(\"+host_workshop_collection $HOST_WORKSHOP_COLLECTION\")\nfi\n\nif [[ -n $WORKSHOP_START_MAP ]]; then\n  SRCDS_ARGUMENTS+=(\"+workshop_start_map $WORKSHOP_START_MAP\")\nfi\n\nif [[ -n $ANNOUNCEMENT_IP ]]; then\n  SRCDS_ARGUMENTS+=(\"+net_public_adr $ANNOUNCEMENT_IP\")\nfi\n\nif [[ $NOMASTER == 1 ]]; then\n  SRCDS_ARGUMENTS+=(\"-nomaster\")\nfi\n\nSRCDS_RUN=\"$CSGO_DIR/srcds_run\"\n\n# Patch srcds_run to fix autoupdates\nif grep -q 'steam.sh' \"$SRCDS_RUN\"; then\n  sed -i 's/steam.sh/steamcmd.sh/' \"$SRCDS_RUN\"\n  echo \"Applied patch to srcds_run to fix autoupdates\"\nfi\n\n# Start the server\nexec \"$BASH\" \"$SRCDS_RUN\" \"${SRCDS_ARGUMENTS[@]}\"\n"
  },
  {
    "path": "docker-compose.yaml",
    "content": "version: \"3.7\"\n\nvolumes:\n  csgo-data:\n    name: csgo-data\n\nservices:\n  csgo:\n    image: kmallea/csgo:latest\n    container_name: csgo-ds\n\n    environment:\n      SERVER_HOSTNAME: \"Counter-Strike: Global Offensive Dedicated Server\"\n      SERVER_PASSWORD:\n      RCON_PASSWORD: changeme\n      STEAM_ACCOUNT: changeme\n      AUTHKEY: changeme\n      SOURCEMOD_ADMINS: comma,delimited,list,of,steam,ids\n      IP: 0.0.0.0\n      PORT: 27015\n      TV_PORT: 27020\n      TICKRATE: 128\n      FPS_MAX: 300\n      GAME_TYPE: 0\n      GAME_MODE: 1\n      MAP: de_dust2\n      MAPGROUP: mg_active\n      MAXPLAYERS: 12\n      TV_ENABLE: 1\n      LAN: 0\n      RETAKES: 0\n\n    volumes:\n      - type: volume\n        source: csgo-data\n        target: /home/steam/csgo\n\n    network_mode: \"host\"\n\n    restart: unless-stopped\n    stdin_open: true\n    tty: true\n"
  },
  {
    "path": "test/bin/bats",
    "content": "#!/usr/bin/env bash\n\nset -e\n\nBATS_READLINK='true'\nif command -v 'greadlink' >/dev/null; then\n  BATS_READLINK='greadlink'\nelif command -v 'readlink' >/dev/null; then\n  BATS_READLINK='readlink'\nfi\n\nbats_resolve_absolute_root_dir() {\n  local cwd=\"$PWD\"\n  local path=\"$1\"\n  local result=\"$2\"\n  local target_dir\n  local target_name\n  local original_shell_options=\"$-\"\n\n  # Resolve the parent directory, e.g. /bin => /usr/bin on CentOS (#113).\n  set -P\n\n  while true; do\n    target_dir=\"${path%/*}\"\n    target_name=\"${path##*/}\"\n\n    if [[ \"$target_dir\" != \"$path\" ]]; then\n      cd \"$target_dir\"\n    fi\n\n    if [[ -L \"$target_name\" ]]; then\n      path=\"$(\"$BATS_READLINK\" \"$target_name\")\"\n    else\n      printf -v \"$result\" -- '%s' \"${PWD%/*}\"\n      set +P \"-$original_shell_options\"\n      cd \"$cwd\"\n      return\n    fi\n  done\n}\n\nexport BATS_ROOT\nbats_resolve_absolute_root_dir \"$0\" 'BATS_ROOT'\nexec \"$BATS_ROOT/libexec/bats-core/bats\" \"$@\"\n"
  },
  {
    "path": "test/csgo/cfg/sourcemod/pugsetup/pugsetup.cfg",
    "content": "sm_pugsetup_snake_captain_picks \"0\"\n"
  },
  {
    "path": "test/libexec/bats-core/bats",
    "content": "#!/usr/bin/env bash\nset -e\n\nexport BATS_VERSION='1.2.0-dev'\n\nversion() {\n  printf 'Bats %s\\n' \"$BATS_VERSION\"\n}\n\nabort() {\n  printf 'Error: %s\\n' \"$1\" >&2\n  usage >&2\n  exit 1\n}\n\nusage() {\n  local cmd=\"${0##*/}\"\n  local line\n\n  while IFS= read -r line; do\n    printf '%s\\n' \"$line\"\n  done <<END_OF_HELP_TEXT\nUsage: $cmd [-cr] [-f <regex>] [-j <jobs>] [-p | -t] <test>...\n       $cmd [-h | -v]\n\n  <test> is the path to a Bats test file, or the path to a directory\n  containing Bats test files (ending with \".bats\").\n\n  -c, --count      Count the number of test cases without running any tests\n  -f, --filter     Filter test cases by names matching the regular expression\n  -h, --help       Display this help message\n  -j, --jobs       Number of parallel jobs to run (requires GNU parallel)\n  -p, --pretty     Show results in pretty format (default for terminals)\n  -r, --recursive  Include tests in subdirectories\n  -t, --tap        Show results in TAP format\n  -v, --version    Display the version number\n\n  For more information, see https://github.com/bats-core/bats-core\n\nEND_OF_HELP_TEXT\n}\n\nexpand_link() {\n  readlink=\"$(type -p greadlink readlink | head -1)\"\n  \"$readlink\" -f \"$1\"\n}\n\nexpand_path() {\n  local path=\"${1%/}\"\n  local dirname=\"${path%/*}\"\n  local result=\"$2\"\n\n  if [[ \"$dirname\" == \"$path\" ]]; then\n    dirname=\"$PWD\"\n  else\n    cd \"$dirname\"\n    dirname=\"$PWD\"\n    cd \"$OLDPWD\"\n  fi\n  printf -v \"$result\" '%s/%s' \"$dirname\" \"${path##*/}\"\n}\n\nBATS_LIBEXEC=\"$(dirname \"$(expand_link \"${BASH_SOURCE[0]}\")\")\"\nexport BATS_CWD=\"$PWD\"\nexport BATS_TEST_PATTERN=\"^[[:blank:]]*@test[[:blank:]]+(.*[^[:blank:]])[[:blank:]]+\\{(.*)\\$\"\nexport BATS_TEST_FILTER=\nexport PATH=\"$BATS_LIBEXEC:$PATH\"\n\narguments=()\n\n# Unpack single-character options bundled together, e.g. -cr, -pr.\nfor arg in \"$@\"; do\n  if [[ \"$arg\" =~ ^-[^-]. ]]; then\n    index=1\n    while option=\"${arg:$((index++)):1}\"; do\n      if [[ -z \"$option\" ]]; then\n        break\n      fi\n      arguments+=(\"-$option\")\n    done\n  else\n    arguments+=(\"$arg\")\n  fi\n  shift\ndone\n\nset -- \"${arguments[@]}\"\narguments=()\n\nunset flags pretty recursive\nflags=()\npretty=\nrecursive=\nif [[ -z \"${CI:-}\" && -t 0 && -t 1 ]] && command -v tput >/dev/null; then\n  pretty=1\nfi\n\nwhile [[ \"$#\" -ne 0 ]]; do\n  case \"$1\" in\n  -h|--help)\n    version\n    usage\n    exit 0\n    ;;\n  -v|--version)\n    version\n    exit 0\n    ;;\n  -c|--count)\n    flags+=('-c')\n    ;;\n  -f|--filter)\n    shift\n    flags+=('-f' \"$1\")\n    ;;\n  -j|--jobs)\n    shift\n    flags+=('-j' \"$1\")\n    ;;\n  -r|--recursive)\n    recursive=1\n    ;;\n  -t|--tap)\n    pretty=\n    ;;\n  -p|--pretty)\n    pretty=1\n    ;;\n  -*)\n    abort \"Bad command line option '$1'\"\n    ;;\n  *)\n    arguments+=(\"$1\")\n    ;;\n  esac\n  shift\ndone\n\nif [[ \"${#arguments[@]}\" -eq 0 ]]; then\n  abort 'Must specify at least one <test>'\nfi\n\nfilenames=()\nfor filename in \"${arguments[@]}\"; do\n  expand_path \"$filename\" 'filename'\n\n  if [[ -d \"$filename\" ]]; then\n    shopt -s nullglob\n    if [[ \"$recursive\" -eq 1 ]]; then\n      while IFS= read -r -d $'\\0' file; do\n        filenames+=(\"$file\")\n      done < <(find \"$filename\" -type f -name '*.bats' -print0 | sort -z)\n    else\n      for suite_filename in \"$filename\"/*.bats; do\n        filenames+=(\"$suite_filename\")\n      done\n    fi\n    shopt -u nullglob\n  else\n    filenames+=(\"$filename\")\n  fi\ndone\n\nformatter=\"cat\"\nif [[ -n \"$pretty\" ]]; then\n  flags+=(\"-x\")\n  formatter=\"bats-format-tap-stream\"\nfi\n\nset -o pipefail execfail\nexec bats-exec-suite \"${flags[@]}\" \"${filenames[@]}\" | \"$formatter\"\n"
  },
  {
    "path": "test/libexec/bats-core/bats-exec-suite",
    "content": "#!/usr/bin/env bash\nset -e\n\ncount_only_flag=''\nextended_syntax_flag=''\nfilter=''\nnum_jobs=1\nhave_gnu_parallel=\nflags=()\n\nwhile [[ \"$#\" -ne 0 ]]; do\n  case \"$1\" in\n  -c)\n    count_only_flag=1\n    ;;\n  -f)\n    shift\n    filter=\"$1\"\n    flags+=('-f' \"$filter\")\n    ;;\n  -j)\n    shift\n    num_jobs=\"$1\"\n    ;;\n  -x)\n    # shellcheck disable=SC2034\n    extended_syntax_flag='-x'\n    flags+=('-x')\n    ;;\n  *)\n    break\n    ;;\n  esac\n  shift\ndone\n\nif ( type -p parallel &>/dev/null ); then\n  # shellcheck disable=SC2034\n  have_gnu_parallel=1\nelif [[ \"$num_jobs\" != 1 ]]; then\n  printf 'bats: cannot execute \"%s\" jobs without GNU parallel\\n' \"$num_jobs\" >&2\n  exit 1\nfi\n\ntrap 'kill 0; exit 1' INT\n\nall_tests=()\nfor filename in \"$@\"; do\n  if  [[ ! -f \"$filename\" ]]; then\n    printf 'bats: %s does not exist\\n' \"$filename\" >&2\n    exit 1\n  fi\n\n  test_names=()\n  test_dupes=()\n  while read -r line; do\n    if [[ ! \"$line\" =~ ^bats_test_function\\  ]]; then\n      continue\n    fi\n    line=\"${line%$'\\r'}\"\n    line=\"${line#* }\"\n\n    all_tests+=( \"$(printf \"%s\\t%s\" \"$filename\" \"$line\")\" )\n    if [[ \" ${test_names[*]} \" == *\" $line \"* ]]; then\n      test_dupes+=(\"$line\")\n      continue\n    fi\n    test_names+=(\"$line\")\n  done < <(BATS_TEST_FILTER=\"$filter\" bats-preprocess \"$filename\")\n\n  if [[ \"${#test_dupes[@]}\" -ne 0 ]]; then\n    printf 'bats warning: duplicate test name(s) in %s: %s\\n' \"$filename\" \"${test_dupes[*]}\" >&2\n  fi\ndone\n\ntest_count=\"${#all_tests[@]}\"\n\nif [[ -n \"$count_only_flag\" ]]; then\n  printf '%d\\n' \"${test_count}\"\n  exit\nfi\n\nstatus=0\nprintf '1..%d\\n' \"${test_count}\"\n\n# No point on continuing if there's no tests.\nif [[ \"${test_count}\" == 0 ]]; then\n  exit\nfi\n\nif [[ \"$num_jobs\" != 1 ]]; then\n  # Only use GNU parallel when we want parallel execution -- there is a small\n  # amount of overhead using it over a simple loop in the serial case.\n  set -o pipefail\n  printf '%s\\n' \"${all_tests[@]}\" | grep -v '^$' | \\\n    parallel -qk -j \"$num_jobs\" --colsep=\"\\t\" -- bats-exec-test \"${flags[@]}\" '{1}' '{2}' '{#}' || status=1\nelse\n  # Just do it serially.\n  test_number=0\n  for test_line in \"${all_tests[@]}\"; do\n    # Only handle non-empty lines\n    if [[ $test_line ]]; then\n      filename=\"${test_line%%$'\\t'*}\"\n      test_name=\"${test_line##*$'\\t'}\"\n      ((++test_number))\n      bats-exec-test \"${flags[@]}\" \"$filename\" \"$test_name\" \"$test_number\" || status=1\n    fi\n  done\n  if [[ \"${test_number}\" != \"${test_count}\" ]]; then\n    printf '# bats warning: Only executed %s of %s tests\\n' \"$test_number\" \"$test_count\"\n    status=1\n  fi\nfi\nexit \"$status\"\n"
  },
  {
    "path": "test/libexec/bats-core/bats-exec-test",
    "content": "#!/usr/bin/env bash\nset -eET\n\n# Variables used in other scripts.\nBATS_COUNT_ONLY=''\nBATS_TEST_FILTER=''\nBATS_EXTENDED_SYNTAX=''\n\nwhile [[ \"$#\" -ne 0 ]]; do\n  case \"$1\" in\n  -c)\n    # shellcheck disable=SC2034\n    BATS_COUNT_ONLY=1\n    ;;\n  -f)\n    shift\n    # shellcheck disable=SC2034\n    BATS_TEST_FILTER=\"$1\"\n    ;;\n  -x)\n    BATS_EXTENDED_SYNTAX='-x'\n    ;;\n  *)\n    break\n    ;;\n  esac\n  shift\ndone\n\nBATS_TEST_FILENAME=\"$1\"\nshift\nif [[ -z \"$BATS_TEST_FILENAME\" ]]; then\n  printf 'usage: bats-exec-test <filename>\\n' >&2\n  exit 1\nelif [[ ! -f \"$BATS_TEST_FILENAME\" ]]; then\n  printf 'bats: %s does not exist\\n' \"$BATS_TEST_FILENAME\" >&2\n  exit 1\nfi\n\nBATS_TEST_DIRNAME=\"${BATS_TEST_FILENAME%/*}\"\nBATS_TEST_NAMES=()\n\nload() {\n  local name=\"$1\"\n  local filename\n\n  if [[ \"${name:0:1}\" == '/' ]]; then\n    filename=\"${name}\"\n  else\n    filename=\"$BATS_TEST_DIRNAME/${name}.bash\"\n  fi\n\n  if [[ ! -f \"$filename\" ]]; then\n    printf 'bats: %s does not exist\\n' \"$filename\" >&2\n    exit 1\n  fi\n\n  # Dynamically loaded user files provided outside of Bats.\n  # shellcheck disable=SC1090\n  source \"${filename}\"\n}\n\nrun() {\n  local origFlags=\"$-\"\n  set +eET\n  local origIFS=\"$IFS\"\n  # 'output', 'status', 'lines' are global variables available to tests.\n  # shellcheck disable=SC2034\n  output=\"$(\"$@\" 2>&1)\"\n  # shellcheck disable=SC2034\n  status=\"$?\"\n  # shellcheck disable=SC2034,SC2206\n  IFS=$'\\n' lines=($output)\n  IFS=\"$origIFS\"\n  set \"-$origFlags\"\n}\n\nsetup() {\n  return 0\n}\n\nteardown() {\n  return 0\n}\n\nskip() {\n  BATS_TEST_SKIPPED=\"${1:-1}\"\n  BATS_TEST_COMPLETED=1\n  exit 0\n}\n\nbats_test_begin() {\n  BATS_TEST_DESCRIPTION=\"$1\"\n  if [[ -n \"$BATS_EXTENDED_SYNTAX\" ]]; then\n    printf 'begin %d %s\\n' \"$BATS_TEST_NUMBER\" \"$BATS_TEST_DESCRIPTION\" >&3\n  fi\n  setup\n}\n\nbats_test_function() {\n  local test_name=\"$1\"\n  BATS_TEST_NAMES+=(\"$test_name\")\n}\n\nbats_capture_stack_trace() {\n  local test_file\n  local funcname\n  local i\n\n  BATS_STACK_TRACE=()\n\n  for ((i=2; i != ${#FUNCNAME[@]}; ++i)); do\n    # Use BATS_TEST_SOURCE if necessary to work around Bash < 4.4 bug whereby\n    # calling an exported function erases the test file's BASH_SOURCE entry.\n    test_file=\"${BASH_SOURCE[$i]:-$BATS_TEST_SOURCE}\"\n    funcname=\"${FUNCNAME[$i]}\"\n    BATS_STACK_TRACE+=(\"${BASH_LINENO[$((i-1))]} $funcname $test_file\")\n    if [[ \"$test_file\" == \"$BATS_TEST_SOURCE\" ]]; then\n      case \"$funcname\" in\n      \"$BATS_TEST_NAME\"|setup|teardown)\n        break\n        ;;\n      esac\n    fi\n  done\n}\n\nbats_print_stack_trace() {\n  local frame\n  local index=1\n  local count=\"${#@}\"\n  local filename\n  local lineno\n\n  for frame in \"$@\"; do\n    bats_frame_filename \"$frame\" 'filename'\n    bats_trim_filename \"$filename\" 'filename'\n    bats_frame_lineno \"$frame\" 'lineno'\n\n    if [[ $index -eq 1 ]]; then\n      printf '# ('\n    else\n      printf '#  '\n    fi\n\n    local fn\n    bats_frame_function \"$frame\" 'fn'\n    if [[ \"$fn\" != \"$BATS_TEST_NAME\" ]]; then\n      printf \"from function \\`%s' \" \"$fn\"\n    fi\n\n    if [[ $index -eq $count ]]; then\n      printf 'in test file %s, line %d)\\n' \"$filename\" \"$lineno\"\n    else\n      printf 'in file %s, line %d,\\n' \"$filename\" \"$lineno\"\n    fi\n\n    ((++index))\n  done\n}\n\nbats_print_failed_command() {\n  local frame=\"${BATS_STACK_TRACE[${#BATS_STACK_TRACE[@]}-1]}\"\n  local filename\n  local lineno\n  local failed_line\n  local failed_command\n\n  bats_frame_filename \"$frame\" 'filename'\n  bats_frame_lineno \"$frame\" 'lineno'\n  bats_extract_line \"$filename\" \"$lineno\" 'failed_line'\n  bats_strip_string \"$failed_line\" 'failed_command'\n  printf '%s' \"#   \\`${failed_command}' \"\n\n  if [[ \"$BATS_ERROR_STATUS\" -eq 1 ]]; then\n    printf 'failed\\n'\n  else\n    printf 'failed with status %d\\n' \"$BATS_ERROR_STATUS\"\n  fi\n}\n\nbats_frame_lineno() {\n  printf -v \"$2\" '%s' \"${1%% *}\"\n}\n\nbats_frame_function() {\n  local __bff_function=\"${1#* }\"\n  printf -v \"$2\" '%s' \"${__bff_function%% *}\"\n}\n\nbats_frame_filename() {\n  local __bff_filename=\"${1#* }\"\n  __bff_filename=\"${__bff_filename#* }\"\n\n  if [[ \"$__bff_filename\" == \"$BATS_TEST_SOURCE\" ]]; then\n    __bff_filename=\"$BATS_TEST_FILENAME\"\n  fi\n  printf -v \"$2\" '%s' \"$__bff_filename\"\n}\n\nbats_extract_line() {\n  local __bats_extract_line_line\n  local __bats_extract_line_index=0\n\n  while IFS= read -r __bats_extract_line_line; do\n    if [[ \"$((++__bats_extract_line_index))\" -eq \"$2\" ]]; then\n      printf -v \"$3\" '%s' \"${__bats_extract_line_line%$'\\r'}\"\n      break\n    fi\n  done <\"$1\"\n}\n\nbats_strip_string() {\n  [[ \"$1\" =~ ^[[:space:]]*(.*)[[:space:]]*$ ]]\n  printf -v \"$2\" '%s' \"${BASH_REMATCH[1]}\"\n}\n\nbats_trim_filename() {\n  printf -v \"$2\" '%s' \"${1#$BATS_CWD/}\"\n}\n\nbats_debug_trap() {\n  if [[ \"${BASH_SOURCE[0]}\" != \"$1\" ]]; then\n    # The last entry in the stack trace is not useful when en error occured:\n    # It is either duplicated (kinda correct) or has wrong line number (Bash < 4.4)\n    # Therefore we capture the stacktrace but use it only after the next debug\n    # trap fired.\n    # Expansion is required for empty arrays which otherwise error\n    BATS_CURRENT_STACK_TRACE=( \"${BATS_STACK_TRACE[@]+\"${BATS_STACK_TRACE[@]}\"}\" )\n    bats_capture_stack_trace\n  fi\n}\n\n# For some versions of Bash, the `ERR` trap may not always fire for every\n# command failure, but the `EXIT` trap will. Also, some command failures may not\n# set `$?` properly. See #72 and #81 for details.\n#\n# For this reason, we call `bats_error_trap` at the very beginning of\n# `bats_teardown_trap` (the `DEBUG` trap for the call will fix the stack trace)\n# and check the value of `$BATS_TEST_COMPLETED` before taking other actions.\n# We also adjust the exit status value if needed.\n#\n# See `bats_exit_trap` for an additional EXIT error handling case when `$?`\n# isn't set properly during `teardown()` errors.\nbats_error_trap() {\n  local status=\"$?\"\n  if [[ -z \"$BATS_TEST_COMPLETED\" ]]; then\n    BATS_ERROR_STATUS=\"${BATS_ERROR_STATUS:-$status}\"\n    if [[ \"$BATS_ERROR_STATUS\" -eq 0 ]]; then\n      BATS_ERROR_STATUS=1\n    fi\n    BATS_STACK_TRACE=( \"${BATS_CURRENT_STACK_TRACE[@]}\" )\n    trap - DEBUG\n  fi\n}\n\nbats_teardown_trap() {\n  bats_error_trap\n  local status=0\n  teardown >>\"$BATS_OUT\" 2>&1 || status=\"$?\"\n\n  if [[ $status -eq 0 ]]; then\n    BATS_TEARDOWN_COMPLETED=1\n  elif [[ -n \"$BATS_TEST_COMPLETED\" ]]; then\n    BATS_ERROR_STATUS=\"$status\"\n  fi\n\n  bats_exit_trap\n}\n\nbats_exit_trap() {\n  local line\n  local status\n  local skipped=''\n  trap - ERR EXIT\n\n  if [[ -n \"$BATS_TEST_SKIPPED\" ]]; then\n    skipped=' # skip'\n    if [[ \"$BATS_TEST_SKIPPED\" != '1' ]]; then\n      skipped+=\" $BATS_TEST_SKIPPED\"\n    fi\n  fi\n\n  if [[ -z \"$BATS_TEST_COMPLETED\" || -z \"$BATS_TEARDOWN_COMPLETED\" ]]; then\n    if [[ \"$BATS_ERROR_STATUS\" -eq 0 ]]; then\n      # For some versions of bash, `$?` may not be set properly for some error\n      # conditions before triggering the EXIT trap directly (see #72 and #81).\n      # Thanks to the `BATS_TEARDOWN_COMPLETED` signal, this will pinpoint such\n      # errors if they happen during `teardown()` when `bats_perform_test` calls\n      # `bats_teardown_trap` directly after the test itself passes.\n      #\n      # If instead the test fails, and the `teardown()` error happens while\n      # `bats_teardown_trap` runs as the EXIT trap, the test will fail with no\n      # output, since there's no way to reach the `bats_exit_trap` call.\n      BATS_STACK_TRACE=( \"${BATS_CURRENT_STACK_TRACE[@]}\" )\n      BATS_ERROR_STATUS=1\n    fi\n    printf 'not ok %d %s\\n' \"$BATS_TEST_NUMBER\" \"$BATS_TEST_DESCRIPTION\" >&3\n    bats_print_stack_trace \"${BATS_STACK_TRACE[@]}\" >&3\n    bats_print_failed_command >&3\n\n    while IFS= read -r line; do\n      printf '# %s\\n' \"$line\"\n    done <\"$BATS_OUT\" >&3\n    if [[ -n \"$line\" ]]; then\n      printf '# %s\\n' \"$line\"\n    fi\n    status=1\n  else\n    printf 'ok %d %s%s\\n' \"$BATS_TEST_NUMBER\" \"$BATS_TEST_DESCRIPTION\" \\\n      \"$skipped\" >&3\n    status=0\n  fi\n\n  rm -f \"$BATS_OUT\"\n  bats_cleanup_preprocessed_source\n  exit \"$status\"\n}\n\nbats_perform_test() {\n  BATS_TEST_NAME=\"$1\"\n  BATS_TEST_NUMBER=\"$2\"\n\n  if ! declare -F \"$BATS_TEST_NAME\" &>/dev/null; then\n    printf \"bats: unknown test name \\`%s'\\n\" \"$BATS_TEST_NAME\" >&2\n    exit 1\n  fi\n\n  # Some versions of Bash will reset BASH_LINENO to the first line of the\n  # function when the ERR trap fires. All versions of Bash appear to reset it\n  # on an unbound variable access error. bats_debug_trap will fire both before\n  # the offending line is executed, and when the error is triggered.\n  # Consequently, we use `BATS_CURRENT_STACK_TRACE` recorded by the\n  # first call to bats_debug_trap, _before_ the ERR trap or unbound variable\n  # access fires.\n  BATS_STACK_TRACE=()\n  BATS_CURRENT_STACK_TRACE=()\n\n  BATS_TEST_COMPLETED=\n  BATS_TEST_SKIPPED=\n  BATS_TEARDOWN_COMPLETED=\n  BATS_ERROR_STATUS=\n  trap 'bats_debug_trap \"$BASH_SOURCE\"' DEBUG\n  trap 'bats_error_trap' ERR\n  trap 'bats_teardown_trap' EXIT\n  \"$BATS_TEST_NAME\" >>\"$BATS_OUT\" 2>&1\n  BATS_TEST_COMPLETED=1\n  trap 'bats_exit_trap' EXIT\n  bats_teardown_trap\n}\n\nif [[ -z \"$TMPDIR\" ]]; then\n  BATS_TMPDIR='/tmp'\nelse\n  BATS_TMPDIR=\"${TMPDIR%/}\"\nfi\n\nBATS_TMPNAME=\"$BATS_TMPDIR/bats.$$\"\nBATS_PARENT_TMPNAME=\"$BATS_TMPDIR/bats.$PPID\"\nBATS_OUT=\"${BATS_TMPNAME}.out\"\n\nbats_preprocess_source() {\n  BATS_TEST_SOURCE=\"${BATS_TMPNAME}.src\"\n  bats-preprocess \"$BATS_TEST_FILENAME\" >\"$BATS_TEST_SOURCE\"\n  trap 'bats_cleanup_preprocessed_source' ERR EXIT\n  trap 'bats_cleanup_preprocessed_source; exit 1' INT\n}\n\nbats_cleanup_preprocessed_source() {\n  rm -f \"$BATS_TEST_SOURCE\"\n}\n\nbats_evaluate_preprocessed_source() {\n  if [[ -z \"$BATS_TEST_SOURCE\" ]]; then\n    BATS_TEST_SOURCE=\"${BATS_PARENT_TMPNAME}.src\"\n  fi\n  # Dynamically loaded user files provided outside of Bats.\n  # shellcheck disable=SC1090\n  source \"$BATS_TEST_SOURCE\"\n}\n\nexec 3<&1\n\n# Run the given test.\nbats_preprocess_source\nbats_evaluate_preprocessed_source\nbats_perform_test \"$@\"\n"
  },
  {
    "path": "test/libexec/bats-core/bats-format-tap-stream",
    "content": "#!/usr/bin/env bash\nset -e\n\nheader_pattern='[0-9]+\\.\\.[0-9]+'\nIFS= read -r header\n\nif [[ \"$header\" =~ $header_pattern ]]; then\n  count=\"${header:3}\"\n  index=0\n  passed=0\n  failures=0\n  skipped=0\n  name=\n  count_column_width=$(( ${#count} * 2 + 2 ))\nelse\n  # If the first line isn't a TAP plan, print it and pass the rest through\n  printf '%s\\n' \"$header\"\n  exec cat\nfi\n\nupdate_screen_width() {\n  screen_width=\"$(tput cols)\"\n  count_column_left=$(( screen_width - count_column_width ))\n}\n\ntrap update_screen_width WINCH\nupdate_screen_width\n\nbegin() {\n  go_to_column 0\n  buffer_with_truncation $(( count_column_left - 1 )) '   %s' \"$name\"\n  clear_to_end_of_line\n  go_to_column $count_column_left\n  buffer \"%${#count}s/${count}\" \"$index\"\n  go_to_column 1\n}\n\npass() {\n  go_to_column 0\n  buffer ' ✓ %s' \"$name\"\n  advance\n}\n\nskip() {\n  local reason=\"$1\"\n  if [[ -n \"$reason\" ]]; then\n    reason=\": $reason\"\n  fi\n  go_to_column 0\n  buffer ' - %s (skipped%s)' \"$name\" \"$reason\"\n  advance\n}\n\nfail() {\n  go_to_column 0\n  set_color 1 bold\n  buffer ' ✗ %s' \"$name\"\n  advance\n}\n\nlog() {\n  set_color 1\n  buffer '   %s\\n' \"$1\"\n  clear_color\n}\n\nsummary() {\n  buffer '\\n%d test' \"$count\"\n  if [[ \"$count\" -ne 1 ]]; then\n    buffer 's'\n  fi\n\n  buffer ', %d failure' \"$failures\"\n  if [[ \"$failures\" -ne 1 ]]; then\n    buffer 's'\n  fi\n\n  if [[ \"$skipped\" -gt 0 ]]; then\n    buffer ', %d skipped' \"$skipped\"\n  fi\n\n  not_run=$((count - passed - failures - skipped))\n  if [[ \"$not_run\" -gt 0 ]]; then\n    buffer ', %d not run' \"$not_run\"\n  fi\n\n  buffer '\\n'\n}\n\nbuffer_with_truncation() {\n  local width=\"$1\"\n  shift\n  local string\n\n  # shellcheck disable=SC2059\n  printf -v 'string' -- \"$@\"\n\n  if [[ \"${#string}\" -gt \"$width\" ]]; then\n    buffer '%s...' \"${string:0:$(( width - 4 ))}\"\n  else\n    buffer '%s' \"$string\"\n  fi\n}\n\ngo_to_column() {\n  local column=\"$1\"\n  buffer '\\x1B[%dG' $(( column + 1 ))\n}\n\nclear_to_end_of_line() {\n  buffer '\\x1B[K'\n}\n\nadvance() {\n  clear_to_end_of_line\n  buffer '\\n'\n  clear_color\n}\n\nset_color() {\n  local color=\"$1\"\n  local weight=22\n\n  if [[ \"$2\" == 'bold' ]]; then\n    weight=1\n  fi\n  buffer '\\x1B[%d;%dm' \"$(( 30 + color ))\" \"$weight\"\n}\n\nclear_color() {\n  buffer '\\x1B[0m'\n}\n\n_buffer=\n\nbuffer() {\n  local content\n  # shellcheck disable=SC2059\n  printf -v content -- \"$@\"\n  _buffer+=\"$content\"\n}\n\nflush() {\n  printf '%s' \"$_buffer\"\n  _buffer=\n}\n\nfinish() {\n  flush\n  printf '\\n'\n}\n\ntrap finish EXIT\n\nwhile IFS= read -r line; do\n  case \"$line\" in\n  'begin '* )\n    ((++index))\n    name=\"${line#* $index }\"\n    begin\n    flush\n    ;;\n  'ok '* )\n    skip_expr=\"ok $index (.*) # skip ?(([[:print:]]*))?\"\n    if [[ \"$line\" =~ $skip_expr ]]; then\n      ((++skipped))\n      skip \"${BASH_REMATCH[2]}\"\n    else\n      ((++passed))\n      pass\n    fi\n    ;;\n  'not ok '* )\n    ((++failures))\n    fail\n    ;;\n  '# '* )\n    log \"${line:2}\"\n    ;;\n  esac\ndone\n\nsummary\n"
  },
  {
    "path": "test/libexec/bats-core/bats-preprocess",
    "content": "#!/usr/bin/env bash\nset -e\n\nbats_encode_test_name() {\n  local name=\"$1\"\n  local result='test_'\n  local hex_code\n\n  if [[ ! \"$name\" =~ [^[:alnum:]\\ _-] ]]; then\n    name=\"${name//_/-5f}\"\n    name=\"${name//-/-2d}\"\n    name=\"${name// /_}\"\n    result+=\"$name\"\n  else\n    local length=\"${#name}\"\n    local char i\n\n    for ((i=0; i<length; i++)); do\n      char=\"${name:$i:1}\"\n      if [[ \"$char\" == ' ' ]]; then\n        result+='_'\n      elif [[ \"$char\" =~ [[:alnum:]] ]]; then\n        result+=\"$char\"\n      else\n        printf -v 'hex_code' -- '-%02x' \\'\"$char\"\n        result+=\"$hex_code\"\n      fi\n    done\n  fi\n\n  printf -v \"$2\" '%s' \"$result\"\n}\n\ntest_file=\"$1\"\ntests=()\n\n{\n  while IFS= read -r line; do\n    line=\"${line//$'\\r'}\"\n    if [[ \"$line\" =~ $BATS_TEST_PATTERN ]]; then\n      name=\"${BASH_REMATCH[1]#[\\'\\\"]}\"\n      name=\"${name%[\\'\\\"]}\"\n      body=\"${BASH_REMATCH[2]}\"\n      bats_encode_test_name \"$name\" 'encoded_name'\n      printf '%s() { bats_test_begin \"%s\"; %s\\n' \"${encoded_name:?}\" \"$name\" \"$body\" || :\n\n      if [[ -z \"$BATS_TEST_FILTER\" || \"$name\" =~ $BATS_TEST_FILTER ]]; then\n        tests+=(\"$encoded_name\")\n      fi\n    else\n      printf '%s\\n' \"$line\"\n    fi\n  done\n} <<< \"$(< \"$test_file\")\"$'\\n'\n\nfor test_name in \"${tests[@]}\"; do\n  printf 'bats_test_function %s\\n' \"$test_name\"\ndone\n"
  },
  {
    "path": "test/srcds_run",
    "content": "#!/usr/bin/env bash\n\n./bin/bats tests.bats\n"
  },
  {
    "path": "test/tests.bats",
    "content": "#!/home/steam/csgo/bin/bats\n\n@test \"steam script filename is patched to fix autoupdates\" {\n  result=\"steamcmd.sh\"\n  [ \"$result\" == \"steamcmd.sh\" ]\n}\n\n@test \"Metamod is installed\" {\n  run test -f csgo/addons/metamod.vdf\n  [ \"$status\" -eq 0 ]\n}\n\n@test \"Sourcemod is installed\" {\n  run test -f csgo/addons/metamod/sourcemod.vdf\n  [ \"$status\" -eq 0 ]\n}\n\n@test \"SteamWorks is installed\" {\n  run test -f csgo/addons/sourcemod/extensions/SteamWorks.ext.so\n  [ \"$status\" -eq 0 ]\n}\n\n@test \"Updater is installed\" {\n  run test -f csgo/addons/sourcemod/plugins/updater.smx\n  [ \"$status\" -eq 0 ]\n}\n\n@test \"PugSetup is installed\" {\n  run test -f csgo/addons/sourcemod/plugins/pugsetup.smx\n  [ \"$status\" -eq 0 ]\n}\n\n@test \"PracticeMode is installed\" {\n  run test -f csgo/addons/sourcemod/plugins/practicemode.smx\n  [ \"$status\" -eq 0 ]\n}\n\n@test \"Retakes is installed and disabled\" {\n  run test -f csgo/addons/sourcemod/plugins/disabled/retakes.smx\n  [ \"$status\" -eq 0 ]\n}\n\n@test \"Retakes Instadefuse is installed and disabled\" {\n  run test -f csgo/addons/sourcemod/plugins/disabled/retakes-instadefuse.smx\n  [ \"$status\" -eq 0 ]\n}\n\n@test \"Retakes Autoplant is installed and disabled\" {\n  run test -f csgo/addons/sourcemod/plugins/disabled/retakes_autoplant.smx\n  [ \"$status\" -eq 0 ]\n}\n\n@test \"Retakes HUD is installed and disabled\" {\n  run test -f csgo/addons/sourcemod/plugins/disabled/retakes-hud.smx\n  [ \"$status\" -eq 0 ]\n}\n\n@test \"Steam IDs were added to admins list\" {\n  run grep -q 'STEAM_1:0:654321' 'csgo/addons/sourcemod/configs/admins_simple.ini' \n  [ \"$status\" -eq 0 ]\n}\n\n@test \"PugSetup config modified correctly\" {\n  run grep -q 'sm_pugsetup_snake_captain_picks \\\"2\\\"' 'csgo/cfg/sourcemod/pugsetup/pugsetup.cfg' \n  [ \"$status\" -eq 0 ]\n}\n\n@test \"Markers created for caching plugins\" {\n  num_marker_files=( csgo/*.marker )\n  result=\"${#num_marker_files[@]}\"\n  [ \"$result\" -eq 10 ]\n}\n"
  }
]