├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── cloud-config.yaml ├── containerfs ├── README.md ├── manage_plugins.sh ├── manage_pugsetup_configs.sh └── start.sh ├── docker-compose.yaml └── test ├── bin └── bats ├── csgo └── cfg │ └── sourcemod │ └── pugsetup │ └── pugsetup.cfg ├── libexec └── bats-core │ ├── bats │ ├── bats-exec-suite │ ├── bats-exec-test │ ├── bats-format-tap-stream │ └── bats-preprocess ├── srcds_run └── tests.bats /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:bionic 2 | 3 | ENV TERM xterm 4 | 5 | ENV STEAM_DIR /home/steam 6 | ENV STEAMCMD_DIR /home/steam/steamcmd 7 | ENV CSGO_APP_ID 740 8 | ENV CSGO_DIR /home/steam/csgo 9 | 10 | SHELL ["/bin/bash", "-c"] 11 | 12 | ARG STEAMCMD_URL=https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz 13 | 14 | RUN set -xo pipefail \ 15 | && apt-get update \ 16 | && DEBIAN_FRONTEND=noninteractive apt-get install --no-install-recommends --no-install-suggests -y \ 17 | lib32gcc1 \ 18 | lib32stdc++6 \ 19 | lib32z1 \ 20 | ca-certificates \ 21 | net-tools \ 22 | locales \ 23 | curl \ 24 | unzip \ 25 | && locale-gen en_US.UTF-8 \ 26 | && adduser --disabled-password --gecos "" steam \ 27 | && mkdir ${STEAMCMD_DIR} \ 28 | && cd ${STEAMCMD_DIR} \ 29 | && curl -sSL ${STEAMCMD_URL} | tar -zx -C ${STEAMCMD_DIR} \ 30 | && mkdir -p ${STEAM_DIR}/.steam/sdk32 \ 31 | && ln -s ${STEAMCMD_DIR}/linux32/steamclient.so ${STEAM_DIR}/.steam/sdk32/steamclient.so \ 32 | && { \ 33 | echo '@ShutdownOnFailedCommand 1'; \ 34 | echo '@NoPromptForPassword 1'; \ 35 | echo 'login anonymous'; \ 36 | echo 'force_install_dir ${CSGO_DIR}'; \ 37 | echo 'app_update ${CSGO_APP_ID}'; \ 38 | echo 'quit'; \ 39 | } > ${STEAM_DIR}/autoupdate_script.txt \ 40 | && mkdir ${CSGO_DIR} \ 41 | && chown -R steam:steam ${STEAM_DIR} \ 42 | && rm -rf /var/lib/apt/lists/* 43 | 44 | ENV LANG=en_US.UTF-8 \ 45 | LANGUAGE=en_US:en \ 46 | LC_ALL=en_US.UTF-8 47 | 48 | COPY --chown=steam:steam containerfs ${STEAM_DIR}/ 49 | 50 | USER steam 51 | WORKDIR ${CSGO_DIR} 52 | VOLUME ${CSGO_DIR} 53 | ENTRYPOINT exec ${STEAM_DIR}/start.sh 54 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | CONTAINER_NAME ?= csgo-dedicated-server 4 | IMAGE_NAME ?= kmallea/csgo:latest 5 | SERVER_HOSTNAME ?= Counter-Strike: Global Offensive Dedicated Server 6 | SERVER_PASSWORD ?= 7 | RCON_PASSWORD ?= changeme 8 | STEAM_ACCOUNT ?= changeme 9 | AUTHKEY ?= changeme 10 | IP ?= 0.0.0.0 11 | PORT ?= 27015 12 | TV_PORT ?= 27020 13 | TICKRATE ?= 128 14 | FPS_MAX ?= 400 15 | GAME_TYPE ?= 0 16 | GAME_MODE ?= 1 17 | MAP ?= de_dust2 18 | MAPGROUP ?= mg_active 19 | HOST_WORKSHOP_COLLECTION ?= 20 | WORKSHOP_START_MAP ?= 21 | MAXPLAYERS ?= 12 22 | TV_ENABLE ?= 1 23 | LAN ?= 1 24 | SOURCEMOD_ADMINS ?= STEAM_1:0:123456,STEAM_1:0:654321 25 | RETAKES ?= 0 26 | NOMASTER ?= 0 27 | 28 | .PHONY: all clean image test stop 29 | 30 | all: image 31 | 32 | clean: 33 | docker rmi $(IMAGE_NAME) 34 | 35 | image: Dockerfile 36 | docker build -t $(IMAGE_NAME) \ 37 | --build-arg STEAMCMD_URL=https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz \ 38 | . 39 | 40 | server: 41 | docker run \ 42 | -i \ 43 | -t \ 44 | -d \ 45 | --net=host \ 46 | --mount source=csgo-data,target=/home/steam/csgo \ 47 | -e "SERVER_HOSTNAME=$(SERVER_HOSTNAME)" \ 48 | -e "SERVER_PASSWORD=$(SERVER_PASSWORD)" \ 49 | -e "RCON_PASSWORD=$(RCON_PASSWORD)" \ 50 | -e "STEAM_ACCOUNT=$(STEAM_ACCOUNT)" \ 51 | -e "AUTHKEY=$(AUTHKEY)" \ 52 | -e "TICKRATE=$(TICKRATE)" \ 53 | -e "FPS_MAX=$(FPS_MAX)" \ 54 | -e "GAME_TYPE=$(GAME_TYPE)" \ 55 | -e "GAME_MODE=$(GAME_MODE)" \ 56 | -e "MAP=$(MAP)" \ 57 | -e "MAPGROUP=$(MAPGROUP)" \ 58 | -e "HOST_WORKSHOP_COLLECTION=$(HOST_WORKSHOP_COLLECTION)" \ 59 | -e "WORKSHOP_START_MAP=$(WORKSHOP_START_MAP)" \ 60 | -e "MAXPLAYERS=$(MAXPLAYERS)" \ 61 | -e "TV_ENABLE=$(TV_ENABLE)" \ 62 | -e "LAN=$(LAN)" \ 63 | -e "SOURCEMOD_ADMINS=$(SOURCEMOD_ADMINS)" \ 64 | -e "RETAKES=$(RETAKES)" \ 65 | --name $(CONTAINER_NAME) \ 66 | $(IMAGE_NAME) 67 | 68 | test: 69 | docker run \ 70 | -i \ 71 | -t \ 72 | --rm \ 73 | --net=host \ 74 | --mount type=bind,source="$(PWD)/test",target=/home/steam/csgo \ 75 | -e "CI=true" \ 76 | -e "SERVER_HOSTNAME=$(SERVER_HOSTNAME)" \ 77 | -e "SERVER_PASSWORD=$(SERVER_PASSWORD)" \ 78 | -e "RCON_PASSWORD=$(RCON_PASSWORD)" \ 79 | -e "STEAM_ACCOUNT=$(STEAM_ACCOUNT)" \ 80 | -e "AUTHKEY=$(AUTHKEY)" \ 81 | -e "TICKRATE=$(TICKRATE)" \ 82 | -e "FPS_MAX=$(FPS_MAX)" \ 83 | -e "GAME_TYPE=$(GAME_TYPE)" \ 84 | -e "GAME_MODE=$(GAME_MODE)" \ 85 | -e "MAP=$(MAP)" \ 86 | -e "MAPGROUP=$(MAPGROUP)" \ 87 | -e "HOST_WORKSHOP_COLLECTION=$(HOST_WORKSHOP_COLLECTION)" \ 88 | -e "WORKSHOP_START_MAP=$(WORKSHOP_START_MAP)" \ 89 | -e "MAXPLAYERS=$(MAXPLAYERS)" \ 90 | -e "TV_ENABLE=$(TV_ENABLE)" \ 91 | -e "LAN=$(LAN)" \ 92 | -e "SOURCEMOD_ADMINS=$(SOURCEMOD_ADMINS)" \ 93 | -e "RETAKES=$(RETAKES)" \ 94 | -e "SM_PUGSETUP_SNAKE_CAPTAIN_PICKS=2" \ 95 | --name $(CONTAINER_NAME) \ 96 | $(IMAGE_NAME) 97 | 98 | stop: 99 | docker stop $(CONTAINER_NAME) 100 | docker rm $(CONTAINER_NAME) 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CSGO containerized 2 | 3 | The Dockerfile will build an image for running a Counter-Strike: Global Offensive dedicated server in a container. 4 | 5 | The following addons and plugins are included by default: 6 | 7 | - [Metamod](https://www.sourcemm.net/) 8 | - [SourceMod](https://www.sourcemod.net/) 9 | - [SteamWorks](https://forums.alliedmods.net/showthread.php?t=229556) 10 | - [Updater](https://bitbucket.org/GoD_Tony/updater/downloads/updater.smx) 11 | - [PugSetup](https://github.com/splewis/csgo-pug-setup) 12 | - [Practice Mode](https://github.com/splewis/csgo-practice-mode) 13 | - [Retakes](https://github.com/splewis/csgo-retakes) (**disabled by default**) 14 | 15 | To get a 10man/gather going, simply connect and type `.setup` in chat. Practice Mode should also be available from the menu. 16 | 17 | Retakes 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. 18 | 19 | ## How to Use 20 | 21 | ```bash 22 | docker pull kmallea/csgo:latest 23 | ``` 24 | 25 | To use the image as-is, run it with a few useful environment variables to configure the server: 26 | 27 | ```bash 28 | docker run \ 29 | --rm \ 30 | --interactive \ 31 | --tty \ 32 | --detach \ 33 | --mount source=csgo-data,target=/home/steam/csgo \ 34 | --network=host \ 35 | --env "SERVER_HOSTNAME=hostname" \ 36 | --env "SERVER_PASSWORD=password" \ 37 | --env "RCON_PASSWORD=rconpassword" \ 38 | --env "STEAM_ACCOUNT=gamelogintoken" \ 39 | --env "AUTHKEY=webapikey" \ 40 | --env "SOURCEMOD_ADMINS=STEAM_1:0:123456,STEAM_1:0:654321" \ 41 | kmallea/csgo 42 | ``` 43 | 44 | Would 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. 45 | 46 | If 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`). 47 | 48 | ### Required Game Login Token 49 | 50 | The `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. 51 | 52 | Remember that if you DO NOT give a valid Game Login Token, your server will be restricted to LAN only 53 | 54 | ### Optional Steam Web API Key for Workshop Content 55 | 56 | To 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 `. 57 | 58 | If you don't have a key you can generate one at http://steamcommunity.com/dev/apikey. 59 | 60 | With 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. 61 | 62 | For 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). 63 | 64 | ### SourceMod admins 65 | 66 | The 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. 67 | 68 | ### Playing on LAN 69 | 70 | If 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. 71 | 72 | ### Environment variable overrides 73 | 74 | Below 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). 75 | 76 | ```bash 77 | SERVER_HOSTNAME=Counter-Strike: Global Offensive Dedicated Server 78 | SERVER_PASSWORD= 79 | RCON_PASSWORD=changeme 80 | STEAM_ACCOUNT=changeme 81 | AUTHKEY=changeme 82 | IP=0.0.0.0 83 | PORT=27015 84 | TV_PORT=27020 85 | TICKRATE=128 86 | FPS_MAX=400 87 | GAME_TYPE=0 88 | GAME_MODE=1 89 | MAP=de_dust2 90 | MAPGROUP=mg_active 91 | HOST_WORKSHOP_COLLECTION= 92 | WORKSHOP_START_MAP= 93 | MAXPLAYERS=12 94 | TV_ENABLE=1 95 | LAN=0 96 | SOURCEMOD_ADMINS= 97 | RETAKES=0 98 | NOMASTER=0 99 | ``` 100 | 101 | For compatibility with the [Docker secrets](https://docs.docker.com/engine/swarm/secrets/) feature the following 102 | environment variables are also available as a '_FILE' variant. 103 | 104 | ```bash 105 | SERVER_PASSWORD_FILE 106 | RCON_PASSWORD_FILE 107 | STEAM_ACCOUNT_FILE 108 | AUTHKEY_FILE 109 | SOURCEMOD_ADMINS_FILE 110 | ``` 111 | 112 | If one of these is set the content of the referred file is used as content for the non-'_FILE" environment variable. If both 113 | environment variables are set, the content of the non-'_FILE' variable takes precedence. 114 | 115 | Usage of _FILE variables allows constructs like this in docker compose files: 116 | 117 | ```yml 118 | version: "3.7" 119 | services: 120 | app: 121 | image: kmallea/csgo 122 | secrets: 123 | - csgo_rcon_password 124 | environment: 125 | - RCON_PASSWORD_FILE=/run/secrets/csgo_rcon_password 126 | 127 | secrets: 128 | csgo_rcon_password: 129 | file: ${SECRETS_DIR}/csgo_rcon_password.txt 130 | ``` 131 | 132 | ### PugSetup ConVars 133 | 134 | PugSetup'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`. 135 | 136 | **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.** 137 | 138 | For 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: 139 | 140 | ```bash 141 | ... 142 | --env "SM_PUGSETUP_SNAKE_CAPTAIN_PICKS=2" \ 143 | --env "SM_PUGSETUP_MESSAGE_PREFIX=[{YELLOW}Sesame Street{NORMAL}]" \ 144 | ... 145 | ``` 146 | 147 | This would set these values in `$CSGODIR/csgo/cfg/sourcemod/pugsetup.cfg`: 148 | 149 | ```bash 150 | ... 151 | sm_pugsetup_snake_captain_picks "2" 152 | sm_pugsetup_message_prefix "[{YELLOW}Sesame Street{NORMAL}]" 153 | ... 154 | ``` 155 | 156 | ### Troubleshooting 157 | 158 | If you're unable to use [`--network=host`](https://docs.docker.com/network/host/), you'll need to publsh the ports instead, e.g.: 159 | 160 | ```bash 161 | docker run \ 162 | --rm \ 163 | --interactive \ 164 | --tty \ 165 | --detach \ 166 | --mount source=csgo-data,target=/home/steam/csgo \ 167 | --publish 27015:27015/tcp \ 168 | --publish 27015:27015/udp \ 169 | --publish 27020:27020/tcp \ 170 | --publish 27020:27020/udp \ 171 | --env "SERVER_HOSTNAME=hostname" \ 172 | --env "SERVER_PASSWORD=password" \ 173 | --env "RCON_PASSWORD=rconpassword" \ 174 | --env "STEAM_ACCOUNT=gamelogintoken" \ 175 | --env "AUTHKEY=webapikey" \ 176 | --env "SOURCEMOD_ADMINS=STEAM_1:0:123456,STEAM_1:0:654321" \ 177 | kmallea/csgo 178 | ``` 179 | 180 | ## Manually Building 181 | 182 | ```bash 183 | docker build -t csgo-dedicated-server . 184 | ``` 185 | 186 | _OR_ 187 | 188 | ```bash 189 | make 190 | ``` 191 | 192 | The 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. 193 | 194 | ### Overriding versions of SteamCMD, Metamod, SourceMod, and/or PugSetup 195 | 196 | #### SteamCMD 197 | 198 | SteamCMD is installed directly into the image at build time. To override the URL it installs from, pass in a build arg named `STEAMCMD_URL`: 199 | 200 | ```bash 201 | docker build \ 202 | -t $(IMAGE_NAME) \ 203 | --build-arg STEAMCMD_URL=https://steamcdn-a.akamaihd.net/client/installer/steamcmd_linux.tar.gz \ 204 | . 205 | ``` 206 | 207 | #### Metamod, SourceMod, PugSetup, Retakes, etc 208 | 209 | All plugins and extensions are installed during the startup of the container. This allows plugins can be managed via an environment variable. 210 | 211 | The 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. 212 | 213 | ```bash 214 | INSTALL_PLUGINS="${INSTALL_PLUGINS:-https://mms.alliedmods.net/mmsdrop/1.10/mmsource-1.10.7-git971-linux.tar.gz 215 | https://sm.alliedmods.net/smdrop/1.10/sourcemod-1.10.0-git6478-linux.tar.gz 216 | https://github.com/splewis/csgo-pug-setup/releases/download/2.0.5/pugsetup_2.0.5.zip 217 | https://github.com/splewis/csgo-retakes/releases/download/v0.3.4/retakes_0.3.4.zip 218 | https://github.com/b3none/retakes-instadefuse/releases/download/1.4.0/retakes-instadefuse.smx 219 | https://github.com/b3none/retakes-autoplant/releases/download/2.3.0/retakes_autoplant.smx 220 | https://github.com/b3none/retakes-hud/releases/download/2.2.5/retakes-hud.smx 221 | }" 222 | ``` 223 | 224 | Lastly, a checksum is generated for each plugin's URL and is stored as `$CSGO_DIR/csgo/.marker` to prevent re-downloading plugins that have already been installed. 225 | 226 | ### Adding your own configs, other files etc. 227 | 228 | #### Build time 229 | 230 | The 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. 231 | 232 | It is recommended to use `INSTALL_PLUGINS` environment variable at run time to install plugins, so that they are decoupled from the image. 233 | 234 | #### Run time 235 | 236 | See `INSTALL_PLUGINS` above in the section above to learn about installing plugins. 237 | 238 | If you're using a data volume, you can use the `docker cp` command to copy files from your host machine into the data volume. 239 | 240 | If 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. 241 | 242 | ### Test Locally 243 | 244 | After building: 245 | 246 | 1. Edit the exported environment variables in the `Makefile` to your liking 247 | 2. Run `make server` to start a local LAN server to test 248 | 3. Run `make test` to run tests 249 | -------------------------------------------------------------------------------- /cloud-config.yaml: -------------------------------------------------------------------------------- 1 | #cloud-config 2 | 3 | # This config is an example of of a one-liner to getting a CSGO server up-and-running 4 | # in just a few minutes on Google Compute Engine with the following one-liner that consumes 5 | # the uncommented parts of this file: 6 | # 7 | # gcloud compute instances create csgo-server \ 8 | # --project=$PROJECT \ 9 | # --zone=$ZONE \ 10 | # --image-family=cos-stable \ 11 | # --image-project=cos-cloud \ 12 | # --boot-disk-size=50GB \ 13 | # --machine-type=c2-standard-4 \ 14 | # --network=default \ 15 | # --metadata-from-file user-data=$(PWD)/cloud-config.yaml 16 | 17 | write_files: 18 | - path: /etc/systemd/system/dynamic-dns.service 19 | permissions: 0644 20 | owner: root 21 | content: | 22 | # /etc/systemd/system/dynamic-dns.service 23 | [Unit] 24 | Description=Updates dynamic DNS record 25 | Wants=dynamic-dns.timer 26 | 27 | [Service] 28 | ExecStart=/bin/sh -c '(\ 29 | export PUBLIC_IP=$$(\ 30 | /usr/bin/curl \ 31 | -s \ 32 | -H "Metadata-Flavor: Google" \ 33 | https://domains.google.com/checkip \ 34 | ) && \ 35 | /usr/bin/curl \ 36 | -s \ 37 | --user : \ 38 | "https://domains.google.com/nic/update?hostname=&myip=$${PUBLIC_IP}" \ 39 | )' 40 | 41 | - path: /etc/systemd/system/dynamic-dns.timer 42 | permissions: 0644 43 | owner: root 44 | content: | 45 | # /etc/systemd/system/dynamic-dns.timer 46 | [Unit] 47 | Description=Runs dynamic-dns.service every 15 minutes 48 | Requires=dynamic-dns.timer 49 | 50 | [Timer] 51 | Unit=dynamic-dns.service 52 | OnUnitInactiveSec=15m 53 | 54 | - path: /etc/systemd/system/csgods.service 55 | permissions: 0644 56 | owner: root 57 | content: | 58 | [Unit] 59 | Description=CSGO Dedicated Server Container 60 | After=docker.service 61 | Requires=docker.service 62 | 63 | [Service] 64 | StandardInput=tty-force 65 | ExecStartPre=/usr/bin/docker pull kmallea/csgo 66 | ExecStart=/usr/bin/docker run --name %n \ 67 | --interactive \ 68 | --tty \ 69 | --rm \ 70 | --network host \ 71 | --cpuset-cpus 3 \ 72 | --mount source=csgo-data,target=/home/steam/csgo \ 73 | -e "SERVER_HOSTNAME=Counter-Strike: Global Offensive Dedicated Server" \ 74 | -e "SERVER_PASSWORD=" \ 75 | -e "RCON_PASSWORD=changeme" \ 76 | -e "STEAM_ACCOUNT=changeme" \ 77 | -e "SOURCEMOD_ADMINS=STEAM_1:0:123456,STEAM_1:1:654321" \ 78 | -e "AUTHKEY=changeme" \ 79 | -e "FPS_MAX=1000" \ 80 | kmallea/csgo 81 | ExecStop=-/usr/bin/docker stop %n 82 | ExecStopPost=-/usr/bin/docker rm %n 83 | 84 | runcmd: 85 | - iptables -w -A INPUT -p tcp --dport 27015 -j ACCEPT 86 | - iptables -w -A INPUT -p udp --dport 27015 -j ACCEPT 87 | - iptables -w -A INPUT -p tcp --dport 27020 -j ACCEPT 88 | - iptables -w -A INPUT -p udp --dport 27020 -j ACCEPT 89 | - iptables -w -A INPUT -p udp --dport 27005 -j ACCEPT 90 | - iptables -w -A INPUT -p udp --dport 51840 -j ACCEPT 91 | - iptables -w -A INPUT -p tcp --dport 26900 -j ACCEPT 92 | - iptables -w -A INPUT -p tcp --dport 80 -j ACCEPT 93 | - iptables -w -A INPUT -p tcp --dport 443 -j ACCEPT 94 | - systemctl daemon-reload 95 | - systemctl enable dynamic-dns.timer 96 | - systemctl start dynamic-dns.service 97 | - systemctl start csgods.service 98 | -------------------------------------------------------------------------------- /containerfs/README.md: -------------------------------------------------------------------------------- 1 | ## Adding your own files, plugins, etc. 2 | 3 | The 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. 4 | 5 | For 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`). 6 | -------------------------------------------------------------------------------- /containerfs/manage_plugins.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ueo pipefail 4 | 5 | : "${CSGO_DIR:?'ERROR: CSGO_DIR IS NOT SET!'}" 6 | 7 | export RETAKES="${RETAKES:-0}" 8 | 9 | INSTALL_PLUGINS="${INSTALL_PLUGINS:-https://mms.alliedmods.net/mmsdrop/1.11/mmsource-1.11.0-git1148-linux.tar.gz 10 | https://sm.alliedmods.net/smdrop/1.11/sourcemod-1.11.0-git6934-linux.tar.gz 11 | http://users.alliedmods.net/~kyles/builds/SteamWorks/SteamWorks-git131-linux.tar.gz 12 | https://bitbucket.org/GoD_Tony/updater/downloads/updater.smx 13 | https://github.com/splewis/csgo-practice-mode/releases/download/1.3.4/practicemode_1.3.4.zip 14 | https://github.com/splewis/csgo-pug-setup/releases/download/2.0.7/pugsetup_2.0.7.zip 15 | https://github.com/splewis/csgo-retakes/releases/download/v0.3.4/retakes_0.3.4.zip 16 | https://github.com/B3none/retakes-instadefuse/releases/download/1.5.0/retakes-instadefuse.smx 17 | https://github.com/B3none/retakes-autoplant/releases/download/2.3.3/retakes-autoplant.smx 18 | https://github.com/b3none/retakes-hud/releases/download/2.2.5/retakes-hud.smx 19 | }" 20 | 21 | get_checksum_from_string () { 22 | local md5 23 | md5=$(echo -n "$1" | md5sum | awk '{print $1}') 24 | echo "$md5" 25 | } 26 | 27 | is_plugin_installed() { 28 | local url_hash 29 | url_hash=$(get_checksum_from_string "$1") 30 | if [[ -f "$CSGO_DIR/csgo/${url_hash}.marker" ]]; then 31 | return 0 32 | else 33 | return 1 34 | fi 35 | } 36 | 37 | create_install_marker() { 38 | echo "$1" > "$CSGO_DIR/csgo/$(get_checksum_from_string "$1").marker" 39 | } 40 | 41 | file_url_exists() { 42 | if curl --output /dev/null --silent --head --fail "$1"; then 43 | return 0 44 | fi 45 | return 1 46 | } 47 | 48 | install_plugin() { 49 | filename=${1##*/} 50 | filename_ext=$(echo "${1##*.}" | awk '{print tolower($0)}') 51 | if ! file_url_exists "$1"; then 52 | echo "Plugin download check FAILED for $filename"; 53 | return 0 54 | fi 55 | if ! is_plugin_installed "$1"; then 56 | echo "Downloading $1..." 57 | case "$filename_ext" in 58 | "gz") 59 | curl -sSL "$1" | tar -zx -C "$CSGO_DIR/csgo" 60 | echo "Extracting $filename..." 61 | create_install_marker "$1" 62 | ;; 63 | "zip") 64 | curl -sSL -o "$filename" "$1" 65 | echo "Extracting $filename..." 66 | unzip -oq "$filename" -d "$CSGO_DIR/csgo" 67 | rm "$filename" 68 | create_install_marker "$1" 69 | ;; 70 | "smx") 71 | (cd "$CSGO_DIR/csgo/addons/sourcemod/plugins/" && curl -sSLO "$1") 72 | create_install_marker "$1" 73 | ;; 74 | *) 75 | echo "Plugin $filename has an unknown file extension, skipping" 76 | ;; 77 | esac 78 | else 79 | echo "Plugin $filename is already installed, skipping" 80 | fi 81 | } 82 | 83 | echo "Installing plugins..." 84 | 85 | mkdir -p "$CSGO_DIR/csgo" 86 | IFS=' ' read -ra PLUGIN_URLS <<< "$(echo "$INSTALL_PLUGINS" | tr "\n" " ")" 87 | for URL in "${PLUGIN_URLS[@]}"; do 88 | install_plugin "$URL" 89 | done 90 | 91 | echo "Finished installing plugins." 92 | 93 | # Add steam ids to sourcemod admin file 94 | mkdir -p "$CSGO_DIR/csgo/addons/sourcemod/configs" 95 | IFS=',' read -ra STEAMIDS <<< "$SOURCEMOD_ADMINS" 96 | for id in "${STEAMIDS[@]}"; do 97 | echo "\"$id\" \"99:z\"" >> "$CSGO_DIR/csgo/addons/sourcemod/configs/admins_simple.ini" 98 | done 99 | 100 | PLUGINS_ENABLED_DIR="$CSGO_DIR/csgo/addons/sourcemod/plugins" 101 | PLUGINS_DISABLED_DIR="$CSGO_DIR/csgo/addons/sourcemod/plugins/disabled" 102 | RETAKES_PLUGINS="retakes.smx retakes-instadefuse.smx retakes-autoplant.smx retakes-hud.smx retakes_standardallocator.smx" 103 | PUGSETUP_PLUGINS="pugsetup.smx pugsetup_teamnames.smx pugsetup_damageprint.smx" 104 | 105 | # Disable Retakes by default so that we have a working and predictable state without plugins conflict 106 | if [[ -f "$PLUGINS_ENABLED_DIR"/retakes.smx ]]; then 107 | mv "$PLUGINS_ENABLED_DIR"/retakes*.smx "$PLUGINS_DISABLED_DIR"/ 108 | fi 109 | 110 | if [ "$RETAKES" = "1" ]; then 111 | if [[ -f "$PLUGINS_ENABLED_DIR"/pugsetup.smx ]]; then 112 | (cd "$PLUGINS_ENABLED_DIR" && mv pugsetup*.smx "$PLUGINS_DISABLED_DIR") 113 | echo "Disabled PugSetup plugins" 114 | fi 115 | # shellcheck disable=SC2086 116 | (cd "$PLUGINS_DISABLED_DIR" && mv $RETAKES_PLUGINS "$PLUGINS_ENABLED_DIR") 117 | echo "Enabled Retakes plugins" 118 | else 119 | if [[ -f "$PLUGINS_DISABLED_DIR"/pugsetup.smx ]]; then 120 | # shellcheck disable=SC2086 121 | (cd "$PLUGINS_DISABLED_DIR" && mv $PUGSETUP_PLUGINS "$PLUGINS_ENABLED_DIR") 122 | echo "Enabled PugSetup plugins" 123 | fi 124 | fi 125 | -------------------------------------------------------------------------------- /containerfs/manage_pugsetup_configs.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ueo pipefail 4 | 5 | : "${CSGO_DIR:?'ERROR: CSGO_DIR IS NOT SET!'}" 6 | 7 | PUGSETUP_CONFIG="$CSGO_DIR/csgo/cfg/sourcemod/pugsetup/pugsetup.cfg" 8 | 9 | if [[ -f "$PUGSETUP_CONFIG" ]]; then 10 | # Update PugSetup cvars specified as envvars. 11 | # e.g., `SM_PUGSETUP_SNAKE_CAPTAIN_PICKS=2` will set sm_pugsetup_snake_captain_picks "2" inside of $PUGSETUP_CONFIG 12 | for var in "${!SM_PUGSETUP_@}"; do 13 | cvar=$(echo "$var" | tr '[:upper:]' '[:lower:]') 14 | value=${!var} 15 | sed -i "s/$cvar \"[^\]*\"/$cvar \"$value\"/g" "$PUGSETUP_CONFIG" 16 | done 17 | fi 18 | -------------------------------------------------------------------------------- /containerfs/start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # These envvars should've been set by the Dockerfile 4 | # If they're not set then something went wrong during the build 5 | : "${STEAM_DIR:?'ERROR: STEAM_DIR IS NOT SET!'}" 6 | : "${STEAMCMD_DIR:?'ERROR: STEAMCMD_DIR IS NOT SET!'}" 7 | : "${CSGO_APP_ID:?'ERROR: CSGO_APP_ID IS NOT SET!'}" 8 | : "${CSGO_DIR:?'ERROR: CSGO_DIR IS NOT SET!'}" 9 | 10 | # set_env_from_file_or_def VAR [DEFAULT] 11 | # e.g. set_env_from_file_or_def 'RCON_PASSWORD' 'test' 12 | # Fills $VAR either with the content of the file with the name $VAR_FILE 13 | # or with DEFAULT. 14 | # If $VAR is already set nothing will be changed 15 | # If both $VAR and $VAR_FILE are set $VAR will keep its value and content 16 | # of $VAR_FILE will be ignored. 17 | function set_env_from_file_or_def() { 18 | local VAR="$1" 19 | local FILEVAR="${VAR}_FILE" 20 | local DEFAULTVAL="${2:-}" 21 | local RETURNVAL="$DEFAULTVAL" 22 | 23 | if [ "${!VAR:-}" ]; then 24 | RETURNVAL="${!VAR}" 25 | elif [ "${!FILEVAR:-}" ]; then 26 | RETURNVAL="$(< "${!FILEVAR}")" 27 | fi 28 | 29 | export "$VAR"="$RETURNVAL" 30 | unset "$FILEVAR" 31 | } 32 | 33 | export SERVER_HOSTNAME="${SERVER_HOSTNAME:-Counter-Strike: Global Offensive Dedicated Server}" 34 | set_env_from_file_or_def 'SERVER_PASSWORD' 35 | set_env_from_file_or_def 'RCON_PASSWORD' 'changeme' 36 | set_env_from_file_or_def 'STEAM_ACCOUNT' 'changeme' 37 | set_env_from_file_or_def 'AUTHKEY' 'changeme' 38 | set_env_from_file_or_def 'IP' '0.0.0.0' 39 | export PORT="${PORT:-27015}" 40 | export TV_PORT="${TV_PORT:-27020}" 41 | export TICKRATE="${TICKRATE:-128}" 42 | export FPS_MAX="${FPS_MAX:-400}" 43 | export GAME_TYPE="${GAME_TYPE:-0}" 44 | export GAME_MODE="${GAME_MODE:-1}" 45 | export MAP="${MAP:-de_dust2}" 46 | export MAPGROUP="${MAPGROUP:-mg_active}" 47 | export HOST_WORKSHOP_COLLECTION="${HOST_WORKSHOP_COLLECTION:-}" 48 | export WORKSHOP_START_MAP="${WORKSHOP_START_MAP:-}" 49 | export MAXPLAYERS="${MAXPLAYERS:-12}" 50 | export TV_ENABLE="${TV_ENABLE:-1}" 51 | export LAN="${LAN:-0}" 52 | set_env_from_file_or_def 'SOURCEMOD_ADMINS' 53 | export RETAKES="${RETAKES:-0}" 54 | export ANNOUNCEMENT_IP="${ANNOUNCEMENT_IP:-}" 55 | export NOMASTER="${NOMASTER:-}" 56 | 57 | 58 | # Create dynamic autoexec config 59 | mkdir -p "$CSGO_DIR/csgo/cfg" 60 | 61 | if [ ! -s "$CSGO_DIR/csgo/cfg/autoexec.cfg" ]; then 62 | cat << AUTOEXECCFG > "$CSGO_DIR/csgo/cfg/autoexec.cfg" 63 | log on 64 | hostname "$SERVER_HOSTNAME" 65 | rcon_password "$RCON_PASSWORD" 66 | sv_password "$SERVER_PASSWORD" 67 | sv_cheats 0 68 | exec banned_user.cfg 69 | exec banned_ip.cfg 70 | AUTOEXECCFG 71 | 72 | else 73 | sed -i "s/^hostname.*/hostname \"$SERVER_HOSTNAME\"/" $CSGO_DIR/csgo/cfg/autoexec.cfg 74 | sed -i "s/^rcon_password.*/rcon_password \"$RCON_PASSWORD\"/" $CSGO_DIR/csgo/cfg/autoexec.cfg 75 | sed -i "s/^sv_password.*/sv_password \"$SERVER_PASSWORD\"/" $CSGO_DIR/csgo/cfg/autoexec.cfg 76 | 77 | fi 78 | 79 | # Create dynamic server config 80 | if [ ! -s "$CSGO_DIR/csgo/cfg/server.cfg" ]; then 81 | cat << SERVERCFG > "$CSGO_DIR/csgo/cfg/server.cfg" 82 | tv_enable $TV_ENABLE 83 | tv_delaymapchange 1 84 | tv_delay 30 85 | tv_deltacache 2 86 | tv_dispatchmode 1 87 | tv_maxclients 10 88 | tv_maxrate 0 89 | tv_overridemaster 0 90 | tv_relayvoice 1 91 | tv_snapshotrate 64 92 | tv_timeout 60 93 | tv_transmitall 1 94 | writeid 95 | writeip 96 | sv_mincmdrate $TICKRATE 97 | sv_maxupdaterate $TICKRATE 98 | sv_minupdaterate $TICKRATE 99 | SERVERCFG 100 | 101 | else 102 | sed -i "s/^tv_enable.*/tv_enable $TV_ENABLE/" $CSGO_DIR/csgo/cfg/server.cfg 103 | 104 | fi 105 | 106 | # Attempt to update CSGO before starting the server 107 | [[ -z ${CI+x} ]] && "$STEAMCMD_DIR/steamcmd.sh" +login anonymous +force_install_dir "$CSGO_DIR" +app_update "$CSGO_APP_ID" +quit 108 | 109 | # Install and configure plugins & extensions 110 | "$BASH" "$STEAM_DIR/manage_plugins.sh" 111 | 112 | # Update PugSetup configuration via environment variables 113 | "$BASH" "$STEAM_DIR/manage_pugsetup_configs.sh" 114 | 115 | SRCDS_ARGUMENTS=( 116 | "-console" 117 | "-usercon" 118 | "-game csgo" 119 | "-autoupdate" 120 | "-authkey $AUTHKEY" 121 | "-steam_dir $STEAMCMD_DIR" 122 | "-steamcmd_script $STEAM_DIR/autoupdate_script.txt" 123 | "-tickrate $TICKRATE" 124 | "-port $PORT" 125 | "-net_port_try 1" 126 | "-ip $IP" 127 | "-maxplayers_override $MAXPLAYERS" 128 | "+fps_max $FPS_MAX" 129 | "+game_type $GAME_TYPE" 130 | "+game_mode $GAME_MODE" 131 | "+mapgroup $MAPGROUP" 132 | "+map $MAP" 133 | "+sv_setsteamaccount" "$STEAM_ACCOUNT" 134 | "+sv_lan $LAN" 135 | "+tv_port $TV_PORT" 136 | ) 137 | 138 | if [[ -n $HOST_WORKSHOP_COLLECTION ]]; then 139 | SRCDS_ARGUMENTS+=("+host_workshop_collection $HOST_WORKSHOP_COLLECTION") 140 | fi 141 | 142 | if [[ -n $WORKSHOP_START_MAP ]]; then 143 | SRCDS_ARGUMENTS+=("+workshop_start_map $WORKSHOP_START_MAP") 144 | fi 145 | 146 | if [[ -n $ANNOUNCEMENT_IP ]]; then 147 | SRCDS_ARGUMENTS+=("+net_public_adr $ANNOUNCEMENT_IP") 148 | fi 149 | 150 | if [[ $NOMASTER == 1 ]]; then 151 | SRCDS_ARGUMENTS+=("-nomaster") 152 | fi 153 | 154 | SRCDS_RUN="$CSGO_DIR/srcds_run" 155 | 156 | # Patch srcds_run to fix autoupdates 157 | if grep -q 'steam.sh' "$SRCDS_RUN"; then 158 | sed -i 's/steam.sh/steamcmd.sh/' "$SRCDS_RUN" 159 | echo "Applied patch to srcds_run to fix autoupdates" 160 | fi 161 | 162 | # Start the server 163 | exec "$BASH" "$SRCDS_RUN" "${SRCDS_ARGUMENTS[@]}" 164 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | 3 | volumes: 4 | csgo-data: 5 | name: csgo-data 6 | 7 | services: 8 | csgo: 9 | image: kmallea/csgo:latest 10 | container_name: csgo-ds 11 | 12 | environment: 13 | SERVER_HOSTNAME: "Counter-Strike: Global Offensive Dedicated Server" 14 | SERVER_PASSWORD: 15 | RCON_PASSWORD: changeme 16 | STEAM_ACCOUNT: changeme 17 | AUTHKEY: changeme 18 | SOURCEMOD_ADMINS: comma,delimited,list,of,steam,ids 19 | IP: 0.0.0.0 20 | PORT: 27015 21 | TV_PORT: 27020 22 | TICKRATE: 128 23 | FPS_MAX: 300 24 | GAME_TYPE: 0 25 | GAME_MODE: 1 26 | MAP: de_dust2 27 | MAPGROUP: mg_active 28 | MAXPLAYERS: 12 29 | TV_ENABLE: 1 30 | LAN: 0 31 | RETAKES: 0 32 | 33 | volumes: 34 | - type: volume 35 | source: csgo-data 36 | target: /home/steam/csgo 37 | 38 | network_mode: "host" 39 | 40 | restart: unless-stopped 41 | stdin_open: true 42 | tty: true 43 | -------------------------------------------------------------------------------- /test/bin/bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -e 4 | 5 | BATS_READLINK='true' 6 | if command -v 'greadlink' >/dev/null; then 7 | BATS_READLINK='greadlink' 8 | elif command -v 'readlink' >/dev/null; then 9 | BATS_READLINK='readlink' 10 | fi 11 | 12 | bats_resolve_absolute_root_dir() { 13 | local cwd="$PWD" 14 | local path="$1" 15 | local result="$2" 16 | local target_dir 17 | local target_name 18 | local original_shell_options="$-" 19 | 20 | # Resolve the parent directory, e.g. /bin => /usr/bin on CentOS (#113). 21 | set -P 22 | 23 | while true; do 24 | target_dir="${path%/*}" 25 | target_name="${path##*/}" 26 | 27 | if [[ "$target_dir" != "$path" ]]; then 28 | cd "$target_dir" 29 | fi 30 | 31 | if [[ -L "$target_name" ]]; then 32 | path="$("$BATS_READLINK" "$target_name")" 33 | else 34 | printf -v "$result" -- '%s' "${PWD%/*}" 35 | set +P "-$original_shell_options" 36 | cd "$cwd" 37 | return 38 | fi 39 | done 40 | } 41 | 42 | export BATS_ROOT 43 | bats_resolve_absolute_root_dir "$0" 'BATS_ROOT' 44 | exec "$BATS_ROOT/libexec/bats-core/bats" "$@" 45 | -------------------------------------------------------------------------------- /test/csgo/cfg/sourcemod/pugsetup/pugsetup.cfg: -------------------------------------------------------------------------------- 1 | sm_pugsetup_snake_captain_picks "0" 2 | -------------------------------------------------------------------------------- /test/libexec/bats-core/bats: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | export BATS_VERSION='1.2.0-dev' 5 | 6 | version() { 7 | printf 'Bats %s\n' "$BATS_VERSION" 8 | } 9 | 10 | abort() { 11 | printf 'Error: %s\n' "$1" >&2 12 | usage >&2 13 | exit 1 14 | } 15 | 16 | usage() { 17 | local cmd="${0##*/}" 18 | local line 19 | 20 | while IFS= read -r line; do 21 | printf '%s\n' "$line" 22 | done <] [-j ] [-p | -t] ... 24 | $cmd [-h | -v] 25 | 26 | is the path to a Bats test file, or the path to a directory 27 | containing Bats test files (ending with ".bats"). 28 | 29 | -c, --count Count the number of test cases without running any tests 30 | -f, --filter Filter test cases by names matching the regular expression 31 | -h, --help Display this help message 32 | -j, --jobs Number of parallel jobs to run (requires GNU parallel) 33 | -p, --pretty Show results in pretty format (default for terminals) 34 | -r, --recursive Include tests in subdirectories 35 | -t, --tap Show results in TAP format 36 | -v, --version Display the version number 37 | 38 | For more information, see https://github.com/bats-core/bats-core 39 | 40 | END_OF_HELP_TEXT 41 | } 42 | 43 | expand_link() { 44 | readlink="$(type -p greadlink readlink | head -1)" 45 | "$readlink" -f "$1" 46 | } 47 | 48 | expand_path() { 49 | local path="${1%/}" 50 | local dirname="${path%/*}" 51 | local result="$2" 52 | 53 | if [[ "$dirname" == "$path" ]]; then 54 | dirname="$PWD" 55 | else 56 | cd "$dirname" 57 | dirname="$PWD" 58 | cd "$OLDPWD" 59 | fi 60 | printf -v "$result" '%s/%s' "$dirname" "${path##*/}" 61 | } 62 | 63 | BATS_LIBEXEC="$(dirname "$(expand_link "${BASH_SOURCE[0]}")")" 64 | export BATS_CWD="$PWD" 65 | export BATS_TEST_PATTERN="^[[:blank:]]*@test[[:blank:]]+(.*[^[:blank:]])[[:blank:]]+\{(.*)\$" 66 | export BATS_TEST_FILTER= 67 | export PATH="$BATS_LIBEXEC:$PATH" 68 | 69 | arguments=() 70 | 71 | # Unpack single-character options bundled together, e.g. -cr, -pr. 72 | for arg in "$@"; do 73 | if [[ "$arg" =~ ^-[^-]. ]]; then 74 | index=1 75 | while option="${arg:$((index++)):1}"; do 76 | if [[ -z "$option" ]]; then 77 | break 78 | fi 79 | arguments+=("-$option") 80 | done 81 | else 82 | arguments+=("$arg") 83 | fi 84 | shift 85 | done 86 | 87 | set -- "${arguments[@]}" 88 | arguments=() 89 | 90 | unset flags pretty recursive 91 | flags=() 92 | pretty= 93 | recursive= 94 | if [[ -z "${CI:-}" && -t 0 && -t 1 ]] && command -v tput >/dev/null; then 95 | pretty=1 96 | fi 97 | 98 | while [[ "$#" -ne 0 ]]; do 99 | case "$1" in 100 | -h|--help) 101 | version 102 | usage 103 | exit 0 104 | ;; 105 | -v|--version) 106 | version 107 | exit 0 108 | ;; 109 | -c|--count) 110 | flags+=('-c') 111 | ;; 112 | -f|--filter) 113 | shift 114 | flags+=('-f' "$1") 115 | ;; 116 | -j|--jobs) 117 | shift 118 | flags+=('-j' "$1") 119 | ;; 120 | -r|--recursive) 121 | recursive=1 122 | ;; 123 | -t|--tap) 124 | pretty= 125 | ;; 126 | -p|--pretty) 127 | pretty=1 128 | ;; 129 | -*) 130 | abort "Bad command line option '$1'" 131 | ;; 132 | *) 133 | arguments+=("$1") 134 | ;; 135 | esac 136 | shift 137 | done 138 | 139 | if [[ "${#arguments[@]}" -eq 0 ]]; then 140 | abort 'Must specify at least one ' 141 | fi 142 | 143 | filenames=() 144 | for filename in "${arguments[@]}"; do 145 | expand_path "$filename" 'filename' 146 | 147 | if [[ -d "$filename" ]]; then 148 | shopt -s nullglob 149 | if [[ "$recursive" -eq 1 ]]; then 150 | while IFS= read -r -d $'\0' file; do 151 | filenames+=("$file") 152 | done < <(find "$filename" -type f -name '*.bats' -print0 | sort -z) 153 | else 154 | for suite_filename in "$filename"/*.bats; do 155 | filenames+=("$suite_filename") 156 | done 157 | fi 158 | shopt -u nullglob 159 | else 160 | filenames+=("$filename") 161 | fi 162 | done 163 | 164 | formatter="cat" 165 | if [[ -n "$pretty" ]]; then 166 | flags+=("-x") 167 | formatter="bats-format-tap-stream" 168 | fi 169 | 170 | set -o pipefail execfail 171 | exec bats-exec-suite "${flags[@]}" "${filenames[@]}" | "$formatter" 172 | -------------------------------------------------------------------------------- /test/libexec/bats-core/bats-exec-suite: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | count_only_flag='' 5 | extended_syntax_flag='' 6 | filter='' 7 | num_jobs=1 8 | have_gnu_parallel= 9 | flags=() 10 | 11 | while [[ "$#" -ne 0 ]]; do 12 | case "$1" in 13 | -c) 14 | count_only_flag=1 15 | ;; 16 | -f) 17 | shift 18 | filter="$1" 19 | flags+=('-f' "$filter") 20 | ;; 21 | -j) 22 | shift 23 | num_jobs="$1" 24 | ;; 25 | -x) 26 | # shellcheck disable=SC2034 27 | extended_syntax_flag='-x' 28 | flags+=('-x') 29 | ;; 30 | *) 31 | break 32 | ;; 33 | esac 34 | shift 35 | done 36 | 37 | if ( type -p parallel &>/dev/null ); then 38 | # shellcheck disable=SC2034 39 | have_gnu_parallel=1 40 | elif [[ "$num_jobs" != 1 ]]; then 41 | printf 'bats: cannot execute "%s" jobs without GNU parallel\n' "$num_jobs" >&2 42 | exit 1 43 | fi 44 | 45 | trap 'kill 0; exit 1' INT 46 | 47 | all_tests=() 48 | for filename in "$@"; do 49 | if [[ ! -f "$filename" ]]; then 50 | printf 'bats: %s does not exist\n' "$filename" >&2 51 | exit 1 52 | fi 53 | 54 | test_names=() 55 | test_dupes=() 56 | while read -r line; do 57 | if [[ ! "$line" =~ ^bats_test_function\ ]]; then 58 | continue 59 | fi 60 | line="${line%$'\r'}" 61 | line="${line#* }" 62 | 63 | all_tests+=( "$(printf "%s\t%s" "$filename" "$line")" ) 64 | if [[ " ${test_names[*]} " == *" $line "* ]]; then 65 | test_dupes+=("$line") 66 | continue 67 | fi 68 | test_names+=("$line") 69 | done < <(BATS_TEST_FILTER="$filter" bats-preprocess "$filename") 70 | 71 | if [[ "${#test_dupes[@]}" -ne 0 ]]; then 72 | printf 'bats warning: duplicate test name(s) in %s: %s\n' "$filename" "${test_dupes[*]}" >&2 73 | fi 74 | done 75 | 76 | test_count="${#all_tests[@]}" 77 | 78 | if [[ -n "$count_only_flag" ]]; then 79 | printf '%d\n' "${test_count}" 80 | exit 81 | fi 82 | 83 | status=0 84 | printf '1..%d\n' "${test_count}" 85 | 86 | # No point on continuing if there's no tests. 87 | if [[ "${test_count}" == 0 ]]; then 88 | exit 89 | fi 90 | 91 | if [[ "$num_jobs" != 1 ]]; then 92 | # Only use GNU parallel when we want parallel execution -- there is a small 93 | # amount of overhead using it over a simple loop in the serial case. 94 | set -o pipefail 95 | printf '%s\n' "${all_tests[@]}" | grep -v '^$' | \ 96 | parallel -qk -j "$num_jobs" --colsep="\t" -- bats-exec-test "${flags[@]}" '{1}' '{2}' '{#}' || status=1 97 | else 98 | # Just do it serially. 99 | test_number=0 100 | for test_line in "${all_tests[@]}"; do 101 | # Only handle non-empty lines 102 | if [[ $test_line ]]; then 103 | filename="${test_line%%$'\t'*}" 104 | test_name="${test_line##*$'\t'}" 105 | ((++test_number)) 106 | bats-exec-test "${flags[@]}" "$filename" "$test_name" "$test_number" || status=1 107 | fi 108 | done 109 | if [[ "${test_number}" != "${test_count}" ]]; then 110 | printf '# bats warning: Only executed %s of %s tests\n' "$test_number" "$test_count" 111 | status=1 112 | fi 113 | fi 114 | exit "$status" 115 | -------------------------------------------------------------------------------- /test/libexec/bats-core/bats-exec-test: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -eET 3 | 4 | # Variables used in other scripts. 5 | BATS_COUNT_ONLY='' 6 | BATS_TEST_FILTER='' 7 | BATS_EXTENDED_SYNTAX='' 8 | 9 | while [[ "$#" -ne 0 ]]; do 10 | case "$1" in 11 | -c) 12 | # shellcheck disable=SC2034 13 | BATS_COUNT_ONLY=1 14 | ;; 15 | -f) 16 | shift 17 | # shellcheck disable=SC2034 18 | BATS_TEST_FILTER="$1" 19 | ;; 20 | -x) 21 | BATS_EXTENDED_SYNTAX='-x' 22 | ;; 23 | *) 24 | break 25 | ;; 26 | esac 27 | shift 28 | done 29 | 30 | BATS_TEST_FILENAME="$1" 31 | shift 32 | if [[ -z "$BATS_TEST_FILENAME" ]]; then 33 | printf 'usage: bats-exec-test \n' >&2 34 | exit 1 35 | elif [[ ! -f "$BATS_TEST_FILENAME" ]]; then 36 | printf 'bats: %s does not exist\n' "$BATS_TEST_FILENAME" >&2 37 | exit 1 38 | fi 39 | 40 | BATS_TEST_DIRNAME="${BATS_TEST_FILENAME%/*}" 41 | BATS_TEST_NAMES=() 42 | 43 | load() { 44 | local name="$1" 45 | local filename 46 | 47 | if [[ "${name:0:1}" == '/' ]]; then 48 | filename="${name}" 49 | else 50 | filename="$BATS_TEST_DIRNAME/${name}.bash" 51 | fi 52 | 53 | if [[ ! -f "$filename" ]]; then 54 | printf 'bats: %s does not exist\n' "$filename" >&2 55 | exit 1 56 | fi 57 | 58 | # Dynamically loaded user files provided outside of Bats. 59 | # shellcheck disable=SC1090 60 | source "${filename}" 61 | } 62 | 63 | run() { 64 | local origFlags="$-" 65 | set +eET 66 | local origIFS="$IFS" 67 | # 'output', 'status', 'lines' are global variables available to tests. 68 | # shellcheck disable=SC2034 69 | output="$("$@" 2>&1)" 70 | # shellcheck disable=SC2034 71 | status="$?" 72 | # shellcheck disable=SC2034,SC2206 73 | IFS=$'\n' lines=($output) 74 | IFS="$origIFS" 75 | set "-$origFlags" 76 | } 77 | 78 | setup() { 79 | return 0 80 | } 81 | 82 | teardown() { 83 | return 0 84 | } 85 | 86 | skip() { 87 | BATS_TEST_SKIPPED="${1:-1}" 88 | BATS_TEST_COMPLETED=1 89 | exit 0 90 | } 91 | 92 | bats_test_begin() { 93 | BATS_TEST_DESCRIPTION="$1" 94 | if [[ -n "$BATS_EXTENDED_SYNTAX" ]]; then 95 | printf 'begin %d %s\n' "$BATS_TEST_NUMBER" "$BATS_TEST_DESCRIPTION" >&3 96 | fi 97 | setup 98 | } 99 | 100 | bats_test_function() { 101 | local test_name="$1" 102 | BATS_TEST_NAMES+=("$test_name") 103 | } 104 | 105 | bats_capture_stack_trace() { 106 | local test_file 107 | local funcname 108 | local i 109 | 110 | BATS_STACK_TRACE=() 111 | 112 | for ((i=2; i != ${#FUNCNAME[@]}; ++i)); do 113 | # Use BATS_TEST_SOURCE if necessary to work around Bash < 4.4 bug whereby 114 | # calling an exported function erases the test file's BASH_SOURCE entry. 115 | test_file="${BASH_SOURCE[$i]:-$BATS_TEST_SOURCE}" 116 | funcname="${FUNCNAME[$i]}" 117 | BATS_STACK_TRACE+=("${BASH_LINENO[$((i-1))]} $funcname $test_file") 118 | if [[ "$test_file" == "$BATS_TEST_SOURCE" ]]; then 119 | case "$funcname" in 120 | "$BATS_TEST_NAME"|setup|teardown) 121 | break 122 | ;; 123 | esac 124 | fi 125 | done 126 | } 127 | 128 | bats_print_stack_trace() { 129 | local frame 130 | local index=1 131 | local count="${#@}" 132 | local filename 133 | local lineno 134 | 135 | for frame in "$@"; do 136 | bats_frame_filename "$frame" 'filename' 137 | bats_trim_filename "$filename" 'filename' 138 | bats_frame_lineno "$frame" 'lineno' 139 | 140 | if [[ $index -eq 1 ]]; then 141 | printf '# (' 142 | else 143 | printf '# ' 144 | fi 145 | 146 | local fn 147 | bats_frame_function "$frame" 'fn' 148 | if [[ "$fn" != "$BATS_TEST_NAME" ]]; then 149 | printf "from function \`%s' " "$fn" 150 | fi 151 | 152 | if [[ $index -eq $count ]]; then 153 | printf 'in test file %s, line %d)\n' "$filename" "$lineno" 154 | else 155 | printf 'in file %s, line %d,\n' "$filename" "$lineno" 156 | fi 157 | 158 | ((++index)) 159 | done 160 | } 161 | 162 | bats_print_failed_command() { 163 | local frame="${BATS_STACK_TRACE[${#BATS_STACK_TRACE[@]}-1]}" 164 | local filename 165 | local lineno 166 | local failed_line 167 | local failed_command 168 | 169 | bats_frame_filename "$frame" 'filename' 170 | bats_frame_lineno "$frame" 'lineno' 171 | bats_extract_line "$filename" "$lineno" 'failed_line' 172 | bats_strip_string "$failed_line" 'failed_command' 173 | printf '%s' "# \`${failed_command}' " 174 | 175 | if [[ "$BATS_ERROR_STATUS" -eq 1 ]]; then 176 | printf 'failed\n' 177 | else 178 | printf 'failed with status %d\n' "$BATS_ERROR_STATUS" 179 | fi 180 | } 181 | 182 | bats_frame_lineno() { 183 | printf -v "$2" '%s' "${1%% *}" 184 | } 185 | 186 | bats_frame_function() { 187 | local __bff_function="${1#* }" 188 | printf -v "$2" '%s' "${__bff_function%% *}" 189 | } 190 | 191 | bats_frame_filename() { 192 | local __bff_filename="${1#* }" 193 | __bff_filename="${__bff_filename#* }" 194 | 195 | if [[ "$__bff_filename" == "$BATS_TEST_SOURCE" ]]; then 196 | __bff_filename="$BATS_TEST_FILENAME" 197 | fi 198 | printf -v "$2" '%s' "$__bff_filename" 199 | } 200 | 201 | bats_extract_line() { 202 | local __bats_extract_line_line 203 | local __bats_extract_line_index=0 204 | 205 | while IFS= read -r __bats_extract_line_line; do 206 | if [[ "$((++__bats_extract_line_index))" -eq "$2" ]]; then 207 | printf -v "$3" '%s' "${__bats_extract_line_line%$'\r'}" 208 | break 209 | fi 210 | done <"$1" 211 | } 212 | 213 | bats_strip_string() { 214 | [[ "$1" =~ ^[[:space:]]*(.*)[[:space:]]*$ ]] 215 | printf -v "$2" '%s' "${BASH_REMATCH[1]}" 216 | } 217 | 218 | bats_trim_filename() { 219 | printf -v "$2" '%s' "${1#$BATS_CWD/}" 220 | } 221 | 222 | bats_debug_trap() { 223 | if [[ "${BASH_SOURCE[0]}" != "$1" ]]; then 224 | # The last entry in the stack trace is not useful when en error occured: 225 | # It is either duplicated (kinda correct) or has wrong line number (Bash < 4.4) 226 | # Therefore we capture the stacktrace but use it only after the next debug 227 | # trap fired. 228 | # Expansion is required for empty arrays which otherwise error 229 | BATS_CURRENT_STACK_TRACE=( "${BATS_STACK_TRACE[@]+"${BATS_STACK_TRACE[@]}"}" ) 230 | bats_capture_stack_trace 231 | fi 232 | } 233 | 234 | # For some versions of Bash, the `ERR` trap may not always fire for every 235 | # command failure, but the `EXIT` trap will. Also, some command failures may not 236 | # set `$?` properly. See #72 and #81 for details. 237 | # 238 | # For this reason, we call `bats_error_trap` at the very beginning of 239 | # `bats_teardown_trap` (the `DEBUG` trap for the call will fix the stack trace) 240 | # and check the value of `$BATS_TEST_COMPLETED` before taking other actions. 241 | # We also adjust the exit status value if needed. 242 | # 243 | # See `bats_exit_trap` for an additional EXIT error handling case when `$?` 244 | # isn't set properly during `teardown()` errors. 245 | bats_error_trap() { 246 | local status="$?" 247 | if [[ -z "$BATS_TEST_COMPLETED" ]]; then 248 | BATS_ERROR_STATUS="${BATS_ERROR_STATUS:-$status}" 249 | if [[ "$BATS_ERROR_STATUS" -eq 0 ]]; then 250 | BATS_ERROR_STATUS=1 251 | fi 252 | BATS_STACK_TRACE=( "${BATS_CURRENT_STACK_TRACE[@]}" ) 253 | trap - DEBUG 254 | fi 255 | } 256 | 257 | bats_teardown_trap() { 258 | bats_error_trap 259 | local status=0 260 | teardown >>"$BATS_OUT" 2>&1 || status="$?" 261 | 262 | if [[ $status -eq 0 ]]; then 263 | BATS_TEARDOWN_COMPLETED=1 264 | elif [[ -n "$BATS_TEST_COMPLETED" ]]; then 265 | BATS_ERROR_STATUS="$status" 266 | fi 267 | 268 | bats_exit_trap 269 | } 270 | 271 | bats_exit_trap() { 272 | local line 273 | local status 274 | local skipped='' 275 | trap - ERR EXIT 276 | 277 | if [[ -n "$BATS_TEST_SKIPPED" ]]; then 278 | skipped=' # skip' 279 | if [[ "$BATS_TEST_SKIPPED" != '1' ]]; then 280 | skipped+=" $BATS_TEST_SKIPPED" 281 | fi 282 | fi 283 | 284 | if [[ -z "$BATS_TEST_COMPLETED" || -z "$BATS_TEARDOWN_COMPLETED" ]]; then 285 | if [[ "$BATS_ERROR_STATUS" -eq 0 ]]; then 286 | # For some versions of bash, `$?` may not be set properly for some error 287 | # conditions before triggering the EXIT trap directly (see #72 and #81). 288 | # Thanks to the `BATS_TEARDOWN_COMPLETED` signal, this will pinpoint such 289 | # errors if they happen during `teardown()` when `bats_perform_test` calls 290 | # `bats_teardown_trap` directly after the test itself passes. 291 | # 292 | # If instead the test fails, and the `teardown()` error happens while 293 | # `bats_teardown_trap` runs as the EXIT trap, the test will fail with no 294 | # output, since there's no way to reach the `bats_exit_trap` call. 295 | BATS_STACK_TRACE=( "${BATS_CURRENT_STACK_TRACE[@]}" ) 296 | BATS_ERROR_STATUS=1 297 | fi 298 | printf 'not ok %d %s\n' "$BATS_TEST_NUMBER" "$BATS_TEST_DESCRIPTION" >&3 299 | bats_print_stack_trace "${BATS_STACK_TRACE[@]}" >&3 300 | bats_print_failed_command >&3 301 | 302 | while IFS= read -r line; do 303 | printf '# %s\n' "$line" 304 | done <"$BATS_OUT" >&3 305 | if [[ -n "$line" ]]; then 306 | printf '# %s\n' "$line" 307 | fi 308 | status=1 309 | else 310 | printf 'ok %d %s%s\n' "$BATS_TEST_NUMBER" "$BATS_TEST_DESCRIPTION" \ 311 | "$skipped" >&3 312 | status=0 313 | fi 314 | 315 | rm -f "$BATS_OUT" 316 | bats_cleanup_preprocessed_source 317 | exit "$status" 318 | } 319 | 320 | bats_perform_test() { 321 | BATS_TEST_NAME="$1" 322 | BATS_TEST_NUMBER="$2" 323 | 324 | if ! declare -F "$BATS_TEST_NAME" &>/dev/null; then 325 | printf "bats: unknown test name \`%s'\n" "$BATS_TEST_NAME" >&2 326 | exit 1 327 | fi 328 | 329 | # Some versions of Bash will reset BASH_LINENO to the first line of the 330 | # function when the ERR trap fires. All versions of Bash appear to reset it 331 | # on an unbound variable access error. bats_debug_trap will fire both before 332 | # the offending line is executed, and when the error is triggered. 333 | # Consequently, we use `BATS_CURRENT_STACK_TRACE` recorded by the 334 | # first call to bats_debug_trap, _before_ the ERR trap or unbound variable 335 | # access fires. 336 | BATS_STACK_TRACE=() 337 | BATS_CURRENT_STACK_TRACE=() 338 | 339 | BATS_TEST_COMPLETED= 340 | BATS_TEST_SKIPPED= 341 | BATS_TEARDOWN_COMPLETED= 342 | BATS_ERROR_STATUS= 343 | trap 'bats_debug_trap "$BASH_SOURCE"' DEBUG 344 | trap 'bats_error_trap' ERR 345 | trap 'bats_teardown_trap' EXIT 346 | "$BATS_TEST_NAME" >>"$BATS_OUT" 2>&1 347 | BATS_TEST_COMPLETED=1 348 | trap 'bats_exit_trap' EXIT 349 | bats_teardown_trap 350 | } 351 | 352 | if [[ -z "$TMPDIR" ]]; then 353 | BATS_TMPDIR='/tmp' 354 | else 355 | BATS_TMPDIR="${TMPDIR%/}" 356 | fi 357 | 358 | BATS_TMPNAME="$BATS_TMPDIR/bats.$$" 359 | BATS_PARENT_TMPNAME="$BATS_TMPDIR/bats.$PPID" 360 | BATS_OUT="${BATS_TMPNAME}.out" 361 | 362 | bats_preprocess_source() { 363 | BATS_TEST_SOURCE="${BATS_TMPNAME}.src" 364 | bats-preprocess "$BATS_TEST_FILENAME" >"$BATS_TEST_SOURCE" 365 | trap 'bats_cleanup_preprocessed_source' ERR EXIT 366 | trap 'bats_cleanup_preprocessed_source; exit 1' INT 367 | } 368 | 369 | bats_cleanup_preprocessed_source() { 370 | rm -f "$BATS_TEST_SOURCE" 371 | } 372 | 373 | bats_evaluate_preprocessed_source() { 374 | if [[ -z "$BATS_TEST_SOURCE" ]]; then 375 | BATS_TEST_SOURCE="${BATS_PARENT_TMPNAME}.src" 376 | fi 377 | # Dynamically loaded user files provided outside of Bats. 378 | # shellcheck disable=SC1090 379 | source "$BATS_TEST_SOURCE" 380 | } 381 | 382 | exec 3<&1 383 | 384 | # Run the given test. 385 | bats_preprocess_source 386 | bats_evaluate_preprocessed_source 387 | bats_perform_test "$@" 388 | -------------------------------------------------------------------------------- /test/libexec/bats-core/bats-format-tap-stream: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | header_pattern='[0-9]+\.\.[0-9]+' 5 | IFS= read -r header 6 | 7 | if [[ "$header" =~ $header_pattern ]]; then 8 | count="${header:3}" 9 | index=0 10 | passed=0 11 | failures=0 12 | skipped=0 13 | name= 14 | count_column_width=$(( ${#count} * 2 + 2 )) 15 | else 16 | # If the first line isn't a TAP plan, print it and pass the rest through 17 | printf '%s\n' "$header" 18 | exec cat 19 | fi 20 | 21 | update_screen_width() { 22 | screen_width="$(tput cols)" 23 | count_column_left=$(( screen_width - count_column_width )) 24 | } 25 | 26 | trap update_screen_width WINCH 27 | update_screen_width 28 | 29 | begin() { 30 | go_to_column 0 31 | buffer_with_truncation $(( count_column_left - 1 )) ' %s' "$name" 32 | clear_to_end_of_line 33 | go_to_column $count_column_left 34 | buffer "%${#count}s/${count}" "$index" 35 | go_to_column 1 36 | } 37 | 38 | pass() { 39 | go_to_column 0 40 | buffer ' ✓ %s' "$name" 41 | advance 42 | } 43 | 44 | skip() { 45 | local reason="$1" 46 | if [[ -n "$reason" ]]; then 47 | reason=": $reason" 48 | fi 49 | go_to_column 0 50 | buffer ' - %s (skipped%s)' "$name" "$reason" 51 | advance 52 | } 53 | 54 | fail() { 55 | go_to_column 0 56 | set_color 1 bold 57 | buffer ' ✗ %s' "$name" 58 | advance 59 | } 60 | 61 | log() { 62 | set_color 1 63 | buffer ' %s\n' "$1" 64 | clear_color 65 | } 66 | 67 | summary() { 68 | buffer '\n%d test' "$count" 69 | if [[ "$count" -ne 1 ]]; then 70 | buffer 's' 71 | fi 72 | 73 | buffer ', %d failure' "$failures" 74 | if [[ "$failures" -ne 1 ]]; then 75 | buffer 's' 76 | fi 77 | 78 | if [[ "$skipped" -gt 0 ]]; then 79 | buffer ', %d skipped' "$skipped" 80 | fi 81 | 82 | not_run=$((count - passed - failures - skipped)) 83 | if [[ "$not_run" -gt 0 ]]; then 84 | buffer ', %d not run' "$not_run" 85 | fi 86 | 87 | buffer '\n' 88 | } 89 | 90 | buffer_with_truncation() { 91 | local width="$1" 92 | shift 93 | local string 94 | 95 | # shellcheck disable=SC2059 96 | printf -v 'string' -- "$@" 97 | 98 | if [[ "${#string}" -gt "$width" ]]; then 99 | buffer '%s...' "${string:0:$(( width - 4 ))}" 100 | else 101 | buffer '%s' "$string" 102 | fi 103 | } 104 | 105 | go_to_column() { 106 | local column="$1" 107 | buffer '\x1B[%dG' $(( column + 1 )) 108 | } 109 | 110 | clear_to_end_of_line() { 111 | buffer '\x1B[K' 112 | } 113 | 114 | advance() { 115 | clear_to_end_of_line 116 | buffer '\n' 117 | clear_color 118 | } 119 | 120 | set_color() { 121 | local color="$1" 122 | local weight=22 123 | 124 | if [[ "$2" == 'bold' ]]; then 125 | weight=1 126 | fi 127 | buffer '\x1B[%d;%dm' "$(( 30 + color ))" "$weight" 128 | } 129 | 130 | clear_color() { 131 | buffer '\x1B[0m' 132 | } 133 | 134 | _buffer= 135 | 136 | buffer() { 137 | local content 138 | # shellcheck disable=SC2059 139 | printf -v content -- "$@" 140 | _buffer+="$content" 141 | } 142 | 143 | flush() { 144 | printf '%s' "$_buffer" 145 | _buffer= 146 | } 147 | 148 | finish() { 149 | flush 150 | printf '\n' 151 | } 152 | 153 | trap finish EXIT 154 | 155 | while IFS= read -r line; do 156 | case "$line" in 157 | 'begin '* ) 158 | ((++index)) 159 | name="${line#* $index }" 160 | begin 161 | flush 162 | ;; 163 | 'ok '* ) 164 | skip_expr="ok $index (.*) # skip ?(([[:print:]]*))?" 165 | if [[ "$line" =~ $skip_expr ]]; then 166 | ((++skipped)) 167 | skip "${BASH_REMATCH[2]}" 168 | else 169 | ((++passed)) 170 | pass 171 | fi 172 | ;; 173 | 'not ok '* ) 174 | ((++failures)) 175 | fail 176 | ;; 177 | '# '* ) 178 | log "${line:2}" 179 | ;; 180 | esac 181 | done 182 | 183 | summary 184 | -------------------------------------------------------------------------------- /test/libexec/bats-core/bats-preprocess: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -e 3 | 4 | bats_encode_test_name() { 5 | local name="$1" 6 | local result='test_' 7 | local hex_code 8 | 9 | if [[ ! "$name" =~ [^[:alnum:]\ _-] ]]; then 10 | name="${name//_/-5f}" 11 | name="${name//-/-2d}" 12 | name="${name// /_}" 13 | result+="$name" 14 | else 15 | local length="${#name}" 16 | local char i 17 | 18 | for ((i=0; i